hidemium's blog

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

Play Framework(Java)でEbeanを使ってリレーションを持つテーブルを操作する

前回、EBeanを使ったデータベースの操作を試してみましたが、Ebeanには複数のModelを関連付けて処理する仕組みも用意されています。Modelクラスのフィールドにアノテーションを付けることで、1対1、1対多、多対多といった関係付けを行うことができます。今回は、Ebeanを使ってリレーションを持つテーブルの操作について試してみました。

構成

Modelの連携の種類について

Modelの連携には以下の種類があります。

  • OneToOne(1対1)
  • OneToMany(1対多)、ManyToOne(多対1)
  • ManyToMany(多対多)

サンプル用のModelについて

それぞれの連携を説明するために、今回以下の2つのModelを用意しました。

  • Message・・・メッセージを管理
  • Member・・・ユーザ名を管理

まずはmodels パッケージに,以下のようにMessage.javaとMember.javaを作成します。

Message.java
package models;

import java.util.*;
import javax.persistence.*;
import javax.validation.*;
import com.avaje.ebean.*;
import play.db.ebean.*;
import play.data.validation.*;

@Entity 
public class Message extends Model {

  @Id
  public Long id;
  
  @Required
  public String name;

  @Required
  public String message;
  
  @CreatedTimestamp
  public Date postdate;
	
  public static Finder<Long, Message> find = 
    new Finder<Long, Message>(Long.class, Message.class);
}
Member.java
package models;

import java.util.*;
import javax.persistence.*;
import javax.validation.*;
import com.avaje.ebean.*;
import play.db.ebean.*;
import play.data.validation.*;

@Entity
public class Member extends Model {

  @Id
  public Long id;

  @Required
  public String name;

  @CreatedTimestamp
  public Date postdate;

  public static Finder<Long, Member> find = 
    new Finder<Long, Member>(Long.class, Member.class);
}

OneToOne

OneToOneは、あるModelのエンティティ1つに対して、別のModelのエンティティ1つが対応する関係です。

Message.java

Messageインスタンスに1つのMemberインスタンスが関連するようにするには、Message.javaに以下の内容を追加します。@OneToOneというアノテーションが、OneToOneの関係を指定するものになります。

@OneToOne(cascade = CascadeType.ALL)
public Member member;

エンティティと関連付けられたエンティティに対する処理を自動で行う場合はCascadeTypeを指定します。cascade = CascadeType.ALLを追加することで、参照や更新などのすべての操作に対して処理を自動で行うことができるようになります。この設定がない場合は、操作ごとにApplication.javaに処理を書く必要があります。

Member.java

ユーザ名からMemberインスタンスを取得できるように、Member.javaにfindByIdメソッドを追加します。

public static Member findById(String input){
	return Member.find.where().eq("id", input).findList().get(0);
}

Application.java

Application.javaに以下のようにfindByIdを追加します。findByIdでMemberインスタンスを取得し、saveによりMessageインスタンスに保存するだけで関連付けをすることができます。

public static Result create() {
	Form<Message> f = new Form(Message.class).bindFromRequest();
	if (!f.hasErrors()) {
		Message data = f.get();
		data.member = Member.findById(data.name);
		data.save();
		return redirect("/");
	} else {
		return badRequest(add.render("ERROR",f));
	}
}

Evolutionを実行すると、以下のDLLが自動的に作成されます。Message.javaに追加したmemberがmember_idとして作成されていることが分かります。

create table message (
  id                        bigint not null,
  name                      varchar(255),
  message                   varchar(255),
  member_id                 bigint,
  postdate                  timestamp not null,
  constraint pk_message primary key (id))
;

OneToMany、ManyToOne

OneToManyは、あるModelのエンティティ1つに対して、別のModelの複数のエンティティが対応する関係です。ManyToOneは、逆にあるModelの複数のエンティティが、別のModelのエンティティ1つが対応します。
Memberインスタンス複数のMessageインスタンスが関連するようにするには、Message.javaとMember.javaに以下の内容を追加します。@OneToMany、@ManyToOneというアノテーションが、OneToMany、ManyToOneの関係を指定するものになります。

Message.java

@OneToMany
public Member members;

Member.java

@ManyToOne(cascade = CascadeType.ALL)
public List<Message> messages;

public static Member findById(String input){
	return Member.find.where().eq("id", input).findList().get(0);

こちらでも、エンティティと関連付けられたエンティティに対する処理を自動で行うため、cascade = CascadeType.ALLを指定しておきます。

Application.java

Application.javaに以下のようにfindByIdを追加します。findByIdでMemberインスタンスを取得し、saveによりMessageインスタンスに保存するだけで関連付けをすることができます。
Messageインスタンスの操作しかしていませんが、Memberインスタンスも自動的に反映されるため処理を追加する必要はありません。

public static Result create() {
	Form<Message> f = new Form(Message.class).bindFromRequest();
	if (!f.hasErrors()) {
		Message data = f.get();
		data.member = Member.findById(data.name);
		data.save();
		return redirect("/");
	} else {
		return badRequest(add.render("ERROR",f));
	}
}

Evolutionを実行すると、DLLが自動的に作成されますが、OneToManyやManyToOneでは変化はありません。

ManyToMany

ManyToManyは、あるModelのの複数のエンティティに対して、別のModelの複数のエンティティが対応する関係です。
複数のMemberインスタンス複数のMessageインスタンスが関連するようにするには、Message.javaとMember.javaに以下の内容を追加します。@ManyToManyというアノテーションが、ManyToManyの関係を指定するものになります。Message.javaには、複数のMemberインスタンスを格納するために、配列を追加しました。

Message.java

public List<String> name;

@ManyToMany(cascade = CascadeType.ALL)
public List<Member> members;

Member.java

@ManyToMany(mappedBy="members", cascade = CascadeType.ALL)
public List<Message> messages;

public static Member findById(String input){
	return Member.find.where().eq("id", input).findList().get(0);

こちらでも、エンティティと関連付けられたエンティティに対する処理を自動で行うため、cascade = CascadeType.ALLを指定しておきます。
また、ManyToManyではエンティティの主従関係を明示的にするためmappedByを指定しています。mappedBy="members"は、Messageが主、Memberが従であることを示しています。また、mappedByを指定しない場合、エンティティと関連付けした状態でインスタンスを削除をすると参照整合性制約違反が発生するため、自動的に処理をするには指定したほうがいいかもしれません。

Application.java

MemberのIDの配列を取得し、それらのIDからfindByIdでMemberインスタンスを取得しています。取得した複数のMemberインスタンスをmembersにaddにより追加します。saveによりMessageインスタンスに保存し関連付けをすることができます。Messageインスタンスの操作しかしていませんが、Memberインスタンスも自動的に反映されるため処理を追加する必要はありません。

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.findById(nm);
    			data.members.add(m);
    		}
		data.save();
		return redirect("/");
	} else {
		return badRequest(add.render("ERROR",f));
	}
}

Evolutionを実行すると、以下のDLLが自動的に作成されます。MessageとMemberの関係テーブルがmessage_memberとして作成されていることが分かります。

create table message_member (
  message_id                     bigint not null,
  member_id                      bigint not null,
  constraint pk_message_member primary key (message_id, member_id))
;