JavaEE使い方メモ(JPA その2 - マッピング)

  • 104
    いいね
  • 0
    コメント

環境構築
JPA の基本的な話
JPQL の話
Criteria API の話

コード

マッピング方法だけを確認しやすいようにした一覧を作成しました。
JPA マッピングカタログ - Qiita

はじめに

オブジェクト指向で考えられたドメインモデルと、正規化などを考慮して考えられたリレーショナルデータベースのテーブルでは、データの持たせ方に違いが生まれる。
この違いをインピーダンスミスマッチと言う。

インピーダンスミスマッチを解決するには、データベースから取得したレコードをオブジェクトにマッピングする処理が必要になる(さらに、永続化するときは逆変換が必要)。

オブジェクトとテーブルの構造が1対1で対応していれば、この変換はそこまで大変ではない。
しかし、そうでない場合、変換を自力で実装するのは非常に骨が折れる。

O/R マッパーはこの変換を自動でやってくれるフレームワークで、 JPA では、様々なマッピングをアノテーションで定義することができるようになっている。

ドメインモデルの実装に具体的な技術要素である JPA のアノテーションが設定されることに若干の拒絶反応が起こりそうになるが、そこはマッピングを自動化できることとのトレードオフとして目をつむるべきかなと個人的に思っている。

基本編

基本

エンティティ
javaee-jpa.JPG

Reimu.java
package sample.javaee.jpa.entity.mapping;

import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
public class Reimu {
    @Id
    private Long id;

    @Override
    public String toString() {
        return "Reimu{" + "id=" + id + '}';
    }
}

データベース
javaee-jpa.JPG

CREATE TABLE `reimu` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  • デフォルトでは、エンティティは同じ名前のテーブルとマッピングされる。
    • エンティティの名前は、特に指定しなければクラス名と同じになる。
  • エンティティには、必ず1つは @Id でアノテートされたフィールドが必要。
  • エンティティは、 public または protected で、引数の無いコンストラクタが必要。
  • フィールドは、可視性が private でもマッピングできる。
  • デフォルトでは、フィールドは同じ名前のカラムとマッピングされる。

テーブル名・カラム名を指定する

エンティティ
javaee-jpa.JPG

KirisameMarisa.java
package sample.javaee.jpa.entity.mapping;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "kirisame_marisa")
public class KirisameMarisa {
    @Id
    @Column(name = "id_column")
    private Long id;

    @Override
    public String toString() {
        return "KirisameMarisa{" + "id=" + id + '}';
    }
}

データベース
javaee-jpa.JPG

CREATE TABLE `kirisame_marisa` (
  `id_column` int(11) NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`id_column`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  • エンティティ名とテーブル名が異なる場合は、 @Table でクラスをアノテートし、 name 属性でテーブル名を指定する。
  • フィールド名とカラム名が異なる場合は、 @Column でフィールドをアノテートし、 name 属性でカラム名を指定する。

ID が自動生成されることを設定する

例えば、 MySQL の場合は AUTO_INCREMENT の仕組みを使って ID が採番される。
また、 Oracle の場合はシーケンスオブジェクトを使って ID を採番する。

ID がアプリケーション以外の仕組みによって生成されることを設定しておくと、 persist() するときにその仕組を利用して ID が自動生成される。
また、 ID が自動生成されることを設定しておくことで、 persist() した後に自動生成された ID を取得できるようになる(設定していないと、取得できない)。

MySQL の AUTO_INCREMENT

エンティティ
javaee-jpa.JPG

SinGyoku.java
package sample.javaee.jpa.entity.mapping;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class SinGyoku {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String value;

    public void setValue(String value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "SinGyoku{" + "id=" + id + ", value=" + value + '}';
    }
}

データベース
javaee-jpa.JPG

CREATE TABLE `singyoku` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `value` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

動作確認

MappingEjb.java
    SinGyoku sinGyoku = new SinGyoku();
    sinGyoku.setValue("test");

    this.em.persist(sinGyoku);

    System.out.println(sinGyoku);

    this.em.flush();

    System.out.println(sinGyoku);
GlassFishコンソール出力
情報:   SinGyoku{id=null, value=test}
情報:   SinGyoku{id=1, value=test}
  • @GeneratedValue でフィールドをアノテートすることで、そのフィールドが自動生成されることを設定できる。
  • strategy 属性に、その項目がどのようにして生成されるかを指定する。
  • MySQL の AUTO_INCREMENT では、 GenerationType.IDENTITY を指定する。
  • MySQL の AUTO_INCREMENT は INSERT 文を発行するときに ID を採番する。よって EntityManager#flush() で明示的にコミットを実行しないと、自動生成された ID を取得できない。

Oracle のシーケンスオブジェクト

※Oracle に接続するための JDBC Connection Pool と JDBC Resource を予め GlassFish に登録しておくこと(サンプル実装では、 OracleUnit という名前で永続ユニットを定義した)。

エンティティ

javaee-jpa.JPG

package sample.javaee.jpa.entity.mapping;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.SequenceGenerator;
import javax.persistence.Table;

@Entity
@Table(name = "yuugen_magan")
public class YuugenMagan {

    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "yuugen_magan_seq_gen")
    @SequenceGenerator(name = "yuugen_magan_seq_gen", sequenceName = "yuugen_magan_seq", allocationSize = 1)
    private Long id;
    private String value;

    public void setValue(String value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "YuugenMagan{" + "id=" + id + ", value=" + value + '}';
    }
}

データベース

CREATE TABLE yuugen_magan (
    ID NUMBER(10),
    VALUE VARCHAR2(128),
    PRIMARY KEY(ID)
);

CREATE SEQUENCE yuugen_magan_seq;

動作確認

MappingEjb.java
    YuugenMagan yuugenMagan = new YuugenMagan();
    yuugenMagan.setValue("test");

    this.em.persist(yuugenMagan);
    System.out.println(yuugenMagan);
GlassFishコンソール出力
情報:   YuugenMagan{id=1, value=test}
  • シーケンスオブジェクトを使用する場合は、 @GeneratedValuestrategyGenerationType.SEQUENCE を指定する。
    • generator 属性には、 ID を生成するジェネレータの名前を指定する。
    • ジェネレータの定義は、 @SequenceGenerator アノテーションで行う。
    • name 属性にジェネレータの名前を指定し、 sequenceName 属性でシーケンスオブジェクトの名前を指定する。
  • シーケンスオブジェクトの場合は、コミット前でも EntityManager が次のシーケンス値を取得できるので、 flush() する前でも自動生成された ID を参照することができる。
  • allocationSize はシーケンス値の増分(デフォルトは 50)。

シーケンス生成用のテーブルを使用する

エンティティ

javaee-jpa.JPG

Elis.java
package sample.javaee.jpa.entity.mapping;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.TableGenerator;

@Entity
public class Elis {

    @Id
    @GeneratedValue(strategy = GenerationType.TABLE, generator = "elis_seq_generator")
    @TableGenerator(name = "elis_seq_generator",
                    table = "seq_generator",
                    pkColumnName = "table_name",
                    valueColumnName = "sequence_number",
                    pkColumnValue = "elis",
                    allocationSize = 1)
    private Long id;
    private String value;

    public void setValue(String value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "Elis{" + "id=" + id + ", value=" + value + '}';
    }
}

データベース
javaee-jpa.JPG

javaee-jpa.JPG

CREATE TABLE `elis` (
  `id` int(11) NOT NULL,
  `value` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `seq_generator` (
  `table_name` varchar(128) NOT NULL,
  `sequence_number` int(11) NOT NULL,
  PRIMARY KEY (`table_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

動作確認

MappingEjb.java
    Elis elis = new Elis();
    elis.setValue("test");

    this.em.persist(elis);
    System.out.println(elis);
GlassFishコンソール出力
情報:   Elis{id=1, value=test}
  • シーケンスを採番するための専用のテーブルを使用する場合、 strategyGenerationType.TABLE を指定する。
  • generator には、 @TableGenerator で定義した名前を指定する。
  • @TableGenerator では、シーケンス採番用のテーブルの情報を定義する。
属性 説明
name ジェネレータの名前
table シーケンスを採番するテーブルの名前
pkColumnName シーケンス採番テーブルの行を特定するためのカラムの名前
valueColumnName シーケンス値を保存しておくカラムの名前
pkColumnValue このジェネレータが採番に用いる行を特定するための値。pkColumnName に保存されている値。
allocationSize シーケンスの増分値(デフォルトは 50)

複合主キーとマッピングする

@EmbeddedId を使用したパターン

エンティティ

javaee-jpa.JPG

Sariel.java
package sample.javaee.jpa.entity.mapping;

import javax.persistence.EmbeddedId;
import javax.persistence.Entity;

@Entity
public class Sariel {

    @EmbeddedId
    private SarielKey key;
    private String value;

    public void setKey(SarielKey key) {
        this.key = key;
    }

    public void setValue(String value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "Sariel{" + "key=" + key + ", value=" + value + '}';
    }
}
SarielKey.java
package sample.javaee.jpa.entity.mapping;

import java.io.Serializable;
import java.util.Objects;
import javax.persistence.Embeddable;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;

@Embeddable
public class SarielKey implements Serializable {

    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long key1;
    private String key2;

    public void setKey2(String key2) {
        this.key2 = key2;
    }

    @Override
    public String toString() {
        return "SarielKey{" + "key1=" + key1 + ", key2=" + key2 + '}';
    }

    @Override
    public int hashCode() {
        // 省略
    }

    @Override
    public boolean equals(Object obj) {
        // 省略
    }
}

データベース
javaee-jpa.JPG

CREATE TABLE `sariel` (
  `key1` int(11) NOT NULL AUTO_INCREMENT,
  `key2` varchar(32) NOT NULL,
  `value` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`key1`,`key2`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

動作確認

MappingEjb.java
    SarielKey key = new SarielKey();
    key.setKey2("test");

    Sariel sariel = new Sariel();
    sariel.setKey(key);
    sariel.setValue("TEST");

    this.em.persist(sariel);

    this.em.flush();

    System.out.println(sariel);
GlassFishコンソール出力
情報:   Sariel{key=SarielKey{key1=1, key2=test}, value=TEST}
  • 複合主キーをマッピングする場合の1つの方法として、 @EmbeddedId アノテーションを使用した方法がある。
  • 複合主キーに指定したカラムを持つ主キー用のクラス(SarielKey)を作成し、そのクラスを @Embeddable でアノテートする。
  • メインのエンティティ側(Sariel)には、主キー用のクラスをフィールドに持たせ、 @EmbeddedId でアノテートする。
  • 主キーの一意性を保証するため、 equals()hashCode() をオーバーライドする(コードは NetBeans の機能で自動生成したもの)。

equals() と hashCode() をオーバーライドする理由

デフォルトの equals() メソッドは、インスタンスが同じかどうかを比較するようになっている。

つまり、キー項目に同じ値が設定されていても、インスタンスが異なれば equals() メソッドは false を返してしまう。
そうなると、同じキー項目をもつ異なるエンティティオブジェクトが生成される可能性が生まれ、主キーの一意性が保証されなくなる。

このため、キー項目を持つクラスは equals() メソッドをオーバーライドしなければならない。
そして、 equals() メソッドをオーバーライドするので、合わせて hashCode() もオーバーライドしなければならない(詳しくは Effective Java を参照)。

@IdClass を使用したパターン

エンティティ

javaee-jpa.JPG

Mima.java
package sample.javaee.jpa.entity.mapping;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.IdClass;

@Entity
@IdClass(MimaKey.class)
public class Mima {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long key1;
    @Id
    private String key2;
    private String value;

    public void setKey2(String key2) {
        this.key2 = key2;
    }

    public void setValue(String value) {
        this.value = value;
    }

    @Override
    public String toString() {
        return "Mima{" + "key1=" + key1 + ", key2=" + key2 + ", value=" + value + '}';
    }
}
MimaKey.java
package sample.javaee.jpa.entity.mapping;

import java.io.Serializable;
import java.util.Objects;

public class MimaKey implements Serializable {

    private Long key1;
    private String key2;

    @Override
    public int hashCode() {
        // 省略
    }

    @Override
    public boolean equals(Object obj) {
        // 省略
    }
}

データベース

javaee-jpa.JPG

CREATE TABLE `mima` (
  `key1` int(11) NOT NULL AUTO_INCREMENT,
  `key2` varchar(45) NOT NULL,
  `value` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`key1`,`key2`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

動作確認

    Mima mima = new Mima();

    mima.setKey2("key2");
    mima.setValue("test");

    this.em.persist(mima);

    this.em.flush();

    System.out.println(mima);
GlassFishコンソール出力
情報:   Mima{key1=2, key2=key2, value=test}
  • @IdClass を使うことで複合主キーをマッピングすることもできる。
  • この場合、主キーの項目はメインとなるエンティティ側(Hoge)に定義し、 @Id でアノテートする。
  • @IdClass に指定したクラスにはアノテーションを一切付与せずに複合主キーを指定できる。
    • つまり、複合主キーが既存のクラスで、自由にコードを書き変えられないときとかに使えるということか?
    • でも、 equals()hashCode() が正しくオーバーライドされていないと、主キーの一意性が担保されなくなるので注意が必要そう。

日付項目をマッピングする

エンティティ

javaee-jpa.JPG

Kikuri.java
package sample.javaee.jpa.entity.mapping;

import java.util.Date;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Temporal;
import javax.persistence.TemporalType;

@Entity
public class Kikuri {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Temporal(TemporalType.DATE)
    private Date date;
    @Temporal(TemporalType.TIME)
    private Date time;
    @Temporal(TemporalType.TIMESTAMP)
    private Date datetime;

    public void setDate(Date date) {
        this.date = this.time = this.datetime = date;
    }
}

データベース

javaee-jpa.JPG

CREATE TABLE `kikuri` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `date` date DEFAULT NULL,
  `time` time DEFAULT NULL,
  `datetime` datetime DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

動作確認

    Kikuri kikuri = new Kikuri();
    kikuri.setDate(new Date());

    this.em.persist(kikuri);

プログラム実行後のテーブルの状態。

javaee-jpa.JPG

  • 日付項目をマッピングする場合は、 @Temporal でフィールドをアノテートする。
  • @Temporal の値には、データベース上の詳細な型(DATE, TIME, TIMESTAMP)を指定する。
    • DATE は日付だけ。
    • TIME は時刻だけ。
    • TIMESTAMP は日付と時刻の両方。

Java8 で追加された LocalTime とかは使える?

コンバーターを自作すれば使えるっぽい?

Using the Java 8 DateTime Classes with JPA! | Java.net

特定のフィールドを永続化の対象外にする

エンティティ

javaee-jpa.JPG

Konngara.java
package sample.javaee.jpa.entity.mapping;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Transient;

@Entity
public class Konngara {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String value;
    @Transient
    private String ignore;

    public void setValue(String value) {
        this.value = value;
    }

    public void setIgnore(String ignore) {
        this.ignore = ignore;
    }
}

データベース

javaee-jpa.JPG

CREATE TABLE `konngara` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `value` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
  • @Transient でフィールドをアノテートすると、そのフィールドは永続化の対象外になり、マッピングでは無視される。

列挙型をマッピングする

デフォルトの動き

エンティティ

javaee-jpa.JPG

Genji.java
package sample.javaee.jpa.entity.mapping;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Genji {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name = "enum_value")
    private GenjiEnum enumValue;

    public void setEnumValue(GenjiEnum enumValue) {
        this.enumValue = enumValue;
    }
}
GenjiEnum.java
package sample.javaee.jpa.entity.mapping;

public enum GenjiEnum {
    HOGE, FUGA, PIYO;
}

データベース

javaee-jpa.JPG

CREATE TABLE `genji` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `enum_value` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

動作確認

MappingEjb.java
    Genji genji = new Genji();
    genji.setEnumValue(GenjiEnum.HOGE);

    this.em.persist(genji);

    genji = new Genji();
    genji.setEnumValue(GenjiEnum.FUGA);

    this.em.persist(genji);

プログラム実行後のテーブルの状態。

javaee-jpa.JPG

  • enum の項目は、デフォルトでは対応する序数が保存される。
  • しかし、これだと列挙子の順番を入れ替えた途端にデータが正しくマッピングされなくなる。

文字列でマッピングする

エンティティ

javaee-jpa.JPG

Rika.java
package sample.javaee.jpa.entity.mapping;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.EnumType;
import javax.persistence.Enumerated;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Rika {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    @Column(name = "enum_value")
    @Enumerated(EnumType.STRING)
    private RikaEnum enumValue;

    public void setEnumValue(RikaEnum enumValue) {
        this.enumValue = enumValue;
    }
}

データベース

javaee-jpa.JPG

CREATE TABLE `rika` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `enum_value` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

動作確認

MappingEjb.java
    Rika rika = new Rika();
    rika.setEnumValue(RikaEnum.HOGE);

    this.em.persist(rika);

    rika = new Rika();
    rika.setEnumValue(RikaEnum.FUGA);

    this.em.persist(rika);

プログラムを実行後のテーブルの様子.

javaee-jpa.JPG

  • @Enumerated でアノテートして、 EnumType.STRING を指定すると、序数ではなく文字列で enum の情報を記録できる。
  • これなら、列挙子の順番を書き変えてもデータマッピングで不整合が起こることはない。

基本型のコレクションのマッピング

エンティティ

javaee-jpa.JPG

Meira.java
package sample.javaee.jpa.entity.mapping;

import java.util.List;
import javax.persistence.CollectionTable;
import javax.persistence.Column;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OrderBy;

@Entity
public class Meira {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ElementCollection(fetch = FetchType.EAGER)
    @CollectionTable(name = "meira_list_value")
    @Column(name = "value")
    @OrderBy("DESC")
    private List<String> list;

    @Override
    public String toString() {
        return "Meira{" + "id=" + id + ", list=" + list + '}';
    }
}

データベース

javaee-jpa.JPG

javaee-jpa.JPG

javaee-jpa.JPG

CREATE TABLE `meira` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `meira_list_value` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `meira_id` int(11) NOT NULL,
  `value` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `meira_fk_idx` (`meira_id`),
  CONSTRAINT `meira_fk` FOREIGN KEY (`meira_id`) REFERENCES `meira` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

動作確認

MappingEjb.java
    Meira meira = this.em.find(Meira.class, 1L);
    System.out.println(meira);
GlassFishコンソール出力
情報:   Meira{id=1, list=[zzz, yyy, xxx]}
  • @ElementCollection でコレクション型のフィールドをアノテートすると、そのフィールドが基本型のコレクションであることを定義できる。
    • コレクション型の対象は、 Collection, Set, List
    • 基本型とは、エンティティではない型のこと。プリミティブ型、 String 型、組み込み可能クラス(詳細後述)などのことを指す。
    • fetch 属性に FetchType.EAGER を指定することで、即時ロードさせることができる(詳細後述)。
  • @CollectionTable アノテーションで、コレクションの各要素を取得するためのテーブルを指定する。
    • 省略した場合は、「エンティティ名_フィールド名(meira_list)」で解決される。
  • @Column アノテーションで、どのカラムの値をコレクションの要素として取得するのかを指定する。
    • 省略した場合は、「フィールド名(list)」で解決される。
  • ソート条件を指定したい場合は、 @OrderBy でフィールドをアノテートする。

結合に用いるカラム名の解決

デフォルトの場合、エンティティ名_エンティティのキープロパティ名 で解決される。
つまり、前述のテーブルの場合は Meira_id で解決される。

meira_list_value テーブルと meria テーブルと結合するときのカラム名は、ちょうどこのデフォルト値と同じになっている(meria_id)。
そのため、特に指定なしで動作している。

もしデフォルト値と実際のカラム名が異なる場合(仮に meria_table_id とした場合)は、以下のように設定する。

    @ElementCollection(fetch = FetchType.EAGER)
-   @CollectionTable(name = "meira_list_value")
+   @CollectionTable(
+       name = "meira_list_value",
+       joinColumns = @JoinColumn(name = "meria_table_id")    
+   )
    @Column(name = "value")
    @OrderBy("DESC")
    private List<String> list;
  • @CollectionTable アノテーションの joinColumns 属性で設定する。

即時ロードと遅延ロード

エンティティが他のクラスをコレクション(ListSet)で持っている場合、デフォルトで遅延ロードが適用される。

遅延ロードの場合、エンティティが EntityManager#find(Class, Object) などで DB からロードされた時点では、まだその項目は DB から取得されていない。
値が取得されるのは、そのコレクションから実際に値を取得しようとしたときになる。

前述の Meira クラスに、 lazyList という遅延ロードするリストを追加して、動作を確認してみる。

Meira.java
package sample.javaee.jpa.entity.mapping;

import java.util.List;
import javax.persistence.CollectionTable;
import javax.persistence.Column;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OrderBy;

@Entity
public class Meira {

    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ElementCollection(fetch = FetchType.EAGER)
    @CollectionTable(name = "meira_list_value")
    @Column(name = "value")
    @OrderBy("DESC")
    private List<String> list;

+   @ElementCollection
+   @CollectionTable(name = "meira_list_value")
+   @Column(name = "value")
+   public List<String> lazyList;

    @Override
    public String toString() {
-       return "Meira{" + "id=" + id + ", list=" + list}';
+       return "Meira{" + "id=" + id + ", list=" + list + ", lazyList=" + lazyList + '}';
    }
}

動作確認

MappingEjb.java
    Meira meira = this.em.find(Meira.class, 1L);
    System.out.println(meira);

+   for (String value : meira.lazyList) {
+       System.out.println(value);
+   }
GlassFishコンソール出力
情報:   Meira{id=1, list=[zzz, yyy, xxx], lazyList={IndirectList: not instantiated}}
情報:   xxx
情報:   yyy
情報:   zzz
  • 遅延ロードの場合、 list をただ toString() しただけでは、値が設定されていないのが分かる。
  • しかし、拡張 for 文などを使って List から値を取得すると、実際の値が取れるようになる。

それぞれのメリットとデメリット

- メリット デメリット
即時ロード 検索に時間がかかる場合は、予めロードしておくことができる。 結局使用しなくても読み込んでしまう。
遅延ロード DB とのアクセスを必要なときだけに限定できる。 予めロードしておけない。

基本は遅延ロード(デフォルト)で、実際に動かしてみたらパフォーマンスに影響があった場合は即時ロードにする、くらいの方針でよいかと。
よく言われているが、動かしてもないのにパフォーマンスを気にして無駄に設定やモデルをいじくりまわしてしまう、というのは避けた方がいい。

また、 JPA 2.1 (Java EE 7) では、 Entity Graph という仕組みが追加されている。これを利用すると、エンティティ間の関連をどこまで掘り下げて一度に読み込むかなどを定義することができる(詳細後述)。

ソート用のカラムを使用する

エンティティ

javaee-jpa.JPG

Shinki.java
package sample.javaee.jpa.entity.mapping;

import java.util.List;
import javax.persistence.Column;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.OrderColumn;

@Entity
public class Shinki {
    @Id
    private Long id;

    @ElementCollection(fetch = FetchType.EAGER)
    @Column(name = "value")
    @OrderColumn(name = "order")
    private List<String> list;

    @Override
    public String toString() {
        return "Shinki{" + "id=" + id + ", list=" + list + '}';
    }
}

データベース

javaee-jpa.JPG

javaee-jpa.JPG

javaee-jpa.JPG

CREATE TABLE `shinki` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

CREATE TABLE `shinki_list` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `shinki_id` int(11) NOT NULL,
  `order` int(11) NOT NULL,
  `value` varchar(45) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `shinki_list_fk_idx` (`shinki_id`),
  CONSTRAINT `shinki_list_fk` FOREIGN KEY (`shinki_id`) REFERENCES `shinki` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;

動作確認

MappingEjb.java
 Shinki shinki = this.em.find(Shinki.class, 1L);
 System.out.println(shinki);
GlassFishコンソール出力
情報: Shinki{id=1, list={[piyo, fuga, hoge]}}
  • @OrderColumn アノテーションを使用することで、ソートのキーとなるカラムを指定することができる。

Map にマッピングする

エンティティ

javaee-jpa.JPG

Eren.java
package sample.javaee.jpa.entity.mapping;

import java.util.Map;
import javax.persistence.CollectionTable;
import javax.persistence.Column;
import javax.persistence.ElementCollection;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.MapKeyColumn;

@Entity
public class Eren {

 @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
 private Long id;

 @ElementCollection(fetch = FetchType.EAGER)
 @CollectionTable(name = "eren_map_value")
 @MapKeyColumn(name = "key")
 @Column(name = "value")
 private Map<String, String> map;

 @Override
 public String toString() {
 return "Eren{" + "id=" + id + ", map=" + map + '}';
 }
}

データベース

javaee-jpa.JPG

javaee-jpa.JPG

javaee-jpa.JPG

CREATE TABLE `eren` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

CREATE TABLE `eren_map_value` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `eren_id` int(11) NOT NULL,
 `key` varchar(45) NOT NULL,
 `value` varchar(45) DEFAULT NULL,
 PRIMARY KEY (`id`),
 KEY `eren_map_velu_fk_idx` (`eren_id`),
 CONSTRAINT `eren_map_velu_fk` FOREIGN KEY (`eren_id`) REFERENCES `eren` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

動作確認

MappingEjb.java
 Eren eren = this.em.find(Eren.class, 1L);

 System.out.println(eren);
GlassFishコンソール
情報: Eren{id=1, map={fuga=FUGA, piyo=PIYO, hoge=HOGE}}
  • @MapKeyColumn でアノテートすることで、 Map のキーバリューと DB の値をマッピングすることができる。
  • @MapKeyColumnname には、 Map のキーとなる値を持つカラム名を指定する。
  • それ以外は、コレクションのときと同じ要領で設定する。
  • String 以外にも、他のエンティティなどをキーやバリューに指定することもできる。

非エンティティクラスをマッピングする

エンティティ

javaee-jpa.JPG

Kotohime.java
package sample.javaee.jpa.entity.mapping;

import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Kotohime {

 @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
 private Long id;
 private String value;
 @Embedded
 private EmbeddedKotohime embeddedKotohime;

 @Override
 public String toString() {
 return "Kotohime{" + "id=" + id + ", value=" + value + ", embeddedKotohime=" + embeddedKotohime + '}';
 }
}
EmbeddedKotohime.java
package sample.javaee.jpa.entity.mapping;

import javax.persistence.Column;
import javax.persistence.Embeddable;

@Embeddable
public class EmbeddedKotohime {

 @Column(name = "embedded_value")
 private String embeddedValue;

 @Override
 public String toString() {
 return "EmbeddedKotohime{" + "embeddedValue=" + embeddedValue + '}';
 }
}

データベース

javaee-jpa.JPG

javaee-jpa.JPG

CREATE TABLE `kotohime` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `value` varchar(45) DEFAULT NULL,
 `embedded_value` varchar(45) DEFAULT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

動作確認

MappingEjb.java
 Kotohime kotohime = this.em.find(Kotohime.class, 1L);

 System.out.println(kotohime);
GlassFishコンソール
情報: Kotohime{id=1, value=hoge, embeddedKotohime=EmbeddedKotohime{embeddedValue=HOGE}}
  • @Embeddable でクラスをアノテートすることで、非エンティティクラスをエンティティにマッピングすることができるようになる。
  • @Embeddable でアノテートしたクラスを、 組み込み可能クラス と呼ぶ。
  • 組み込み可能クラスは ID を持たない = 識別する必要がない、すなわち DDD の ValueObject に該当する。
  • エンティティ側では、 @Embedded を使ってフィールドが組み込み可能クラスであることを指定する。

組み込み可能クラスの List をマッピングする

エンティティ

javaee-jpa.JPG

KanaAnaberaru.java
package sample.javaee.jpa.entity.mapping;

import java.util.List;
import javax.persistence.CollectionTable;
import javax.persistence.ElementCollection;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.Table;

@Entity
@Table(name = "kana_anaberaru")
public class KanaAnaberaru {
 @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
 private Long id;

 @Embedded
 @ElementCollection(fetch = FetchType.EAGER)
 @CollectionTable(
 name = "kana_anaberaru_list_value",
 joinColumns = @JoinColumn(name = "kana_anaberaru_id")
 )
 private List<EmbeddedKanaAnaberaru> embeddedKanaAnaberaruList;

 @Override
 public String toString() {
 return "KanaAnaberaru{" + "id=" + id + ", embeddedKanaAnaberaruList=" + embeddedKanaAnaberaruList + '}';
 }
}
EmbeddedKanaAnaberaru.java
package sample.javaee.jpa.entity.mapping;

import javax.persistence.Column;
import javax.persistence.Embeddable;

@Embeddable
public class EmbeddedKanaAnaberaru {
 @Column(name = "embedded_value")
 private String embeddedValue;

 @Override
 public String toString() {
 return "EmbeddedKanaAnaberaru{" + "embeddedValue=" + embeddedValue + '}';
 }
}

データーベース

javaee-jpa.JPG

javaee-jpa.JPG

javaee-jpa.JPG

CREATE TABLE `kana_anaberaru` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

CREATE TABLE `kana_anaberaru_list_value` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `kana_anaberaru_id` int(11) NOT NULL,
 `embedded_value` varchar(45) DEFAULT NULL,
 PRIMARY KEY (`id`),
 KEY `kana_anaberaru_list_value_fk_idx` (`kana_anaberaru_id`),
 CONSTRAINT `kana_anaberaru_list_value_fk` FOREIGN KEY (`kana_anaberaru_id`) REFERENCES `kana_anaberaru` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;

動作確認

MappingEjb.java
 KanaAnaberaru kotohime = this.em.find(KanaAnaberaru.class, 1L);

 System.out.println(kotohime);
GlassFishコンソール出力
情報: KanaAnaberaru{id=1, embeddedKanaAnaberaruList=[EmbeddedKanaAnaberaru{embeddedValue=hoge}, EmbeddedKanaAnaberaru{embeddedValue=fuga}, EmbeddedKanaAnaberaru{embeddedValue=piyo}]}
  • @Embedded アノテーションをフィールドに追加すれば、組み込み可能クラスを List でマッピングすることができる。

組み込み可能クラスを Map にマッピングする

エンティティ

javaee-jpa.JPG

AsakuraRikako.java
package sample.javaee.jpa.entity.mapping;

import java.util.Map;
import javax.persistence.CollectionTable;
import javax.persistence.ElementCollection;
import javax.persistence.Embedded;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.Table;

@Entity
@Table(name = "asakura_rikako")
public class AsakuraRikako {

 @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
 private Long id;

 @Embedded
 @ElementCollection(fetch = FetchType.EAGER)
 @CollectionTable(
 name = "asakura_rikako_map_values",
 joinColumns = @JoinColumn(name = "asakura_rikako_id")
 )
 private Map<AsakuraRikakoKey, AsakuraRikakoValue> map;

 @Override
 public String toString() {
 return "AsakuraRikako{" + "id=" + id + ", map=" + map + '}';
 }
}
AsakuraRikakoKey.java
package sample.javaee.jpa.entity.mapping;

import java.util.Objects;
import javax.persistence.Column;
import javax.persistence.Embeddable;

@Embeddable
public class AsakuraRikakoKey {
 @Column(name = "embedded_key")
 private String key;

 @Override
 public String toString() {
 return "AsakuraRikakoKey{" + "key=" + key + '}';
 }

 @Override
 public int hashCode() {
 // NetBeans で自動生成
 }

 @Override
 public boolean equals(Object obj) {
 // NetBeans で自動生成
 }
}
AsakuraRikakoValue.java
package sample.javaee.jpa.entity.mapping;

import javax.persistence.Column;
import javax.persistence.Embeddable;

@Embeddable
public class AsakuraRikakoValue {
 @Column(name = "embedded_value")
 private String value;

 @Override
 public String toString() {
 return "AsakuraRikakoValue{" + "value=" + value + '}';
 }
}

データベース

javaee-jpa.JPG

javaee-jpa.JPG

javaee-jpa.JPG

CREATE TABLE `asakura_rikako` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

CREATE TABLE `asakura_rikako_map_values` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `asakura_rikako_id` int(11) NOT NULL,
 `embedded_key` varchar(45) NOT NULL,
 `embedded_value` varchar(45) DEFAULT NULL,
 PRIMARY KEY (`id`),
 KEY `asakura_rikako_map_values_fk_idx` (`asakura_rikako_id`),
 CONSTRAINT `asakura_rikako_map_values_fk` FOREIGN KEY (`asakura_rikako_id`) REFERENCES `asakura_rikako` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;

動作確認

MappingEjb.java
 AsakuraRikako asakuraRikako = this.em.find(AsakuraRikako.class, 1L);

 System.out.println(asakuraRikako);
GlassFishコンソール出力
情報: AsakuraRikako{id=1, map={AsakuraRikakoKey{key=fuga}=AsakuraRikakoValue{value=FUGA}, AsakuraRikakoKey{key=piyo}=AsakuraRikakoValue{value=PIYO}, AsakuraRikakoKey{key=hoge}=AsakuraRikakoValue{value=HOGE}}}
  • @Embedded をフィールドに追加することで、組み込み可能クラスを Map のキーやバリューとして使用することができる。

Lob カラムをマッピングする

エンティティ

javaee-jpa.JPG

PatchouliKnowledge.java
package sample.javaee.jpa.entity.mapping;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Lob;
import javax.persistence.Table;

@Entity
@Table(name = "patchouli_knowledge")
public class PatchouliKnowledge {
 @Id
 private Long id;
 @Lob
 @Column(name = "blob_value")
 private byte[] blobValue;
 @Lob
 @Column(name = "clob_value")
 private String clobValue;

 public void setBlobValue(byte[] blobValue) {
 this.blobValue = blobValue;
 }

 public void setClobValue(String clobValue) {
 this.clobValue = clobValue;
 }
}

データベース

javaee-jpa.JPG

CREATE TABLE `patchouli_knowledge` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `blob_value` blob,
 `clob_value` longtext,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

動作確認

MappingEjb.java
 PatchouliKnowledge patchouliKnowledge = new PatchouliKnowledge();
 patchouliKnowledge.setBlobValue("blob text".getBytes(Charset.forName("UTF-8")));
 patchouliKnowledge.setClobValue("clob text");

 this.em.persist(patchouliKnowledge);

実行後のデータベースの様子。

javaee-jpa.JPG

  • @Lob でフィールドをアノテートすると、 LOB 型のデータとしてマッピングできる。
  • BLOB にマッピングされるか、 CLOB にマッピングされるかは、フィールドの型によって以下のように決定される。
    • byte[]Byte[]Serializable の場合は BLOB。
    • char[]Character[]String の場合は CLOB。

リレーションのマッピング

複数のエンティティが関連を持つ場合の、 DB とのマッピング方法について。

1対1

単方向

エンティティ

javaee-jpa.JPG

KitashirakawaChiyuri.java
package sample.javaee.jpa.entity.mapping;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.Table;

@Entity
@Table(name = "kitashirakawa_chiyuri")
public class KitashirakawaChiyuri {
 @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
 private Long id;
 @JoinColumn(name = "okazaki_yumemi_id")
 private OkazakiYumemi okazakiYumemi;

 @Override
 public String toString() {
 return "KitashirakawaChiyuri{" + "id=" + id + ", okazakiYumemi=" + okazakiYumemi + '}';
 }
}
OkazakiYumemi.java
package sample.javaee.jpa.entity.mapping;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "okazaki_yumemi")
public class OkazakiYumemi {
 @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
 private Long id;

 @Override
 public String toString() {
 return "OkazakiYumemi{" + "id=" + id + '}';
 }
}

データベース

javaee-jpa.JPG

javaee-jpa.JPG

javaee-jpa.JPG

CREATE TABLE `kitashirakawa_chiyuri` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `okazaki_yumemi_id` int(11) NOT NULL,
 PRIMARY KEY (`id`),
 KEY `kitashirakawa_chiyuri_fk_idx` (`okazaki_yumemi_id`),
 CONSTRAINT `kitashirakawa_chiyuri_fk` FOREIGN KEY (`okazaki_yumemi_id`) REFERENCES `okazaki_yumemi` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

CREATE TABLE `okazaki_yumemi` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

動作確認

MappingEjb.java
 KitashirakawaChiyuri kitashirakawaChiyuri = this.em.find(KitashirakawaChiyuri.class, 1L);

 System.out.println(kitashirakawaChiyuri);
GlassFishコンソール出力
情報: KitashirakawaChiyuri{id=1, okazakiYumemi=OkazakiYumemi{id=1}}
  • エンティティ間の多重度が1対1で、関連の向きが単方向の場合は、特に何も指定することなくマッピングすることができる。
  • ただし、結合に用いられるカラム名が、デフォルトでは フィールド名_相手のキーカラム名 で解決される(okazakiYumemi_id)ので、 @JoinColumn アノテーションを使って結合に用いるカラム名を明示している。
  • デフォルトのフェッチ方法は EAGER になる。これを変更したい場合は、 @OneToOne(fetch = FetchType.LAZY) の指定を追加する。

双方向

エンティティ

javaee-jpa.JPG

Orange.java
package sample.javaee.jpa.entity.mapping;

import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
public class Orange {
 @Id
 private Long id;

 private Kurumi kurumi;

 public Long getId() {
 return id;
 }

 @Override
 public String toString() {
 return "Orange{" + "id=" + id + ", kurumi.id=" + kurumi.getId() + '}';
 }
}
Kurumi.java
package sample.javaee.jpa.entity.mapping;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.OneToOne;

@Entity
public class Kurumi {
 @Id
 private Long id;

 @OneToOne(mappedBy = "kurumi")
 private Orange orange;

 public Long getId() {
 return id;
 }

 @Override
 public String toString() {
 return "Kurumi{" + "id=" + id + ", orange.id=" + orange.getId() + '}';
 }
}

データベース

javaee-jpa.JPG

javaee-jpa.JPG

javaee-jpa.JPG

CREATE TABLE `kurumi` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

CREATE TABLE `orange` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `kurumi_id` int(11) NOT NULL,
 PRIMARY KEY (`id`),
 KEY `orange_fk_idx` (`kurumi_id`),
 CONSTRAINT `orange_fk` FOREIGN KEY (`kurumi_id`) REFERENCES `kurumi` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

動作確認

MappingEjb.java
 Orange orange = this.em.find(Orange.class, 1L);
 System.out.println(orange);

 Kurumi kurumi = this.em.find(Kurumi.class, 2L);
 System.out.println(kurumi);
GlassFishコンソール出力
情報: Orange{id=1, kurumi.id=1}
情報: Kurumi{id=2, orange.id=2}
  • DB のテーブル上で、関連するエンティティを識別するためのカラムを持っている方のエンティティを 所有者 と呼ぶ。
    • もしくは、エンティティ A がエンティティ B の参照を持っている場合、 A を所有者と呼ぶ。
  • 逆側のエンティティを、 被所有者 と呼ぶ。
    • 上記例でいうと、 Orange が所有者で、 Kurumi が被所有者となる。
  • しかしエンティティが相互参照していると、それだけではどちらが所有者でどちらが被所有者なのか判断することができない。
  • そこで、 Kurumi が被所有者であることを明示するために、 @OneToOne アノテーションの mappedBy 属性を使用する。
    • mappedBy には、所有者が持っている被所有者を指すプロパティ名を指定する(上記例の場合は、 kurumi)。

そもそも相互参照するようなクラス設計はダメじゃね?

1対多

単方向

結合テーブルを使用する

エンティティ

javaee-jpa.JPG

Elliy.java
package sample.javaee.jpa.entity.mapping;

import java.util.List;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.OneToMany;

@Entity
public class Elliy {
 @Id
 private Long id;

 @OneToMany(fetch = FetchType.EAGER)
 @JoinTable(inverseJoinColumns = @JoinColumn(name = "yuuka_id"))
 private List<Yuuka> yuukaList;

 @Override
 public String toString() {
 return "Elliy{" + "id=" + id + ", yuukaList=" + yuukaList + '}';
 }
}
Yuuka.java
package sample.javaee.jpa.entity.mapping;

import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
public class Yuuka {
 @Id
 private Long id;

 @Override
 public String toString() {
 return "Yuuka{" + "id=" + id + '}';
 }
}

データベース

javaee-jpa.JPG

javaee-jpa.JPG

javaee-jpa.JPG

javaee-jpa.JPG

CREATE TABLE `elliy` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

CREATE TABLE `yuuka` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8;

CREATE TABLE `elliy_yuuka` (
 `elliy_id` int(11) NOT NULL,
 `yuuka_id` int(11) NOT NULL,
 PRIMARY KEY (`elliy_id`,`yuuka_id`),
 KEY `elliy_yuuka_fk1_idx` (`elliy_id`),
 KEY `elliy_yuuka_fk2_idx` (`yuuka_id`),
 CONSTRAINT `elliy_yuuka_fk1` FOREIGN KEY (`elliy_id`) REFERENCES `elliy` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION,
 CONSTRAINT `elliy_yuuka_fk2` FOREIGN KEY (`yuuka_id`) REFERENCES `yuuka` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

動作確認

MappingEjb.java
 Elliy elliy = this.em.find(Elliy.class, 1L);
 System.out.println(elliy);
GlassFishコンソール出力
情報: Elliy{id=1, yuukaList=[Yuuka{id=1}, Yuuka{id=2}, Yuuka{id=3}, Yuuka{id=4}]}
  • 関連テーブルを使って1対多のエンティティをマッピングするケース。
  • デフォルトでは、以下のようにテーブル名やカラム名が解決される。
対象 デフォルト設定で解決された場合
関連テーブル名 所有者エンティティ名_被所有者エンティティ名 Elliy_Yuuka
所有者を結合するためのカラム名 所有者エンティティ名_ID項目名 Elliy_id
被所有者を結合するためのカラム名 プロパティ名_ID項目名 yuukaList_id
  • 関連テーブルの名前と所有者側を結合するためのカラム名は、デフォルトのままでも問題ない。
  • しかし、被所有者側を結合するためのカラム名が、デフォルトのままだと問題がある。
  • そこで、 @JoinTable アノテーションの inverseJoinColumns 属性を使ってカラム名を明示的に指定している。
  • 今回は必要なかったが、関連テーブル名や所有者を結合するためのカラム名を指定する場合は、以下のようにする。
 @JoinTable(
 name = "relation_table_name",
 joinColumns = @JoinColumn(name = "join_column")
 )
 private List<Yuuka> yuukaList;
  • name 属性で、関連テーブルの名前を指定する。
  • joinColumns 属性で、所有者側を結合するためのカラム名を指定する。

結合テーブルを使用しない

エンティティ

javaee-jpa.JPG

Mugetsu.java
package sample.javaee.jpa.entity.mapping;

import java.util.List;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToMany;

@Entity
public class Mugetsu {
 @Id
 private Long id;

 @OneToMany(fetch = FetchType.EAGER)
 @JoinColumn(name = "mugetsu_id")
 private List<Gengetsu> gengetsuList;

 @Override
 public String toString() {
 return "Mugetsu{" + "id=" + id + ", gengetsuList=" + gengetsuList + '}';
 }
}
Gengetsu.java
package sample.javaee.jpa.entity.mapping;

import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
public class Gengetsu {
 @Id
 private Long id;

 @Override
 public String toString() {
 return "Gengetsu{" + "id=" + id + '}';
 }
}

データベース

javaee-jpa.JPG

javaee-jpa.JPG

javaee-jpa.JPG

CREATE TABLE `mugetsu` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

CREATE TABLE `gengetsu` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `mugetsu_id` int(11) NOT NULL,
 PRIMARY KEY (`id`),
 KEY `gengetsu_fk_idx` (`mugetsu_id`),
 CONSTRAINT `gengetsu_fk` FOREIGN KEY (`mugetsu_id`) REFERENCES `mugetsu` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB AUTO_INCREMENT=7 DEFAULT CHARSET=utf8;

動作確認

MappingEjb.java
 Mugetsu mugetsu = this.em.find(Mugetsu.class, 1L);
 System.out.println(mugetsu);
GlassFishコンソール出力
情報: Mugetsu{id=1, gengetsuList=[Gengetsu{id=1}, Gengetsu{id=2}, Gengetsu{id=3}]}
  • 関連テーブルを使わない単純なケース。
  • 1対多の関連を解決する場合、 JPA は、デフォルトだと関連テーブルを使った方法を想定して関連の解決を行おうとする。
  • そうではなく、上記のように単純な方法で1対多のデータを格納している場合は、所有者側に @JoinColumn アノテーションを設定する。
    • 結合条件となるカラムの名前は、デフォルトだと プロパティ名_IDカラム名 で解決される(上述の例の場合は gengetsuList_id)。
    • これを上書きする場合は、 name 属性を指定する。

双方向

結合テーブルを使用する

エンティティ

javaee-jpa.JPG

Sara.java
package sample.javaee.jpa.entity.mapping;

import java.util.List;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.OneToMany;

@Entity
public class Sara {
 @Id
 private Long id;

 @OneToMany(fetch = FetchType.EAGER)
 @JoinTable(
 inverseJoinColumns = @JoinColumn(name = "luize_id")
 )
 private List<Luize> luizeList;

 public Long getId() {
 return id;
 }

 @Override
 public String toString() {
 return "Sara{" + "id=" + id + ", luizeList=" + luizeList + '}';
 }
}
Luize.java
package sample.javaee.jpa.entity.mapping;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinTable;

@Entity
public class Luize {
 @Id
 private Long id;

 @JoinTable(name="sara_luize")
 private Sara sara;

 @Override
 public String toString() {
 return "Luize{" + "id=" + id + ", sara.id=" + sara.getId() + '}';
 }
}

データベース

javaee-jpa.JPG

javaee-jpa.JPG

javaee-jpa.JPG

javaee-jpa.JPG

CREATE TABLE `sara` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

CREATE TABLE `luize` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;

CREATE TABLE `sara_luize` (
 `sara_id` int(11) NOT NULL,
 `luize_id` int(11) NOT NULL,
 PRIMARY KEY (`sara_id`,`luize_id`),
 KEY `sara_luize_fk1_idx` (`sara_id`),
 KEY `sara_luize_fk2_idx` (`luize_id`),
 CONSTRAINT `sara_luize_fk1` FOREIGN KEY (`sara_id`) REFERENCES `sara` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION,
 CONSTRAINT `sara_luize_fk2` FOREIGN KEY (`luize_id`) REFERENCES `luize` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

動作確認

MappingEjb.java
 Sara sara = this.em.find(Sara.class, 1L);
 System.out.println(sara);

 Luize luize = this.em.find(Luize.class, 1L);
 System.out.println(luize);
GlassFishコンソール出力
情報: Sara{id=1, luizeList=[Luize{id=1, sara.id=1}, Luize{id=2, sara.id=1}, Luize{id=3, sara.id=1}]}
情報: Luize{id=1, sara.id=1}
  • 多の方のエンティティ(Luize クラス)が持つフィールド(sara)を、 @JoinTable アノテートしてテーブル名を明示する。
  • これをしないと、関連テーブルが luize_sara で解決されてしまう。
  • Luize クラスの sara フィールドのフェッチを変更したい場合は、 @ManyToOne アノテーションを使用する。

結合テーブルを使用しない

エンティティ

javaee-jpa.JPG

Yuki.java
package sample.javaee.jpa.entity.mapping;

import java.util.List;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToMany;

@Entity
public class Yuki {
 @Id
 private Long id;

 @OneToMany(fetch = FetchType.EAGER)
 @JoinColumn(name = "yuki_id")
 private List<Mai> maiList;

 public Long getId() {
 return id;
 }

 @Override
 public String toString() {
 return "Yuki{" + "id=" + id + ", maiList=" + maiList + '}';
 }
}
Mai.java
package sample.javaee.jpa.entity.mapping;

import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
public class Mai {
 @Id
 private Long id;

 private Yuki yuki;

 @Override
 public String toString() {
 return "Mai{" + "id=" + id + ", yuki.id=" + yuki.getId() + '}';
 }
}

データベース

javaee-jpa.JPG

javaee-jpa.JPG

javaee-jpa.JPG

CREATE TABLE `yuki` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

CREATE TABLE `mai` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `yuki_id` int(11) NOT NULL,
 PRIMARY KEY (`id`),
 KEY `mai_fk_idx` (`yuki_id`),
 CONSTRAINT `mai_fk` FOREIGN KEY (`yuki_id`) REFERENCES `yuki` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB AUTO_INCREMENT=6 DEFAULT CHARSET=utf8;

動作確認

MappingEjb.java
 Yuki yuki = this.em.find(Yuki.class, 1L);
 System.out.println(yuki);

 Mai mai = this.em.find(Mai.class, 1L);
 System.out.println(mai);
GlassFishコンソール出力
情報: Yuki{id=1, maiList=[Mai{id=1, yuki.id=1}, Mai{id=2, yuki.id=1}, Mai{id=3, yuki.id=1}]}
情報: Mai{id=1, yuki.id=1}
  • 多(Mai)から1(Yuki)側を結合するときのカラム名は、1対1のときと同じ方法で解決される。
  • 明示したい場合は、 @JoinColumn を使う。

多対多

エンティティ

javaee-jpa.JPG

Alice.java
package sample.javaee.jpa.entity.mapping;

import java.util.ArrayList;
import java.util.List;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.JoinTable;
import javax.persistence.ManyToMany;

@Entity
public class Alice {
 @Id
 private Long id;

 @ManyToMany(fetch = FetchType.EAGER)
 @JoinTable(
 joinColumns = @JoinColumn(name = "alice_id"),
 inverseJoinColumns = @JoinColumn(name = "yumeko_id")
 )
 private List<Yumeko> yumekoList;

 public Long getId() {
 return id;
 }

 @Override
 public String toString() {
 List<String> ids = new ArrayList<>();

 for (Yumeko yumeko : this.yumekoList) {
 ids.add(String.valueOf(yumeko.getId()));
 }

 return "Alice{" + "id=" + id + ", yumekoList=" + String.join(", ", ids) + '}';
 }
}
Yumeko.java
package sample.javaee.jpa.entity.mapping;

import java.util.ArrayList;
import java.util.List;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.Id;
import javax.persistence.ManyToMany;

@Entity
public class Yumeko {
 @Id
 private Long id;

 @ManyToMany(fetch = FetchType.EAGER, mappedBy = "yumekoList")
 private List<Alice> aliceList;

 public Long getId() {
 return id;
 }

 @Override
 public String toString() {
 List<String> ids = new ArrayList<>();

 for (Alice alice : this.aliceList) {
 ids.add(String.valueOf(alice.getId()));
 }

 return "Yumeko{" + "id=" + id + ", aliceList=" + String.join(", ", ids) + '}';
 }
}

データベース

javaee-jpa.JPG

javaee-jpa.JPG

javaee-jpa.JPG

javaee-jpa.JPG

CREATE TABLE `alice` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

CREATE TABLE `yumeko` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

CREATE TABLE `alice_yumeko` (
 `alice_id` int(11) NOT NULL,
 `yumeko_id` int(11) NOT NULL,
 PRIMARY KEY (`alice_id`,`yumeko_id`),
 KEY `alice_yumeko_fk1_idx` (`alice_id`),
 KEY `alice_yumeko_fk2_idx` (`yumeko_id`),
 CONSTRAINT `alice_yumeko_fk1` FOREIGN KEY (`alice_id`) REFERENCES `alice` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION,
 CONSTRAINT `alice_yumeko_fk2` FOREIGN KEY (`yumeko_id`) REFERENCES `yumeko` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

動作確認

MappingEjb.java
 Alice alice = this.em.find(Alice.class, 1L);
 System.out.println(alice);

 Yumeko yumeko = this.em.find(Yumeko.class, 1L);
 System.out.println(yumeko);
GlassFishコンソール出力
情報: Alice{id=1, yumekoList=1, 2, 3}
情報: Yumeko{id=1, aliceList=1, 2}
  • 双方向の多対多をマッピングする場合は、 @ManyToMany アノテーションを使う。
  • どちらか一方を所有者に決めて、そちらに @JoinTable アノテーションで結合の条件を設定する。
  • 被所有者側は、 @ManyToMany アノテーションの mappedBy 属性で、所有者側のどのフィールドを紐付いているのかだけを指定すればいい。
  • 実装してて気づいたけど、 toString() メソッド内で Stream API を使うと、アプリケーションロード時にエラーになるっぽい。原因はよくわからない。仕方ないので、 for 文を使って文字列を整形している。

継承のマッピング

クラス階層に属するエンティティを1つのテーブルにマッピングする

エンティティ

javaee-jpa.JPG

ParentRumia.java
package sample.javaee.jpa.entity.mapping;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "rumia")
public class ParentRumia {
 @Id
 protected Long id;

 protected String name;

 public void setName(String name) {
 this.name = name;
 }

 @Override
 public String toString() {
 return "ParentRumia{" + "id=" + id + ", name=" + name + '}';
 }
}
ChildRumia.java
package sample.javaee.jpa.entity.mapping;

import javax.persistence.Entity;

@Entity
public class ChildRumia extends ParentRumia {

 private String value;

 public void setValue(String value) {
 this.value = value;
 }

 @Override
 public String toString() {
 return "ChildRumia{id=" + id + ", name=" + name + ", value=" + value + '}';
 }
}

データベース

javaee-jpa.JPG

CREATE TABLE `rumia` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `dtype` varchar(45) NOT NULL,
 `name` varchar(45) DEFAULT NULL,
 `value` varchar(45) DEFAULT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

動作確認

MappingEjb.java
 ParentRumia parentRumia = new ParentRumia();
 parentRumia.setName("parent name");

 this.em.persist(parentRumia);

 ChildRumia childRumia = new ChildRumia();
 childRumia.setName("child name");
 childRumia.setValue("child value");

 this.em.persist(childRumia);

実行後のデータベースの様子。

javaee-jpa.JPG

  • 特に指定が無い場合、1つのクラス階層に属するエンティティは、1つのテーブルにマッピングされる。
  • マッピングされるテーブルの名前は、デフォルトでは階層のルートとなるエンティティ名で解決される。
  • テーブルには dtype という名前の文字列型のカラムを用意しておく。
    • このカラムに、そのレコードがどのクラスのデータなのかを識別するための値が格納される。
    • デフォルトでは、エンティティの名前が格納される。
  • テーブルには、クラス階層の各エンティティが持つ全てのプロパティに対応するカラムを用意しなければならない。

メリット

  • データの持ち方が単純で理解しやすい。
  • 検索時にテーブルの JOIN が必要ない。

デメリット

  • 階層にクラスが追加されると、テーブルのカラムも変更しなければならない。
  • 項目に NOT NULL 制約を設けることができない。
    • 子クラスで定義されたカラムには、親クラスのレコードでは NULL 値が設定されるため。

エンティティ名を保存するカラムの名前や値を変更する

デフォルトでは DTYPE という文字列型のカラムにエンティティ名が保存される。
これは、 @DiscriminatorColumn@DiscriminatorValue アノテーションで変更することができる。

エンティティ

javaee-jpa.JPG

ParentDaiyousei.java
package sample.javaee.jpa.entity.mapping;

import javax.persistence.DiscriminatorColumn;
import javax.persistence.DiscriminatorType;
import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "daiyousei")
@DiscriminatorColumn(name = "entity_type", discriminatorType = DiscriminatorType.INTEGER)
@DiscriminatorValue("1")
public class ParentDaiyousei {
 @Id
 private Long id;
 private String value;

 public void setValue(String value) {
 this.value = value;
 }
}
ChildDaiyousei.java
package sample.javaee.jpa.entity.mapping;

import javax.persistence.DiscriminatorValue;
import javax.persistence.Entity;

@Entity
@DiscriminatorValue("2")
public class ChildDaiyousei extends ParentDaiyousei {
}

データベース

javaee-jpa.JPG

CREATE TABLE `daiyousei` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `entity_type` int(11) NOT NULL,
 `value` varchar(45) DEFAULT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

動作確認

MappingEjb.java
 ParentDaiyousei parentDaiyousei = new ParentDaiyousei();
 parentDaiyousei.setValue("parent value");

 this.em.persist(parentDaiyousei);

 ChildDaiyousei childDaiyousei = new ChildDaiyousei();
 childDaiyousei.setValue("child value");

 this.em.persist(childDaiyousei);

実行後のデータベースの様子。

javaee-jpa.JPG

  • ルートのエンティティを @DiscriminatorColumn でアノテートして、 name 属性でカラムの名前を指定できる。
  • discriminatorType 属性で型を指定することができる。
  • 各エンティティでなんという値を設定するかは、 @DiscriminatorValue アノテーションで指定する。

サブクラスごとにテーブルをマッピングする

エンティティ

javaee-jpa.JPG

ParentCirno.java
package sample.javaee.jpa.entity.mapping;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.Table;

@Entity
@Table(name = "parent_cirno")
@Inheritance(strategy = InheritanceType.JOINED)
public class ParentCirno {
 @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
 protected Long id;

 protected String name;

 public void setName(String name) {
 this.name = name;
 }

 @Override
 public String toString() {
 return "ParentCirno{" + "id=" + id + ", name=" + name + '}';
 }
}
ChildCirno.java
package sample.javaee.jpa.entity.mapping;

import javax.persistence.Entity;
import javax.persistence.Table;

@Entity
@Table(name = "child_cirno")
public class ChildCirno extends ParentCirno {

 private String value;

 public void setValue(String value) {
 this.value = value;
 }

 @Override
 public String toString() {
 return "ChildCirno{" + "id=" + id + ", name=" + name + ", value=" + value + '}';
 }
}

データベース

javaee-jpa.JPG

CREATE TABLE `parent_cirno` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `name` varchar(45) DEFAULT NULL,
 `dtype` varchar(45) NOT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

CREATE TABLE `child_cirno` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `value` varchar(45) DEFAULT NULL,
 PRIMARY KEY (`id`),
 KEY `child_cirno_fk_idx` (`id`),
 CONSTRAINT `child_cirno_fk` FOREIGN KEY (`id`) REFERENCES `parent_cirno` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

動作確認

MappingEjb.java
 ParentCirno parentCirno = new ParentCirno();
 parentCirno.setName("parent name");

 this.em.persist(parentCirno);

 ChildCirno childCirno = new ChildCirno();
 childCirno.setName("child name");
 childCirno.setValue("child value");

 this.em.persist(childCirno);

実行後のデータベースの様子。

javaee-jpa.JPG

javaee-jpa.JPG

  • ルートのエンティティを @Inheritance でアノテートし、 strategyInheritanceType.JOINED を指定する。
    • デフォルトは、 InheritanceType.SINGLE_TABLE を指定したのと同じになる。
  • すると、クラス階層上の各エンティティを、それぞれ個別のテーブルにマッピングすることができる。
  • 各テーブルは、 PK を外部参照制約で紐付ける。
  • ルートのテーブルには、クラスを特定するための dtype カラムを定義する。
    • こちらも @DiscriminatorColumn アノテーションを使って変更することができる。

メリット

  • 項目に NOT NULL 制約を設けることができる(SINGLE_TABLE と比べた場合の話)。
  • データが正規化される。

デメリット

  • サブクラスのエンティティを取得するには、テーブルの結合が必要になる。

エンティティごとに独立したテーブルにマッピングする

エンティティ

javaee-jpa.JPG

ParentHongMeiling.java
package sample.javaee.jpa.entity.mapping;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Inheritance;
import javax.persistence.InheritanceType;
import javax.persistence.Table;

@Entity
@Table(name = "parent_hong_meiling")
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public class ParentHongMeiling {
 @Id
 private Long id;

 private String name;

 public void setName(String name) {
 this.name = name;
 }
}
ChildHongMeiling.java
package sample.javaee.jpa.entity.mapping;

import javax.persistence.Entity;
import javax.persistence.Table;

@Entity
@Table(name = "child_hong_meiling")
public class ChildHongMeiling extends ParentHongMeiling {
 private String value;

 public void setValue(String value) {
 this.value = value;
 }
}

データベース

javaee-jpa.JPG

CREATE TABLE `parent_hong_meiling` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `name` varchar(45) DEFAULT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

CREATE TABLE `child_hong_meiling` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `name` varchar(45) DEFAULT NULL,
 `value` varchar(45) DEFAULT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

動作確認

MappingEjb.java
 ParentHongMeiling parentHongMeiling = new ParentHongMeiling();
 parentHongMeiling.setName("parent name");

 this.em.persist(parentHongMeiling);

 ChildHongMeiling childHongMeiling = new ChildHongMeiling();
 childHongMeiling.setName("child name");
 childHongMeiling.setValue("child value");

 this.em.persist(childHongMeiling);

実行後のデータベースの状態。

javaee-jpa.JPG

javaee-jpa.JPG

  • @InheritanceInheritanceType.TABLE_PER_CLASS を指定することで、各エンティティをそれぞれ独立したテーブルにマッピングできる。

メリット

  • SINGLE_TABLE と同様で、単一のエンティティの検索でテーブルの JOIN が発生することはない。

デメリット

  • データが正規化されていない。
  • 多態を利用したフィールドがあった場合、データの読み込みで複数回のデータベースアクセスが発生する可能性がある。
    • 例えば、 List<ParentHongMeiling> という型のフィールドがあった場合、リストの中身は実は ParentHongMeilingChildHongMeiling のインスタンスが入り乱れて入っている可能性があり得る。
    • その場合、 parent_hong_meilingchild_hong_meiling のテーブルにそれぞれ検索しにいかなければならない。
    • UNION が使えればいいが、サポートしていないデータベースも存在する。
  • このマッピング方法は、オプションらしい(実装されていなくても JPA の仕様的にはかまわない)。

その他

マッピングのためだけのスーパークラスを定義する

エンティティ

javaee-jpa.JPG

MappedKoakuma.java
package sample.javaee.jpa.entity.mapping;

import javax.persistence.Id;
import javax.persistence.MappedSuperclass;

@MappedSuperclass
public class MappedKoakuma {
 @Id
 private Long id;

 private String name;

 public void setName(String name) {
 this.name = name;
 }
}
Koakuma.java
package sample.javaee.jpa.entity.mapping;

import javax.persistence.Entity;

@Entity
public class Koakuma extends MappedKoakuma {

 private String value;

 public void setValue(String value) {
 this.value = value;
 }
}

データベース

javaee-jpa.JPG

CREATE TABLE `koakuma` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `name` varchar(45) DEFAULT NULL,
 `value` varchar(45) DEFAULT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;

動作確認

MappingEjb.java
 Koakuma koakuma = new Koakuma();
 koakuma.setName("name");
 koakuma.setValue("value");

 this.em.persist(koakuma);

実行後のデータベースの様子。

javaee-jpa.JPG

  • @MappedSuperclass でクラスをアノテートすると、そのクラスはエンティティとしては扱われず、マッピングの定義だけが有効になる。
  • 既存の実装で共通項目が親クラスに定義されているけど、そのクラスは永続化する必要がない、という場合に使えると思われる。
  • 新規に作るのであれば、共通項目は継承関係で解決するよりも組み込み可能クラスを利用して依存関係で解決した方がいいと思う。

Entity Graph で関連するエンティティの読み込みを制御する

関連するエンティティをどのタイミングでロードするかどうかは、これまではフィールド単位に静的にしか指定できなかった(即時ロードと遅延ロード)。
つまり、ある検索のときは即時ロードしたいけど、別の検索のときは遅延ロードにしたい、という細かい制御はできなかった。

しかし、 JPA 2.1 で追加された Entity Graph を使えば、この制御ができるようになる。

エンティティ

javaee-jpa.JPG

IzayoiSakuya.java
package sample.javaee.jpa.entity.mapping;

import java.util.List;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.NamedAttributeNode;
import javax.persistence.NamedEntityGraph;
import javax.persistence.Table;

@Entity
@Table(name = "izayoi_sakuya")
@NamedEntityGraph(
 name = "IzayoiSakuya.remiliaScarletList",
 attributeNodes = @NamedAttributeNode("remiliaScarletList")
)
public class IzayoiSakuya {
 @Id
 private Long id;
 @JoinColumn(name = "izayoi_sakuya_id")
 private List<RemiliaScarlet> remiliaScarletList;
 @JoinColumn(name = "izayoi_sakuya_id")
 private List<FrandreScarlet> frandreScarletList;

 @Override
 public String toString() {
 return "IzayoiSakuya{" + "id=" + id + ", remiliaScarletList=" + remiliaScarletList + ", frandreScarletList=" + frandreScarletList + '}';
 }
}
RemiliaScarlet.java
package sample.javaee.jpa.entity.mapping;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "remilia_scarlet")
public class RemiliaScarlet {
 @Id
 private Long id;

 @Override
 public String toString() {
 return "RemiliaScarlet{" + "id=" + id + '}';
 }
}
FrandreScarlet.java
package sample.javaee.jpa.entity.mapping;

import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name = "frandre_scarlet")
public class FrandreScarlet {
 @Id
 private Long id;

 @Override
 public String toString() {
 return "FrandreScarlet{" + "id=" + id + '}';
 }
}

データベース

javaee-jpa.JPG

javaee-jpa.JPG

javaee-jpa.JPG

javaee-jpa.JPG

CREATE TABLE `izayoi_sakuya` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

CREATE TABLE `frandre_scarlet` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `izayoi_sakuya_id` int(11) NOT NULL,
 PRIMARY KEY (`id`),
 KEY `frandre_scarlet_fk_idx` (`izayoi_sakuya_id`),
 CONSTRAINT `frandre_scarlet_fk` FOREIGN KEY (`izayoi_sakuya_id`) REFERENCES `izayoi_sakuya` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8;

CREATE TABLE `remilia_scarlet` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `izayoi_sakuya_id` int(11) NOT NULL,
 PRIMARY KEY (`id`),
 KEY `remilia_scarlet_fk_idx` (`izayoi_sakuya_id`),
 CONSTRAINT `remilia_scarlet_fk` FOREIGN KEY (`izayoi_sakuya_id`) REFERENCES `izayoi_sakuya` (`id`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB AUTO_INCREMENT=8 DEFAULT CHARSET=utf8;

動作確認

MappingEjb.java
 // デフォルト
 IzayoiSakuya izayoiSakuya = this.em.find(IzayoiSakuya.class, 1L);
 System.out.println(izayoiSakuya);

 // Entity Graph を指定
 EntityGraph graph = this.em.getEntityGraph("IzayoiSakuya.remiliaScarletList");

 Map<String, Object> hint = new HashMap<>();
 hint.put("javax.persistence.fetchgraph", graph);

 izayoiSakuya = this.em.find(IzayoiSakuya.class, 2L, hint);
 System.out.println(izayoiSakuya);
GlassFishコンソール出力
情報: IzayoiSakuya{id=1, remiliaScarletList={IndirectList: not instantiated}, frandreScarletList={IndirectList: not instantiated}}
情報: IzayoiSakuya{id=2, remiliaScarletList={[RemiliaScarlet{id=4}, RemiliaScarlet{id=5}]}, frandreScarletList={IndirectList: not instantiated}}
  • @NamedEntityGraph アノテーションをエンティティに設定することで、 Entity Graph を静的に定義できる。
  • name 属性で Entity Graph の名前を、 attributeNodes 属性で即時ロードするフィールドを指定する。
  • Entity Graph を使うかどうかは、 EntityManager.find() の最後に Map を渡すことで指定できる。
    • Entity Graph は、 EntityManager#getEntityGraph(String) を使って取得できる。
    • キーに javax.persistence.fetchgraph を、値に Entity Graph を設定した Map を、 find() メソッドに渡す。
    • javax.persistence.loadgraph っていうのも指定できるみたいだけど、 fetchgraph との違いがよく分からなかった。。。
  • Entity Graph を指定したときだけ、 attributeNodes で設定したフィールドが即時ロードされる。
  • subgraphs 属性を指定すれば、さらに掘り下げて読み込み方を指定することができる。

Entity Graph を動的に作成する

MappingEjb.java
 EntityGraph dynamicGraph = this.em.createEntityGraph(IzayoiSakuya.class);
 dynamicGraph.addAttributeNodes("frandreScarletList");

 hint = new HashMap<>();
 hint.put("javax.persistence.fetchgraph", dynamicGraph);

 izayoiSakuya = this.em.find(IzayoiSakuya.class, 3L, hint);
 System.out.println(izayoiSakuya);
GlassFishコンソール出力
情報: IzayoiSakuya{id=3, remiliaScarletList={IndirectList: not instantiated}, frandreScarletList={[FrandreScarlet{id=6}, FrandreScarlet{id=7}]}}
  • EntityManager#createEntityGraph(Class) で、 Entity Graph を動的に作成することができる。

コンバーターを使用する

JPA 2.1 で、コンバーターという仕組みが追加された。

コンバーターを使えば、データベースとのマッピングを自由に実装することができる。

エンティティ

javaee-jpa.JPG

LettyWhiterock.java
package sample.javaee.jpa.entity.mapping;

import javax.persistence.Convert;
import javax.persistence.Entity;
import javax.persistence.Id;
import javax.persistence.Table;
import sample.javaee.jpa.converter.LettyWhiterockConverter;

@Entity
@Table(name = "letty_whiterock")
public class LettyWhiterock {
 @Id
 private Long id;

 @Convert(converter = LettyWhiterockConverter.class)
 private String value;

 @Override
 public String toString() {
 return "LettyWhiterock{" + "id=" + id + ", value=" + value + '}';
 }
}

コンバーター

LettyWhiterockConverter.java
package sample.javaee.jpa.converter;

import java.util.Objects;
import javax.persistence.AttributeConverter;
import javax.persistence.Converter;

@Converter
public class LettyWhiterockConverter implements AttributeConverter<String, Integer>{

 @Override
 public Integer convertToDatabaseColumn(String attribute) {
 return "hoge".equals(attribute) ? 1 : 0;
 }

 @Override
 public String convertToEntityAttribute(Integer dbData) {
 return Objects.equals(dbData, 1) ? "hoge" : "fuga";
 }
}

データベース

javaee-jpa.JPG

javaee-jpa.JPG

CREATE TABLE `letty_whiterock` (
 `id` int(11) NOT NULL AUTO_INCREMENT,
 `value` int(11) DEFAULT NULL,
 PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8;

動作確認

MappingEjb.java
 LettyWhiterock lettyWhiterock = this.em.find(LettyWhiterock.class, 1L);
 System.out.println(lettyWhiterock);
GlassFishコンソール出力
情報: LettyWhiterock{id=1, value=hoge}
  • AttributeConverter インターフェースを実装することで、コンバーターを作成することができる。
    • convertToDatabaseColumn() メソッドで「Java → データベース」の変換を実装する。
    • convertToEntityAttribute() メソッドで「データベース → Java」の変換を実装する。
  • @Converter でアノテートすることで、 JavaEE サーバーが自動でそのクラスをコンバーターとして認識してくれる。
  • コンバーターを使用するときは、フィールドを @Convert でアノテートして、 converter 属性でコンバーターの Class を指定する。
  • @Converter アノテーションの autoApply 属性に true を設定すると、 @Convet でアノテートしなくてもフィールドの型から推測して自動でコンバーターが適用されるようになる(Integer など基本的な型にも適用されるので、独自の型以外では使わない方がいい)。

参考