Spring JPA saveはメインキーの繰り返しスロー異常を実現します。

10493 ワード


Introduction
まず、やるべき機能について話してください。
データベースのduplicate primary keyで簡単なロック機能を実現したいです。ロックが成功するかどうかはinsertが成功するかどうかによって決まります。ここでは明確にinsert 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のセーブを言います。
JPAのsaveはデフォルトでは新しいデータかどうかを判断します。新しいものならinsert/persist、そうでなければudate/merge、JPAは「新しい」という定義です。
実際にセーブする時は二つの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はudateになりました。
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をしてからセーブしたくないなら、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ではなく、矢印の方向は実現/継承/依存……いずれにしても出発点が終点となります。サボるのを許してください
 
一番下の階は実現です。上は全部インターフェースです。key-value、monogoについてはここの範囲ではないです。
org.spring frame ewark.data.repository.Repository/CrudRepository/Paging AndSortingRepotory
これは全部インターフェースです。Springは自動的に注入できます。ビーンを生成するためのデフォルトの実装があります。デフォルトで何が実現されていても、解決策とは関係がないようです。私達がカスタマイズしていない限り、(0.0)解決策です。
ここでは自分でカスタマイズしたRepository方法を調べて、キーワードを提供します。@EnbaleJparepositors、@EnbaleDiscoveryCient、@NoRepositoryBern
次の図は主にSimpleJparepositoryについて検討します。
 
  • org.springframe ewark.data.jpa.repository.SimpleJparepository
    注意してください。ここでpackageはJPAに属しています。Mongo、Key-valueなどに対して他の対応があります。彼のsave appiを見てください。
    JpaEntityInformation entityInformation;
    @Transactional
    public  S save(S entity) {
    
        if (entityInformation.isNew(entity)) {
            em.persist(entity);
            return entity;
        } else {
            return em.merge(entity);
        }
    }
    
  • org.springframe ewark.data.jpa.repository.support.Jpan Entit Information
    これもインターフェースです。isNewには触れていません。
  • org.springframe ewark.data.repository.co re.Enttity Information
    注意してください。ここでまたSpring.data packageに戻りました。isNewはJPAではなくrepositoryです。
  • org.springframe ewark.data.repository.co.support.Abstract Entit Information
    最初はここを見ましたが、SimpleJparepositoryが呼び出したentityInformationがこれで実現されたという不思議な思いをしました。彼は基本タイプかどうかだけを判断しました。nullかどうか…。そして優先的にソリューション2を使って問題を解決しました。
    public boolean isNew(T entity) {
    
    ID id = getId(entity);
    Class 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.springframe ewark.data.jpa.repository.support.Jpan Entit Information Support
    これがJPAの舞台です。
    public abstract class JpaEntityInformationSupport extends AbstractEntityInformation
        implements JpaEntityInformation 
    
  • org.springframe ewark.data.jpa.repository.support.JpaMetamodel Entit Information extens Jpanity Information Support
  • org.springframe ewark.data.jpa.repository.support.JpaPersistable Entit,ID>extens JpaMetamodel EntitInformation
  • 現在Abstract EnttityInformationのisNewはもう書き換えられました。XXXX Entity Informationシリーズのインターフェースではなく、entityのisNewインターフェースを使います。
    注意類声明:この実現を触発したいなら、EntityはPersistable interfaceを実現しなければならない。以下、Persistableを見てください。
    Persistable
  • org.springframe ewark.data.domann.Persistableインターフェース
    public class JpaPersistableEntityInformation, ID>
        extends JpaMetamodelEntityInformation {
    
    public JpaPersistableEntityInformation(Class 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();
    }
    }
    
  • org.springfraamew.data.support.IsNewStrategyインターフェース
  • org.springfraamewark.data.support.IsNewStrategyFactory Support
    ID getId();
    boolean isNew();
    
  • Persistable IsNewStrategy
    IsNewStrategyを実現しました。中にはPersistableも含まれています。これを重点的に実現しました。もしentityがPersistableインターフェースを実現したら、entity自身のisNewを呼び出します。
    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()));
    }
    
  • 結び目
    この説明は、entityがPersistableインターフェースを実現すれば、entity対応のEnttityInformationで実現できるということです。JpaPersistable Enttity Informationと、1波操作によりEntity InformationのisNewをPersistable IsNewStrategyのNeiseに実際に呼び出すことができます。
    次は深く掘り続けます。テーブルのnameはどこにありますか?
    深く掘り続ける
    実はもう一つのPersistableEntityInformationがあります。org.springframework.data.repository.core.suppor.PersistableEntityInformation
     
    JPAが特別にPersistable EntityInformationの実現を世話したことを見た以上、Jparsistable Entity Informationが何をしたか見てください。
    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();
    }
    }
    
    public class JpaPersistableEntityInformation, ID>
            extends JpaMetamodelEntityInformation {
    
        public JpaPersistableEntityInformation(Class domainClass, Metamodel metamodel) {
            super(domainClass, metamodel);
        }
    }
    
    特殊な構造関数を見てください。domann Class、metamodel
    彼の相続関係を見なくなりました。何かを多く見ました。構造関数:domanClass、metamodel
    モデルを買います。
     
    public class PersistableEntityInformation, ID> extends AbstractEntityInformation {
        @SuppressWarnings("unchecked")
        public PersistableEntityInformation(Class 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) idClass;
        }
    }
    
    はい、これでentityの名前の由来が分かりました。あるいは「table name」と言います。
    Coologicブログ、転載は明記してください:Coologic>Spring JPA saveはキーを実現して繰り返し投げます。