47

I am trying to persist an object that has a many-to-many relationship with other objects already persisted.

Here is my persisted object (they are already persisted in the db, which is a MySql):-

Product

@Entity
@Table(name="PRODUCT")
public class Product {
    private int productId;
    private String productName;
    private Set<Reservation> reservations = new HashSet<Reservation>(0);

    @Id @GeneratedValue(strategy=GenerationType.AUTO)
    public int getProductId() {
        return productId;
    }

    public void setProductId(int productId) {
        this.productId = productId;
    }

@Column(nullable = false)
    public String getProduct() {
        return product;
    }
    public void setProduct(String product) {
        this.product = product;
    }

    @ManyToMany(fetch = FetchType.LAZY, mappedBy = "products")
    public Set<Reservation> getReservations() {
        return reservations;
    }
    public void setReservations(Set<Reservation> reservations) {
        this.reservations = reservations;
    }
}

Here is my no persisted object, which I am trying to create

@Entity
@Table(name = "RESERVATION")
public class Reservation {

    private int reservationId;

    private Set<Product> products = new HashSet<Product>(0);

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    public int getReservationId() {
        return reservationId;
    }

    public void setReservationId(int reservationId) {
        this.reservationId = reservationId;
    }

    @ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinTable(name = "product_reservation", joinColumns = { @JoinColumn(name = "reservationId", nullable = false, updatable = false) }, inverseJoinColumns = { @JoinColumn(name = "productId", 
            nullable = false, updatable = false) })
    public Set<Product> getProducts() {
        return products;
    }

    public void setProducts(Set<Product> products) {
        this.products = products;
    }
}

This is my ReservationService class, which receives an array of products names, look the products using the name and put them into the reservation object.

@Service
public class ReservationServiceImpl implements ReservationService {

    @Autowired
    private ProductDAO productDAO;
    @Autowired
    private ReservationDAO reservationDAO;

    @Transactional
    public void createReservation(String[] productNames) {

            Set<Product> products = new HashSet<Product>();
            for (String productName : productNames) {
                Product pi = productDAO.findByProductName(productName);
                products.add(pi);
            }
            Reservation reservation = new Reservation();
            reservation.setProducts(products);
            reservationDAO.save(reservation);   ---> Here I am getting detached entity passed to persist
    }
}

Here is my ProductDAO interface:

public interface ProductDAO extends JpaRepository<Product, Integer> {

    public Product findByProductName(String productName);
}

This is my spring config file:

@Configuration
@PropertySource(value = { "classpath:base.properties" })
@EnableTransactionManagement
@EnableJpaRepositories(basePackages = "com.reservation.dao")
public class RepositoryConfig {

    @Autowired
    private Environment env;

    @Bean
    public static PropertySourcesPlaceholderConfigurer placeHolderConfigurer() {
        return new PropertySourcesPlaceholderConfigurer();
    }

    @Bean
    public PlatformTransactionManager transactionManager() {
        EntityManagerFactory factory = entityManagerFactory().getObject();
        return new JpaTransactionManager(factory);
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() {
        HibernateJpaVendorAdapter vendorAdapter = new HibernateJpaVendorAdapter();
        vendorAdapter.setGenerateDdl(Boolean.valueOf(env
                .getProperty("hibernate.generate.ddl")));
        vendorAdapter.setShowSql(Boolean.valueOf(env
                .getProperty("hibernate.show_sql")));

        Properties jpaProperties = new Properties();
        jpaProperties.put("hibernate.hbm2ddl.auto",
                env.getProperty("hibernate.hbm2ddl.auto"));
        jpaProperties.put("hibernate.dialect", env.getProperty("hibernate.dialect"));

        LocalContainerEntityManagerFactoryBean factory = new LocalContainerEntityManagerFactoryBean();
        factory.setDataSource(dataSource());
        factory.setJpaVendorAdapter(vendorAdapter);
        factory.setPackagesToScan("com.reservation.service.domain");
        factory.setJpaProperties(jpaProperties);
        factory.afterPropertiesSet();
        factory.setLoadTimeWeaver(new InstrumentationLoadTimeWeaver());
        return factory;
    }

    @Bean
    public HibernateExceptionTranslator hibernateExceptionTranslator() {
        return new HibernateExceptionTranslator();
    }

    @Bean
    public DataSource dataSource() {
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName(env.getProperty("jdbc.driverClassName"));
        dataSource.setUrl(env.getProperty("jdbc.url"));
        dataSource.setUsername(env.getProperty("jdbc.username"));
        dataSource.setPassword(env.getProperty("jdbc.password"));
        return dataSource;
    }
}

Here is the full stack trace:

SEVERE: Servlet.service() for servlet [dispatcher] in context with path [/web] threw exception [Request processing failed; nested exception is org.springframework.dao.InvalidDataAccessApiUsageException: detached entity passed to persist: com.reservation.service.domain.Product; nested exception is org.hibernate.PersistentObjectException: detached entity passed to persist: com.reservation.service.domain.Product] with root cause org.hibernate.PersistentObjectException: detached entity passed to persist: com.reservation.service.domain.Product at org.hibernate.event.internal.DefaultPersistEventListener.onPersist(DefaultPersistEventListener.java:141)

Sana
  • 360
  • 3
  • 13
nspessot
  • 765
  • 2
  • 7
  • 11

8 Answers8

83

I had the same problem and solved it by removing the cascade = CascadeType.PERSIST.

In your case you use CascadeType.ALL, which is equivalent to also using the PERSIST, according to the documentation:

Defines the set of cascadable operations that are propagated to the associated entity. The value cascade=ALL is equivalent to cascade={PERSIST, MERGE, REMOVE, REFRESH, DETACH}.

It means when you try to save the reservation on reservationDAO.save(reservation) it will also try to persist the associated Product object. But this object is not attached to this session. So the error occur.

Denis Abakumov
  • 355
  • 3
  • 11
Renato Borges
  • 1,043
  • 9
  • 12
58

The exception comes as Hibernate is trying to persist associated products when you save your reservation. Persisting the products is only successful if they have no id because the id of Product is annotated

@GeneratedValue(strategy=GenerationType.AUTO)

But you got products from the repository and their ids are not null.

There are 2 options to resolve your issue:

  1. remove (cascade = CascadeType.ALL) on products of Reservation
  2. or remove @GeneratedValue(strategy=GenerationType.AUTO) on id of Product
Denis Abakumov
  • 355
  • 3
  • 11
My Pham
  • 689
  • 5
  • 4
  • That fixed my problem! – Stugal Feb 28 '18 at 13:16
  • This just causes me to get `org.hibernate.TransientPropertyValueException: Not-null property references a transient value - transient instance must be saved before current operation` instead. – Zyl Oct 01 '20 at 13:40
13

You need to ensure both side of the relationship are properly maintained in your code.

Update Reservation as below and then add the corresponding methods to Product.

@Entity
@Table(name = "RESERVATION")
public class Reservation {

    private int reservationId;

    private Set<Product> products = new HashSet<Product>(0);

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    public int getReservationId() {
        return reservationId;
    }

    public void setReservationId(int reservationId) {
        this.reservationId = reservationId;
    }

    @ManyToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
    @JoinTable(name = "product_reservation", joinColumns = { @JoinColumn(name = "reservationId", nullable = false, updatable = false) }, inverseJoinColumns = { @JoinColumn(name = "productId", 
            nullable = false, updatable = false) })
    public Set<Product> getProducts() {
        
        //force clients through our add and remove methods
        return Collections.unmodifiableSet(products);
    }
    
    public void addProduct(Product product){
    
        //avoid circular calls : assumes equals and hashcode implemented
        if(! products.contains(product){
            products.add(product);
            
            //add method to Product : sets 'other side' of association
            product.addReservation(this);
        }
    }
    
    public void removeProduct(Product product){
        
        //avoid circular calls: assumes equals and hashcode implemented: 
        if(product.contains(product){
            products.remove(product);
            
            //add method to Product: set 'other side' of association: 
            product.removeReservation(this);
        }
    }
}

And in Products:

public void addReservation(Reservation reservation){

    //assumes equals and hashcode implemented: avoid circular calls
    if(! reservations.contains(reservation){
        reservations.add(reservation);

        //add method to Product : sets 'other side' of association
        reservation.addProduct(this);
    }
}

public void removeReservation(Reservation reservation){

    //assumes equals and hashcode implemented: avoid circular calls
    if(reservations.contains(reservation){
        reservations.remove(reservation);

        //add method to Product : sets 'other side' of association
        reservation.reomveProduct(this);
    }
}

Now you should be able to call save on either Product or Reservation and everything should work as expected, so you would by happy.

bohuss
  • 336
  • 2
  • 8
Alan Hay
  • 22,665
  • 4
  • 56
  • 110
  • 2
    This should be the accepted answer, and should have the highest upvotes. One should always resist the urge to follow answers that say "put this line in... or remove this..." etc. While I don't doubt there are good cases to disable cascade or auto id generation, I think the far majority of cases -- and people landing here -- are because the real problem is their code is not managing the relationships quite right. This answer is probably what most who land here are missing. – Thomas Carlisle Mar 02 '21 at 15:19
4

Remove @ CascadeType.ALL in the @ManytoMany relation, this worked for me.

Karthik C Yadav
  • 177
  • 1
  • 3
  • 16
2

I had similar issue. I replaced CascadeType.ALL with CascadeType.MERGE and it worked for me.

Mapping can be ManyToMany or OneToOne.

Shrip
  • 101
  • 1
  • 6
1

entityManager.merge() is a good option. It will merge the detached objects in the session. No need to change any cascadeType.

Matteo Baldi
  • 5,613
  • 10
  • 39
  • 51
0

I have a feeling that your annotations are slightly incorrect. Not by much though, have a look at Example 7.24 here and see if it matches your annotations. Ignore the Collection datatype though, as you shouldn't have any problems using a Set. I notice that you are missing a cascade=CascadeType.ALL on your Product collection, but I can't work out if that is the problem or not.

What the actual exception is saying is that your Product object(s) haven't actually been saved when its trying to save the collection of Product's. That's why I think its a problem with your annotations.

Try it out and let me know if you get anywhere.

JamesENL
  • 6,400
  • 6
  • 39
  • 64
0

you have to use persist to save and merge to update but first validate if the id comes null

@Override
public Registration save(Registration registration) {
    if(registration.getId() == null) {
        _entityManager.persist(registration);
    }else {
        _entityManager.merge(registration);
    }
    return registration;
}
Pedro Ávila
  • 115
  • 5