Spring Data JPA同一参数在加密/非加密列查询的转换异常问题
解决Spring Data JPA混合查询加密列与明文列的参数转换问题
背景与问题
我们需要对数据量较大的表中某列逐步加密:新数据存入加密列,旧数据保留在原明文列并缓慢迁移。通过新增加密列+AttributeConverter实现自动加解密,实体类定义如下:
public class AccountCard { @Column(name = "card_number") private String number; @Column(name = "encrypted_card_number") @Convert(converter = CardNumberConverter.class) private String encryptedCardNumber; }
编写Repository查询时,需要同时匹配加密列和原明文列,但Spring Data JPA会对同一个参数cardNumber自动执行加密转换,导致原明文列的匹配条件也使用了加密后的值,无法正确匹配旧数据。目前通过两个同名参数的写法可以解决,但代码不够优雅:
@Query(""" SELECT a FROM AccountCard a WHERE a.encryptedCardNumber = :cardNumber1 OR a.number = :cardNumber2 """) AccountCard findByCardNumber(@Param("cardNumber1") String cardNumber1, @Param("cardNumber2") String cardNumber2);
解决方案
方案1:利用SpEL表达式显式控制加密
借助Spring Data JPA的SpEL支持,直接在查询中调用转换器的加密方法处理加密列参数,明文列则使用原始参数:
@Query(""" SELECT a FROM AccountCard a WHERE a.encryptedCardNumber = #{T(com.yourpackage.CardNumberConverter).convertToDatabaseColumn(#cardNumber)} OR a.number = :cardNumber """) AccountCard findByCardNumber(@Param("cardNumber") String cardNumber);
如果CardNumberConverter是Spring管理的Bean,也可以通过Bean名称调用:
@Query(""" SELECT a FROM AccountCard a WHERE a.encryptedCardNumber = #{@cardNumberConverter.convertToDatabaseColumn(#cardNumber)} OR a.number = :cardNumber """) AccountCard findByCardNumber(@Param("cardNumber") String cardNumber);
这种方式无需修改转换器,直接在查询中区分参数处理逻辑。
方案2:自定义参数类型区分明文/加密值
创建一个明文参数包装类,让转换器仅对普通String类型生效:
- 定义明文包装类:
public class PlainText { private final String value; public PlainText(String value) { this.value = value; } public String getValue() { return value; } }
- 保持原转换器不变(仅处理
String类型):
public class CardNumberConverter implements AttributeConverter<String, String> { @Override public String convertToDatabaseColumn(String s) { return AES.encrypt(s); } @Override public String convertToEntityAttribute(String s) { return AES.decrypt(s); } }
- 修改Repository查询,使用包装类传递明文参数:
@Query(""" SELECT a FROM AccountCard a WHERE a.encryptedCardNumber = :cardNumber OR a.number = :plainCardNumber.value """) AccountCard findByCardNumber(@Param("cardNumber") String cardNumber, @Param("plainCardNumber") PlainText plainCardNumber);
调用时只需将明文参数包装:
accountCardRepository.findByCardNumber(cardNumber, new PlainText(cardNumber));
方案3:使用原生SQL手动控制加密
如果JPQL的SpEL方式受限,可改用原生SQL,手动对加密列参数执行加密:
@Query(value = """ SELECT * FROM account_card a WHERE a.encrypted_card_number = ?1 OR a.card_number = ?2 """, nativeQuery = true) AccountCard findByCardNumber(String encryptedCardNumber, String plainCardNumber);
调用时提前加密参数:
String encrypted = new CardNumberConverter().convertToDatabaseColumn(cardNumber); accountCardRepository.findByCardNumber(encrypted, cardNumber);
内容的提问来源于stack exchange,提问作者Milad Amery
相关产品推荐
相关产品推荐

