JPA实体中JSON类型Map字段持久化成功但查询失败的问题排查
问题:Oracle JSON列与EclipseLink转换器单向生效问题
需要将标识名称+布尔状态的键值对配置信息以JSON格式持久化到Oracle数据库,写入操作正常,但查询时抛出SQL异常,且转换器的convertToEntityAttribute方法从未被调用。
数据库表结构
CREATE TABLE "SCHEMA"."CONFIG" ( "ID" NUMBER(38,0) NOT NULL ENABLE, "ITEMS" JSON DEFAULT '' NOT NULL ENABLE, CONSTRAINT "PK_CONFIG" PRIMARY KEY ("ID") )
实体类定义
@Entity @Table(name = "CONFIG") @SequenceGenerator(name = "ConfigIdGenerator", sequenceName = "CONFIG_ID_SEQ", allocationSize = 1) public class ConfigEntity { private long id; private Map<String, Boolean> items; @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "ConfigIdGenerator") @Column(name = "ID", nullable = false) @Id public long getId() { return id; } public void setId(long id) { this.id = id; } @Convert(converter = ConfigItemsConverter.class) @Column(name = "ITEMS", nullable = false, columnDefinition = "JSON") public Map<String, Boolean> getItems() { return items; } public void setItems(Map<String, Boolean> items) { this.items = items; } }
自定义属性转换器
@Converter public class ConfigItemsConverter implements AttributeConverter<Map<String, Boolean>, String> { private static final ObjectMapper MAPPER = new ObjectMapper(); @Override public String convertToDatabaseColumn(Map<String, Boolean> value) { try { return MAPPER.writeValueAsString(value); } catch (JsonProcessingException ex) { // Should not happen. Map values should be in correct format due to generics. ex.printStackTrace(); } return null; } @Override public Map<String, Boolean> convertToEntityAttribute(String dbValue) { try { return MAPPER.readValue(dbValue, new TypeReference<Map<String, Boolean>>() {}); } catch (JsonProcessingException ex) { // Should not happen. DB data are in JSON format due to JSON datatype ex.printStackTrace(); } return null; } }
查询时抛出的异常
jakarta.persistence.PersistenceException: Exception [EclipseLink-4002] (Eclipse Persistence Services - 4.0.2.v202306161219): org.eclipse.persistence.exceptions.DatabaseException Internal Exception: java.sql.SQLException: Invalid column type: getOracleObject not implemented for class oracle.jdbc.driver.T4CJsonAccessor Error Code: 17004 Call: SELECT * FROM (SELECT /*+ FIRST_ROWS */ a.*, ROWNUM rnum FROM (SELECT t1.ID AS a1, t1.ITEMS AS a2 FROM CONFIG t1 ) a WHERE ROWNUM <= ?) WHERE rnum > ? bind => [1, 0] Query: ReadAllQuery(name="ConfigEntity.FIND_ALL" referenceClass=ConfigEntity sql="SELECT * FROM (SELECT /*+ FIRST_ROWS */ a.*, ROWNUM rnum FROM (SELECT t1.ID AS a1, t1.ITEMS AS a2 FROM CONFIG t1 ) a WHERE ROWNUM <= ?) WHERE rnum > ?") at org.eclipse.persistence.internal.jpa.QueryImpl.getDetailedException(QueryImpl.java:392) ~[org.eclipse.persistence.jpa-4.0.2.jar:?] at org.eclipse.persistence.internal.jpa.QueryImpl.executeReadQuery(QueryImpl.java:265) ~[org.eclipse.persistence.jpa-4.0.2.jar:?] at org.eclipse.persistence.internal.jpa.QueryImpl.getResultList(QueryImpl.java:483) ~[org.eclipse.persistence.jpa-4.0.2.jar:?] at jakarta.persistence.TypedQuery.getResultStream(TypedQuery.java:93) ~[jakarta.persistence-api-3.2.0.jar:3.2.0] ... Caused by: java.sql.SQLException: Invalid column type: getOracleObject not implemented for class oracle.jdbc.driver.T4CJsonAccessor at oracle.jdbc.driver.GeneratedAccessor.getOracleObject(GeneratedAccessor.java:1221) ~[ojdbc11-21.10.0.0.jar:21.10.0.0.0] at oracle.jdbc.driver.JsonAccessor.getObject(JsonAccessor.java:263) ~[ojdbc11-21.10.0.0.jar:21.10.0.0.0] at oracle.jdbc.driver.GeneratedStatement.getObject(GeneratedStatement.java:196) ~[ojdbc11-21.10.0.0.jar:21.10.0.0.0] at oracle.jdbc.driver.GeneratedScrollableResultSet.getObject(GeneratedScrollableResultSet.java:334) ~[ojdbc11-21.10.0.0.jar:21.10.0.0.0] at oracle.ucp.jdbc.proxy.oracle.ResultSetProxy.getObject(ResultSetProxy.java:152) ~[ucp11-21.10.0.0.jar:21.10.0.0.0] at oracle.ucp.jdbc.proxy.oracle$1ucp$1jdbc$1proxy$1oracle$1ResultSetProxy$2oracle$1jdbc$1internal$1OracleResultSet$$$Proxy.getObject(Unknown Source) ~[ucp11-21.10.0.0.jar:21.10.0.0.0] at org.eclipse.persistence.internal.databaseaccess.DatabasePlatform.getObjectFromResultSet(DatabasePlatform.java:1230) ~[org.eclipse.persistence.core-4.0.2.jar:?] at org.eclipse.persistence.platform.database.OraclePlatform.getObjectFromResultSet(OraclePlatform.java:575) ~[org.eclipse.persistence.core-4.0.2.jar:?] at org.eclipse.persistence.internal.databaseaccess.DatabaseAccessor.getObject(DatabaseAccessor.java:1375) ~[org.eclipse.persistence.core-4.0.2.jar:?] at org.eclipse.persistence.internal.databaseaccess.DatabaseAccessor.fetchRow(DatabaseAccessor.java:1094) ~[org.eclipse.persistence.core-4.0.2.jar:?]
核心疑问
- 为何转换器仅写入方向生效,查询时不触发
convertToEntityAttribute? - 缺少哪些配置导致该问题?
- 如何修复?
问题原因与修复方案
原因分析
EclipseLink对Oracle的原生JSON列有内置处理逻辑,它会将JSON类型列默认映射为Oracle的OracleJsonValue类型,而非字符串。此时@Convert注解会被忽略——JPA提供者优先处理原生数据库类型映射,不会调用自定义转换器,导致查询时尝试直接将OracleJsonValue转换为Map<String, Boolean>,引发JDBC类型转换异常。
修复方案
方案1:修改列映射,强制按字符串处理
在@Column注解中指定jdbcType为VARCHAR,告诉EclipseLink将JSON列当作字符串处理,从而触发自定义转换器:
import jakarta.persistence.JDBCType; // ... @Convert(converter = ConfigItemsConverter.class) @Column(name = "ITEMS", nullable = false, columnDefinition = "JSON", jdbcType = JDBCType.VARCHAR) public Map<String, Boolean> getItems() { return items; }
方案2:使用EclipseLink的JSON原生映射支持
如果不想依赖自定义转换器,可以直接利用EclipseLink的原生JSON处理能力:
- 移除
@Convert注解 - 修改字段映射,指定
jdbcType为OTHER并添加@Type注解:
import jakarta.persistence.JDBCType; import org.eclipse.persistence.annotations.Type; // ... @Column(name = "ITEMS", nullable = false, columnDefinition = "JSON", jdbcType = JDBCType.OTHER) @Type(type = "json") public Map<String, Boolean> getItems() { return items; }
注:此方案要求EclipseLink版本≥2.7,确保原生JSON映射功能可用。
方案3:调整版本兼容性
异常中的getOracleObject not implemented可能是JDBC驱动与EclipseLink版本不匹配导致的,尝试升级EclipseLink到4.0.3+或ojdbc11到21.11.0.0+,确保两者版本兼容。
验证
修复后执行查询,确认convertToEntityAttribute方法被调用,且能正常将JSON字符串转换为Map<String, Boolean>,无类型转换异常。
内容的提问来源于stack exchange,提问作者Jan Stanicek
相关产品推荐
相关产品推荐

