この記事は Play framework Advent Calendar 2014の 14 日目です。
前回、Ebeanを使ってリレーションを持つテーブルの操作を試してみました。しかし、多対多のリレーションについて、更新時に関係付けしたインスタンスが削除される事象が発生し、しばらくはまってしまいました。今回は、その時に確認した内容についてまとめました。*1
構成
- Windows 8.1
- Java 7
- Play Framework 2.2.3
サンプル用のアプリについて
ソースコードはhideakihal/play-ebean-relation · GitHubで公開しています。
ManyToManyを試すために以下のの2つのModelを用意しました。
- Message・・・メッセージを管理
- Member・・・ユーザ名を管理
models パッケージに,Message.javaとMember.javaを作成し、以下のように@ManyToManyのアノテーションを追加します。
Message.java
@ManyToMany(cascade = CascadeType.ALL) public List<Member> members;
Member.java
@ManyToMany(mappedBy="members", cascade = CascadeType.ALL) public List<Message> messages;
controller パッケージに,以下のようにApplication.javaとContacts.javaを作成します。
Application.java
Messageインスタンスを作成、更新するためにApplication.javaを用意します。CRUD処理で必要なcreateメソッド、updateメソッド、deleteメソッドは以下通りです。
public static Result create() { Form<Message> f = new Form(Message.class).bindFromRequest(); if (!f.hasErrors()) { Message data = f.get(); List<String> names = data.name; for (String nm : names){ Member m = Member.findByName(nm); data.members.add(m); } data.save();
public static Result update() { Form<Message> f = new Form(Message.class).bindFromRequest(); if (!f.hasErrors()) { Message data = f.get(); List<String> names = data.name; for (String nm : names){ Member m = Member.findByName(nm); data.members.add(m); } data.update();
public static Result delete(Long id) { Form<Message> f = new Form(Message.class).fill( Message.find.byId(id) ); if (!f.hasErrors()) { Message data = f.get(); if(data != null){ data.delete();
Contacts.java
Memberインスタンスを作成、更新するためにContacts.javaを用意します。CRUD処理で必要なcreateメソッド、updateメソッド、deleteメソッドは以下通りです。
public static Result create() { Form<Member> f = new Form(Member.class).bindFromRequest(); if (!f.hasErrors()) { Member data = f.get(); data.save();
public static Result update() { Form<Member> f = new Form(Member.class).bindFromRequest(); if (!f.hasErrors()) { Member data = f.get(); data.update();
public static Result delete(Long id) { Form<Member> f = new Form(Member.class).fill( Member.find.byId(id) ); if (!f.hasErrors()) { Member data = f.get(); if(data != null){ data.delete();
サンプルアプリの実行画面
実行画面は以下のようになっています。MessageとMemberに対して作成、読み込み、更新、削除ができるCRUDのアプリになります。
以下は右サイドバーのMemberをクリックした画面になります。messagesには関係付けたMessageインスタンスのIDが表示されます。
Member側からユーザ名のみを登録することができ、以下はMember>+addをクリックした画面になります。
以下は右サイドバーのMesssageをクリックした画面になります。memberには関係付けたMemberインスタンスの名前が表示されます。
Messsage側からはMemberを複数登録できることができ、以下はMesssage>+addをクリックした画面になります。
問題になったこと
Message側からの作成、更新、削除は問題ありませんでしたが、Member側からMemberインスタンスの更新を行った際に、関係付けたMessageインスタンスが削除されてしまう事象が発生しました。
Model | Create | Update | Delete | Annotation |
---|---|---|---|---|
Message | ○ | ○ | ○ | @ManyToMany(cascade = CascadeType.ALL) |
Member | ○ | × | ○ | @ManyToMany(mappedBy="members", cascade = CascadeType.ALL) |
画面イメージとして、以下はMember>+editをクリックした画面になります。
以下のようにMemberのpasswordを変更して更新を行います。
Memberの一覧を見ると、messagesにてMessageのidが削除されていることが分かります。
原因について
当初は、cascadeやmappedByといったアノテーションの指定方法に問題があるかと思い、いくつかバリエーションを試してみましたが、問題は解決しませんでした。
次に、Memberインスタンスの更新には、Contacts.javaのupdateメソッドを使用しているため、updateメソッド注目してどこでMessageインスタンスが消失するか確認しました。
updateメソッドでは、bindFromRequestでフォームから渡された値を保管するFormインスタンスを作成し、FormインスタンスからMemberインスタンスを取得しています。
public static Result update() { Form<Member> f = new Form(Member.class).bindFromRequest(); if (!f.hasErrors()) { Member data = f.get(); data.update();
このMemberインスタンスの値を確認したとろ、すでにMessageインスタンスが消失した状態となっていました。つまり、bindFromRequestはフォームに入力されてpostされたものしか取得されないため、関係レコードのオブジェクトは取得できないことが原因となっていました。当初は、bindFromRequestで関係レコードのオブジェクトも取得できると勘違いしていたため原因をつかむのに時間がかかってしまいました。
解決方法について
原因は判明しましたが、すでに関係付けたMessageインスタンスをどのように取得するか悩みました。例えば、View側で関係付けたMessageインスタンスを一旦IDにし、IDの配列として渡す方法など考えましたが、あまりいい方法とも思えませんでした。最終的に、MemberのIDからfindByIdでMemberインスタンスを取得することで、更新前に関係付けたMessageインスタンスが取得することが確認できたため、以下のような実装を行いました。
Contacts.java
public static Result update() { Form<Member> f = new Form(Member.class).bindFromRequest(); if (!f.hasErrors()) { Member data = f.get(); Member name = Member.findById(data.id.toString()); List<Message> arr = name.messages; for(Message m : arr){ data.messages.add(m); } data.update();
Member.findById(data.id.toString())で更新前に関係付けたMessageインスタンスを取得しています。また、cascade = CascadeType.ALLを使用しているため、messages側からaddするだけで自動的に更新を行うことができます。
Member.java
public static Member findById(String input){ return Member.find.where().eq("id", input).findList().get(0); }
細かな実装については、hideakihal/play-ebean-relation · GitHubを確認してみてください。
おわりに
Ebeanを使ったManyToManyのリレーションについて、更新処理が上手くできずにはまっていましたが、なんとか解決することができました。今回の問題は、Stack Overflowに似たような問題が上がっていましたが、欲しい情報が見つからず解決に時間がかかってしまいました。
今回の記事がみなさんのお役に立てられればと思います。
*1:あくまで個人的に検証した結果であるため、正しい実装ではないかもしれません。よい方法があればどなかた教えてください。