Spring JpaRepositroy.save()似乎不会对重复保存抛出异常



我目前正在Spring boot 1.4.2上玩,我已经在Spring-boot-starter-web和Spring-boot-starter-jpa中拉了。

我的主要问题是,当我保存一个新实体时,它工作得很好(很酷)。

但是,如果我保存一个具有相同id的新产品实体(例如重复条目),它不会抛出异常。我期待的是ConstrintViolationException或类似的东西。

给定如下设置:

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

JpaConfig.java和Application.java在同一个包中。

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

和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;
}

getter, setter和equalsHashcode..是lombok注释。

杂项:

弹簧启动:1.4.2

Hibernate ORM: 5.2.2.FINAL

无论我是否用@Transactional注释控制器,这个问题都会发生

底层数据库清楚地显示异常

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

我知道把数据访问的东西分解到它自己的服务层,而不是把它倾倒在控制器中,这是更好的(更常见的)。

控制器的语义不是ReST

我尝试过的事情:

Spring CrudRepository异常

我已经尝试实现这个问题的答案,不幸的是,我的代码从来没有命中DataAccesException异常

如果保存功能不成功,Spring JPA会抛出错误吗?

对上面的问题再次做出类似的回答。

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

我尝试将bean添加到我的JPAconfig.java类,即:

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

但似乎什么也没发生。

抱歉,提前发了这么长时间

我的解决方案要干净得多。Spring Data已经为我们提供了一种很好的方式来定义如何将一个实体视为新实体。这可以很容易地通过在我们的实体上实现Persistable来完成,如参考文献中所述。

在我的情况下,就像OP的情况一样,id来自外部源,不能自动生成。因此,如果ID为null, Spring Data将实体视为新实体的默认逻辑将不起作用。

@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;
    }
    @PrePersist
    @PostLoad
    void markUpdated() {
        this.update = true;
    }
}

在这里,我为实体提供了一种机制,通过另一个称为update的瞬时布尔属性来表示它是否认为自己是新的。由于update的默认值将是false,因此该类型的所有实体都被认为是新的,并且当您尝试使用相同的ID调用repository.save(entity)时将导致抛出DataIntegrityViolationException

如果您确实希望执行合并,您可以在尝试保存之前始终将update属性设置为true。当然,如果您的用例从不要求您更新实体,您可以始终从isNew方法返回true,并去掉update字段。

与保存之前检查数据库中是否存在具有相同ID的实体相比,这种方法的优点有很多:

  1. 避免到数据库的额外往返
  2. 我们不能保证当一个线程确定这个实体不存在并准备持久化时,另一个线程不会尝试做同样的事情并导致数据不一致。
  3. 更好的性能,因为必须避免昂贵的锁定机制。
  4. 原子简单
编辑:不要忘记使用JPA回调实现一个方法,在持久化之前和从数据库加载之后设置update布尔字段的正确状态。如果忘记这样做,在JPA存储库上调用deleteAll将没有效果,正如我痛苦地发现的那样。这是因为deleteAll的Spring Data实现现在在执行删除之前检查实体是否是新的。如果您的isNew方法返回true,则永远不会考虑删除该实体。

我想你知道CrudRepository.save()是用于插入和更新的。如果一个Id不存在,那么它将被认为是插入,如果Id存在,它将被认为是更新。如果你发送的Id为空,你可能会得到一个异常。

因为在id变量上除了@Id没有任何其他注释,唯一Id的生成必须由你的代码处理,否则你需要使用@GeneratedValue注释。

以Shazins的回答为基础,进行澄清。 crudrepositroyy .save() jparerepositroyy . saveandflush ()都委托给以下方法

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

因此,如果用户尝试创建一个新实体,而这个实体恰好与现有实体具有相同的id, Spring data将只更新该实体。

为了实现我最初想要的,我唯一能找到的就是只回到JPA,那就是

@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;
}

如果我们尝试持久化和new实体的id已经存在于数据库中,它将会抛出我们最初想要的约束违反异常。

注意这里有3个场景:

1。手动设置ID

如果没有选择(如OP),即如果你手动设置自己的id, Spring Data JPA假设你想检查是否有重复(因此SELECT),所以它会做一个"(i)SELECT + (ii)INSERT"如果没有现有的记录一个"(i)SELECT + (ii)UPDATE"如果已经有一个现有的记录。

简而言之,2个sql !

2。使用ID生成器

清洁,更好的,例如:

 @Id
 @GeneratedValue(generator = "my-uuid")
 @GenericGenerator(name = "my-uuid", strategy = "uuid2")
 private UUID id;

结果:有ALWAYS only 1 INSERT语句

3。实现Persistable和isNew()

@adarshr已经很好地回答了这个问题,但也更痛苦,即实现Persistable(而不是Serializable),并实现isNew()方法。

Result: Also, 1 INSERT statement。

根据Spring Data文档,如果实体不存在或合并,Spring将保留实体,这意味着更新现有实体:

保存实体可以通过CrudRepository.save(…)-方法执行。它将使用底层JPA EntityManager持久化或合并给定的实体。如果实体尚未持久化,Spring Data JPA将通过调用entityManager.persist(…)-方法来保存实体,否则将调用entityManager.merge(…)-方法。

最新更新