この投稿はJavaEEのアーキテクチャを再興する。(まとめページ)に属する投稿です。他の記事もお楽しみあれ。
JavaでWebシステムを作るとき、たいていはRDBも触ると思います。JavaでEntityを作成して、それと同じ形でDBのCreate文を作成して、フィールド名かカラム名をTypoしてバグを埋め込む。みなさんも一度は体験したことがあるかと存じます。
JPA2.0を使うと、JPAがEntityからRDBのテーブルを作成してくれるので、Create文を見ることなく、RDBにオブジェクトを保存できます。db:migrateとかコマンドも打つことなく、JPAがすべてを整えてくれます。便利。
本稿では、システムを作る上では絶対に出てくる、テーブル同士の関連をJPAで表現するためにはどうするかをお伝えします。
とはいえ、Javaの普通の関連を作り、アノテーションを貼るだけです。前置きよりも簡単に読めるかと思います。
なお、JPAの簡単な説明はこちらをご覧ください。
サンプル
サンプルはGithubのこちらに乗せてあります。
RelationMain.javaで、CartとItemというクラスを保存、取得を行っています。
サンプルではJPAの実装として、EclipseLink、RDBにH2を使用しています。
解説
まずは保存対象のクラスを見ていきます。
@ManyToOne(cascade = CascadeType.ALL)
private Cart cart;
@OneToMany(mappedBy = "cart", cascade = CascadeType.ALL)
private List<Item> items = new ArrayList<Item>();
ItemとCartそれぞれでお互いを参照しています。また、@ManyToOne、@OneToManyでそれぞれの関係性を定義しています。cascadeは、関連付けを保存時や参照時に有効にするかを選択します。ここではすべての場合で関連付けを有効にしたいので、CascadeType.ALLを指定しています。また、mappedByでは関連先、Itemのフィールド名を指定しています。
関連付けはこれだけ。簡単ですね。
Cart cart = new Cart();
cart.addItem(new Item("ITEM1"));
cart.addItem(new Item("ITEM2"));
cart.setKey(UUID.randomUUID().toString());
em.persist(cart);
cartを作成し、itemを関連付けています。Cart#addItemでは、Itemとcartを相互に関連付けています。
これだけでDBに外部キー関連が保存されます。
List<Cart> carts = em.createQuery(cq.select(r)).getResultList();
System.out.println("carts length = " + carts.size());
for (Cart cart : carts) {
System.out.println("# cart id = " + cart.getId());
List<Item> items = cart.getItems();
for (Item item : items) {
System.out.println("\t item id = " + item.getId() + ", name = " + item.getName() + ", cartId");
}
}
Cartを検索すると、Itemも一緒に取得できることがわかります。
逆に、Itemを検索しても、Cartが取得できます。
List<Item> items = em.createQuery(query.select(r)).getResultList();
System.out.println("items length = " + items.size());
for (Item item : items) {
System.out.println("item cartId = " + item.getCart().getId() + " id = " + item.getId() + ", name = " + item.getName());
}
RDBを使っていることを意識せずに保存、取得ができていますね!
更に詳しく見てみる。
JPAは便利すぎます!しかしながら、どういう処理をしているかとても気になるので、中を見てみます。
テーブル定義
JPAでは勝手にテーブルを作ってくれています。どのようなテーブルができているか見てみます。
CREATE CACHED TABLE PUBLIC.ITEM(
ID BIGINT NOT NULL,
NAME VARCHAR,
CART_ID BIGINT
);
CREATE PRIMARY KEY PUBLIC.PRIMARY_KEY_2 ON PUBLIC.ITEM(ID);
eALTER TABLE PUBLIC.ITEM ADD CONSTRAINT PUBLIC.CONSTRAINT_2 PRIMARY KEY(ID) INDEX PUBLIC.PRIMARY_KEY_2,
JCREATE CACHED TABLE PUBLIC.CART(
ID BIGINT NOT NULL,
KEY VARCHAR
);
CREATE PRIMARY KEY PUBLIC.PRIMARY_KEY_1F ON PUBLIC.CART(ID);
ALTER TABLE PUBLIC.CART ADD CONSTRAINT PUBLIC.CONSTRAINT_1F PRIMARY KEY(ID) INDEX PUBLIC.PRIMARY_KEY_1;
CREATE INDEX PUBLIC.FK_ITEM_CART_ID_INDEX_2 ON PUBLIC.ITEM(CART_ID);
ALTER TABLE PUBLIC.ITEM ADD CONSTRAINT PUBLIC.FK_ITEM_CART_ID FOREIGN KEY(CART_ID) INDEX PUBLIC.FK_ITEM_CART_ID_INDEX_2 REFERENCES PUBLIC.CART(ID) NOCHECK;
ITEMテーブルにCART_IDというカラムができて、ALTER TABLEでそこに外部キーを張っているのがわかります。
Select文
EclipseLinkのログをアプリの出力と交えて見てみます。
query cart start
[EL Fine]: sql: 2014-06-27 23:09:07.716--ServerSession(253338012)--Connection(1998581014)--Thread(Thread[main,5,main])--SELECT ID, KEY FROM CART
carts length = 3
# cart id = 1
[EL Fine]: sql: 2014-06-27 23:09:07.721--ServerSession(253338012)--Connection(1998581014)--Thread(Thread[main,5,main])--SELECT ID, NAME, CART_ID FROM ITEM WHERE (CART_ID = ?)
bind => [1]
item id = 2, name = ITEM1, cartId
item id = 3, name = ITEM2, cartId
Cartの検索はSELECT ID, KEY FROM CARTで普通に行っていますね。興味深いのは、この時はまだItemは取得しておらず、cart.getItems()の時に初めてSELECT ID, NAME, CART_ID FROM ITEM WHERE (CART_ID = ?)を行っているのがわかります。これで余分なデータ取得をすることがなくてハッピーですね。
JPAはとても簡単。使わない手はありません。
DBアクセスのような、いつもいつも同じコードを書くような実装は、できるだけ楽をして作って、UXや便利な機能などに力を入れ、ものづくりの価値を上げたいものです。
そのためにも、JPAを使ってみてはいかがでしょうか?