You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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]
问题原因
  1. 转换器泛型导致类型识别错误:DataEncryptionConverter实现的是AttributeConverter<Object, String>,Hibernate解析HQL时会认为u.firstName和u.lastName的类型是Object,但EmailFirstLastAccountId的构造器期望String类型参数,因此找不到匹配的构造器抛出异常。
  2. 单表查询正常的原因:单表查询直接加载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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.25 04:04:55