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.levelFINE を設定しています。

(a) 単方向: @OneToMany + @JoinColumn

まずは、@OneToMany@JoinColumn を使った単方向の一対多の関連を実現します。

エンティティクラス

product テーブル用のエンティティクラス Product 内で @OneToMany@JoinColumn を使います。

@JoinColumnname を使って 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@OneToManyProductVariation@ManyToOne を用います。

@OneToManymappedBy を使って @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 によって適用されてしまう) ProductProductVariationtoString を交互に呼び出し続ける事になってしまうので、@ToString(exclude = "product") としています。

実行クラス

基本的に (a) と同じですが、ProductVariationProduct を設定しなければならない点が異なります。

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 も設定するようになっています。