Hibernateの識別子の概要

Hibernateの識別子の概要

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. 生成された識別子

主キー値を自動的に生成する場合は、we can add the @GeneratedValue annotationを使用します。

これには、AUTO、IDENTITY、SEQUENCE、TABLEの4つの世代タイプを使用できます。

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

3.1. AUTO世代

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

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

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

@Entity
public class Student {

    @Id
    @GeneratedValue
    private long studentId;

    // ...
}

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

An interesting feature introduced in Hibernate 5 is the UUIDGenerator.これを使用するには、@GeneratedValueアノテーションを付けてタイプUUIDのIDを宣言するだけです。

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

    // ...
}

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

3.3. SEQUENCE世代

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

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

シーケンス名をカスタマイズするには、SequenceStyleGenerator strategy:@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ドキュメントで推奨されている世代タイプです。

The generated values are unique per sequence.シーケンス名を指定しない場合、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. カスタムジェネレーター

すぐに使用できる戦略を使用したくない場合は、we can define our custom generator by implementing the IdentifierGenerator interfaceを使用します。

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");
    }
}

この例では、we override the generate() method from the IdentifierGenerator interfaceで、最初にprefix-XX.の形式の既存の主キーから最大の番号を見つけます。

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

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

次に、このカスタムジェネレータをエンティティに追加しましょう。 このため、we can use the @GenericGenerator annotation with a strategy parameter that contains the full class name of our generator class

@Entity
public class Product {

    @Id
    @GeneratedValue(generator = "prod-generator")
    @GenericGenerator(name = "prod-generator",
      parameters = @Parameter(name = "prefix", value = "prod"),
      strategy = "com.example.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アノテーションは、属性がそれぞれに@Idを使用してメインエンティティクラスで定義されることを除いて、@EmbeddedId,に似ています。

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

OrderEntryの例を@IdClass:で書き直してみましょう

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

    // ...
}

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

@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では、@Idアノテーションと組み合わせた@ManyToOneアソシエーションで構成される主キーを定義することもできます。 この場合、エンティティクラスはプライマリキークラスの条件も満たす必要があります。

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

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で識別子を定義する方法をいくつか見てきました。

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