How to Keep Default camelCase to snake_case Conversion in Spring Data JPA and Prevent Conversion of @Column Annotation Names

Bota5ky
3 min readMar 19, 2024

--

Background

During service refactoring and migration, it’s often necessary to map MySQL table columns. However, legacy services may have issues with inconsistent column naming. While most columns still use snake_case, there are also camelCase and PascalCase columns. Changing column names directly in the original service would require modifying both sides of the code, which is costly. Ideally, the new service should adapt to the original column names. After completing all migrations, a migration script can be used to unify the names.

By default, columns without a @Column annotation are converted to snake_case for mapping, while those with @Column annotations use the specified name but are also automatically converted to snake_case.

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

Most online solutions suggest specifying a physical naming strategy:

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

However, this approach has a drawback: all column names default to direct mapping from the field name in the code, ignoring the @Column annotation. Yet, conventional Java field naming follows camelCase.

Approach

First, let’s understand a few Hibernate strategies. The physical-strategy determines how cached data is stored physically, while the implicit-strategy relates to Hibernate session cache, determining how entity objects are cached in the session.

There are 2 physical-strategy options:

# Direct mapping without much processing
org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
# Default configuration, representing table and column names as snake_case
org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy

And 5 implicit-strategy options:

# Default configuration, directly mapping table and column names without any modifications
org.hibernate.boot.model.naming.ImplicitNamingStrategyJpaCompliantImpl

These naming strategies also apply transformations to attributes, foreign keys, indexes, primary keys, etc.

So far, it seems there are 2 approaches:

  • If Spring modifies the @Column name as a bean component, a @Configuration can replace this bean and override the method
  • Customize the naming strategy to replace the default strategy

Next, I debugged the code using spring-data-jpa version 2.7.x. I found that the logic calls for getting the annotation name, then enters the Ejb3Column class:

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

The content of the mappingColumn attribute setting is all within the redefineColumnName method. By default, the implicitNamingStrategy does not modify the column name. The implementation of the physicalNamingStrategy strategy is primarily in these two lines:

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

When setting the mappingColumn attribute, the render() method merely adds quotation marks (if necessary) to the text attribute of the Identifier. Column name conversion is implemented by the toPhysicalColumnName() method. During debugging, I found that the actual implementation class of the strategy is 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;
}
}

This piece of code is the main culprit for always converting column names to snake_case. Thus, it seems that we can only use a custom strategy because, regardless of whether there is an annotation, the same physical naming strategy is used to convert column names during the final setting of the mapping attribute.

In the redefineColumnName method, regardless of whether there is an annotation, the same physical naming strategy is used to convert column names. Unfortunately, we seem unable to override the redefineColumnName method in the Ejb3Column class. Therefore, it appears that using special symbols for identification, preserving the original format for names enclosed in $, might be a solution:

# `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

If anyone has a better solution, please share~

--

--