1. 概述
Hibernate 中的标识符(Identifier)代表实体的主键。这意味着标识符值必须满足三个核心条件:
- ✅ 唯一性:能唯一标识特定实体
- ✅ 非空性:不能为 null
- ✅ 不可变性:创建后不应修改
Hibernate 提供了多种定义标识符的方式。本文将系统梳理每种实体 ID 映射方法,帮你避开常见坑点。
2. 简单标识符
最直接的方式是使用 @Id
注解。简单标识符映射到 Java 基本类型、包装类型、String
、Date
、BigDecimal
和 BigInteger
等单一属性。
基本类型示例
@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");
}
}
核心逻辑:
- 查询现有主键中
prefix-XX
格式的最大值 - 最大值 +1 并拼接前缀生成新 ID
- 通过
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 中标识符的五种核心定义方式:
- 简单标识符:基本类型需警惕默认值陷阱,包装类型强制显式赋值
- 生成式标识符:
- AUTO:智能选择策略
- IDENTITY:数据库自增(禁用批量)
- SEQUENCE:官方推荐
- TABLE:性能较差
- 自定义生成器:完全控制生成逻辑
- 复合标识符:
@EmbeddedId
(封装性好) vs@IdClass
(使用直接) - 派生标识符:通过关联共享主键
选择策略时需权衡:
- ✅ 数据库兼容性
- ✅ 性能需求(批量操作)
- ✅ 业务场景(手动/自动分配)
- ✅ 代码可维护性
掌握这些机制,能让你在实体建模时游刃有余,避开主键设计的常见坑点。