1. 概述

Hibernate 中的标识符(Identifier)代表实体的主键。这意味着标识符值必须满足三个核心条件:

  • ✅ 唯一性:能唯一标识特定实体
  • ✅ 非空性:不能为 null
  • ✅ 不可变性:创建后不应修改

Hibernate 提供了多种定义标识符的方式。本文将系统梳理每种实体 ID 映射方法,帮你避开常见坑点。

2. 简单标识符

最直接的方式是使用 @Id 注解。简单标识符映射到 Java 基本类型、包装类型、StringDateBigDecimalBigInteger 等单一属性。

基本类型示例

@Entity
public class TennisPlayer {

    @Id
    private long playerId;
    private String name;

    public TennisPlayer(String name) {
        this.name = name;
    }
    // getters, setters
}

测试用例揭示了一个潜在问题:

@Test
public void whenSavingTennisPlayerWithoutAnId_thenSavingEntityOk() {
    TennisPlayer tennisPlayer = new TennisPlayer("Tom");
    session.save(tennisPlayer);
    assertThat(tennisPlayer.getPlayerId()).isEqualTo(0L);
}

⚠️ 踩坑警告:虽然实体成功持久化,但这是因为基本类型 long 的默认值是 0。这通常不是我们期望的行为——我们更希望显式设置标识符值。

包装类型示例

改用包装类型 Long 会更安全:

@Entity
public class BaseballPlayer {

    @Id
    private Long playerId; // 注意 Long 而非 long
    private String name;
 
    public BaseballPlayer(String name) {
        this.name = name;
    }
    // getters, setters
}

测试用例展示了包装类型的行为差异:

@Test
public void whenSavingBaseballPlayerWithoutAnId_thenSavingEntityFails() {
    BaseballPlayer baseballPlayer = new BaseballPlayer("Jerry");
    assertThatThrownBy(() -> session.save(baseballPlayer))
      .isInstanceOf(IdentifierGenerationException.class)
      .hasMessageContaining("ids for this class must be manually assigned before calling save()");
}

关键区别:当 @Id 字段为 null 时,Hibernate 无法确定主键值,直接抛出异常。错误信息明确提示:保存前必须手动分配 ID。

正确做法是显式设置 ID:

@Test
public void whenSavingBaseballPlayerWithAManualId_thenSavingEntityOK() {
    BaseballPlayer baseballPlayer = new BaseballPlayer("Jerry");
    baseballPlayer.setPlayerId(42L);
    session.save(baseballPlayer);
}

简单标识符虽然直观,但非基本类型时必须手动赋值。这能强制我们在持久化前记住设置标识符值。接下来看看如何自动生成 ID。

3. 生成式标识符

要自动生成主键值,添加 @GeneratedValue 注解即可。它支持四种生成策略:

  • AUTO(默认)
  • IDENTITY
  • SEQUENCE
  • TABLE

3.1 AUTO 生成

默认策略下,持久化提供者根据主键属性类型决定生成方式:

  • 数值类型:基于序列或表生成器
  • UUID 类型:使用 UUIDGenerator

数值类型示例:

@Entity
public class Student {

    @Id
    @GeneratedValue
    private long studentId;

    // ...
}

Hibernate 5+ 引入了 UUIDGenerator,使用方式同样简单:

@Entity
public class Course {

    @Id
    @GeneratedValue
    private UUID courseId;

    // ...
}

生成的 UUID 格式如:8dd5f315-9788-4d00-87bb-10eed9eff566

3.2 IDENTITY 生成

依赖数据库的 identity 列(自增列),使用 IdentityGenerator

@Entity
public class Student {

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

    // ...
}

⚠️ 性能提示:IDENTITY 生成会禁用批量更新操作。

3.3 SEQUENCE 生成

Hibernate 提供 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;
    
    // ...
}

最佳实践:SEQUENCE 是 Hibernate 官方推荐的生成策略。生成的值在序列内唯一,未指定序列名时默认复用 hibernate_sequence

3.4 TABLE 生成

TableGenerator 使用数据库表存储 ID 生成片段:

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

    // ...
}

性能缺陷:此方法扩展性差,可能严重影响性能。

3.5 自定义生成器

当内置策略不满足需求时,实现 IdentifierGenerator 接口即可:

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

核心逻辑:

  1. 查询现有主键中 prefix-XX 格式的最大值
  2. 最大值 +1 并拼接前缀生成新 ID
  3. 通过 Configurable 接口配置前缀参数

在实体中使用:

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

    // ...
}

测试验证生成逻辑:

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

3.6 自定义 @IdGeneratorType 支持手动分配

Hibernate 6.5+ 提供更灵活的控制:@IdGeneratorType 允许手动分配 ID,同时保留自动生成能力。关键在于 allowAssignedIdentifiers() 方法。

自定义生成器示例:

public class MovieIdGenerator extends SequenceStyleGenerator {
    @Override
    public Object generate(SharedSessionContractImplementor session, Object owner) throws HibernateException {
        final Long id;
        if (this.allowAssignedIdentifiers() && owner instanceof Movie) {
            id = ((Movie) owner).getId();
        } else {
            id = null;
        }
        return id != null ? id : super.generate(session, owner);
    }

    @Override
    public boolean allowAssignedIdentifiers() {
        return true;
    }
}

创建配套注解:

@IdGeneratorType(MovieIdGenerator.class)
@Target({ FIELD })
@Retention(RetentionPolicy.RUNTIME)
@interface MovieGeneratedId {
}

在实体中使用:

@Entity
public class Movie {
    @Id
    @MovieGeneratedId
    private Long id;

    private String title;
    private String director;
}

测试两种场景:

自动生成 ID

@Test
void givenMovie_whenCreatingAndRetrievingMovie_thenCorrectMovieIsRetrieved() {
    Movie movie = new Movie("3 Idiots", "Rajkumar Hirani");
    Movie savedMovie = movieRepository.save(movie);
    assertNotNull(savedMovie.getId());
    assertEquals("3 Idiots", savedMovie.getTitle());
    assertEquals("Rajkumar Hirani", savedMovie.getDirector());
}

手动分配 ID

@Test
void givenManualId_whenCreatingAndRetrievingMovie_thenCorrectMovieIsRetrieved() {
    Movie movie = new Movie(10L, "Inception", "Christopher Nolan");
    Movie savedMovie = movieRepository.save(movie);
    assertEquals(10L, savedMovie.getId());
    assertEquals("Inception", savedMovie.getTitle());
    assertEquals("Christopher Nolan", savedMovie.getDirector());
}

4. 复合标识符

Hibernate 支持复合主键,由主键类(Primary Key Class)表示。主键类必须满足:

  • ✅ 使用 @EmbeddedId@IdClass 注解
  • ✅ 公开、可序列化、有无参构造函数
  • ✅ 实现 equals()hashCode()
  • ❌ 避免集合和 OneToOne 属性(支持基本类型、复合类型、ManyToOne

4.1 @EmbeddedId 方式

先定义主键类:

@Embeddable
public class OrderEntryPK implements Serializable {

    private long orderId;
    private long productId;

    // 标准构造器、getter/setter
    // equals() 和 hashCode() 
}

在实体中使用:

@Entity
public class OrderEntry {

    @EmbeddedId
    private OrderEntryPK entryId;

    // ...
}

使用示例:

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

4.2 @IdClass 方式

主键类定义相同,但在实体类中直接使用 @Id

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

使用方式更直接:

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

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

两种方式都支持 @ManyToOne 属性:

@Embeddable
public class OrderEntryPK implements Serializable {

    private long orderId;
    private long productId;
    @ManyToOne
    private User user;

    // ...
}
@Entity
@IdClass(OrderEntryPK.class)
public class OrderEntryIdClass {
    @Id
    private long orderId;
    @Id
    private long productId;
    @ManyToOne
    private User user;

    // ...
}

缺点:直接在实体类中使用 @Id + @ManyToOne 会导致实体对象与标识符耦合,缺乏清晰分离。

5. 派生标识符

通过 @MapsId 从实体关联中派生标识符。典型场景:一对一关联共享主键。

@Entity
public class UserProfile {

    @Id
    private long profileId;
    
    @OneToOne
    @MapsId
    private User 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/JPA 中标识符的五种核心定义方式:

  1. 简单标识符:基本类型需警惕默认值陷阱,包装类型强制显式赋值
  2. 生成式标识符
    • AUTO:智能选择策略
    • IDENTITY:数据库自增(禁用批量)
    • SEQUENCE:官方推荐
    • TABLE:性能较差
    • 自定义生成器:完全控制生成逻辑
  3. 复合标识符@EmbeddedId(封装性好) vs @IdClass(使用直接)
  4. 派生标识符:通过关联共享主键

选择策略时需权衡:

  • ✅ 数据库兼容性
  • ✅ 性能需求(批量操作)
  • ✅ 业务场景(手动/自动分配)
  • ✅ 代码可维护性

掌握这些机制,能让你在实体建模时游刃有余,避开主键设计的常见坑点。


原始标题:An Overview of Identifiers in Hibernate/JPA | Baeldung