1. 概述
在 SQL 表字段中映射数据集合是一种常见做法,当我们需要在实体中存储非关系型数据时特别有用。在 Hibernate 6 中,默认映射机制发生了变化,使得这类数据在数据库端的存储更加高效。
本文将深入探讨这些变化,并讨论从 Hibernate 5 迁移现有数据的可行方案。
2. Hibernate 6.x 中的基础数组/集合映射新特性
在 Hibernate 6 之前,集合映射默认使用 SqlTypes.VARBINARY
类型码,底层通过 Java 序列化实现。现在,由于映射机制的改变,我们可以将集合映射为原生数组、JSON 或 XML 格式。
让我们先看看几种主流 SQL 方言如何处理集合类型字段。首先添加最新的 Spring Data JPA 依赖(已内置 Hibernate 6.x):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
再添加 H2 数据库依赖(方便切换不同方言测试):
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
</dependency>
创建测试实体类:
public class User {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
Long id;
List<String> tags;
// getters and setters
}
2.1 PostgreSQL 方言
在 PostgreSQLDialect
中重写了 supportsStandardArrays()
方法,该驱动支持集合的原生数组实现。
配置数据库:
spring:
datasource:
url: jdbc:h2:mem:mydb;MODE=PostgreSQL
username: sa
password: password
driverClassName: org.h2.Driver
jpa:
database-platform: org.hibernate.dialect.PostgreSQLDialect
show-sql: true
验证字段类型映射:
static int ARRAY_TYPE_CODE = 2003;
@PersistenceContext
EntityManager entityManager;
@Test
void givenPostgresDialect_whenGetUserEntityFieldsTypes_thenExpectedTypeShouldBePresent() {
MappingMetamodelImpl mapping = (MappingMetamodelImpl) entityManager.getMetamodel();
EntityMappingType entityMappingType = mapping
.getEntityDescriptor(User.class.getName())
.getEntityMappingType();
entityMappingType.getAttributeMappings()
.forEach(attributeMapping -> {
if (attributeMapping.getAttributeName().equals("tags")) {
JdbcType jdbcType = attributeMapping.getSingleJdbcMapping().getJdbcType();
assertEquals(ARRAY_TYPE_CODE, jdbcType.getJdbcTypeCode());
}
});
}
日志显示 tags
字段被映射为 varchar array
类型:
Hibernate:
create table users (
id bigint not null,
tags varchar(255) array,
primary key (id)
)
2.2 Oracle 方言
OracleDialect
虽然没有重写 supportsStandardArrays()
,但在 getPreferredSqlTypeCodeForArray()
中无条件支持数组类型。
配置 Oracle 模式:
spring:
datasource:
url: jdbc:h2:mem:mydb;MODE=Oracle
username: sa
password: password
driverClassName: org.h2.Driver
jpa:
database-platform: org.hibernate.dialect.OracleDialect
show-sql: true
验证类型映射:
@Test
void givenOracleDialect_whenGetUserEntityFieldsTypes_thenExpectedTypeShouldBePresent() {
MappingMetamodelImpl mapping = (MappingMetamodelImpl) entityManager.getMetamodel();
EntityMappingType entityMappingType = mapping
.getEntityDescriptor(User.class.getName())
.getEntityMappingType();
entityMappingType.getAttributeMappings()
.forEach(attributeMapping -> {
if (attributeMapping.getAttributeName().equals("tags")) {
JdbcType jdbcType = attributeMapping.getSingleJdbcMapping().getJdbcType();
assertEquals(ARRAY_TYPE_CODE, jdbcType.getJdbcTypeCode());
}
});
}
日志显示使用 StringArray
类型:
Hibernate:
create table users (
id number(19,0) not null,
tags StringArray,
primary key (id)
)
2.3 自定义方言
默认没有方言支持 JSON/XML 映射。下面创建一个使用 JSON 作为集合默认类型的自定义方言:
public class CustomDialect extends Dialect {
@Override
public int getPreferredSqlTypeCodeForArray() {
return supportsStandardArrays() ? ARRAY : JSON;
}
@Override
protected void registerColumnTypes(TypeContributions typeContributions, ServiceRegistry serviceRegistry) {
super.registerColumnTypes( typeContributions, serviceRegistry );
final DdlTypeRegistry ddlTypeRegistry =
typeContributions.getTypeConfiguration().getDdlTypeRegistry();
ddlTypeRegistry.addDescriptor( new DdlTypeImpl( JSON, "jsonb", this ) );
}
}
配置使用 PostgreSQL 模式(支持 jsonb)和自定义方言:
spring:
datasource:
url: jdbc:h2:mem:mydb;MODE=PostgreSQL
username: sa
password: password
driverClassName: org.h2.Driver
jpa:
database-platform: com.baeldung.arrayscollections.dialects.CustomDialect
验证 JSON 类型映射:
static int JSON_TYPE_CODE = 3001;
@Test
void givenCustomDialect_whenGetUserEntityFieldsTypes_thenExpectedTypeShouldBePresent() {
MappingMetamodelImpl mapping = (MappingMetamodelImpl) entityManager.getMetamodel();
EntityMappingType entityMappingType = mapping
.getEntityDescriptor(User.class.getName())
.getEntityMappingType();
entityMappingType.getAttributeMappings()
.forEach(attributeMapping -> {
if (attributeMapping.getAttributeName().equals("tags")) {
JdbcType jdbcType = attributeMapping.getSingleJdbcMapping().getJdbcType();
assertEquals(JSON_TYPE_CODE, jdbcType.getJdbcTypeCode());
}
});
}
日志显示使用 jsonb
类型:
Hibernate:
create table users (
id bigint not null,
tags jsonb,
primary key (id)
)
3. 从 Hibernate 5.x 迁移到 6.x
Hibernate 5.x 和 6.x 对集合映射使用不同的默认类型。要迁移到原生数组或 JSON/XML 类型,需要:
- 通过 Java 序列化读取现有数据
- 使用新类型的 JDBC 方法重写数据
创建待迁移实体:
@Entity
@Table(name = "migrating_users")
public class MigratingUser {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE)
private Long id;
@JdbcTypeCode(SqlTypes.VARBINARY) // Hibernate 5 默认类型
private List<String> tags;
private List<String> newTags; // 新字段使用默认数组映射
// getters, setters
}
创建 Repository:
public interface MigratingUserRepository extends JpaRepository<MigratingUser, Long> {
}
执行迁移逻辑:
@Autowired
MigratingUserRepository migratingUserRepository;
@Test
void givenMigratingUserRepository_whenMigrateTheUsers_thenAllTheUsersShouldBeSavedInDatabase() {
prepareData();
migratingUserRepository
.findAll()
.stream()
.peek(u -> u.setNewTags(u.getTags())) // 复制数据到新字段
.forEach(u -> migratingUserRepository.save(u));
}
迁移步骤关键点:
4. 总结
本文深入探讨了 Hibernate 6.x 中的集合映射新特性:
- ✅ 原生数组类型(PostgreSQL/Oracle)
- ✅ JSON/XML 字段支持(通过自定义方言)
- ✅ 从 Hibernate 5 的序列化方案迁移方法
新映射机制让我们无需手动实现即可使用更高效的集合存储方案,但迁移时需注意数据兼容性问题。对于新项目,建议直接使用原生类型;对于遗留系统,可按需逐步迁移。