Spring Data JPAピットの使用を忘れないでください:キャッシングとスナップショットについて。

最近、グループ内のプロジェクトのバグを修正したところ、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属性値は引き続き「ジャック」です。自定义Repositoryfind()findxx()
ここに写真の説明を挿入updateUser()selectUser 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)コード実行の効果が上書きされます。

UserServiceupdateUser()方法でメソッド変更し場合

    @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>、あなたがカスタム呼び出すときことがわかりますRepositorysave時間方法を、実際の実装はJpaRepository<T, ID>サブクラスSimpleJpaRepository<T, ID>saveメソッド、メソッド@Transactional注釈付きの分離レベルはデフォルト値であるPropagation.REQUIREDので、あなたの場合serviceメソッド層呼び出し元のsaveメソッドが変更されたエンティティオブジェクトの状態情報をデータベースに保存すると、

  1. serviceメソッドが@Transactionalトランザクションを開くことができない場合、メソッドはトランザクションsaveを開始します。saveメソッドが実行され、トランザクションがコミットされると、SQLステートメントによって変更されたオブジェクト情報がデータベースに同期されます。
  2. 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日かかりました。確かに、この種のことを書くには練習に時間がかかります。私は怠惰すぎると自分を責めます。

おすすめ

転載: blog.csdn.net/weixin_40759863/article/details/109273688