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」を追加すればいいだけなんですけどね。