1. 概要

Hibernateの識別子は、エンティティの主キーを表します。 これは、値が一意であるため、特定のエンティティを識別でき、nullではなく、変更されないことを意味します。

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アノテーションを追加できます。

これは、AUTO、IDENTITY、SEQUENCE、およびTABLEの4つの生成タイプを使用できます。

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

3.1. AUTO生成

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

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

まず、自動生成戦略を使用してエンティティの主キーをマッピングしましょう。

@Entity
public class Student {

    @Id
    @GeneratedValue
    private long studentId;

    // ...
}

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

次に、Hibernate5で導入されたUUIDGeneratorを見ていきます。

この機能を使用するには、@GeneratedValueアノテーションを付けたタイプUUIDのIDを宣言する必要があります。

@Entity
public class Course {

    @Id
    @GeneratedValue
    private UUID courseId;

    // ...
}

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

3.2. IDENTITY生成

このタイプの生成は、 IdentityGenerator に依存します。これは、データベースのIdentity列によって生成された値を想定しています。 これは、それらが自動インクリメントされることを意味します。

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

@Entity
public class Student {

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

    // ...
}

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

3.3. SEQUENCE生成

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

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

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

@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;

    // ...
}

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

ただし、この方法の欠点は、拡張性が低く、パフォーマンスに悪影響を与える可能性があることです。

要約すると、これらの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プロパティ値を設定できます。

次に、このカスタムジェネレーターをエンティティに追加しましょう。

このために、ジェネレータークラスの完全なクラス名を含む戦略パラメーターで@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;

    // ...
}

また、プレフィックスパラメータを「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アノテーションを使用して定義する必要があります。
  • パブリックで、シリアル化可能で、パブリックの引数なしコンストラクターが必要です。
  • 最後に、 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 オブジェクトには、orderIdproductIdの2つの属性で構成されるOrderEntryPKプライマリIDがあります。

4.2. @IdClass

@IdClass アノテーションは、@EmbeddedIdに似ています。 @IdClass との違いは、属性がそれぞれに@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で識別子を定義する複数の方法を見てきました。

例の完全なソースコードは、GitHubにあります。