まーぽんって誰がつけたの?

iOS→Scala→インフラなおじさん技術メモ

Play Framework 2.3 For Java ことはじめ #11 リレーションを持つCRUD編

f:id:masato47744:20140720125846p:plainPlay Framework 2.3 For Java 入門記事一覧

第11回はリレーションを持つエンティティに関するCRUDについてです。 #6 データベース接続(JPA with Hibernate)編で一つのテーブルだけ持つ場合のCRUDはできることが分かって、これでだいたいOKかなと思ってたけど全然まだまだだった。

実際の世界ではそんな単純な構造や関係というのはめったになくて、必ず1対11対多多対多などのリレーションを持ったエンティティによって構成されます。なのでこのリレーションを持った場合のサンプルアプリケーションを作ってみました。

これがほんとに正しいかは分からないけど、ひとまず一通りのリレーションとCRUDを考慮してあります。

mpon/play-jpa-relation · GitHub

サンプルアプリケーションのERの説明

サンプルとして、こんなERを作ってみた。

"ユーザー" 1..1 "住所"
"ユーザー" 1..* "ブログの記事"
"ブログの記事" *..* "ブログのカテゴリ"
  • ユーザーは、名前、メールアドレスなどを持っている
  • 住所は、都市や、郵便番号を持っている
  • ユーザーと住所は(1対1)で結びついている
  • ユーザーと住所は同じ主キーでユーザーのidを利用する
  • ユーザーはブログの記事を複数持つことができる(1対多)
  • ブログは複数のブログのカテゴリを登録できて、ブログのカテゴリは、複数のブログの記事に登録できる(多対多)

それぞれのモデルはこんな風に定義。多対多の部分は、中間テーブルを持つ形にして、その中間テーブルと各モデルが1対多で結びつくイメージ。

"ユーザー" : User (usersテーブル)
"住所" : Address (addressesテーブル)
"ブログの記事" : Post (postsテーブル)
"ブログのカテゴリ" : Tag (tagsテーブル)
"ブログの記事とブログのカテゴリの中間テーブル" : PostTag (posts_tagsテーブル)

1対1のリレーション

まず、同じ主キーを使うということを、アノテーションでどう書くかというところが難しかった。まず、User側でAddressを宣言するところはこう。

@Valid
@OneToOne(cascade = CascadeType.ALL)
@PrimaryKeyJoinColumn
private Address address;

こんな風にCascadeType.Allを宣言しておいて、あと、@PrimaryKeyJoinColumnをつけることが重要みたいです。ちなみに、話がそれちゃいますが、@Validというのをつけておかないと、Address側で宣言しているplayのformのvalidationが効きません。(これも結構はまった)

そして、関連先のAddress側でUserを宣言する方ではこう。

@MapsId 
@OneToOne(mappedBy = "address")
@JoinColumn(name = "user_id")
private User owner;

こんな風に、@MapsIdというのをつけるのがポイント。これで主キーを共有するということを表現するみたいだ。ownerという変数名は適当(適当という意味はなんでもいいということです)

んで、最後に、モデルを保存するときだけどこの一手間が必要。

public void save() {
    this.address.setOwner(this);
    JPA.em().persist(this);
    JPA.em().flush();
}

フォームからbindFromRequestした時点では、まだ、このAddressオブジェクトは、自分を保持しているのが誰かなんて知らないんです。なので、保存する前に、オーナーとなるユーザーを設定してあげなきゃいけないってところがミソ

1対多のリレーション

1対多は比較的簡単だった。基本的に、User側で、List<Post>で宣言しておいて、@OneToManyアノテーションつけて、Post側で、Userで宣言しておいて、@ManyToOneつけるという感じ。ちょっとはまったところはPost.javaで保存処理をしている以下のところ。

public void save(User user) {
    this.setAuthor(user);
    // 中略   
    JPA.em().persist(this);
    JPA.em().flush();
}

新しいpost(ブログ記事)を保存するときに、やっぱり、このpostは、誰のものなのか?ってことを教えてあげなきゃいけない。なので、保存するときに、postにuserをセットしてる。これで、あとはHibernateがうまくやってくれる。ちなみにこのAuthorという変数名は適当です。(つまり、変数名はなんでもいいということ)

多対多のリレーション

これが一番難しかった。帰ってからと休日でやってたとはいえ、普通に1週間ぐらいはまりましたね。

  1. Playでformに複数の値を持つものをセレクトボックスにバインドするやり方が分からない
  2. リスト形式の複数のデータを一度に登録するときのやり方が分からない
  3. リスト形式の複数のデータを一度に更新するときのやり方が分からない

1については、Formattes.registerGlobal.javaに書くことで対応できました。

view上で、ブログを投稿する際に、複数のタグを登録したいから、@select(postForm("postTags"), options(Tag.options), 'multiple -> "multiple")と書いたときに、このpostForm("postTags")のところをなんて書けばいいか分からない。

普通に考えて、Postモデルが持ってる変数は、postTagsなので、そのまま書くと最初はバリデーションエラーになります。なので、これを、postTags.tag.idとかやるとうまくいくけどバインドされない。

色々悩んだあげく、公式ドキュメントにcustom data binderみたいのがあったのを思い出し、これかなと思ってやってみたらビンゴ。

Global.javaを作って、のonStart(Application app)に次のように、変換メソッドを書いてやります。

        Formatters.register(PostTag.class, new SimpleFormatter<PostTag>() {
            @Override
            public PostTag parse(String tagId, Locale l) throws ParseException {
                Tag tag = Tag.findById(Long.valueOf(tagId));
                PostTag postTag = new PostTag();
                postTag.setTag(tag);
                return postTag;
            }

            @Override
            public String print(PostTag postTag, Locale l) {
                return String.valueOf(postTag.getTag().getId());
            }
        });

これを書くと、@select(postForm("postTags"), options(Tag.options), 'multiple -> "multiple")というところが、controllerへsubmitされたあとのbindFromRequestと、編集時に、画面上のセレクトボックスにcheckしてくれるbindもやってくれます。

2と3については、まずアノテーションCascadeType.ALLorphanRemoval = trueを宣言しておく。

@OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true)
private List<PostTag> postTags;

新規登録する場合は、今までと似てて、親のオブジェクトをセットしてあげる必要がある。

postTagはformでbindFromRequestされた時点では、どのpostのものかということが分からない。なので、セットしてあげる。

for (PostTag postTag : this.postTags) {
    postTag.setPost(this);
}

更新する場合は、一旦、既に登録されているタグは全部削除したいので、削除処理を以下のように書く。

post.getPostTags().clear();
JPA.em().flush();

この、オブジェクトを操作しただけで削除させるというのは、orphanRemoval = trueというのを設定しておかないとできないんです。この辺がはまりポイント。

さらっとしか説明してませんが、ソースコードの方にはポイントとなる部分にコメントをたくさん書いておいたのでそっちも合わせて確認してみてください。

mpon/play-jpa-relation · GitHub

まとめ

Play Frameworkでリレーションを持つCRUDJPA(with Hibernate)で書くというサンプルを作ってみました。が、結構大変でした。。。

もっとJavaアノテーションの分かりやすいサンプルとかもっと転がっててもいいんじゃないんかと思うんですが、意外とないんですね。初心者にはなかなか苦しいものがある。

そもそも、なぜ、難しいかと思ったかというのは、どこまでをHibernateがやってくれて、どこからは自分自身で書かなきゃいけないの?というところが未だにうまくつかめてません。

あと、PlayのbindFromRequestでどこまでbindしてくれるのかってのが難しかった。これも色んなパターンでサンプルとかチュートリアル転がってると助かるなー。

第12回は、sessionとCacheについて簡単に説明してみます。