首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >Spring JpaRepositroy.save()似乎不会在重复保存时引发异常

Spring JpaRepositroy.save()似乎不会在重复保存时引发异常
EN

Stack Overflow用户
提问于 2016-11-15 17:08:07
回答 4查看 60K关注 0票数 28

我目前正在使用Spring boot 1.4.2,其中我引入了Spring-boot-starter-web和Spring-boot-starter-jpa。

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

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

给定以下设置:

Application.java

代码语言:javascript
运行
复制
@SpringBootApplication
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }

ProductRepository.java

代码语言:javascript
运行
复制
@Repository
public interface ProductRepository extends JpaRepository<Product, String> {}

JpaConfig.java

代码语言:javascript
运行
复制
@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

代码语言:javascript
运行
复制
@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

代码语言:javascript
运行
复制
@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注解。

Miscellaneous:

Spring boot : 1.4.2

Hibernate ORM: 5.2.2

无论我使用或不使用@Transactional注释控制器,都会发生此问题

基础db清楚地显示了异常。

代码语言:javascript
运行
复制
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 exceptions

我已经尝试实现了这个问题的答案,不幸的是,我的代码从未遇到过DataAccesException异常

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

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

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

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

代码语言:javascript
运行
复制
   @Bean
   public PersistenceExceptionTranslationPostProcessor exceptionTranslation(){
      return new PersistenceExceptionTranslationPostProcessor();
   }

但似乎什么也没发生。

很抱歉发了这么长的帖子,太早了。

EN

回答 4

Stack Overflow用户

回答已采纳

发布于 2016-11-15 19:38:34

我想你应该知道CrudRepository.save()同时用于插入和更新。如果Id不存在,那么它将被认为是插入,如果Id存在,它将被认为是更新。如果您将Id作为null发送,则可能会出现异常。

由于您的id变量上除了@Id之外没有任何其他注释,因此惟一Id的生成必须由您的代码处理,否则您需要使用@GeneratedValue注释。

票数 9
EN

Stack Overflow用户

发布于 2017-08-17 18:37:46

我的解决方案要干净得多。Spring Data已经为我们提供了一种很好的方式来定义一个实体是如何被认为是新的。这可以很容易地通过在我们的实体上实现Persistable来实现,比如documented in the reference

在我的例子中,就像OP一样,In来自外部来源,不能自动生成。因此,Spring Data使用的默认逻辑在ID为null时将实体视为新实体将不起作用。

代码语言:javascript
运行
复制
@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调用DataIntegrityViolationException时抛出repository.save(entity)

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

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

  1. 避免了到数据库的额外往返
  2. 我们不能保证,当一个线程确定此实体不存在并且将要持久化时,另一个线程不会尝试执行相同的操作并导致数据不一致。
  3. 由于1和必须避免代价高昂的锁定mechanisms.
  4. Atomic
  5. Simple

而获得更好的性能

编辑:不要忘记使用JPA回调实现一个方法,该回调在持久化之前和从数据库加载之后设置update布尔值字段的正确状态。如果您忘记这样做,那么在JPA存储库上调用deleteAll将不会有任何效果,正如我痛苦地发现的那样。这是因为deleteAll的Spring Data实现现在在执行删除之前检查实体是否是新的。如果您的isNew方法返回true,则永远不会考虑删除该实体。

票数 26
EN

Stack Overflow用户

发布于 2016-11-18 07:29:30

以Shazins的答案为基础并加以澄清。CrudRepositroy.save()或JpaRespository.saveAndFlush()都委托给以下方法

SimpleJpaRepository.java

代码语言:javascript
运行
复制
@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,也就是

代码语言:javascript
运行
复制
@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;
}

这里,如果我们尝试持久化和数据库中已经存在的id的新实体,它将抛出违反约束的异常,正如我们最初希望的那样。

票数 1
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/40605834

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档