どうもSuです。
最近、Javaを学びなおしているのですが、HibernateのOneToManyを使ったDelete-Insert処理にはまったので、備忘録として解決した結果を残しておきます。
やりたかったこと
親子関係のEntityクラスとデータがあり、親はPost(ブログ記事)クラス、子はComment(ブログに対するコメントリスト)クラスです。Postクラスには、Commentリストが@OneToManyアノテーション付きであります。この状況で、entityManagerを使って、CommentリストのDelete→Insertをしたかったということです。
クラス構造とデータ
クラス
Postクラス
public class Post {
@Id
@Column(name = "post_id")
private Long postId;
@Length(max = 4000)
private String content;
@OneToMany(mappedBy = "parent" cascade = CascadeType.ALL)
private List<Comment> comments;
}
Commentクラス
public class Comment {
@Id
@Column(name = "comment_id")
private Long commentId;
@Length(max = 500)
private String message;
@ManyToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
@JoinColumn(name = "post_id", nullable = false)
private Post parent;
}
テーブル
Postテーブル
POST_ID | CONTENT |
1 | 今日は良い天気なので、お散歩をしました。 |
2 | 明日は、お祭りなので、今日は準備が忙しかったです。 |
Commentテーブル
COMMENT_ID | MESSAGE | POST_ID |
1 | とても良い天気でしたね! | 1 |
2 | こちらも、とても良い天気で、私も散歩しました! | 1 |
Delete -> Insertできなかったコード
※このコードはサンプルであるため、このコードだけでは正確に動きません。サンプルコードはあくまでどのような実装をするかのイメージと捉えてください。
function deleteInsertComment(ArrayList<> newCommentList) {
// Delete all old comments
Post post = entityManager.find(Post.class, 1);
post.setComments(New ArralyList<>());
// Insert all new comments
post.setComments(newCommentList);
entityManger.flush();
}
Delete -> Insertできたコード
function deleteInsertComment(ArrayList<> newCommentList) {
// Delete all old comments
Post post = entityManager.find(Post.class, 1);
// ▼▼▼この操作をしなければならなかった▼▼▼
post.getComments().forEach(comment -> {
comment.setParent(null);
entityManager.remove(comment);
})
// ▲▲▲この操作をしなければならなかった▲▲▲
post.setComments(New ArralyList<>());
// Insert all new comments
// ▼▼▼この操作をしなければならなかった▼▼▼
newCommentList.forEach(comment -> {
comment.setParent(post);
entityManager.persist(comment);
entityManager.flush();
}
// ▲▲▲この操作をしなければならなかった▲▲▲
post.setComments(newCommentList);
entityManger.flush();
}
どこが悪かったのか?
結論を言うと、「entityManagerに古いコメントがすべて削除されたことと、新しいコメントを登録する必要があることを正確に伝えられなかった」ことだと思います。
entityManagerは、管理下にあるインスタンス変数における変更点を読み取り、SQLに変換・実行してくれる君です。つまり、entityManagerにちゃんと伝えれば、ちゃんとした順でSQLを実行してくれるということですね。
どこを直したか(既存レコードの削除)
(1) setParent(null)で親Postクラスとの紐づきを削除する
comment.setParent(null);
これは、以下のように、Postクラスの、OneToManyアノテーションに、「CascadeType.ALL」が指定されていたため、単純にremoveすると、削除が親Postクラスに伝搬してしまい、POSTテーブルのDELETE文が実行されてしまいました。ですので、事前にparentにnullを指定することで、伝搬が起きないようにしました。
@OneToMany(mappedBy = "parent" cascade = CascadeType.ALL)
private List<Comment> comments;
(2) entityManagerのremoveメソッドを呼ぶ
entityManager.remove(comment);
entityManagerに既存レコードを削除するために、removeメソッドを呼ぶ必要がありました。単純に配列の中身をあたらしい内容に変更しただけでは、削除をしてくれません。明示的にremoveメソッドを呼び出し、削除を行ってもらいました。
どこを直したか(新規レコードの追加)
(1) setParent(parent)で親Postクラスとの紐づきを追加する
comment.setParent(post);
新しいCommentの配列には親Postクラスとの紐づきがないため、紐づきを追加しました。紐づきを追加することで、POSTテーブルのPOST‗IDがCOMMENTテーブルのPOST_IDに入るようになりました。
(2) entityManagerのpersistメソッドを呼ぶ
entityManager.persist(comment);
persistメソッドを呼ぶことにより、新しいCommentをentityManagerの管理下に置くことができるようになります。そうすると、新しく追加されたCommentのInsert文が発行されるようになります。
(3) entityManagerのflushメソッドをループ毎に呼ぶ
newCommentList.forEach(comment -> {
comment.setParent(post);
entityManager.persist(comment);
entityManager.flush();
}
これは別な問題で、理由が分かっていませんが、新しいレコードのINSERT文の順序がArrayListの順番通りに実行されない問題がありました。
最初は、entityManager.flush();はループ中に呼び出していませんでしたが、最終的なentityManagerのInsert文の実行順序がバラバラだったため、ループ中に呼び出すことで、Insert文の実行順序を制御することにしました。いろいろ試した結果こういう処理になってしまったのですが、もう少しentityManagerの仕様を理解すれば、正しいやり方が分かるかもしれません。
最後に
entityManagerは、変数を操作するだけで、いい感じの順序でいい感じにSQLを発行してくれる優れものです。しかし、結局のところentityManagerが内部でどのような動きをするのかを理解し、自分がやりたいことを実現する必要があります。
エンジニアとしては、簡単だからと言って、中身を理解しないで使うのは危険だと思いますので、分からない点は調べてしっかり理解したいと思います。