Spring JpaRepositroy.save() does not appear to thr

2019-02-04 18:21发布

I'm currently playing around on Spring boot 1.4.2 in which I've pulled in Spring-boot-starter-web and Spring-boot-starter-jpa.

My main issue is that when I save a new entity it works fine (all cool).

However if I save a new product entity with the same id (eg a duplicate entry), it does not throw an exception. I was expecting ConstrintViolationException or something similar.

Given the following set up:

Application.java

@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

ProductRepository.java

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

JpaConfig.java

@Configuration
@EnableJpaRepositories(basePackages = "com.verric.jpa.repository" )
@EntityScan(basePackageClasses ="com.verric.jpa")
@EnableTransactionManagement
public class JpaConfig {

    @Bean
    JpaTransactionManager transactionManager() {
        return new JpaTransactionManager();
    }
}

Note JpaConfig.java and Application.java are in the same package.

ProductController.java

@RestController
@RequestMapping(path = "/product")
public class ProductController {

    @Autowired
    ProductRepository productRepository;

    @PostMapping("createProduct")
    public void handle(@RequestBody @Valid CreateProductRequest request) {
        Product product = new Product(request.getId(), request.getName(), request.getPrice(), request.isTaxable());
        try {
            productRepository.save(product);
        } catch (DataAccessException ex) {
            System.out.println(ex.getCause().getMessage());
        }
    }
}

and finally Product.java

@Entity(name = "product")
@Getter
@Setter
@AllArgsConstructor
@EqualsAndHashCode(of = "id")
public class Product {

    protected Product() { /* jpa constructor*/ }

    @Id
    private String id;

    @Column
    private String name;

    @Column
    private Long price;

    @Column
    private Boolean taxable;
}

The getter, setter and equalsHashcode.. are lombok annotations.

Miscellaneous:

Spring boot : 1.4.2

Hibernate ORM: 5.2.2.FINAL

This issue happens regardless if I annotate the controller with or without @Transactional

The underlying db shows the exception clearly

2016-11-15 18:03:49 AEDT [40794-1] verric@stuff ERROR:  duplicate key value violates unique constraint "product_pkey"
2016-11-15 18:03:49 AEDT [40794-2] verric@stuff DETAIL:  Key (id)=(test001) already exists

I know that is better (more common) to break the data access stuff into its own service layer instead of dumping it in the controller

The semantics of the controller aren't ReST

Things I've tried:

Spring CrudRepository exceptions

I've tried implementing the answer from this question, unfortunately my code never ever hits the DataAccesException exception

Does Spring JPA throw an error if save function is unsuccessful?

Again similar response to the question above.

http://www.baeldung.com/spring-dataIntegrityviolationexception

I tried adding the bean to my JPAconfig.java class that is:

   @Bean
   public PersistenceExceptionTranslationPostProcessor exceptionTranslation(){
      return new PersistenceExceptionTranslationPostProcessor();
   }

But nothing seemed to happen.

Sorry for long post, ty in advance

3条回答
放我归山
2楼-- · 2019-02-04 19:00

I think you are aware of CrudRepository.save() is used for both insert and update. If an Id is non existing then it will considered an insert if Id is existing it will be considered update. You may get an Exception if your send the Id as null.

Since you don't have any other annotations apart from @Id on your id variable, The Unique Id generation must be handled by your code Or else you need to make use of @GeneratedValue annotation.

查看更多
该账号已被封号
3楼-- · 2019-02-04 19:02

To build upon Shazins answer and to clarify. the CrudRepositroy.save() or JpaRespository.saveAndFlush() both delegate to the following method

SimpleJpaRepository.java

@Transactional
public <S extends T> S save(S entity) {

    if (entityInformation.isNew(entity)) {
        em.persist(entity);
        return entity;
    } else {
        return em.merge(entity);
    }
}

Hence if a user tries to create a new entity that so happens to have the same id as an existing entity Spring data will just update that entity.

To achieve what I originally wanted the only thing I could find was to drop back down to JPA solely, that is

@Transactional
@PostMapping("/createProduct")
public Product createProduct(@RequestBody @Valid Product product) {
    try {
        entityManager.persist(product);
        entityManager.flush();
    }catch (RuntimeException ex) {
        System.err.println(ex.getCause().getMessage());
    }

    return product;
}

Here if we try to persist and new entity with an id already existing in the database it will throw will throw the constraint violation exception as we originally wanted.

查看更多
男人必须洒脱
4楼-- · 2019-02-04 19:06

My solution is a lot cleaner. Spring Data already provides a nice way for us to define how an entity is considered to be new. This can easily be done by implementing Persistable on our entities, as documented in the reference.

In my case, as is the OP's, the IDs come from an external source and cannot be auto generated. So the default logic used by Spring Data to consider an entity as new if the ID is null wouldn't have worked.

@Entity
public class MyEntity implements Persistable<UUID> {

    @Id
    private UUID id;

    @Transient
    private boolean update;

    @Override
    public UUID getId() {
        return this.id;
    }

    public void setId(UUID id) {
        this.id = id;
    }

    public boolean isUpdate() {
        return this.update;
    }

    public void setUpdate(boolean update) {
        this.update = update;
    }

    @Override
    public boolean isNew() {
        return !this.update;
    }
}

Here, I have provided a mechanism for the entity to express whether it considers itself new or not by means of another transient boolean property called update. As the default value of update will be false, all entities of this type are considered new and will result in a DataIntegrityViolationException being thrown when you attempt to call repository.save(entity) with the same ID.

If you do wish to perform a merge, you can always set the update property to true before attempting a save. Of course, if your use case never requires you to update entities, you can always return true from the isNew method and get rid of the update field.

The advantages of this approach over checking whether an entity with the same ID already exists in the database before saving are many:

  1. Avoids an extra round trip to the database
  2. We cannot guarantee that by the time one thread has determined that this entity doesn't exist and is about to persist, another thread doesn't attempt to do the same and result in inconsistent data.
  3. Better performance as a result of 1 and having to avoid expensive locking mechanisms.
  4. Atomic
  5. Simple
查看更多
登录 后发表回答