読者です 読者をやめる 読者になる 読者になる

hidemium's blog

日々学んだことをアウトプットする。

Play Framework(Java)でEbeanを使ってManyToManyのリレーションを行う

この記事は Play framework Advent Calendar 2014の 14 日目です。

前回、Ebeanを使ってリレーションを持つテーブルの操作を試してみました。しかし、多対多のリレーションについて、更新時に関係付けしたインスタンスが削除される事象が発生し、しばらくはまってしまいました。今回は、その時に確認した内容についてまとめました。*1

構成

サンプル用のアプリについて

ソースコード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が表示されます。

f:id:hidemium:20141214100433p:plain

Member側からユーザ名のみを登録することができ、以下はMember>+addをクリックした画面になります。

f:id:hidemium:20141214100421p:plain

以下は右サイドバーのMesssageをクリックした画面になります。memberには関係付けたMemberインスタンスの名前が表示されます。

f:id:hidemium:20141214100341p:plain

Messsage側からはMemberを複数登録できることができ、以下はMesssage>+addをクリックした画面になります。

f:id:hidemium:20141214100358p:plain

問題になったこと

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を変更して更新を行います。

f:id:hidemium:20141214113111p:plain

Memberの一覧を見ると、messagesにてMessageのidが削除されていることが分かります。

f:id:hidemium:20141214113125p:plain

原因について

当初は、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のfindByIdメソッドは以下のように作成します。

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:あくまで個人的に検証した結果であるため、正しい実装ではないかもしれません。よい方法があればどなかた教えてください。