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 类型,需要:

  1. 通过 Java 序列化读取现有数据
  2. 使用新类型的 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 的序列化方案迁移方法

新映射机制让我们无需手动实现即可使用更高效的集合存储方案,但迁移时需注意数据兼容性问题。对于新项目,建议直接使用原生类型;对于遗留系统,可按需逐步迁移。


原始标题:Storing Basic Arrays and Collections using Array/JSON/XML Types in Hibernate | Baeldung