Detached entity passed to persist in Spring-Data

Ilya Orlov :

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?

Leonardo Cruz :

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);

}

Guess you like

Origin http://10.200.1.11:23101/article/api/json?id=452482&siteId=1