Ilya Orlov Ilya Orlov - 1 month ago 31
Java Question

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?

Answer

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 cenarios:

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

Cenario 1 happens because JPA is trying to persist BRAND after persist PRODUCT (CascadeType.ALL). Once BRAND already exists you got an error. Cenario 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);

}