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について検討します。
注意してください。ここで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);
}
}
これもインターフェースです。isNewには触れていません。
注意してください。ここでまたSpring.data packageに戻りました。isNewはJPAではなくrepositoryです。
最初はここを見ましたが、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));
}
これがJPAの舞台です。
public abstract class JpaEntityInformationSupport extends AbstractEntityInformation
implements JpaEntityInformation
注意類声明:この実現を触発したいなら、EntityはPersistable interfaceを実現しなければならない。以下、Persistableを見てください。
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();
}
}
ID getId();
boolean isNew();
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はキーを実現して繰り返し投げます。