Hibernateの識別子の概要

1前書き

Hibernateの識別子はエンティティの主キーを表します。これは、値が固有のものであるために特定のエンティティを識別できること、それらが無効ではないこと、およびそれらが変更されることがないことを意味します。

Hibernateは識別子を定義するためのいくつかの異なる方法を提供します。この記事では、ライブラリを使用してエンティティIDをマッピングする各方法について説明します。

2単純な識別子

識別子を定義する最も簡単な方法は、 @ Id アノテーションを使用することです。

単純なIDは、 @ Id を使用して、Javaプリミティブ型およびプリミティブ型のラッパー型、__String、Date、BigDecimal、BigIntegerのいずれかの型の単一のプロパティにマッピングされます。

__long型の主キーでエンティティを定義する簡単な例を見てみましょう。

@Entity
public class Student {

    @Id
    private long studentId;

   //standard constructor, getters, setters
}

3生成された識別子

主キーの値を自動的に生成したい場合は、** @ GeneratedValue__アノテーションを追加できます。

これは4つの生成タイプを使うことができます:AUTO、IDENTITY、SEQUENCE、TABLE。

値を明示的に指定しないと、生成タイプはデフォルトのAUTOになります。

3.1. オート ジェネレーション

デフォルトの生成タイプを使用している場合、永続性プロバイダは主キー属性のタイプに基づいて値を決定します。

このタイプは数値または__UUIDです。

数値の場合、生成はシーケンスジェネレータまたはテーブルジェネレータに基づきますが、 UUID 値は__UUIDGeneratorを使用します。

AUTO生成戦略を使用してエンティティの主キーをマッピングする例を見てみましょう。

@Entity
public class Student {

    @Id
    @GeneratedValue
    private long studentId;

   //...
}

この場合、主キーの値はデータベースレベルで一意になります。

  • Hibernate 5で導入された面白い機能は__UUIDGeneratorです。

@Entity
public class Course {

    @Id
    @GeneratedValue
    private UUID courseId;

   //...
}

Hibernateは「8dd5f315-9788-4d00-87bb-10eed9eff566」という形式のIDを生成します。

3.2. IDENTITY 生成

このタイプの生成は、データベース内の identity 列によって生成された値を期待する IdentityGenerator に依存します。つまり、それらの値は自動増分されます。

この生成タイプを使用するには、 strategy パラメータを設定するだけです。

@Entity
public class Student {

    @Id
    @GeneratedValue (strategy = GenerationType.IDENTITY)
    private long studentId;

   //...
}

注意すべきことは、IDENTITY生成によってバッチ更新が無効になることです。

3.3. SEQUENCE ジェネレーション

シーケンスベースのIDを使用するために、Hibernateは SequenceStyleGenerator クラスを提供します。

このジェネレータは、データベースでサポートされている場合はシーケンスを使用し、サポートされていない場合はテーブル生成に切り替えます。

シーケンス名をカスタマイズするために、 SequenceStyleGeneratorストラテジーで @ GenericGenerator__アノテーションを使用することができます。

@Entity
public class User {
    @Id
    @GeneratedValue(generator = "sequence-generator")
    @GenericGenerator(
      name = "sequence-generator",
      strategy = "org.hibernate.id.enhanced.SequenceStyleGenerator",
      parameters = {
        @Parameter(name = "sequence__name", value = "user__sequence"),
        @Parameter(name = "initial__value", value = "4"),
        @Parameter(name = "increment__size", value = "1")
        }
    )
    private long userId;

   //...
}

この例では、シーケンスの初期値も設定しています。つまり、主キーの生成は4から始まります。

SEQUENCE はHibernateのドキュメントで推奨されている生成タイプです。

  • 生成された値はシーケンスごとに一意です** シーケンス名を指定しない場合、Hibernateは異なるタイプに対して同じ hibernate sequence__を再利用します。

3.4. テーブル生成

TableGenerator は、識別子生成値のセグメントを保持する基礎となるデータベーステーブルを使用します。

@ TableGenerator アノテーションを使用してテーブル名をカスタマイズしましょう。

@Entity
public class Department {
    @Id
    @GeneratedValue(strategy = GenerationType.TABLE,
      generator = "table-generator")
    @TableGenerator(name = "table-generator",
      table = "dep__ids",
      pkColumnName = "seq__id",
      valueColumnName = "seq__value")
    private long depId;

   //...
}

この例では、 pkColumnName valueColumnName などの他の属性もカスタマイズできることがわかります。

この方法の不利な点は、スケールがうまくいかず、パフォーマンスに悪影響を及ぼす可能性があることです。

まとめると、これら4つの生成タイプは同じような値が生成されることになりますが、異なるデータベースメカニズムを使用します。

** 3.5. カスタムジェネレータ

すぐに使える戦略を使用したくない場合は、 IdentifierGenerator インターフェースを実装することで カスタムジェネレータを定義できます

String プレフィックスと数字を含む識別子を作成するジェネレータを作成しましょう。

public class MyGenerator
  implements IdentifierGenerator, Configurable {

    private String prefix;

    @Override
    public Serializable generate(
      SharedSessionContractImplementor session, Object obj)
      throws HibernateException {

        String query = String.format("select %s from %s",
            session.getEntityPersister(obj.getClass().getName(), obj)
              .getIdentifierPropertyName(),
            obj.getClass().getSimpleName());

        Stream ids = session.createQuery(query).stream();

        Long max = ids.map(o -> o.replace(prefix + "-", ""))
          .mapToLong(Long::parseLong)
          .max()
          .orElse(0L);

        return prefix + "-" + (max + 1);
    }

    @Override
    public void configure(Type type, Properties properties,
      ServiceRegistry serviceRegistry) throws MappingException {
        prefix = properties.getProperty("prefix");
    }
}

この例では、** IdentifierGenerator インターフェースから generate() メソッドをオーバーライドして、まず__prefix-XXという形式の既存の主キーから最大の番号を見つけます。

次に、見つかった最大数に1を加え、 prefix プロパティを追加して、新しく生成されたID値を取得します。

このクラスは Configurable インターフェースも実装しているので、 configure() メソッドで prefix プロパティ値を設定できます。

次に、このカスタムジェネレータをエンティティに追加しましょう。これには、** ジェネレータクラスの完全なクラス名を含む strategy パラメータを使用して @ GenericGenerator アノテーションを使用できます。

@Entity
public class Product {

    @Id
    @GeneratedValue(generator = "prod-generator")
    @GenericGenerator(name = "prod-generator",
      parameters = @Parameter(name = "prefix", value = "prod"),
      strategy = "com.baeldung.hibernate.pojo.generator.MyGenerator")
    private String prodId;

   //...
}

また、prefixパラメータを“ prod”に設定したことにも注意してください。

生成されたID値をより明確に理解するための簡単なJUnitテストを見てみましょう。

@Test
public void whenSaveCustomGeneratedId__thenOk() {
    Product product = new Product();
    session.save(product);
    Product product2 = new Product();
    session.save(product2);

    assertThat(product2.getProdId()).isEqualTo("prod-2");
}

ここで、「prod」プレフィックスを使用して生成された最初の値は「prod-1」であり、その後に「prod-2」が続きます。

4複合識別子

これまで見てきた単純な識別子以外に、Hibernateでは複合識別子を定義することもできます。

複合IDは、1つ以上の永続属性を持つ主キークラスによって表されます。

主キークラスはいくつかの条件を満たさなければなりません:

  • @ EmbeddedId または @ IdClass アノテーションを使用して定義する必要があります。

  • それはpublicで、直列化可能で、publicな引数のないコンストラクタを持つべきです

  • equals() および hashCode() メソッドを実装する必要があります。

コレクションと OneToOne 属性を避けながら、クラスの属性は基本、複合、またはManyToOneにすることができます。

4.1. @ EmbeddedId

@ EmbeddedIdを使用してIDを定義するには、まず @ Embeddableというアノテーションを付けた主キークラスが必要です。

@Embeddable
public class OrderEntryPK implements Serializable {

    private long orderId;
    private long productId;

   //standard constructor, getters, setters
   //equals() and hashCode()
}

次に、@ EmbeddedId を使用してエンティティに OrderEntryPK 型のIDを追加できます。

@Entity
public class OrderEntry {

    @EmbeddedId
    private OrderEntryPK entryId;

   //...
}

エンティティの主キーを設定するためにこのタイプの複合IDを使用する方法を見てみましょう。

@Test
public void whenSaveCompositeIdEntity__thenOk() {
    OrderEntryPK entryPK = new OrderEntryPK();
    entryPK.setOrderId(1L);
    entryPK.setProductId(30L);

    OrderEntry entry = new OrderEntry();
    entry.setEntryId(entryPK);
    session.save(entry);

    assertThat(entry.getEntryId().getOrderId()).isEqualTo(1L);
}

ここで、 OrderEntry オブジェクトは、 orderId productIdの2つの属性で構成される OrderEntryPK__基本IDを持ちます。

4.2. @ IdClass

@ IdClass アノテーションは @ EmbeddedId に似ていますが、メインエンティティクラスでそれぞれ属性に @ Id を使用して属性が定義されている点が異なります。

主キークラスは以前と同じように見えます。

OrderEntry の例を__ @ IdClassに書き換えてみましょう。

@Entity
@IdClass(OrderEntryPK.class)
public class OrderEntry {
    @Id
    private long orderId;
    @Id
    private long productId;

   //...
}

その後、 OrderEntry オブジェクトに直接id値を設定できます。

@Test
public void whenSaveIdClassEntity__thenOk() {
    OrderEntry entry = new OrderEntry();
    entry.setOrderId(1L);
    entry.setProductId(30L);
    session.save(entry);

    assertThat(entry.getOrderId()).isEqualTo(1L);
}

どちらのタイプのコンポジットIDについても、主キークラスに @ ManyToOne 属性を含めることもできます。

Hibernateでは、 @ ManyToOne アソシエーションと @ Id アノテーションを組み合わせた主キーを定義することもできます。この場合、エンティティクラスも主キークラスの条件を満たす必要があります。

この方法の欠点は、エンティティオブジェクトと識別子の間に区別がないことです。

5派生識別子

派生識別子は、 @ MapsId アノテーションを使用してエンティティの関連付けから取得されます。

まず、 User エンティティとの1対1の関連からIDを取得する UserProfile エンティティを作成しましょう。

@Entity
public class UserProfile {

    @Id
    private long profileId;

    @OneToOne
    @MapsId
    private User user;

   //...
}

次に、 UserProfile インスタンスが、関連付けられた User インスタンスと同じIDを持つことを確認しましょう。

@Test
public void whenSaveDerivedIdEntity__thenOk() {
    User user = new User();
    session.save(user);

    UserProfile profile = new UserProfile();
    profile.setUser(user);
    session.save(profile);

    assertThat(profile.getProfileId()).isEqualTo(user.getUserId());
}

6. 結論

この記事では、Hibernateで識別子を定義できる複数の方法について説明しました。

例の完全なソースコードはhttps://github.com/eugenp/tutorials/tree/master/persistence-modules/hibernate5[GitHubに載っています]。