shardingsphere 使用 hibernate jpa save 方法修改数据时 提示 Can not update sharding key 的解决方案

本文探讨了在使用ShardingSphere进行数据库分库处理时遇到的更新分库键问题。当尝试通过Spring Data JPA的save方法进行编辑操作时,系统提示不能更新分库键。为解决此问题,提出了三种方案:1) 使用@Column(updatable=false)注解,但可能导致不必要的全量更新;2) 使用复合主键和@Embeddable,但可能不适用于所有更新场景;3) 实现Hibernate的EmptyInterceptor拦截器动态添加分库键到更新语句中,但存在SQL注入风险。文章提供了具体的拦截器实现代码,并讨论了其潜在的安全隐患,同时邀请读者分享优化建议。

问题

如同标题所描述使用场景为
使用shardingsphere进行分库处理 包含分库键 String did 后面直接使用了
当使用save方法进行新增操作时没问题 但是当使用相同方法进行编辑操作时则会提示
Can not update sharding key xxxx的信息
虽然可以使用@Query注解进行修改操作类似于下面代码

    @Transactional
    @Modifying
    @Query(value = "update post set update_dt = now(),text=:#{#post.text} , images=:#{#post.images},videos=:#{#post.videos}" +
            ",base_like=:#{#post.baseLike},base_share=:#{#post.baseShare},is_top=:#{#post.getIsTop()},tag=:#{#post.tag}  " +
            "where did = :#{#post.did} and id = :#{#post.id}", nativeQuery = true)
    void updatePost(Post post);

但是问题很明显如果我需要更新好几十个字段的时候会很麻烦而且仅仅只是在where语句后面增加了did=xxx这个语句而已
于是经过搜索找到相关issue经过研究发现帖子中出现是因为无法更新sharding column 分库键
目前5.1.0的shardingsphere可以通过比对update where后面的语句如果出现did 并且分库键相同则会进行跳过处理
所以当前问题为如何修改原有save 方法使其能够直接在where id=? 之前或者之后增加分库键的判断语句

方案1

增加注解
@Column(name = “xxx”, updatable = false)
设置不可更新
但是缺点是更新语句会执行全部分片不够完美
原贴见末尾issue链接
在这里插入图片描述

方案2 是一种查询方案的替代

@Embeddable
@Data
public class CompositeId implements Serializable {
    private Long id; // pk
    private String accountId; // sharding key
}

@Table("xxxx")
@Entity 
public class YourClass {
    @EmbeddedId
    private CompositeId compositeId;
}

Then you can do findByCompositeIdAccountId.

没有实际测试过但是update应该是满足不了需求的

在这里之后官方进行了相关更新及支持就是那个where语句判断

方案3 使用拦截器

拦截器如下

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.hibernate.EmptyInterceptor;
import java.util.HashMap;
import java.util.Map;
@Slf4j
public class AutoAddDidInterceptor extends EmptyInterceptor {

    public static ThreadLocal<Map<String, String>> myTable = new ThreadLocal<>();

    public static void setInterceptorDid(String did){
        Map<String, String> map = new HashMap<>();
        map.put("did_update", did);
        myTable.set(map);
    }
    @Override
    public String onPrepareStatement(String sql) {
        log.debug("sql interceptor worked" );
        Map<String, String> map = myTable.get();
        if (map == null) {
            return sql;
        }
        String did = map.get("did_update");
        if (StringUtils.isEmpty(did) ) {
            return sql;
        }
        //judge update sql
        if(sql.startsWith("update")){
            String[] wheres = sql.split("where");
            StringBuffer stringBuffer = new StringBuffer();
            stringBuffer.append(wheres[0]).append(" where did = '"+did+"' and ").append(wheres[1]);
            log.warn("sql has been modify and now is {}",stringBuffer.toString());
            //clean threadLocal
            myTable.remove();
            return stringBuffer.toString();
        }else{
            return sql;
        }
    }

}

设置启动参数

spring.jpa.properties.hibernate.session_factory.session_scoped_interceptor=com.AutoAddDidInterceptor

使用方法:

   AutoAddDidInterceptor.setInterceptorDid(post.getDid());
  postRepository.save(post);

经过测试可以正常使用

问题

** 问题就是是否会出现sql注入的问题以及相关安全风险**

欢迎各位能提出优化及改进的方案 比如在使用处进行切面或者注解进行优化 再或者对于拦截器的进阶使用目前所有sql都会进行判断这里也是可以优化的点

希望能帮助到有需要的程序猿吧(~ ̄▽ ̄)~

相关资料

shardingsphere官方相关问题及讨论
spring data jpa 动态表名(自定义拦截器)

<think>我们正在解决一个JPA save方法参数类型不匹配的错误。错误信息类似于:java:incompatible types: java.lang.Integercannot beconverted toorg.zhengshuangliang.test.dbmodel.PrinterInfo这个错误通常发生在调用save方法传递的参数类型与实体类型不匹配。可能的原因包括:1.在调用save方法,传递了一个基本类型(如Integer)而不是实体对象。2.实体类定义不正确,或者JPARepository的泛型参数设置错误。解决方案:步骤1:检查调用save方法传递的参数确保你传递的是一个实体对象,而不是一个基本类型或错误的对象。例如,假设你有一个实体类PrinterInfo,那么你的Repository应该定义为:public interfacePrinterInfoRepository extendsJpaRepository<PrinterInfo, Integer> {... }然后,在调用save方法,应该传递一个PrinterInfo对象:PrinterInfo printerInfo =new PrinterInfo(...);//设置属性printerInfoRepository.save(printerInfo);步骤2:检查Repository的泛型类型确保Repository的第一个泛型参数是实体类型,第二个是主键类型。例如,如果PrinterInfo的主键是Integer类型,则定义为:JpaRepository<PrinterInfo, Integer>步骤3:检查实体类的主键字段在实体类中,主键字段应该使用正确的注解(如@Id)进行标记,并且类型应该与Repository中定义的第二个泛型参数一致。步骤4:检查方法重载有候,可能因为方法重载导致调用了错误的方法。确保你调用的是Repository的save方法,而不是自定义的其他方法。步骤5:检查方法签名如果你在Repository中自定义了save方法(通常不需要),请确保它的参数是实体类型。步骤6:检查IDE的提示查看错误发生的行号(165行),确认该行代码中save方法的参数是什么。如果按照以上步骤检查,通常可以解决参数类型不匹配的问题。下面是一个简单的示例代码:实体类:@Entitypublic classPrinterInfo{@Id@GeneratedValue(strategy= GenerationType.IDENTITY)privateInteger id;//其他属性和getter/setter} Repository接口:publicinterface PrinterInfoRepositoryextends JpaRepository<PrinterInfo,Integer>{}使用Repository:@Autowiredprivate PrinterInfoRepositoryprinterInfoRepository;publicvoid savePrinterInfo(){PrinterInfoprinterInfo= newPrinterInfo();//设置属性printerInfoRepository.save(printerInfo); //正确:传递PrinterInfo对象}错误示例:printerInfoRepository.save(123);//错误:传递了整数,而期望的是PrinterInfo对象如果问题仍然存在,请检查是否有其他自定义代码干扰。相关问题:1.JPAsave方法如何正确使用?2.如何定义JPARepository接口?3.JPA实体类的主键如何配置?4.如何解决SpringData JPA中的类型不匹配错误?5.JPA中保存实体常见的错误有哪些?</think>### JPA save方法参数类型不匹配错误解决方案使用JPA的`save()`方法出现`incompatible types`错误(如`java.lang.Integer cannot be converted to XxxEntity`),通常由以下原因导致: #### 错误原因分析 1. **参数类型错误** `save()`方法需要实体对象作为参数,但代码中传递了非实体类型(如基本类型、包装类或错误对象)。 2. **Repository泛型定义错误** Repository接口的泛型声明不正确,例如: ```java public interface WrongRepository extends JpaRepository<Integer, Integer> { ... } // 错误 ``` 3. **方法签名冲突** 自定义Repository中声明了与`save()`签名冲突的方法,如: ```java @Query("...") void save(Integer id); // 与CrudRepository.save(S entity)冲突 ``` --- #### 解决方案步骤 ##### 1. 检查save()调用参数(最常见原因) 确保传递的是**实体对象实例**而非ID或基本类型: ```java // 错误示例 ❌ repository.save(123); // 传递了Integer而非实体对象 // 正确示例 ✅ MyEntity entity = new MyEntity(); entity.setId(123); // 设置ID(如果是更新) repository.save(entity); // 传递实体对象 ``` ##### 2. 验证Repository泛型定义 检查Repository接口的泛型参数: ```java // 正确声明 ✅ public interface PrinterInfoRepository extends JpaRepository<PrinterInfo, Integer> { // <实体类, 主键类型> } ``` - 第一个泛型:实体类(如`PrinterInfo`) - 第二个泛型:主键类型(如`Integer`) ##### 3. 排查自定义方法冲突 若在Repository中添加了自定义方法,避免与`save`同名: ```java // 错误 ❌ public interface PrinterInfoRepository extends JpaRepository<PrinterInfo, Integer> { void save(Integer id); // 与内置save(S entity)冲突 } // 正确 ✅ 重命名自定义方法 void customSave(Integer id); // 避免方法签名冲突 ``` ##### 4. 检查实体类映射 确认实体类的主键定义正确: ```java @Entity public class PrinterInfo { @Id // 必须标注主键 @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; // 类型需与Repository的第二个泛型匹配 ... } ``` ##### 5. 调试技巧 在IDE中通过`Ctrl+左键`点击`save()`方法,查看实际调用的方法签名: ```java <S extends T> S save(S entity); // Spring Data JPA标准方法签名 ``` 确认调用`S`的具体类型是否匹配。 --- #### 完整正确示例 ```java // 实体类 @Entity public class PrinterInfo { @Id private Integer id; private String model; // getters/setters } // Repository定义 public interface PrinterInfoRepository extends JpaRepository<PrinterInfo, Integer> { // <实体类, 主键类型> } // 调用代码 @Autowired private PrinterInfoRepository repo; public void savePrinter(int id, String model) { PrinterInfo entity = new PrinterInfo(); entity.setId(id); // 设置主键 entity.setModel(model); // 设置属性 repo.save(entity); // ✅ 传递实体对象 } ``` --- ### 相关问题 1. JPA中`save()`和`saveAndFlush()`方法有什么区别? 2. 如何解决JPA更新实体出现的`DetachedEntityException`? 3. Spring Data JPA的自定义Repository方法应如何正确声明? 4. JPA实体类的主键生成策略有哪些?如何选择? 5. 使用JPA进行批量插入如何优化性能? [^1]: 此引用为示例占位符,实际解决方案需结合具体代码上下文分析
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值