Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Detached entity passed to persist in Spring-Data

I have two tables in my db Brand and Product with the next simple structure:

| Brand | id PK |

| Product | id PK | brand_id FK |

and entities for that tables:

@Entity
@Table(name = "Brand")
public class Brand {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(name = "brand")
    private String brand;

    /* getters and setters */
}

@Entity
@Table(name = "Product")
public class Product {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "brand_id")
    private Brand brand;

    /* getters and setters */
}

As I use Spring-Data I have repository and service with implementation for Brand:

@Repository
public interface BrandRepository extends JpaRepository<Brand, Long> {

    Brand findByBrand(String brand);
}

public interface BrandService {

    Brand findByBrand(String brand);
}

@Service
public class BrandServiceImpl implements BrandService {

    @Autowired
    private BrandRepository brandRepository;

    @Override
    public Brand findByBrand(String brand) {

        return brandRepository.findByBrand(brand);
    }
}

and for Product:

@Repository
public interface ProductRepository extends JpaRepository<Product, Long> {
}

public interface ProductService {

    Product save(Product product);
}

@Service
public class ProductServiceImpl implements ProductService {

    @Autowired
    private ProductRepository productRepository;

    @Override
    public Product save(Product product) {
        return productRepository.save(product);
    }
}

The goal is to save Product object. Brand object should be saved automatically if it doesn't exist in db or should be set to Product otherwise:

Brand brand = brandService.findByBrand(brandName);
if (brand == null) {
    brand = new Brand();
    brand.setBrand("Some name");
}
product.setBrand(brand);
productService.save(product);

It works fine if Brand object with specified brandName is not in my db. But if it is I get:

PersistentObjectException: detached entity passed to persist

for Brand.

I can change cascade type to MERGE and it will work fine. But if I run the code with MERGE cascade type and Brand object with specified brandName is not in my db I get

IllegalStateException:
org.hibernate.TransientPropertyValueException:
object references an unsaved transient instance - save the transient instance before flushing

for Brand (that's really not surprised).

What Cascade Type should be? Ot what I did wrong?

like image 668
Ilya Orlov Avatar asked Oct 25 '16 18:10

Ilya Orlov


People also ask

How do you handle a detached entity passed to persist?

A detached entity is a Java object that is no longer tracked by the persistence context. Entities can reach this state if we close or clear the session. Similarly, we can detach an entity by manually removing it from the persistence context.

How do you persist detached entity in JPA?

saveOrUpdate method, and its cousin Session. update, attach the passed entity to the persistence context while EntityManager. merge method copies the state of the passed object to the persistent entity with the same identifier and then return a reference to that persistent entity.

How do I fix org hibernate Persistentobjectexception detached entity passed to persist?

The solution is simple, just use the CascadeType. MERGE instead of CascadeType. PERSIST or CascadeType. ALL .

What is a detached entity?

A detached entity is just an ordinary entity POJO whose identity value corresponds to a database row. The difference from a managed entity is that it's not tracked anymore by any persistence context. An entity can become detached when the Session used to load it was closed, or when we call Session.


1 Answers

Short answer:

There is no problem with your cascade annotation. You should not rely on automatic cascade and implement this logic by hand and inside your service layer.

Long answer:

You have two scenarios:

  • Scenario 1 - CascadeType.ALL + existing brand = detached entity passed to persist
  • Scenario 2 - CascadeType.MERGE + new brand = save the transient instance before flushin

Scenario 1 happens because JPA is trying to persist BRAND after persist PRODUCT (CascadeType.ALL). Once BRAND already exists you got an error.

Scenario 2 happend because JPA is not trying to persist BRAND (CascadeType.MERGE) and BRAND was not persisted before.

It's hard to figure out a solution because there are so many abstraction layers. Spring data abstracts JPA that abstracts Hibernate that abstracts JDBC and so on.

A possible solution would be use EntityManager.merge instead of EntityManager.persist so that CascadeType.MERGE could work. I belive you can do that re-implementing Spring Data save method. There is some reference about that here : Spring Data: Override save method

Another solution would be the short answer.

Example:

@Override
public Product save(Product product, String brandName) {

    Brand brand = brandService.findByBrand(brandName);
    if (brand == null) {
        brand = brandService.save(brandName);
    }
    return productRepository.save(product);

}
like image 86
Leonardo Cruz Avatar answered Sep 17 '22 16:09

Leonardo Cruz