zoukankan      html  css  js  c++  java
  • 如何解决使用mybatis-plus提供的多租户插件出现Column ‘tenant_id‘ specified twice问题

    前言

    本文案例来源于业务开发部门进行多租户开发时发生的案例。用过mybatis-plus多租户插件的朋友,可能会知道,该插件的租户id值基本都是从上下文得来,这个上下文可以是cookie、session、threadlocal等。据业务部门反馈,在某次插入时,他们发现获取不到租户id值,于是他们在他们的代码层面上做了这么一层操作,在保存的时候,设置租户id。保存的时候,很成功的出现了Column 'tenant_id' specified twice

    问题来源

    在mybatis-plus 3.4版本之前,mybatis-plus进行多租户插入时是不会对已经存在的tenant_id进行过滤的,这就导致出现Column 'tenant_id' specified twice问题。其3.4版本之前多租户sql解析器处理insert语句源码如下

      @Override
        public void processInsert(Insert insert) {
            if (tenantHandler.doTableFilter(insert.getTable().getName())) {
                // 过滤退出执行
                return;
            }
            insert.getColumns().add(new Column(tenantHandler.getTenantIdColumn()));
            if (insert.getSelect() != null) {
                processPlainSelect((PlainSelect) insert.getSelect().getSelectBody(), true);
            } else if (insert.getItemsList() != null) {
                // fixed github pull/295
                ItemsList itemsList = insert.getItemsList();
                if (itemsList instanceof MultiExpressionList) {
                    ((MultiExpressionList) itemsList).getExprList().forEach(el -> el.getExpressions().add(tenantHandler.getTenantId(false)));
                } else {
                    ((ExpressionList) insert.getItemsList()).getExpressions().add(tenantHandler.getTenantId(false));
                }
            } else {
                throw ExceptionUtils.mpe("Failed to process multiple-table update, please exclude the tableName or statementId");
            }
        }
    
    

    问题解决方案

    1、方案一:在业务代码插入时,实体不要设置租户id值,统一由多租户插件进行设值

    2、方案二:升级mybatis-plus版本为3.4.1或者之后的版本

    不过此时的多租户插件的写法就不要按之前那种方式写,虽然之前写法3.4.1也兼容,不过官方已经打了@Deprecated标注,说明官方已经不推荐之前那种写法了,因此采用官方最新提供租户插件拦截器。其示例代码如下

      /**
         * 新多租户插件配置,一缓和二缓遵循mybatis的规则,需要设置 MybatisConfiguration#useDeprecatedExecutor = false 避免缓存万一出现问题
         */
        @Bean
        public MybatisPlusInterceptor mybatisPlusInterceptor() {
            MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
            interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(new TenantLineHandler() {
                @Override
                public Expression getTenantId() {
                    return new LongValue(1);
                }
    
                // 这是 default 方法,默认返回 false 表示所有表都需要拼多租户条件
                @Override
                public boolean ignoreTable(String tableName) {
                    return !"user".equalsIgnoreCase(tableName);
                }
            }));
            // 如果用了分页插件注意先 add TenantLineInnerInterceptor 再 add PaginationInnerInterceptor
            // 用了分页插件必须设置 MybatisConfiguration#useDeprecatedExecutor = false
    //        interceptor.addInnerInterceptor(new PaginationInnerInterceptor());
            return interceptor;
        }
    
        @Bean
        public ConfigurationCustomizer configurationCustomizer() {
            return configuration -> configuration.setUseDeprecatedExecutor(false);
        }
    

    TenantLineInnerInterceptor这个拦截器的包在com.baomidou.mybatisplus.extension.plugins.inner这个包下

    3、方案三:如果是使用mybatis-plus3.4.1之前的版本,可以通过自定义一个TenantSqlParser解析器并重写processInsert方法,其核心代码如下

      */
        @Override
        public void processInsert(Insert insert) {
            if (getTenantHandler().doTableFilter(insert.getTable().getName())) {
                // 过滤退出执行
                return;
            }
            if (isAleadyExistTenantColumn(insert)) {
                return;
            }
            insert.getColumns().add(new Column(getTenantHandler().getTenantIdColumn()));
            if (insert.getSelect() != null) {
                processPlainSelect((PlainSelect) insert.getSelect().getSelectBody(), true);
            } else if (insert.getItemsList() != null) {
                // fixed github pull/295
                ItemsList itemsList = insert.getItemsList();
                if (itemsList instanceof MultiExpressionList) {
                    ((MultiExpressionList) itemsList).getExprList().forEach(el -> el.getExpressions().add(getTenantHandler().getTenantId()));
                } else {
                    ((ExpressionList) insert.getItemsList()).getExpressions().add(getTenantHandler().getTenantId());
                }
            } else {
                throw ExceptionUtils.mpe("Failed to process multiple-table update, please exclude the tableName or statementId");
            }
        }
    
        /**
         * 判断是否存在租户id列字段
         * @param insert
         * @return 如果已经存在,则绕过不执行
         */
        private boolean isAleadyExistTenantColumn(Insert insert) {
            List<Column> columns = insert.getColumns();
            if(CollectionUtils.isEmpty(columns)){
                return false;
            }
            String tenantIdColumn = getTenantHandler().getTenantIdColumn();
            return columns.stream().map(Column::getColumnName).anyMatch(tenantId -> tenantId.equals(tenantIdColumn));
        }
    
    

    总结

    以上三种方案如何选择?如果是项目初期阶段,推荐使用方案一,就是不要在业务层面直接去设置租户id,由租户插件统一处理。如果是全新项目,mybatis-plus推荐使用最新版。如果项目已经业务层面已经多处地方设置了租户id且mybatis-plus版本是3.4之前版本,推荐方案三直接扩展mybatis-plus的租户插件功能,就不推荐方案一了,避免漏改

    demo链接

    https://github.com/lyb-geek/springboot-learning/tree/master/springboot-mybatisplus-tenant

  • 相关阅读:
    bootstrap-table 数据表格行内修改
    java文件上传(单文件 多文件)与删除
    bootstrap-table之通用方法( 时间控件,导出,动态下拉框, 表单验证 ,选中与获取信息)
    bootstrap-table 大量字段整体表单上传之时间处理
    Java实习问题记录
    Playbook剧本初识
    自动化运维工具-Ansible基础
    性能优化概述
    Rewrite基本概述
    Nginx常见问题
  • 原文地址:https://www.cnblogs.com/linyb-geek/p/14307967.html
Copyright © 2011-2022 走看看