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?
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.
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.
The solution is simple, just use the CascadeType. MERGE instead of CascadeType. PERSIST or CascadeType. ALL .
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.
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 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);
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With