Play Framework 2.3 For Java ことはじめ #11 リレーションを持つCRUD編
Play Framework 2.3 For Java 入門記事一覧
第11回はリレーションを持つエンティティに関するCRUDについてです。 #6 データベース接続(JPA with Hibernate)編で一つのテーブルだけ持つ場合のCRUDはできることが分かって、これでだいたいOKかなと思ってたけど全然まだまだだった。
実際の世界ではそんな単純な構造や関係というのはめったになくて、必ず1対1
、1対多
、多対多
などのリレーションを持ったエンティティによって構成されます。なのでこのリレーションを持った場合のサンプルアプリケーションを作ってみました。
これがほんとに正しいかは分からないけど、ひとまず一通りのリレーションと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週間ぐらいはまりましたね。
- Playでformに複数の値を持つものをセレクトボックスにバインドするやり方が分からない
- リスト形式の複数のデータを一度に登録するときのやり方が分からない
- リスト形式の複数のデータを一度に更新するときのやり方が分からない
1については、Formattes.register
をGlobal.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.ALL
とorphanRemoval = 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でリレーションを持つCRUDをJPA(with Hibernate)で書くというサンプルを作ってみました。が、結構大変でした。。。
もっとJavaのアノテーションの分かりやすいサンプルとかもっと転がっててもいいんじゃないんかと思うんですが、意外とないんですね。初心者にはなかなか苦しいものがある。
そもそも、なぜ、難しいかと思ったかというのは、どこまでをHibernateがやってくれて、どこからは自分自身で書かなきゃいけないの?というところが未だにうまくつかめてません。
あと、PlayのbindFromRequestでどこまでbindしてくれるのかってのが難しかった。これも色んなパターンでサンプルとかチュートリアル転がってると助かるなー。
第12回は、sessionとCacheについて簡単に説明してみます。