Hibernate 6中的序列命名策略(详细指南)

689 阅读6分钟

Hibernate 6引入了一个新的配置参数和一个接口,用于定义用于生成主键值的数据库序列和表的隐式命名策略。当你将一个现有的应用程序迁移到Hibernate 6时,你很快就会认识到这种变化,因为默认的命名策略已经改变了。由于这个原因,Hibernate可能会尝试使用一个在你的数据库中不存在的序列。但这并不是你应该熟悉这个新设置的唯一情况。它也可以帮助映射一个具有奇怪命名序列的传统数据库,或者如果你需要遵循一些内部命名惯例。

从Hibernate 6开始,你可以使用配置属性hibernate.id.db_structure_naming_strategy来定义Hibernate应使用的命名策略,如果你没有在映射中明确定义序列名称

<persistence>
    <persistence-unit name="my-persistence-unit">
        ...
        <properties>
            <property name="hibernate.id.db_structure_naming_strategy" value="standard" />
			...
        </properties>
    </persistence-unit>
</persistence>

内容

Hibernate 6支持的命名策略

以前的Hibernate版本提供了1种默认行为,如果你想使用不同的序列名称,你必须指定序列名称。在Hibernate 6中,你可以为数据库序列选择4种隐式命名策略。

  • 标准
    这是在Hibernate 6中新的默认值。
    它将配置的序列后缀(默认为*_SEQ*)与实体类映射的表的名称相连接。

  • 遗产
    这种命名策略为你提供了与Hibernate版本>=5.3但默认使用的<6版本相同的行为。
    序列名称取决于你的实体映射定义:

    • 如果你引用了一个生成器而没有定义序列名称,Hibernate会使用生成器的名称。如果你只想定义序列名,这简化了映射,是Hibernate 5.3中引入的优化 。 如果你的映射没有引用生成器,Hibernate会使用其默认的序列名hibernate_sequence

  • 单一的
    这种命名策略为你提供了与Hibernate在<5.3版本中默认使用的相同行为。
    它总是使用Hibernate的默认序列名hibernate_sequence

  • ImplicitDatabaseObjectNamingStrategy 实现的完全合格的类名,
    这使你可以提供你自己的命名策略。我将在本文的最后向你展示如何做到这一点。

让我们仔细看看所有4种命名策略。

ID_DB_STRUCTURE_NAMING_STRATEGY= 标准

与之前的Hibernate版本相比,Hibernate 6默认为每个实体类使用一个单独的数据库序列。该序列的名称由实体类被映射到的数据库表的名称和后缀_SEQ组成。

隐式表的映射

如果你没有指定数据库表的名称,Hibernate会使用其隐式命名策略。默认策略使用实体类的简单类名作为表名。

@Entity
public class ChessPlayer {

	@Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
	private Long id;

    private String firstName;
    
    private String lastName;

    private LocalDate birthDate;

    @Version
    private int version;
	
	...
}

所以,ChessPlayer实体类会被映射到ChessPlayer 表。而如果你使用Hibernate 6的数据库序列标准命名策略,Hibernate会使用序列ChessPlayer_SEQ 来生成主键值:

ChessPlayer player = new ChessPlayer();
player.setFirstName("Thorben");
player.setLastName("Janssen");
em.persist(player);

16:15:04,917 DEBUG [org.hibernate.SQL] - 
    select
        nextval('ChessPlayer_SEQ')
16:15:04,947 DEBUG [org.hibernate.SQL] - 
    insert 
    into
        ChessPlayer
        (birthDate, firstName, lastName, version, id) 
    values
        (?, ?, ?, ?, ?)

自定义表的映射

你可以通过给你的实体类加上*@Table*注解并设置数据库表的名称来定制表的映射:

@Entity
@Table(name="player")
public class ChessPlayer {

	@Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
	private Long id;

	...
}

让我们用前面的测试案例来使用这种映射。你可以在日志输出中看到,Hibernate现在调用数据库序列player_SEQ来生成主键值。它还将ChessPlayer实体对象持久化到player表中。

16:17:04,094 DEBUG [org.hibernate.SQL] - 
    select
        nextval('player_SEQ')
16:17:04,120 DEBUG [org.hibernate.SQL] - 
    insert 
    into
        player
        (birthDate, firstName, lastName, version, id) 
    values
        (?, ?, ?, ?, ?)

ID_DB_STRUCTURE_NAMING_STRATEGY=legacy

传统的命名 策略 可以得到与Hibernate在>=5.3和<6版本中使用的相同策略。你可以通过将persistence.xml配置中的属性hibernate.id.db_structure_naming_strategy 设置为legacy 来激活它。

<persistence>
    <persistence-unit name="my-persistence-unit">
        ...
        <properties>
            <property name="hibernate.id.db_structure_naming_strategy" value="legacy" />
			...
        </properties>
    </persistence-unit>
</persistence>

这个命名策略的行为取决于你的实体映射。

没有生成器引用的映射

Hibernate对所有用*@GeneratedValue@GeneratedValue(strategy = GenerationType.SEQUENCE)*注释的主键属性使用1个默认序列。这2个映射的重要之处在于,它们没有引用生成器。

@Entity
public class ChessPlayer {

	@Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
	private Long id;

    private String firstName;
    
    private String lastName;

    private LocalDate birthDate;

    @Version
    private int version;
	
	...
}

当你使用传统的命名策略持久化这个ChessPlayer实体时,Hibernate使用数据库序列hibernate_sequence来生成主键值。

16:51:10,742 DEBUG [org.hibernate.SQL] - 
    select
        nextval('hibernate_sequence')
16:51:10,771 DEBUG [org.hibernate.SQL] - 
    insert 
    into
        ChessPlayer
        (birthDate, firstName, lastName, version, id) 
    values
        (?, ?, ?, ?, ?)

有生成器引用但没有序列名称的映射

如果你的主键映射引用了一个不存在的生成器,或者没有定义sequenceName,Hibernate会使用生成器的名称作为序列名称。这个Hibernate特有的优化是在5.3版本中引入的,以简化最常用的映射定义,它只定制了数据库序列的名称。

@Entity
public class ChessPlayer {

	@Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator="player_seq")
	private Long id;

    private String firstName;
    
    private String lastName;

    private LocalDate birthDate;

    @Version
    private int version;
	
	...
}

当你持久化这个实体类的一个对象时,Hibernate使用数据库序列player_seq 来生成主键值。

16:51:50,304 DEBUG [org.hibernate.SQL] - 
    select
        nextval('player_seq')
16:51:50,343 DEBUG [org.hibernate.SQL] - 
    insert 
    into
        ChessPlayer
        (birthDate, firstName, lastName, version, id) 
    values
        (?, ?, ?, ?, ?)

ID_DB_STRUCTURE_NAMING_STRATEGY = single

命名策略single传统 策略的一个简单版本,可以得到Hibernate在<5.3版本中的默认命名。你可以通过将persistence.xml 配置中的属性hibernate.id.db_structure_naming_strategy 设置为single 来激活它。

<persistence>
    <persistence-unit name="my-persistence-unit">
        ...
        <properties>
            <property name="hibernate.id.db_structure_naming_strategy" value="single" />
			...
        </properties>
    </persistence-unit>
</persistence>

如果你没有在映射定义中指定一个序列名称,这个策略总是使用数据库序列hibernate_sequence

@Entity
public class ChessPlayer {

	@Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
	private Long id;

    private String firstName;
    
    private String lastName;

    private LocalDate birthDate;

    @Version
    private int version;
	
	...
}

你可以在Hibernate的日志输出中看到,如果你使用命名策略single来持久化这个ChessPlayer 类的一个对象。

16:57:15,706 DEBUG [org.hibernate.SQL] - 
    select
        nextval('hibernate_sequence')
16:57:15,734 DEBUG [org.hibernate.SQL] - 
    insert 
    into
        ChessPlayer
        (birthDate, firstName, lastName, version, id) 
    values
        (?, ?, ?, ?, ?)

ID_DB_STRUCTURE_NAMING_STRATEGY= custom class

我在前面的章节中向你展示了Hibernate对数据库序列的3种标准命名策略。你可以使用同样的机制来提供你自己的命名策略。你只需要提供一个ImplicitDatabaseObjectNamingStrategy 接口的自定义实现,并在你的persistence.xml中配置它。

ImplicitDatabaseObjectNamingStrategy接口的实现不需要很复杂。该接口只定义了两个方法,它们都返回一个QualifiedName对象。

public class MyImplicitDatabaseObjectNamingStrategy implements ImplicitDatabaseObjectNamingStrategy {
	public static final String STRATEGY_NAME = "custom";

    @Override
    public QualifiedName determineSequenceName(Identifier catalogName, Identifier schemaName, Map<?, ?> configValues,
            ServiceRegistry serviceRegistry) {
        final JdbcEnvironment jdbcEnvironment = serviceRegistry.getService(JdbcEnvironment.class);

        String seqName = "seq_".concat(((String) configValues.get("jpa_entity_name")));

        return new QualifiedSequenceName(
                catalogName,
                schemaName,
                jdbcEnvironment.getIdentifierHelper().toIdentifier(seqName));
    }

    @Override
    public QualifiedName determineTableName(Identifier catalogName, Identifier schemaName, Map<?, ?> configValues,
            ServiceRegistry serviceRegistry) {
        final JdbcEnvironment jdbcEnvironment = serviceRegistry.getService(JdbcEnvironment.class);

        return new QualifiedNameParser.NameParts(
                catalogName,
                schemaName,
                jdbcEnvironment.getIdentifierHelper().toIdentifier(DEF_TABLE));
    }

}

determineSequenceName方法返回Hibernate应使用的数据库序列的名称。determineTableName方法返回Hibernate用来模拟序列的数据库表的名称。

在这篇文章中,我没有涉及到实现determineTableName 方法的任何细节。你可以用与数据库序列的名称解析相同的方式来定制它。但是模拟序列会导致很多可扩展性问题,而且所有现代数据库都支持序列或自动递增列。因此,这种机制已经不再具有实际意义。请坚持使用返回Hibernate默认表名的默认实现,并使用序列或自动递增的列来生成主键值。

determineSequenceName方法的实现完全取决于你的表模型和应用需求。Map configValues方法参数包含几个关于实体类和数据库表的映射信息,你可以用它来生成你的序列名。在这个例子中,我实现了一个简单的命名策略,使用seq_作为所有序列名称的前缀,并将其与实体类的逻辑名称连接起来。

实体类的逻辑名称是你的实体类的简单类名或你在*@Entity*注解中定义的名称。

@Entity(name="Player")
public class ChessPlayer {

	@Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
	private Long id;

    private String firstName;
    
    private String lastName;

    private LocalDate birthDate;

    @Version
    private int version;
	
	...
}

实现了ImplicitDatabaseObjectNamingStrategy接口后,你需要在你的配置中引用它。你可以通过将配置属性hibernate.id.db_structure_naming_strategy设置为你的接口实现的全限定类名来实现。

<persistence>
    <persistence-unit name="my-persistence-unit">
        ...
        <properties>
            <property name="hibernate.id.db_structure_naming_strategy" value="com.thorben.janssen.sample.model.MyImplicitDatabaseObjectNamingStrategy" />
			...
        </properties>
    </persistence-unit>
</persistence>

当你使用与前面例子中相同的测试案例来持久化一个ChessPlayer实体对象时,你可以看到Hibernate现在使用数据库序列seq_Player 来生成主键值。

17:06:51,325 DEBUG [org.hibernate.SQL] - 
    select
        nextval('seq_Player')
17:06:51,352 DEBUG [org.hibernate.SQL] - 
    insert 
    into
        Player
        (birthDate, firstName, lastName, version, id) 
    values
        (?, ?, ?, ?, ?)

修复Hibernate 6中的迁移问题

当你将现有的应用程序迁移到Hibernate 6时,默认的命名策略会从单一(如果你一直在使用Hibernate <5.3)或传统(如果你一直在使用Hibernate >=5.3)变为标准。正如我前面所描述的,这改变了Hibernate用来生成主键值的序列的名称。

如果你遇到了这个问题,你可以通过在映射中明确定义序列名称迁移你的数据库模式,或者在你的 persistence.xml中配置旧的命名策略来解决它。

<persistence>
    <persistence-unit name="my-persistence-unit">
        ...
        <properties>
            <property name="hibernate.id.db_structure_naming_strategy" value="single" />
			...
        </properties>
    </persistence-unit>
</persistence>

结论

ImplicitDatabaseObjectNamingStrategy接口和配置属性hibernate.id.db_structure_naming_strategyHibernate 6中引入了一种新的隐式命名策略。它定义了如果你没有在实体映射定义中指定它们的名字,Hibernate如何确定数据库序列或用于模拟序列的数据库表的名称。

大多数开发者在将他们的应用程序迁移到Hibernate 6时需要使用这个配置,因为Hibernate的默认隐式命名策略已经改变。Hibernate现在不再对所有没有指定序列的实体类使用1个默认序列,而是生成了一个实体特定的默认名称。你可以通过设置配置属性hibernate.id.db_structure_naming_strategy来告诉Hibernate使用旧的命名策略,如果你是从Hibernate版本<5.3迁移过来的,可以设置为single;如果你是从Hibernate版本>=5.3迁移过来的,可以设置为legacy

你也可以为数据库序列提供你自己的命名策略。要做到这一点,你需要实现ImplicitDatabaseObjectNamingStrategy接口,并提供完全限定的类名称作为配置属性hibernate.id.db_structure_naming_strategy的值。