Play! frameworkが使いにくい点
Play! frameworkを使っていると、私はどうしてもRuby on Railsと比較してしまいます。
自動的にJOINしてくれない?
Ruby on Railsの場合、モデルのfindメソッドに「:joins =>」があると、自動的にJOIN句を含んだSQL文が生成されますが、Play! frameworkのModelクラスは、やってくれない場合があります。
Play! framework 1.0.3をベースに、Userモデル(app/models/User.java)とPrefectureモデル(app/models/Prefecture.java)を作成しました。この2つは、一対多の関係にあります。
Userモデルは、firstName(名)、lastName(姓)、prefecture(都道府県)の3メンバ変数があり、prefectureは、Prefectureテーブルへの外部キーになります。
@Entity public class User extends Model { @Required public String firstName; @Required public String lastName; @ManyToOne public Prefecture prefecture; public User(String firstName, String lastName, Prefecture prefecture) { this.firstName = firstName; this.lastName = lastName; this.prefecture = prefecture; } }
次にPrefectureモデルは、name(都道府県名)、user(ユーザ)メンバ変数の2つです。
@Entity public class Prefecture extends Model { @Required public String name; @OneToMany(mappedBy="prefecture", cascade=CascadeType.ALL) public List<User> user; public Prefecture(String name) { this.name = name; } }
app/controllers/Application.javaでは、User.findAll()でUserテーブルを全件取得しています。
public class Application extends Controller { public static void index() { List<User> user = User.findAll(); render(user); } }
app/views/Application/index.htmlは、名、姓、都道府県名をループで表示しています。
<ul> #{list user} <li>${_.firstName} ${_.lastName} (${_.prefecture.name})</li> #{/list} </ul>
実行されているSQL文を見たいので、conf/application.confでDEBUGログにSQL文を出力するように設定しました。
jpa.debugSQL=true
これを実行すると、SELECT文が2回実行されます。1回目は、Application.javaのfindAllメソッドで、2回目はindex.htmlの「${_.prefecture.name}」です。Userモデルのprefectureメンバに「@ManyToOne(fetch=FetchType.EAGER)」と修正しても何も変わりませんでした。
19:42:45,408 DEBUG ~ select user0_.id as id0_, user0_.firstName as firstName0_, user0_.lastName as lastName0_, user0_.prefecture_id as prefecture4_0_ from User user0_
19:42:45,651 DEBUG ~ select prefecture0_.id as id1_0_, prefecture0_.name as name1_0_ from Prefecture prefecture0_ where prefecture0_.id=?
UserにPrefectureをJOINしてくれないので、Userモデルに「findWithPrefecture」というメソッドを追加し、JPQLでUserにPrefectureをleft joinした問い合わせ文を書きました。
public static List<User> findWithPrefecture() { return find("select new map(u.firstName as firstName, u.lastName as lastName, p.name as prefectureName) from User u left join u.prefecture p").fetch(); }
また、都道府県の名前はprefectureNameになる、index.htmlも修正。
<ul> #{list user} <li>${_.firstName} ${_.lastName} (${_.prefectureName})</li> #{/list} </ul>
その結果、1回の問い合わせで済むようになりました。
20:02:11,637 DEBUG ~ select user0_.firstName as col_0_0_, user0_.lastName as col_1_0_, prefecture1_.name as col_2_0_ from User user0_ left outer join Prefecture prefecture1_ on user0_.prefecture_id=prefecture1_
見た目上、実行結果はどちらも変わらないのですが、データベースに問い合わせする回数が違ってくるので注意が必要です。
@ManyToOneアノテーションを付加されたメンバ変数があり、且つ、「fetch=FetchType.EAGER」と設定してあったら、自動的にJOINして欲しいんですが。これは、単純にPlay! frameworkのバグなのでしょうか。Prefectureモデルのuserメンバ変数に付加した@OneToManyに「fetch=FetchType.EAGER」を追加した場合、ちゃんとJOINしてくれるんですが。
21:48:44,954 DEBUG ~ select prefecture0_.id as id41_1_, prefecture0_.name as name41_1_, user1_.prefecture_id as prefecture4_3_, user1_.id as id3_, user1_.id as id40_0_, user1_.firstName as firstName40_0_, user1_.lastName as lastName40_0_, user1_.prefecture_id as prefecture4_40_0_ from Prefecture prefecture0_ left outer join User user1_ on prefecture0_.id=user1_.prefecture_id where prefecture0_.id=?
削除時、NULLをいれてくれるような設定ができないのか?
Prefectureのレコードを削除する際、そのidを外部キーとしているUserレコード(prefecture_id)が存在した場合、エラーが発生します。
PersistenceException occured : org.hibernate.exception.ConstraintViolationException: Could not execute JDBC batch update
そこでPrefectureモデルに「remove」というメソッドを追加し、Prefectureレコードを削除する前に該当するUserレコードのprefecture_idにNULLを設定するようにしました。
public void remove() { if (this.user != null) { for(User u:this.user) { u.prefecture = null; u.save(); } } this.delete(); }
また、Prefectureモデルのuserメンバ変数に付与した、@OneToManyアノテーションのCascadeTypeがALLだとUserレコードが削除されてしまうので、PRESISTとMERGEに変更。
@OneToMany(mappedBy="prefecture", cascade={CascadeType.PERSIST, CascadeType.MERGE}) public List<User> user;
Ruby on Railsの場合、ActiveRecord::Baseを継承したモデルのhas_manyに「:dependent => :nullify」を追加すればいいだけなんですけどね。