JPA における一対多のリレーションシップ - EclipseLink
EclipseLink 2.6.1 RC1 を使って JPA の一対多リレーションシップを下記 2通りで試し、SQL の実行内容などを調査してみました。
- (a) 単方向: @OneToMany + @JoinColumn
- (b) 双方向: @OneToMany + @ManyToOne
簡単にまとめると次の通りです。
タイプ | 使用したアノテーション | one側のデータ登録SQL | many側のデータ登録SQL |
---|---|---|---|
(a) 単方向 | @OneToMany, @JoinColumn | insert | insert, update |
(b) 双方向 | @OneToMany, @ManyToOne | insert | insert |
(a) の場合に insert だけでは無く update も実施していました。
ソースは http://github.com/fits/try_samples/tree/master/blog/20150720/
はじめに
テーブル構成
今回使用したテーブル構成は以下の通りです。
product と product_variation が一対多の関係になっています。 (product_variation.product_id で product を参照)
product テーブル
id | name | price |
---|---|---|
1 | test1 | 100 |
2 | test2 | 200 |
product_variation テーブル
id | product_id | color | size |
---|---|---|---|
1 | 1 | Green | F |
2 | 1 | Blue | S |
3 | 2 | Red | S |
DDL 文は以下の通りです。
DDL 文
CREATE TABLE `product` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `name` varchar(30) NOT NULL, `price` decimal(10,0) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; CREATE TABLE `product_variation` ( `id` bigint(20) NOT NULL AUTO_INCREMENT, `product_id` bigint(20) NOT NULL DEFAULT 0, `color` varchar(10) NOT NULL, `size` varchar(10) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
(a) の単方向の場合、データ登録時に下記のような挙動となるため、product_id へ外部キー制約を付けたりすると不都合が生じます。 (product_id のデフォルト値を 0 としているのもそのためです)
- (1) product_id を指定せずに product_variation へ insert 文を実行
- (2) update 文で product_id を設定
ビルド定義ファイル
Gradle 用のビルド定義ファイルを以下のようにしました。 動作確認に SampleApp クラスを実行するようになっています。
また、コードを簡素化するため lombok を使っています。
build.gradle
apply plugin: 'application' def enc = 'UTF-8' tasks.withType(AbstractCompile)*.options*.encoding = enc mainClassName = 'sample.SampleApp' repositories { jcenter() } dependencies { compile 'javax:javaee-api:7.0' compile 'org.projectlombok:lombok:1.16.4' runtime 'org.eclipse.persistence:eclipselink:2.6.1-RC1' runtime 'mysql:mysql-connector-java:5.1.36' runtime 'org.slf4j:slf4j-nop:1.7.12' }
JPA 設定ファイル
JPA の設定ファイルは以下のようにしました。
src/main/resources/META-INF/persistence.xml
<persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0"> <persistence-unit name="jpa"> <class>sample.model.Product</class> <class>sample.model.ProductVariation</class> <properties> <property name="javax.persistence.jdbc.driver" value="com.mysql.jdbc.Driver" /> <property name="javax.persistence.jdbc.url" value="jdbc:mysql://localhost:3306/jpa_sample" /> <property name="javax.persistence.jdbc.user" value="root" /> <!-- SQL の内容をログ出力するための設定 --> <property name="eclipselink.logging.level" value="FINE" /> </properties> </persistence-unit> </persistence>
SQL の内容をログ出力 (標準出力) するため eclipselink.logging.level
へ FINE
を設定しています。
(a) 単方向: @OneToMany + @JoinColumn
まずは、@OneToMany
と @JoinColumn
を使った単方向の一対多の関連を実現します。
エンティティクラス
product テーブル用のエンティティクラス Product
内で @OneToMany
と @JoinColumn
を使います。
@JoinColumn
の name
を使って join するテーブル (product_variation) の外部キー項目 (product_id) を指定します。
ちなみに、FetchType
の指定は必須ではありません。 (今回は FetchType.EAGER
時の SQL 内容を確認するため指定しました)
src/main/java/sample/model/Product.java
package sample.model; import lombok.AccessLevel; import lombok.Data; import lombok.Setter; import javax.persistence.*; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; @Data @Entity public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private BigDecimal price; // setter メソッドの未定義化 @Setter(AccessLevel.NONE) // 一対多の関連 @OneToMany(fetch = FetchType.EAGER, cascade= CascadeType.ALL) @JoinColumn(name = "product_id") private List<ProductVariation> variationList = new ArrayList<>(); }
lombok の @Data
を使うと各フィールドの getter・setter メソッドを自動的に定義してくれますが、variationList
の setter メソッドは不要なので @Setter(AccessLevel.NONE)
を使って無効化しています。
また、product_variation テーブルのエンティティクラス ProductVariation
に対する特別な設定は不要です。
src/main/java/sample/model/ProductVariation.java
package sample.model; import lombok.Data; import javax.persistence.*; @Data @Entity @Table(name = "product_variation") public class ProductVariation { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String color; private String size; }
実行クラス
動作確認のための実行クラスです。
2つのバリエーションを追加した商品データを永続化した後、全件検索するようにしてみました。
src/main/java/sample/SampleApp.java
package sample; import lombok.val; import sample.model.Product; import sample.model.ProductVariation; import javax.persistence.Persistence; import java.math.BigDecimal; import java.util.List; public class SampleApp { public static void main(String... args) throws Exception { val emf = Persistence.createEntityManagerFactory("jpa"); val em = emf.createEntityManager(); val tx = em.getTransaction(); tx.begin(); val p1 = product( "sample1", "50", variation("White", "L"), variation("Black", "M") ); // 永続化 em.persist(p1); tx.commit(); val cq = em.getCriteriaBuilder().createQuery(Product.class); // 全件検索 List<Product> res = em.createQuery(cq).getResultList(); // 結果出力 res.forEach(System.out::println); em.close(); } // Product の作成 private static Product product(String name, String price, ProductVariation... variations) { val res = new Product(); res.setName(name); res.setPrice(new BigDecimal(price)); for (val v : variations) { // バリエーションの追加 res.getVariationList().add(v); } return res; } // ProductVariation の作成 private static ProductVariation variation(String color, String size) { val res = new ProductVariation(); res.setColor(color); res.setSize(size); return res; } }
実行
Gradle による実行結果です。
実行結果
> gradle run ・・・ Product(id=1, name=test1, price=100, variationList=[ProductVariation(id=1, color=Green, size=F), ProductVariation(id=2, color=Blue, size=S)]) Product(id=2, name=test2, price=200, variationList=[ProductVariation(id=3, color=Red, size=S)]) Product(id=3, name=sample1, price=50, variationList=[ProductVariation(id=5, color=White, size=L), ProductVariation(id=4, color=Black, size=M)])
SQL の実行内容です。
SQL の実行内容
INSERT INTO PRODUCT (NAME, PRICE) VALUES (?, ?) bind => [sample1, 50] SELECT LAST_INSERT_ID() INSERT INTO product_variation (COLOR, SIZE) VALUES (?, ?) bind => [Black, M] SELECT LAST_INSERT_ID() INSERT INTO product_variation (COLOR, SIZE) VALUES (?, ?) bind => [White, L] SELECT LAST_INSERT_ID() UPDATE product_variation SET product_id = ? WHERE (ID = ?) bind => [3, 5] UPDATE product_variation SET product_id = ? WHERE (ID = ?) bind => [3, 4] ---------- SELECT ID, NAME, PRICE FROM PRODUCT SELECT ID, COLOR, SIZE FROM product_variation WHERE (product_id = ?) bind => [1] SELECT ID, COLOR, SIZE FROM product_variation WHERE (product_id = ?) bind => [2]
特徴は次の通りです。
- product_variation を insert してから update している (update 時に product_id を設定)
- product_variation 1件毎に 1回 update 文を実行している
- product_variation を product_id 毎に検索している
- 永続化した p1 に対しては SQL を実行していない
(b) 双方向: @OneToMany + @ManyToOne
次に@OneToMany
と @ManyToOne
を使った双方向の一対多の関連を実現します。
エンティティクラス
Product
で @OneToMany
、ProductVariation
で @ManyToOne
を用います。
@OneToMany
の mappedBy
を使って @ManyToOne
を付与したフィールド名を指定します。
src/main/java/sample/model/Product.java
package sample.model; import lombok.AccessLevel; import lombok.Data; import lombok.Setter; import javax.persistence.*; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; @Data @Entity public class Product { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private BigDecimal price; @Setter(AccessLevel.NONE) // 一対多の関連 @OneToMany(mappedBy = "product", fetch = FetchType.EAGER, cascade= CascadeType.ALL) private List<ProductVariation> variationList = new ArrayList<>(); }
src/main/java/sample/model/ProductVariation.java
package sample.model; import lombok.Data; import lombok.ToString; import javax.persistence.*; @Data @ToString(exclude = "product") // Product.toString() を呼び出すのを防止 @Entity @Table(name = "product_variation") public class ProductVariation { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String color; private String size; // 多対一の関連 @ManyToOne private Product product; }
ここで、toString
の内容に product
フィールドが含まれると (@Data
によって適用されてしまう) Product
と ProductVariation
の toString
を交互に呼び出し続ける事になってしまうので、@ToString(exclude = "product")
としています。
実行クラス
基本的に (a) と同じですが、ProductVariation
へ Product
を設定しなければならない点が異なります。
mappedBy
で指定しているのだから、自動で設定してくれても良さそうな気がするのですが。 (もしかすると何か方法があるのかもしれません)
src/main/java/sample/SampleApp.java
・・・ public class SampleApp { public static void main(String... args) throws Exception { ・・・ } private static Product product(String name, String price, ProductVariation... variations) { val res = new Product(); res.setName(name); res.setPrice(new BigDecimal(price)); for (val v : variations) { res.getVariationList().add(v); // 下記の設定が必要な点が (a) との違い v.setProduct(res); } return res; } ・・・ }
実行
Gradle による実行結果です。
実行結果
> gradle run ・・・ Product(id=1, name=test1, price=100, variationList=[ProductVariation(id=1, color=Green, size=F), ProductVariation(id=2, color=Blue, size=S)]) Product(id=2, name=test2, price=200, variationList=[ProductVariation(id=3, color=Red, size=S)]) Product(id=3, name=sample1, price=50, variationList=[ProductVariation(id=5, color=White, size=L), ProductVariation(id=4, color=Black, size=M)])
SQL の実行内容です。
SQL の実行内容
INSERT INTO PRODUCT (NAME, PRICE) VALUES (?, ?) bind => [sample1, 50] SELECT LAST_INSERT_ID() INSERT INTO product_variation (COLOR, SIZE, PRODUCT_ID) VALUES (?, ?, ?) bind => [Black, M, 3] SELECT LAST_INSERT_ID() INSERT INTO product_variation (COLOR, SIZE, PRODUCT_ID) VALUES (?, ?, ?) bind => [White, L, 3] SELECT LAST_INSERT_ID() ----- SELECT ID, NAME, PRICE FROM PRODUCT SELECT ID, COLOR, SIZE, PRODUCT_ID FROM product_variation WHERE (PRODUCT_ID = ?) bind => [1] SELECT ID, COLOR, SIZE, PRODUCT_ID FROM product_variation WHERE (PRODUCT_ID = ?) bind => [2]
(a) とは違って product_variation の insert 時に product_id も設定するようになっています。