需求背景

对服务进行重构、迁移时,需要对MySQL表列进行映射,但一些老服务上往往存在列命名不规范的问题,大部分仍是snake_case,但也还是存在一些camelCase和PascalCase。如果直接更改原服务中的列命名,需要配合修改两边服务中的代码,代价比较大。尽量希望新服务能够适配原列名,等全部迁移完成后,再用迁移脚本进行统一更改。

默认设置下,没有@Column注解的列名会转为snake_case进行映射,有@Column注解的会采用注解的名字,但也会自动被转为snake_case。

# convert automatically to `foo_bar` by default
@Column(name = "fooBar")
private String fooBar;

网上大部分的解决方案是采用指定物理命名策略:

spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl

但这种方式有一个问题,所有列名会默认采用代码中字段名直接映射,@Column注解不生效,而常规的Java字段命名规则是camelCase。

解决思路

先了解一下Hibernate提供几种策略,physical-strategy物理命名策略是与缓存数据的物理存储相关的策略,决定了缓存数据在底层存储介质上的存储方式,而implicit-strategy隐式命名策略是与Hibernate会话缓存(Session Cache)相关的策略,决定了实体对象在会话缓存中的缓存方式。

其中physical-strategy物理命名策略有2种:

# 直接映射,不会做过多的处理
org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
# 默认配置,表名、列名转snake_case表示
org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy

implicit-strategy隐式命名策略有5种配置,默认采用

# 默认配置,表名、列名直接映射,不进行任何修改
org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl

以上的命名策略还会对属性、外键、索引、主键等进行名称转换,有兴趣可以自行了解。

所以了解到目前,感觉有2种思路:

  • 如果Spring是以某个bean组件的方式对注解@Column的name进行修改的话,可以写个@Configuration替换掉这个bean,重写这个方法
  • 自定义命名策略,替换掉默认的策略

随后我就开始了调试代码,代码使用的spring-data-jpa的版本是2.7.x,发现代码调用的逻辑顺序是,获得注解的name,随后就进入到了Ejb3Column这个类:

# calling method path
get annotation name -> bind -> initMappingColumn -> redefineColumnName

设置mappingColumn属性的内容都在redefineColumnName这个方法内,默认的implicitNamingStrategy并没有对列名进行修改,physicalNamingStrategy策略的实施主要在下面两行代码中:

# redefineColumnName method
Identifier physicalName = physicalNamingStrategy.toPhysicalColumnName(implicitName, database.getJdbcEnvironment());
this.mappingColumn.setName(physicalName.render(database.getDialect()));

在第二行渲染设置mappingColumn属性时,render()渲染方法只是把Identifier类型的physicalName中的text属性加上了引用符号(如果需要的话),列名的转换是由toPhysicalColumnName()方法实现的,调试时发现策略的实际实现类是CamelCaseToSnakeCaseNamingStrategy

# CamelCaseToSnakeCaseNamingStrategy
public Identifier toPhysicalColumnName(Identifier name, JdbcEnvironment context) {
    return this.formatIdentifier(super.toPhysicalColumnName(name, context));
}
private Identifier formatIdentifier(Identifier identifier) {
    if (identifier != null) {
        String name = identifier.getText();
        String formattedName = name.replaceAll("([a-z]+)([A-Z]+)", "$1\\_$2").toLowerCase();
        return !formattedName.equals(name) ? Identifier.toIdentifier(formattedName, identifier.isQuoted()) : identifier;
    } else {
        return null;
    }
}

这段代码就是对列名始终进行snake_case转换的”罪魁祸首“,到这里就发现只能使用自定义策略的方式,因为无论如何获取注解的name,最后设置映射属性时,始终会走到这里使用命名策略进行转换。

redefineColumnName方法中,无论是否有注解,都用了同一个物理命名策略对列名进行转换,我们貌似也无法对Ejb3ColumnredefineColumnName方法进行重写,所以最后考虑使用特殊符号进行识别,对左右带有$符号的保留原来的格式:

# `fooBar` in database
@Column(name = "$fooBar$")
private String fooBar;
# strategy managed by spring as bean
@Component
public class CustomNamingStrategy extends CamelCaseToSnakeCaseNamingStrategy {
   @Override
   public Identifier toPhysicalColumnName(Identifier name, JdbcEnvironment context) {
       String text = name.getText();
       if (text.startsWith("$") && name.getText().endsWith("$")) {
           return Identifier.toIdentifier(text.substring(1, text.length() - 1), name.isQuoted());
       }
       return super.toPhysicalColumnName(name, context);
   }
}
# application.properties
spring.jpa.hibernate.naming.physical-strategy=com.sample.CustomNamingStrategy

大家如果还有更好的实现方式,欢迎分享。


野小白
1 声望0 粉丝