Spring Boot JPA 使用自定义复合主键保存实体的异常与性能问题咨询
问题1:为什么JPA自带的save方法会触发唯一约束异常?
- 首先是Spring Data JPA的save方法内置的实体状态判断逻辑问题:默认的
SimpleJpaRepository中,判断实体是否为新实体的规则是:如果主键没有配置@GeneratedValue自动生成策略,就以「主键是否为null」作为判断依据。你的实体使用@EmbeddedId复合主键,创建实例时已经主动给identity字段赋值,主键不为null,所以JPA会认为这是一个已存在的实体,先走merge流程,merge会先发起一次select查询确认实体是否存在。 - 其次是并发场景下的竞态条件:即使JPA内部会做查询,在默认的事务隔离级别(比如读提交)下,多个并发请求同时执行查询时都查不到对应记录,之后都会执行insert操作,触发数据库层面的唯一约束冲突,这个问题是JVM层面的查询逻辑无法避免的。
问题2:无需额外select的保存方案
方案1:使用数据库原生UPSERT语法(最优,支持并发场景)
直接利用数据库的原子UPSERT能力(插入更新二合一),全程只需要一次数据库交互,没有额外select,且数据库层面保证并发操作不会触发唯一约束。
以MySQL为例,自定义Repository方法:
@Repository public interface CustomerRepository extends JpaRepository<Customer, CustomerCompositeKey> { @Modifying @Transactional @Query(value = "INSERT INTO customer (key_one, key_two, otherdata) " + "VALUES (:#{#customer.identity.keyOne}, :#{#customer.identity.keyTwo}, :#{#customer.otherdata}) " + "ON DUPLICATE KEY UPDATE otherdata = VALUES(otherdata)", nativeQuery = true) void upsert(@Param("customer") Customer customer); }
如果是其他数据库,可以替换为对应方言的UPSERT语法:
- PostgreSQL:
INSERT ... ON CONFLICT (key_one, key_two) DO UPDATE SET ... - Oracle:
MERGE INTO ... USING ... ON ... WHEN MATCHED THEN UPDATE ... WHEN NOT MATCHED THEN INSERT ...
方案2:实现Persistable接口自定义新实体判断(适合可以明确区分新增/更新场景)
如果你的业务逻辑可以明确判断当前操作是新增还是更新,可以让实体类实现Persistable<CustomerCompositeKey>接口,重写isNew方法,避免JPA为了判断实体状态发起额外的select查询:
@Entity @Table(name = "customer") @Data @NoArgsConstructor @AllArgsConstructor public class Customer implements Persistable<CustomerCompositeKey> { @EmbeddedId private CustomerCompositeKey identity; private String otherdata; @Transient private boolean isNew; @Override public CustomerCompositeKey getId() { return identity; } @Override public boolean isNew() { return isNew; } // 新增时手动设置isNew为true,更新时设置为false public void setNew(boolean aNew) { isNew = aNew; } }
设置正确的isNew标记后调用save方法,JPA会直接走persist(新增)或者merge(更新)流程,不会额外发起查询判断实体状态。注意这个方案在并发插入相同主键的场景下还是会触发唯一约束异常,只适合非并发或者能保证主键不重复的场景。
内容的提问来源于stack exchange,提问作者Mish
相关产品推荐
相关产品推荐

