JDBC不识别自定义类型安全字段包装类的解决方案咨询
问题描述
为数据库ID字段实现类型安全包装类后,将其作为PreparedStatement参数使用时遇到类型不支持异常,使用的是ActiveJDBC,问题根源在JDBC层面。
示例代码:
ModelDelegate.count(PSetAnswer::class.java, "psetproblemid in (?)", new PSetProblemId(333661));
执行时抛出异常:
org.javalite.activejdbc.DBException: java.sql.SQLException: Type com.ktbyte.dto.id.PSetProblemId not supported type, query: SELECT COUNT(*) FROM psetanswers WHERE psetproblemid in (?), params: 333661 at [...] Caused by: java.sql.SQLException: Type com.ktbyte.dto.id.PSetProblemId not supported type at [...]
ID类结构:
public class PSetProblemId extends Id.IntId { public PSetProblemId(Integer value) { super(value); } } abstract public class Id<T> { public final T value; public Id(T value) { this.value = requireNonNull(value); } @Override public String toString() { return value.toString(); } // 其他省略方法 public static abstract class IntId extends Id<Integer> { public IntId(Integer value) { super(value); } } }
通过栈跟踪发现,MariaDB JDBC驱动的Configuration包含Codecs数组,用于定义Java类到特定类型的转换逻辑:
// deep in org.mariadb.jdbc.BasePreparedStatement for (Codec<?> codec : con.getContext().getConf().codecs()) { if (codec.canEncode(x)) { Parameter p = new Parameter(codec, x, scaleOrLength); parameters.set(parameterIndex - 1, p); return; } } throw new SQLException(String.format("Type %s not supported type", x.getClass().getName()));
尝试让Id实现RowId接口无效,需要找到可行的转换方案。
解决方案
方案1:自定义MariaDB JDBC Codec
实现MariaDB的Codec接口,让驱动自动识别IntId子类并转换为整数:
- 编写自定义Codec类
import org.mariadb.jdbc.codec.Codec; import org.mariadb.jdbc.codec.DataType; import org.mariadb.jdbc.message.server.ColumnDefinitionPacket; import java.sql.SQLException; import java.util.Optional; public class IntIdCodec implements Codec<Id.IntId> { @Override public String className() { return Id.IntId.class.getName(); } @Override public boolean canDecode(ColumnDefinitionPacket column, Class<?> type) { // 仅处理编码场景,解码返回false return false; } @Override public Id.IntId decode(ColumnDefinitionPacket column, byte[] data, Class<?> type, long maxLength, boolean useAlias) throws SQLException { throw new SQLException("Decode not supported for IntId"); } @Override public boolean canEncode(Object value) { // 判断是否为IntId子类实例 return value instanceof Id.IntId; } @Override public void encode(StringBuilder buffer, Object value) { // 直接拼接包装的整数值 buffer.append(((Id.IntId) value).value); } @Override public byte[] encodeBinary(Object value) throws SQLException { // 二进制编码转换为整数字节数组 Integer intValue = ((Id.IntId) value).value; return new byte[]{ (byte) (intValue >> 24), (byte) (intValue >> 16), (byte) (intValue >> 8), (byte) intValue }; } @Override public Optional<DataType> getBinaryEncodeType() { return Optional.of(DataType.LONG); } }
- 注册Codec到连接配置
在获取JDBC连接时,通过连接属性添加自定义Codec:
Properties props = new Properties(); props.setProperty("user", "your_user"); props.setProperty("password", "your_password"); // 注册自定义Codec,多个用逗号分隔 props.setProperty("codec", "com.ktbyte.dto.id.IntIdCodec"); Connection conn = DriverManager.getConnection("jdbc:mariadb://host:port/db", props);
如果使用ActiveJDBC连接池,需在对应的连接配置中添加该属性。
方案2:利用ActiveJDBC类型转换器
ActiveJDBC支持自定义类型转换器,自动在参数传递时拆包:
- 实现
Converter接口
import org.javalite.activejdbc.Converter; public class IntIdConverter implements Converter<Id.IntId, Integer> { @Override public Integer toDb(Id.IntId source) { return source.value; } @Override public Id.IntId fromDb(Integer source) { // 若无需从数据库读取转换回IntId,可抛出异常或留空 throw new UnsupportedOperationException("Conversion from DB to IntId not implemented"); } @Override public Class<Id.IntId> sourceClass() { return Id.IntId.class; } @Override public Class<Integer> destinationClass() { return Integer.class; } }
- 注册转换器
在应用启动时执行注册:
import org.javalite.activejdbc.Registry; Registry.registerConverter(new IntIdConverter());
注册后,ActiveJDBC会自动将IntId子类转换为Integer传递给JDBC驱动。
方案3:手动拆包(临时快速方案)
若不想修改框架配置,可在调用时手动取出包装的数值:
ModelDelegate.count(PSetAnswer::class.java, "psetproblemid in (?)", new PSetProblemId(333661).value);
优点是快速生效,缺点是每次调用都需手动拆包,削弱了类型安全的便利性。
内容的提问来源于stack exchange,提问作者Malcolm Crum
相关产品推荐
相关产品推荐

