需求背景
對服務進行重構、遷移時,需要對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
大家如果還有更好的實現方式,歡迎分享。