Spring JPA save 實現主鍵重復拋異常


Introduction

先說說要做的功能

希望用數據庫的 duplicate primary key 來實現一個簡易的鎖功能,加鎖成功與否取決於是否成功 insert,此時必須要明確的執行 insert sql,而不是 update sql

本文以一個簡單地訂單鎖作為例子,數據庫字段信息如下:

create table order_lock (
    order_number varchar(20) not null primary key,
    user_name varchar(100) not null,
    created_time datetime default CURRENT_TIMESTAMP null
);

再說說 JPA 的 save

JPA 的 save 默認會判斷是否為新數據,若為新的則 insert / persist,否則 update / merge,而 JPA 對於是否為“新”的定義是。。。。

實際上在 save 時會生成兩條 sql 語句分別執行:

Hibernate: 
    select
        orderl0_.order_number as order_nu1_12_0_,
        orderl0_.created_time as created_2_12_0_,
        orderl0_.user_name as user_nam4_12_0_ 
    from
        order_lock orderl0_ 
    where
        orderl0_.order_number=?
[V][2019-11-04 15:03:06,602][INFO ][http-nio-9091-exec-9][OrderLockService][lockOrder][][][][] - order lock start lock: order number:aaaaaaaaaaaaaaaa, user name: zzz2
Hibernate: 
    insert 
    into
        order_lock
        (created_time user_name, order_number) 
    values
        (?, ?, ?)

當我們第一次調用的時候,createdTime 自動生成,當第二次調用的時候,因為包含了這個字段,select 有了結果,第二個 sql 成為了 update:

Hibernate: 
    select
        orderl0_.order_number as order_nu1_12_0_,
        orderl0_.created_time as created_2_12_0_,
        orderl0_.user_name as user_nam4_12_0_ 
    from
        order_lock orderl0_ 
    where
        orderl0_.order_number=?
Hibernate: 
    update
            order_lock 
        set
            created_time=? 
        where
            order_number=?

good,我們第二次創建沒有報錯,但是 createdTime 成了 null

作為數據庫的主鍵,唯一性已經保證了不會出現一個訂單有多個鎖的情況,若不希望自己主動地 find 后再 save,那就必須讓 JPA 固定的生成 insert sql,利用 db 報錯來發現重復鎖的問題

除此以外第二個問題,每次 save 都會先 select,對於 db 通過主鍵就能判斷成功與否的需求,卻執行了兩個 sql 性能上浪費 50%

結論

時間很寶貴,先給出最后的結論,再記錄翻源碼的過程

方案 1 – 優雅的解決問題

自定義的 Entity class 實現 Persistable interface 的 isNew method,固定返回 true,則在 JPA save 時一定會執行 insert sql,對於簡單地訂單鎖的 Entity 如下:

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "order_lock")
public class OrdertLock implements Persistable {
    @Id
    private String orderNumber;

    @Column(updatable = false, nullable = false)
    private String userName;

    @CreationTimestamp
    private Date createdTime;

    @Override
    public Object getId() {
        return orderNumber;
    }

    @Override
    public boolean isNew() {
        return true;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        OrderEditLock orderEditLock = (OrderEditLock) o;
        return Objects.equals(orderNumber, orderEditLock.orderNumber);
    }

    @Override
    public int hashCode() {
        return Objects.hash(orderNumber);
    }
}

看看修改后的 JPA 行為

JPA 直接生成了 insert 語句,select 也沒有生成,一個 sql 解決問題。

Hibernate: 
    insert 
    into
        order_lock
        (created_time, user_name, order_number) 
    values
        (?, ?, ?)

當主鍵重復的時候拋出org.springframework.dao.DataIntegrityViolationException;com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry 'XXXXXXXXX' for key 'PRIMARY'

方案 2 – 萬能的 @Query 解決一切

Entity 不做改變,直接 Repository 自定義 Query

@Modifying
@Query(nativeQuery = true,
       value = "INSERT INTO " +
       "order_lock(order_number, user_name) " +
       "VALUES (:orderNumber, :userName);")
void lockOrder(@Param("orderNumber") String orderNumber,
               @Param("userName") String userName);

此時也是生成一個 sql:

Hibernate: 
    INSERT 
    INTO
        service_order_edit_lock
        (order_number, user_name) 
    VALUES
        (?, ?);

若主鍵重復拋出異常一樣是:org.springframework.dao.DataIntegrityViolationException;com.mysql.jdbc.exceptions.jdbc4.MySQLIntegrityConstraintViolationException: Duplicate entry 'XXXXXXXXX' for key 'PRIMARY'

注意:生成的 sql 對於未填寫的字段處理方式不同,一個是 field 全量生成 sql, 一個是可以自定義傳遞哪些 field

沉入源碼

先上個關系圖,Markdown寫的,不是 UML,箭頭指向方向為實現/繼承/依賴……反正就是起點用到了終點。。。。原諒我偷懶

 
 
 
 
 
 
 
 
 
Repository
CrudRepository
PagingAndSortingRepository
KeyValueRepository
MongoRepository
JpaRepository
SimpleKeyValueRepository
SimpleMongoRepository
SimpleJpaRepository
QuerydslJpaRepository

最下面一層是實現,上面都是 interface,對於 key-value、mongo 不在本文范圍內

org.springframework.data.repository.Repository/CrudRepository/PagingAndSortingRepository

這都是接口,Spring 可以自動注入,肯定有默認的一個實現用於生成 Bean。無論是默認的是什么實現,好像都與解決方案無關,除非我們自定義一個,(0.0),這應該算是解決方案 3 吧

此處可以自行查閱自定義 Repository 方法,提供關鍵詞:@EnableJpaRepositories、@EnableDiscoveryClient、@NoRepositoryBean

下面一個圖主要針對 SimpleJpaRepository 研究

 
 
 
 
 
 
 
 
 
 
importance
SimpleJpaRepository
EntityManager
persist(entity)
merge(entity)
JpaEntityInformation
EntityInformation
AbstractEntityInformation
JpaEntityInformationSupport
JpaMetamodelEntityInformation
JpaPersistableEntityInformation
Persistable
  • org.springframework.data.jpa.repository.SimpleJpaRepository

    注意,這里 package 屬於 JPA 了,對於 Mongo,Key-value 等也有還有其他對應的實現,看一下他的 save api

    JpaEntityInformation<T, ?> entityInformation;
    @Transactional
    public <S extends T> S save(S entity) {
    
        if (entityInformation.isNew(entity)) {
            em.persist(entity);
            return entity;
        } else {
            return em.merge(entity);
        }
    }
    
  • org.springframework.data.jpa.repository.support.JpaEntityInformation

    這也是一個接口,沒有涉及到 isNew

  • org.springframework.data.repository.core.EntityInformation

    注意,這里又回到了 Spring.data package 有 isNew 了,看 package,isNew 屬於 repository 而不是 JPA

  • org.springframework.data.repository.core.support.AbstractEntityInformation

    開始就是看到了這里,很神奇的認為 SimpleJpaRepository 調用的 entityInformation 就是這個實現了,他只判斷了是否為基本類型,是否為 null……於是就優先用方案 2 解決了問題

    public boolean isNew(T entity) {
    
    ID id = getId(entity);
    Class<ID> idType = getIdType();
    
    if (!idType.isPrimitive()) {
        return id == null;
    }
    
    if (id instanceof Number) {
        return ((Number) id).longValue() == 0L;
    }
    
    throw new IllegalArgumentException(String.format("Unsupported primitive id type %s!", idType));
    }
    
  • org.springframework.data.jpa.repository.support.JpaEntityInformationSupport

    這才是 JPA 的舞台

    public abstract class JpaEntityInformationSupport<T, ID> extends AbstractEntityInformation<T, ID>
        implements JpaEntityInformation<T, ID> 
    
  • org.springframework.data.jpa.repository.support.JpaMetamodelEntityInformation extends JpaEntityInformationSupport<T, ID>

     

  • org.springframework.data.jpa.repository.support.JpaPersistableEntityInformation, ID> extends JpaMetamodelEntityInformation<T, ID>
    public class JpaPersistableEntityInformation<T extends Persistable<ID>, ID>
        extends JpaMetamodelEntityInformation<T, ID> {
    
    public JpaPersistableEntityInformation(Class<T> domainClass, Metamodel metamodel) {
        super(domainClass, metamodel);
    }
    
    @Override
    public boolean isNew(T entity) {
        return entity.isNew();
    }
    
    @Nullable
    @Override
    public ID getId(T entity) {
        return entity.getId();
    }
    }
    

現在 AbstractEntityInformation 的 isNew 已經被重寫了,不再是使用 XXXXXXXEntityInformation 系列的接口,而是使用 entity 的 isNew 接口

注意類聲明:想要觸發這個實現,Entity 必須實現 Persistable interface,下面看看 Persistable

Persistable

  • org.springframework.data.domain.Persistable 接口
    ID getId();
    boolean isNew();
    
  • org.springframework.data.support.IsNewStrategy 接口

     

  • org.springframework.data.support.IsNewStrategyFactorySupport
    public final IsNewStrategy getIsNewStrategy(Class<?> type) {
    
        Assert.notNull(type, "Type must not be null!");
    
        if (Persistable.class.isAssignableFrom(type)) {
            return PersistableIsNewStrategy.INSTANCE;
        }
    
        IsNewStrategy strategy = doGetIsNewStrategy(type);
    
        if (strategy != null) {
            return strategy;
        }
    
        throw new IllegalArgumentException(
                String.format("Unsupported entity %s! Could not determine IsNewStrategy.", type.getName()));
    }
    
  • PersistableIsNewStrategy

    實現了 IsNewStrategy,里面也涉及到了 Persistable,
    重點在這個實現了,如果 entity 實現了 Persistable 接口,則調用 entity 自己的 isNew

    public enum PersistableIsNewStrategy implements IsNewStrategy {
    @Override
    public boolean isNew(Object entity) {
    
    Assert.notNull(entity, "Entity must not be null!");
    
    if (!(entity instanceof Persistable)) {
        throw new IllegalArgumentException(
                String.format("Given object of type %s does not implement %s!", entity.getClass(), Persistable.class));
    }
    
    return ((Persistable<?>) entity).isNew();
    }
    }
    

小結

這說明只要 entity 實現了 Persistable 接口,那么就可以在使 entity 對應的 EntityInformation 實現是:JpaPersistableEntityInformation,並通過一波操作將 EntityInformation 的 isNew 實際調用到 PersistableIsNewStrategy 的 isNew

下面繼續深挖一下,可以看到 table name 存在哪里

繼續深挖

其實還有一個 PersistableEntityInformation:org.springframework.data.repository.core.suppor.PersistableEntityInformation

 
 
 
 
 
 
 
 
AbstractEntityInformation
PersistentEntityInformation
JpaEntityInformationSupport
PersistableEntityInformation
ReflectionEntityInformation
MappingMongoEntityInformation
MappingRedisEntityInformati
JpaMetamodelEntityInformation
JpaPersistableEntityInformation

既然看到了 JPA 特殊照顧了 PersistableEntityInformation 的實現,那看看 JpaPersistableEntityInformation 還做了什么

public class JpaPersistableEntityInformation<T extends Persistable<ID>, ID>
        extends JpaMetamodelEntityInformation<T, ID> {

    public JpaPersistableEntityInformation(Class<T> domainClass, Metamodel metamodel) {
        super(domainClass, metamodel);
    }
}
public class PersistableEntityInformation<T extends Persistable<ID>, ID> extends AbstractEntityInformation<T, ID> {
    @SuppressWarnings("unchecked")
    public PersistableEntityInformation(Class<T> domainClass) {

        super(domainClass);

        Class<?> idClass = ResolvableType.forClass(Persistable.class, domainClass).resolveGeneric(0);

        if (idClass == null) {
            throw new IllegalArgumentException(String.format("Could not resolve identifier type for %s!", domainClass));
        }

        this.idClass = (Class<ID>) idClass;
    }
}

看一下特殊的構造函數:domainClass, metamodel

不看他的繼承關系了,只看多了什么 method: 構造函數:domainClass, metamodel

買它模型(metamodel)

 
 
 
 
EntityMetadata
JpaEntityMetadata
DefaultJpaEntityMetadata
JpaEntityInformation
JpaEntityInformationSupport
public class DefaultJpaEntityMetadata<T> implements JpaEntityMetadata<T> {

    private final Class<T> domainType;

    public DefaultJpaEntityMetadata(Class<T> domainType) {

        Assert.notNull(domainType, "Domain type must not be null!");
        this.domainType = domainType;
    }

    @Override
    public Class<T> getJavaType() {
        return domainType;
    }

    @Override
    public String getEntityName() {

        Entity entity = AnnotatedElementUtils.findMergedAnnotation(domainType, Entity.class);
        return null != entity && StringUtils.hasText(entity.name()) ? entity.name() : domainType.getSimpleName();
    }
}

OK,到此我們知道了 entity 的名字來源了,或者說 “table name”

 

| 版權聲明: 本站文章采用 CC 4.0 BY-SA 協議 進行許可,轉載請附上原文出處鏈接和本聲明。
| 本文鏈接: Cologic Blog - Spring JPA save 實現主鍵重復拋異常 - https://www.coologic.cn/2019/11/1672/


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM