需求背景
对服务进行重构、迁移时,需要对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
方法中,无论是否有注解,都用了同一个物理命名策略对列名进行转换,我们貌似也无法对Ejb3Column
的redefineColumnName
方法进行重写,所以最后考虑使用特殊符号进行识别,对左右带有$
符号的保留原来的格式:
# `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
大家如果还有更好的实现方式,欢迎分享。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。