Jpa@Column注解的name失效的问题

1.问题

首先我jpa使用的spring-boot-data-jpa 。springboot 版本 2.2.6.RELEASE

jpa也没有去配置命名策略,使用的默认驼峰转下划线。

表结构大致如下

1
locationId |  locationName 

实体如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Getter
@Setter
@NoArgsConstructor
@Accessors(chain = true)
@Entity
@Access(AccessType.FIELD)
@Table(name = "FDYJXH_INSTRUCTOR_TALK_LOCATION")
public class StudentTalkLocation implements Serializable {
private static final long serialVersionUID = -95507753868472868L;

/**
* 地点编号
**/
@Id
@Column(name = "locationId")
@NotBlank
private String locationId;

/**
* 地点名
**/
@Column(name = "locationName")
private String locationName;


}

因为数据库表中的字段就是使用的驼峰式命名,所以我加上了@Column注解,希望jpa不去转换下划线直接使用@column中的name属性。但是事与愿违。报错如下

1
ERROR org.hibernate.engine.jdbc.spi.SqlExceptionHelper - 列名 'location_id' 无效。

2.解决方法(一)

网上百度了下,很容易的找到了一个答案。修改 jpa 的命名策略 。

在application配置文件加上如下配置。

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

测试了下确实可以解决这个问题。

但是随着这个配置的增加,原先需要自动驼峰转下划线的地方都需要你去加上@Column配置

所以这里没采用这个解决方法,毕竟需要加 @Column注解的只有这一个表(由于一些原因,我无法去修改表结构)

ps:假设这个解决方法解决了你的问题,但我还是希望你能看下解决方法二,因为看完你会发现,并不是 @Column注解失效,而是Jpa的默认命名策略就是这样的。

3.解决方法(二)

百度上大部分都是**解决方法(一)**,所以找了好久没找到,于是想先了解下jpa的命名转化原理。

学习Spring-Data-Jpa(四)—Naming命名策略,源码跟踪 这个博客不错。可以去看下。

jpa的命名转化规则

首先在Entity实体中,命名方式有两种;
一种是显示命名,即通过@Table的name属性指定对应的数据库表名称,@Column的name属性指定实体字段对应数据库字段的名称。
另一种是隐式命名,显示命名一般不是必要的,所以可以不设置名称,交给框架来进行隐式命名。

命名策略分两步走:

第一步:如果我们没有使用@Table或@Column指定了表或字段的名称,则由SpringImplicitNamingStrategy为我们隐式处理,表名隐式处理为类名,列名隐式处理为字段名。如果指定了表名列名,SpringImplicitNamingStrategy不起作用。
第二步:将上面处理过的逻辑名称解析成物理名称。无论在实体中是否显示指定表名列名,SpringPhysicalNamingStrategy都会被调用。

所以如果我们想要自定义命名策略,可以根据自己的需求选择继承二者,并在配置文件中通过spring.jpa.hibernate.naming.implicit-strategy 或 spring.jpa.hibernate.naming.physical-strategy 进行指定自己的策略(例如为表名添加指定前缀)。

我们可以看下 SpringPhysicalNamingStrategy 的源码,只贴了其中比较关键的一个方法

1
2
3
4
5
6
7
8
9
10
11
12
13
private Identifier apply(Identifier name, JdbcEnvironment jdbcEnvironment) {
if (name == null) {
return null;
}
// 驼峰转下划线的逻辑
StringBuilder builder = new StringBuilder(name.getText().replace('.', '_'));
for (int i = 1; i < builder.length() - 1; i++) {
if (isUnderscoreRequired(builder.charAt(i - 1), builder.charAt(i), builder.charAt(i + 1))) {
builder.insert(i++, '_');
}
}
return getIdentifier(builder.toString(), name.isQuoted(), jdbcEnvironment);
}

从代码可以得知 将locationId 转成 location_id 发生在 第二步显示命名的转换中。所以我们即使使用了 @Column 指定的字段名依然还是无法阻止他驼峰转下划线。

解决方法

自定义一个 显示命名策略 , 他的转换规则如下

当 columnName 包含 # 字符时不去驼峰转下划线,只是把#字符去除。

ps : columnName 指的是经过了隐式转换接下来在显示转换需要去转换的值。可以看做 @Column注解中 name 的值

自定义 CustomPhysicalNamingStrategy 的代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
package com.qx.yxy.xljkzx.common.jpa;

import org.hibernate.boot.model.naming.Identifier;
import org.hibernate.boot.model.naming.PhysicalNamingStrategy;
import org.hibernate.engine.jdbc.env.spi.JdbcEnvironment;

import java.util.Locale;

/**
* 自定义的jpa显式(物理)转换规则
* 代码来自于
* {@link org.springframework.boot.orm.jpa.hibernate.SpringPhysicalNamingStrategy}
* 只修改了部分在 apply() 方法中
* 当经过显示转换后的 columnName 中包含#号时不进行 驼峰转下划线处理 而是只去除#号
* 比如 #locationId -> locationId
* @author xia17
* @date 2021/1/20 11:09
*/
public class CustomPhysicalNamingStrategy implements PhysicalNamingStrategy {

/** 不需要驼峰转下划线的标识 */
private static final String NOT_CAST_MARK = "#";

@Override
public Identifier toPhysicalCatalogName(Identifier name, JdbcEnvironment jdbcEnvironment) {
return apply(name, jdbcEnvironment);
}

@Override
public Identifier toPhysicalSchemaName(Identifier name, JdbcEnvironment jdbcEnvironment) {
return apply(name, jdbcEnvironment);
}

@Override
public Identifier toPhysicalTableName(Identifier name, JdbcEnvironment jdbcEnvironment) {
return apply(name, jdbcEnvironment);
}

@Override
public Identifier toPhysicalSequenceName(Identifier name, JdbcEnvironment jdbcEnvironment) {
return apply(name, jdbcEnvironment);
}

@Override
public Identifier toPhysicalColumnName(Identifier name, JdbcEnvironment jdbcEnvironment) {
return apply(name, jdbcEnvironment);
}


private Identifier apply(Identifier name, JdbcEnvironment jdbcEnvironment) {
if (name == null) {
return null;
}
if (name.getText().contains(NOT_CAST_MARK)){
// 当包含#字符时 不需要 驼峰转下划线 只需要去除 # 标识即可
return getIdentifier(name.getText().replace(NOT_CAST_MARK,""), name.isQuoted(), jdbcEnvironment);
}
// 驼峰转下划线
StringBuilder builder = new StringBuilder(name.getText().replace('.', '_'));
for (int i = 1; i < builder.length() - 1; i++) {
if (isUnderscoreRequired(builder.charAt(i - 1), builder.charAt(i), builder.charAt(i + 1))) {
builder.insert(i++, '_');
}
}
return getIdentifier(builder.toString(), name.isQuoted(), jdbcEnvironment);
}

/**
* Get an identifier for the specified details. By default this method will return an
* identifier with the name adapted based on the result of
* {@link #isCaseInsensitive(JdbcEnvironment)}
* @param name the name of the identifier
* @param quoted if the identifier is quoted
* @param jdbcEnvironment the JDBC environment
* @return an identifier instance
*/
protected Identifier getIdentifier(String name, boolean quoted, JdbcEnvironment jdbcEnvironment) {
if (isCaseInsensitive(jdbcEnvironment)) {
name = name.toLowerCase(Locale.ROOT);
}
return new Identifier(name, quoted);
}

/**
* Specify whether the database is case sensitive.
* @param jdbcEnvironment the JDBC environment which can be used to determine case
* @return true if the database is case insensitive sensitivity
*/
protected boolean isCaseInsensitive(JdbcEnvironment jdbcEnvironment) {
return true;
}

private boolean isUnderscoreRequired(char before, char current, char after) {
return Character.isLowerCase(before) && Character.isUpperCase(current) && Character.isLowerCase(after);
}




}

然后在 application配置文件中指定 显式命名策略为我们自定义的

1
spring.jpa.hibernate.naming.physical-strategy=com.qx.yxy.xljkzx.common.jpa.CustomPhysicalNamingStrategy

然后我们的Entity代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class StudentTalkLocation implements Serializable {
private static final long serialVersionUID = -95507753868472868L;

/**
* 地点编号
**/
@Id
@Column(name = "#locationId")
@NotBlank
private String locationId;

/**
* 地点名
**/
@Column(name = "#locationName")
private String locationName;


}

测试通过