【问题标题】:Spring JpaRepositroy.save() does not appear to throw exception on duplicate savesSpring JpaRepositroy.save() 似乎不会在重复保存时抛出异常
【发布时间】:2017-03-29 02:22:41
【问题描述】:

我目前正在使用 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

休眠 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 exceptions

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

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

再次对上述问题做出类似回应。

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

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

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

但似乎什么也没发生。

抱歉发帖太长,请提前联系

【问题讨论】:

    标签: java spring-data spring-data-jpa


    【解决方案1】:

    我的解决方案更干净。 Spring Data 已经为我们提供了一种很好的方式来定义实体如何被认为是新的。这可以通过在我们的实体上实现Persistable 来轻松完成,例如documented in the reference

    就我而言,与 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. 1 带来更好的性能,并且必须避免使用昂贵的锁定机制。
    4. 原子
    5. 简单

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

    【讨论】:

    • 我在集成阶段使用 PostgreSQL 进行了尝试,一切都很好,但是在单元测试时内存中的 H2 失败了。你知道为什么吗?
    • 绝妙的解决方案!
    • 这个解决方案一定很好用——只是我们现在必须用实现和一堆额外的字段和方法来污染我们的实体..
    【解决方案2】:

    我想你知道CrudRepository.save() 用于插入和更新。如果 Id 不存在,那么如果 Id 存在,它将被视为插入,它将被视为更新。如果您将 Id 发送为 null,您可能会收到异常。

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

    【讨论】:

    • 玩了一会儿之后,我确实看到 save 既是 save 又是 update。并且 id 将由客户端提供,它们很可能(如果不确定)是某种形式的 UUID。所以没有办法得到数据库显示的异常?
    • 如果 ID 为空,可以得到异常。如果不是,则认为是UPDATE,不会抛出异常。
    【解决方案3】:

    以 Shazins 的回答为基础并进行澄清。 CrudRepositroy.save()JpaRespository.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 数据只会更新该实体。

    为了实现我最初想要的,我唯一能找到的就是完全退回到 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 已经存在于数据库中,它将抛出我们最初想要的约束违规异常。

    【讨论】:

    • 是的。如果您使用具有指定值的键,Spring Data 的设计方式就没有任何实际意义——该设计真正依赖于使用自动键生成过程来确保没有重复。还有其他选择:在单个事务中,在调用 save() 之前检查 repository.exists(id) 会起作用,但老实说,最简单的方法可能是添加一个额外的自动生成的密钥,然后在您的域类型密钥上添加一个唯一约束。
    【解决方案4】:

    注意这里有3个场景:

    首先,如果没有选择(如 OP),即如果您“手动”设置自己的 id,Spring Data JPA 假设您要检查是否有重复项(因此 SELECT),所以如果没有现有记录 ,它将执行 "(i)SELECT + (ii)INSERT" "(i)SELECT + (ii)UPDATE" 如果已有记录。简而言之,2 个 SQL!

    其次,更简洁更好的是使用ID生成器,例如:

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

    在这种情况下,总是只有 1 INSERT 语句。

    第三个,已经被@adarshr 精彩回答,但也比较痛苦,就是实现Persistable(而不是Serializable),实现isNew() 方法。另外,1 INSERT 语句。

    干杯

    【讨论】:

      【解决方案5】:

      根据Spring Data documentationSpring 会持久化一个实体,如果不存在或合并,这意味着更新,现有的:

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

      【讨论】:

        猜你喜欢
        • 2012-05-23
        • 1970-01-01
        • 1970-01-01
        • 2023-03-13
        • 2014-08-24
        • 1970-01-01
        • 2019-01-24
        • 2015-12-12
        • 1970-01-01
        相关资源
        最近更新 更多