Spring Boot关联查询中自定义AttributeConverter失效问题解决
问题描述
在Spring Boot应用中实现了自定义AttributeConverter用于实体字段加解密,单表JPA查询(如User findByFirstNameAndLastName)可正常工作,但关联查询的自定义HQL抛出"Validation failed for query for method"错误。
转换器代码
@Converter public class DataEncryptionConverter implements AttributeConverter<Object, String> { @Value("${encryption.key}") private String encryptionKey; private final String encryptionCipher = "AES"; private Key key; private Cipher cipher; private Key getKey(){ if(key == null){ key = new SecretKeySpec(encryptionKey.getBytes(), encryptionCipher); } return key; } private Cipher getCipher() throws GeneralSecurityException { if(cipher == null){ cipher = Cipher.getInstance(encryptionCipher); } return cipher; } private void initCipher(int encryptMode) throws GeneralSecurityException{ getCipher().init(encryptMode, getKey()); } @SneakyThrows @Override public String convertToDatabaseColumn(Object attribute) { if (attribute == null) { return null; } initCipher(Cipher.ENCRYPT_MODE); byte[] bytes = SerializationUtils.serialize(attribute); return Base64.getEncoder().encodeToString(getCipher().doFinal(bytes)); } @SneakyThrows @Override public Object convertToEntityAttribute(String dbData) { if (dbData == null) { return null; } initCipher(Cipher.DECRYPT_MODE); byte[] bytes = getCipher().doFinal(Base64.getDecoder().decode(dbData)); return SerializationUtils.deserialize(bytes); } }
User实体字段配置
public class User{ // 其他字段 @Column(name = "first_name") @Convert(converter = DataEncryptionConverter.class) private String firstName; @Column(name = "last_name") @Convert(converter = DataEncryptionConverter.class) private String lastName; }
关联查询代码
public class UserAccount{ // 其他字段 @ManyToOne(optional = false) @JoinColumn(name = "user_id", referencedColumnName = "user_id") private User fkUserId; } @Query("select new EmailFirstLastAccountId(a.email, a.accountId, u.firstName, u.lastName ) from UserAccount a inner join a.fkUserId u where a.orgId=:orgId") List<EmailFirstLastAccountId> getEmailAccountIdFirstAndLastNameByOrgId(Long orgId);
核心异常信息
Caused by: org.hibernate.hql.internal.ast.QuerySyntaxException: Unable to locate appropriate constructor on class [com.tse.core_application.custom.model.EmailFirstLastAccountId]. Expected arguments are: java.lang.String, long, java.lang.Object, java.lang.Object [select new com.tse.EmailFirstLastAccountId(a.email, a.accountId, u.firstName, u.lastName ) from com.tse.UserAccount a inner join a.fkUserId u where a.orgId=:orgId]
问题原因
- 转换器泛型导致类型识别错误:
DataEncryptionConverter实现的是AttributeConverter<Object, String>,Hibernate解析HQL时会认为u.firstName和u.lastName的类型是Object,但EmailFirstLastAccountId的构造器期望String类型参数,因此找不到匹配的构造器抛出异常。 - 单表查询正常的原因:单表查询直接加载
User实体,Hibernate会在实体字段映射阶段调用转换器,将数据库的String转换为实体的String字段,类型匹配;而关联构造查询是在HQL层面直接提取字段,Hibernate根据转换器泛型返回类型做校验,导致不匹配。
解决方案
方案一:修改转换器泛型为<String, String>(推荐)
由于实际仅处理String类型字段,直接修改转换器泛型,让Hibernate正确识别字段类型:
@Converter public class DataEncryptionConverter implements AttributeConverter<String, String> { @Value("${encryption.key}") private String encryptionKey; private final String encryptionCipher = "AES"; private Key key; private Cipher cipher; private Key getKey(){ if(key == null){ key = new SecretKeySpec(encryptionKey.getBytes(), encryptionCipher); } return key; } private Cipher getCipher() throws GeneralSecurityException { if(cipher == null){ cipher = Cipher.getInstance(encryptionCipher); } return cipher; } private void initCipher(int encryptMode) throws GeneralSecurityException{ getCipher().init(encryptMode, getKey()); } @SneakyThrows @Override public String convertToDatabaseColumn(String attribute) { if (attribute == null) { return null; } initCipher(Cipher.ENCRYPT_MODE); byte[] bytes = attribute.getBytes(StandardCharsets.UTF_8); return Base64.getEncoder().encodeToString(getCipher().doFinal(bytes)); } @SneakyThrows @Override public String convertToEntityAttribute(String dbData) { if (dbData == null) { return null; } initCipher(Cipher.DECRYPT_MODE); byte[] bytes = getCipher().doFinal(Base64.getDecoder().decode(dbData)); return new String(bytes, StandardCharsets.UTF_8); } }
注:如果之前已用旧方式存储数据,可保留原
SerializationUtils序列化逻辑,仅修改泛型即可兼容。
方案二:在HQL中显式转换类型
若需保留转换器处理多类型的能力,可在HQL中对加密字段做显式类型转换:
@Query("select new EmailFirstLastAccountId(a.email, a.accountId, cast(u.firstName as string), cast(u.lastName as string) ) from UserAccount a inner join a.fkUserId u where a.orgId=:orgId") List<EmailFirstLastAccountId> getEmailAccountIdFirstAndLastNameByOrgId(Long orgId);
内容的提问来源于stack exchange,提问作者Mohan
相关产品推荐
相关产品推荐

