最近、グループ内のプロジェクトのバグを修正したところ、Spring DataJPAの不適切な使用が原因であることが判明しました。このバグの修復に成功した後、Spring Data JPAについてよく知らないので、バグを解決する過程で参考にした関連情報についてブログを書く予定です。ブログの内容は主に初心者向けで、内容はシンプルです。
まず、バグ生成のプロセスをシミュレートします。次のコードのロジックは、通常作成するコードのロジックと少し矛盾している可能性がありますが、コードを通じてバグの原因を理解することが重要です。
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Transactional
public void updateUser(String name) {
User user = userRepository.findById(1).get();
System.out.println("user name: " + user.getName());
System.out.println("user age: " + user.getAge());
System.out.println("name will be updated to " + name);
userRepository.updateUserName(name, 1);
User user1 = userRepository.findById(1).get();
System.out.println("user1 age: " + user1.getAge());
System.out.println("age will be updated to " + 18);
user1.setAge(18);
userRepository.save(user1);
}
}
@Repository
public interface UserRepository extends JpaRepository<User, Integer> {
@Query(value = "update User set name = ?1 where id = ?2 ")
@Modifying
void updateUserName(String name, Integer id);
}
UserService
メソッドのupdateUser(String name)
当初の想定される機能は、最初にUserRepository.findById()
メソッドを呼び出してUserテーブルのID 1のレコードを検索し、次に呼び出しUserRepository.updateUserName(String name, Integer id)
て、渡されたパラメーター名に従ってUserテーブルのID1のレコードのname列の値を変更してからUserRepository.findById()
メソッドを呼び出します。名前が変更された後、UserテーブルでID 1のレコードを照会し、レコードの年齢列の値を18に変更します。しかし、メソッドが実行された後の結果は少し予想外です
。UserテーブルのID 1のレコードが18に正常に変更されたにもかかわらず、名前がまだジャックであることがわかります。なぜですか?この現象を説明する前に、Spring DataJPAに関するいくつかの概念を理解する必要があります。
レベル1キャッシュ
スプリングデータJPAキャッシュが使用される場合、またはクエリへの記録方法、データベースへの最初のクエリは、クエリの結果がキャッシュとしてメモリに保存され、その後、同じ記録バッファに直接バックを照会する時期返された結果。データベースを照会しなくなりました。上記のメソッドの実行のログでは、1つのステートメントのみが実行されたため、このステートメントはデータベースクエリを実行しなかったことがわかります。User1は実際にはuserです。このコード実行の結果、データベース内のUserテーブルのIDが1に変更されます。レコード内のname値、およびキャッシュ内のUserオブジェクトのname属性値は引き続き「ジャック」です。自定义Repository
find()
findxx()
updateUser()
select
User user1 = userRepository.findById(1).get();
UserRepository.updateUserName(name, 1)
流す()
flush()方法
キャッシュ内の変更されたすべてのエンティティのステータス情報をデータベースに同期します。saveメソッドを使用して、トランザクション内のデータベースから照会されたエンティティを更新する場合、updateステートメントは実行されませんが、スナップショット関数を使用して、キャッシュ内の変更されたエンティティ状態情報をに同期します。データベースで、トランザクションがコミットされる前に変更されたエンティティの状態情報をデータベースに同期する場合はsave()
、flush()
メソッドを呼び出した後に手動でメソッドを呼び出して変更されたエンティティをデータベースに同期するか、saveAndFlush()
メソッドを使用して変更されたエンティティを保存する必要があります(実際、saveAndFlush()
メソッドはsave()
メソッドを呼び出した後、メソッドを呼び出しflush()
てデータを同期します)。
スナップショット
Spring Data JPAには、第1レベルのキャッシュに加えて、スナップショット領域もあります。クエリ結果が第1レベルのキャッシュに配置されると、データのコピーが同時にスナップショット領域にコピーされます。SpringDataJPAは、スナップショット領域とキャッシュ内のデータを使用します。データベースから照会された後にデータが変更されたかどうかを判断するのに一貫性があるかどうか。
実行時には、上記の例では、User user = userRepository.findById(1).get();
このコード、バッファーゾーン、およびスナップショットを以下のように、ユーザインスタンスながら保存される
方法が行われた場合user1.setAge(18);
、バッファ状態情報とスナップショットインスタンスユーザ領域は次のように
場合トランザクションがコミットされると、データベースデータの同期を維持するために、Hibernateは第1レベルのキャッシュをクリーンアップし、第1レベルのキャッシュ内のオブジェクトがプライマリキーフィールドの値に従ってスナップショット内のオブジェクトと一致しているかどうかを判断します。2つのオブジェクトのプロパティが変更された場合は、実行します。 updateステートメントは、キャッシュされたコンテンツをデータベースに同期し、スナップショットを更新します。それらが一貫している場合、updateステートメントは実行されません。したがって、上記のコード実行のログでは、updateUser()
メソッドの実行が終了すると(updateUser()
メソッドは、@ Transactionalアノテーションを使用し、メソッドの実行が終了した後にトランザクションが送信されます)、キャッシュ内のユーザーの属性値に従って渡されることがわかります(名前は「jack」、 18歳)対応するデータベースレコードを変更し(更新ステートメントが出力されます)、UserRepository.updateUserName(name, 1)
コード実行の効果が上書きされます。
次UserService
のupdateUser()
方法でメソッドを変更した場合:
@Transactional
public void updateUser(String name) {
User user = userRepository.findById(1).get();
System.out.println("user name: " + user.getName());
System.out.println("user age: " + user.getAge());
System.out.println("name will be updated to " + name);
user.setName(name);
userRepository.save(user);
User user1 = userRepository.findById(1).get();
System.out.println("user1 age: " + user1.getAge());
System.out.println("age will be updated to " + 18);
user1.setAge(18);
userRepository.save(user1);
}
名前と年齢の両方が「tom」と18に正常に変更されたことがわかります。これは、最初に照会されたユーザーの名前を「jack」に設定し、user1とuserが同じオブジェクトであるためです。したがってuser1.setAge(18)
、user1の名前と年齢は実行後それぞれ「tom」と18です。同時に、上記のコード実行ログでは、メソッドの実行時に更新ステートメントのみが出力されていることがわかります。これは、メソッドがsave()
有効になっていないことも示していますが、JPAは、トランザクションの送信後にスナップショット機能を使用してデータベース内のデータを更新します。 (2つのsaveメソッド呼び出しを削除しても、結果は同じです)。
上記のコードuserRepository.save(user);
をuserRepository.saveAndFlush(user);
afterに変更すると、userRepository.saveAndFlush(user);
次の図に示すように、メソッドが実行後にupdateステートメントを出力することがわかります。
ただしsaveAndFlush()
、変更されたエンティティ情報を使用してデータベースと同期する場合でも、トランザクションを実行している場合は注意が必要です。送信前のメソッドの実行中に異常なトランザクションがロールバックされると、それに応じてデータベース内のデータがロールバックされます。
問題がどこにあるかがわかったので、それを解決する方法は?
解決策:カスタム更新ステートメントの@ModifyingアノテーションのclearAutomatically属性の値をtrueに設定します。
clearAutomatically
プロパティをtrueに設定すると、カスタム更新ステートメントのUser user1 = userRepository.findById(1).get();
実行後に第1レベルのキャッシュが空になります。このステートメントは、実行時にデータベースで再クエリして、オブジェクトuser1の名前の値が更新されるようにする必要があります。値「tom」。コードと実行ログは次のとおりです。
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Transactional
public void updateUser(String name) {
User user = userRepository.findById(1).get();
System.out.println("user name: " + user.getName());
System.out.println("user age: " + user.getAge());
System.out.println("name will be updated to " + name);
userRepository.updateUserName(name, 1);
User user1 = userRepository.findById(1).get();
System.out.println("user1 age: " + user1.getAge());
System.out.println("age will be updated to " + 18);
user1.setAge(18);
}
}
@Repository
public interface UserRepository extends JpaRepository<User, Integer> {
@Query(value = "update User set name = ?1 where id = ?2 ")
@Modifying(clearAutomatically = true)
void updateUserName(String name, Integer id);
}
まとめ
更新操作にSpringData JPAを使用する場合は、トランザクションの使用と、バッファーとデータベース間のデータ同期に注意してください。
カスタムするとRepository
継承するクラスはJpaRepository<T, ID>
、あなたがカスタム呼び出すときことがわかりますRepository
のsave
時間方法を、実際の実装はJpaRepository<T, ID>
サブクラスSimpleJpaRepository<T, ID>
のsave
メソッド、メソッド@Transactional
注釈付きの分離レベルはデフォルト値であるPropagation.REQUIRED
ので、あなたの場合service
メソッド層呼び出し元のsave
メソッドが変更されたエンティティオブジェクトの状態情報をデータベースに保存すると、
service
メソッドが@Transactional
トランザクションを開くことができない場合、メソッドはトランザクションsave
を開始します。save
メソッドが実行され、トランザクションがコミットされると、SQLステートメントによって変更されたオブジェクト情報がデータベースに同期されます。service
メソッドが@Transactional
トランザクションを開く場合、save
実行はデータをデータベースに保存しません(SQLステートメントは実行されません)が、トランザクションがコミットされた後、スナップショット関数を介してデータをデータベースに同期します。
flush()
このメソッドは、バッファー内のデータをデータベースに同期するために使用されます。JPAでは、トランザクションが送信されると、JPAは自動的にflush()
メソッドを呼び出して、データをデータベースに同期し、キャッシュをクリアします。ただし、カスタムSQLステートメントを実行する前に、キャッシュ内のデータがスナップショット内のデータと一致しない場合、実行されたステートメントの内容が不明であるため、JPAはデータベースとキャッシュされたデータの一貫性を保つために、自動的に呼び出します。flush()
キャッシュ内のデータをデータベースに同期する方法。例として次のコードを取り上げます。
@Service
public class UserService {
@Autowired
private UserRepository userRepository;
@Transactional
public void updateUser(String name) {
User user = userRepository.findById(1).get();
System.out.println("user age is " + user.getAge());
user.setAge(18);
System.out.println("user age change to 18");
userRepository.updateUserName(name, 1);
}
}
@Repository
public interface UserRepository extends JpaRepository<User, Integer> {
@Query(value = "update User set name = ?1 where id = ?2 ")
@Modifying
void updateUserName(String name, Integer id);
}
コード実行ログは次のとおりです。このコードが
実行される前userRepository.updateUserName(name, 1);
に、キャッシュ内のユーザーオブジェクトの経過時間値が変更されているため、Spring DataJPAはflush()
メソッドを介してキャッシュ内のオブジェクト情報をデータベースに更新します。
Spring Data JPAは、プログラマー向けの多くの実用的なメソッドをカプセル化します。プログラマーは、Spring Data JPAを使用してデータアクセスレイヤーコードを簡単に記述できますが、フレームワークが多すぎる場合もありますが、不利になることがあります。フレームワークのメカニズムが理解されていない場合、フレームワークによって提供されるメソッドが誤って使用され、エラーが発生します。エラーは、デバッグでは見つけるのが難しい場合があります。したがって、フレームワークを使用するときは、そのメカニズムを正しく理解する必要があります。
PS:私は長い間ブログを書いていません。このブログを書くのに1日かかりました。確かに、この種のことを書くには練習に時間がかかります。私は怠惰すぎると自分を責めます。