Решение SaaS, реализованное идентификатором арендатора

MySQL

Обзор

В середине разработки проекта пользователь неожиданно предложил использовать несколько веток вместе, что потребовало проектирования системы как SaaS-архитектуры, чтобы изолировать данные каждой ветки.

Схема внедрения SaaS

  • независимая база данных

    Каждое предприятие имеет независимую физическую базу данных с хорошей изоляцией и высокой стоимостью.

  • Общая база данных, независимая схема

    Это физическая машина и несколько логических баз данных. Oracle называется схемой, mysql называется базой данных, и каждое предприятие имеет независимую схему.

  • Общая база данных и таблица базы данных (на этот раз):

    Добавьте в таблицу поле «предприятие» или «арендатор», чтобы различать данные предприятия. При работе запрашивайте соответствующие данные в соответствии с полем «арендатор».

    Преимущества: все арендаторы используют одну и ту же базу данных, поэтому стоимость низкая.

    Недостатки: низкий уровень изоляции, низкая безопасность, необходимость увеличения объема защиты при разработке, а резервное копирование и восстановление данных являются наиболее сложными.

Идеи трансформации

  1. Принято на этот раз共享数据库、数据库表SaaS-программа. При ремонте необходимо выполнить следующие работы:
  • Создайте таблицу сведений об арендаторе.
  • Сначала добавьте поле идентификатора арендатора во все таблицы.tenant_id. Используется для связывания информационных таблиц арендаторов.
  • будетtenant_idСоздайте совместный первичный ключ с исходным идентификатором таблицы. Обратите внимание на порядок первичных ключей, первичный ключ исходной таблицы должен быть слева.
  • Измените таблицу, чтобы она стала секционированной.
  1. После преобразования при добавлении информации о арендаторе раздел арендатора добавляется во все таблицы одновременно, и раздел используется для сохранения данных арендатора.
  2. При последующем добавлении записей необходимоtenant_idЗначение поля при поиске удаления и модификации должно быть в условии where сtenant_idУсловие для работы с данными арендатора.

Введение в тестовую среду

В тестовой библиотеке есть 5 таблиц, которые я использую нижеsys_logстол для проверки.

sys_logОператор создания таблицы:

CREATE TABLE `sys_log` (
  `log_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
  `type` TINYINT(1) DEFAULT NULL COMMENT '类型',
  `content` VARCHAR(255) DEFAULT NULL COMMENT '内容',
  `create_id` BIGINT(18) DEFAULT NULL COMMENT '创建人ID',
  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `tenant_id` INT NOT NULL,
  PRIMARY KEY (`log_id`,`tenant_id`) USING BTREE
) ENGINE=INNODB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='系统日志'

таблица добавить поле идентификатора арендатора

узнать, что не добавлено租户id(tenant_id)字段Таблица.

SELECT 
    table_name 
  FROM
    INFORMATION_SCHEMA.TABLES
  WHERE table_schema = 'my'   -- my 是我的测试数据库名称
    AND table_name NOT IN 
    (SELECT 
      t.table_name 
    FROM
      (SELECT 
        table_name,
        column_name 
      FROM
        information_schema.columns 
      WHERE table_name IN 
        (SELECT 
          table_name 
        FROM
          INFORMATION_SCHEMA.TABLES 
        WHERE table_schema = 'my')) t 
    WHERE t.column_name = 'tenant_id') ;

Выполняем, находим две таблицы удовлетворяющие условиям, подтверждаем в базе, правда в таблице нет таблицыtenant_idполе.

Создать форму информации о арендаторе

К вашему сведению, для сохранения информации о арендаторе

CREATE TABLE `t_tenant` (
  `tenant_id` varchar(40) NOT NULL DEFAULT 'c12dee54f652452b88142a0267ec74b7' COMMENT '租户id',
  `tenant_code` varchar(100) DEFAULT NULL COMMENT '租户编码',
  `name` varchar(50) DEFAULT NULL COMMENT '租户名称',
  `desc` varchar(500) DEFAULT NULL COMMENT '租户描述',
  `logo` varchar(255) DEFAULT NULL COMMENT '公司logo地址',
  `status` smallint(6) DEFAULT NULL COMMENT '状态1有效0无效',
  `create_by` varchar(100) DEFAULT NULL COMMENT '创建者',
  `create_time` datetime DEFAULT NULL COMMENT '创建时间',
  `last_update_by` varchar(100) DEFAULT NULL COMMENT '最后修改人',
  `last_update_time` datetime DEFAULT NULL COMMENT '最后修改时间',
  `street_address` varchar(200) DEFAULT NULL COMMENT '街道楼号地址',
  `province` varchar(20) DEFAULT NULL COMMENT '一级行政单位,如广东省,上海市等',
  `city` varchar(20) DEFAULT NULL COMMENT '城市, 如广州市,佛山市等',
  `district` varchar(20) DEFAULT NULL COMMENT '行政区,如番禺区,天河区等',
  `link_man` varchar(50) DEFAULT NULL COMMENT '联系人',
  `link_phone` varchar(50) DEFAULT NULL COMMENT '联系电话',
  `longitude` decimal(10,6) DEFAULT NULL COMMENT '经度',
  `latitude` decimal(10,6) DEFAULT NULL COMMENT '纬度',
  `adcode` varchar(8) DEFAULT NULL COMMENT '区域编码,用于通过区域id快速匹配后展示, 如广州是440100',
  PRIMARY KEY (`tenant_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='租户的基本信息表';

добавить все таблицыtenant_idполе

DROP PROCEDURE IF EXISTS addColumn ;

DELIMITER ?

CREATE PROCEDURE addColumn () 
BEGIN
  -- 定义表名变量
  DECLARE s_tablename VARCHAR (100) ;
  /*显示表的数据库中的所有表
 SELECT table_name FROM information_schema.tables WHERE table_schema='databasename' Order by table_name ;
 */
  #显示所有
  DECLARE cur_table_structure CURSOR FOR 
  SELECT 
    table_name 
  FROM
    INFORMATION_SCHEMA.TABLES
  WHERE table_schema = 'my'     -- my = 我的测试数据库名称
    AND table_name NOT IN 
    (SELECT 
      t.table_name 
    FROM
      (SELECT 
        table_name,
        column_name 
      FROM
        information_schema.columns 
      WHERE table_name IN 
        (SELECT 
          table_name 
        FROM
          INFORMATION_SCHEMA.TABLES 
        WHERE table_schema = 'my')) t 
    WHERE t.column_name = 'tenant_id') ;
  DECLARE CONTINUE HANDLER FOR SQLSTATE '02000' SET s_tablename = NULL ;
  OPEN cur_table_structure ;
  FETCH cur_table_structure INTO s_tablename ;
  WHILE
    (s_tablename IS NOT NULL) DO SET @MyQuery = CONCAT(
      "alter table `",
      s_tablename,
      "` add COLUMN `tenant_id` INT not null COMMENT '租户id'"
    ) ;
    PREPARE msql FROM @MyQuery ;
    EXECUTE msql ;
    #USING @c; 
    FETCH cur_table_structure INTO s_tablename ;
  END WHILE ;
  CLOSE cur_table_structure ;
END ?

DELIMITER ;

#执行存储过程
CALL addColumn () ;

Реализовать секционирование таблицы

Цель достигнута: добавление разделов ко всем таблицам при добавлении арендаторов.

Требуемые условия:

  • Таблица должна быть секционированной. Если это не секционированная таблица, ее нужно изменить на секционированную таблицу.
  • tenant_idобязательный и оригинальный столlog_idПервичный ключ образует первичный ключ объединения.

Превратить таблицу в секционированную

Есть три способа добавить разделы в таблицу:

  • Создайте временную таблицу разделовsys_log_copy, после копирования данных удалить старыйsys_log, а потомsys_log_copyпревратиться вsys_log(На этот раз подробности см. ниже)
  • Изменение таблицы непосредственно в таблицу разделов не требует данных в исходной таблице, иначе произойдет сбой:
-- 如果表中没数据,可以直接将表进行分区
ALTER TABLE sys_log PARTITION BY LIST COLUMNS (tenant_id)
(
    PARTITION a1 VALUES IN (1) ENGINE = INNODB,
    PARTITION a2 VALUES IN (2) ENGINE = INNODB,
    PARTITION a3 VALUES IN (3) ENGINE = INNODB
);
  • Добавление нового раздела в многораздельную таблицу требует, чтобы таблица уже была многораздельной таблицей, в противном случае это не удастся:
-- 已经是分区表中添加分区
ALTER TABLE sys_log_copy ADD PARTITION
(
    PARTITION a4 VALUES IN (4) ENGINE = INNODB,
    PARTITION a5 VALUES IN (5) ENGINE = INNODB,
    PARTITION a6 VALUES IN (6) ENGINE = INNODB
);

пройти через创建临时分区表способ преобразования исходной таблицы в таблицу разделов

  1. Просмотрите оператор создания таблицы:
SHOW CREATE TABLE `sys_log`;
  1. Обратитесь к оператору создания таблицы, чтобы создать копию таблицы:
CREATE TABLE `sys_log_copy` (
  `log_id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键',
  `type` TINYINT(1) DEFAULT NULL COMMENT '类型',
  `content` VARCHAR(255) DEFAULT NULL COMMENT '内容',
  `create_id` BIGINT(18) DEFAULT NULL COMMENT '创建人ID',
  `create_time` DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `tenant_id` INT NOT NULL,
  PRIMARY KEY (`log_id`,`tenant_id`) USING BTREE
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC COMMENT='系统日志'
PARTITION BY LIST COLUMNS (tenant_id)
(
    PARTITION a1 VALUES IN (1) ENGINE = INNODB,
    PARTITION a2 VALUES IN (2) ENGINE = INNODB,
    PARTITION a3 VALUES IN (3) ENGINE = INNODB
);

Обратите внимание на приведенное вышеDEFAULT CHARSET=utf8mb4 ROW_FORMAT=DYNAMIC

  • CHARSET=utf8mb4Это потому, что utf8 - ненадежная кодировка в mysql.
  • ROW_FORMAT=DYNAMICЭто делается для того, чтобы избежать следующей ошибки, если длина слишком велика:
ERROR 1709 (HY000): Index column size too large. The maximum column size is 767 bytes.

Для решения этой проблемы также можно установить значение true в файле конфигурации my.ini, но будет сложнее перезапустить базу данных.

[mysqld]
innodb_large_prefix=true
  1. Проверить статус раздела:
SELECT 
  partition_name part,
  partition_expression expr,
  partition_description descr,
  table_rows 
FROM
  information_schema.partitions 
WHERE TABLE_SCHEMA = SCHEMA() 
  AND TABLE_NAME = 'sys_log_copy' ;

Вы можете просмотреть добавленные 3 раздела

  1. Скопировать данные в таблицу копирования
INSERT INTO `sys_log_copy` SELECT * FROM `sys_log`
  1. удалить таблицуsys_log, а затем изменитьsys_log_copyИмена в таблицеsys_log.

Напишите складские процедуры, которые автоматически создают разделы

Реализовано через хранимую процедуру, добавляющую раздел в секционированную таблицу.

DELIMITER ?

USE `my`?

DROP PROCEDURE IF EXISTS `add_table_partition`?

CREATE DEFINER=`root`@`%` PROCEDURE `add_table_partition`(IN _tenantId INT)
BEGIN
  DECLARE IS_FOUND INT DEFAULT 1 ;
  -- 用于记录游标中存在分区的表名
  DECLARE v_tablename VARCHAR (200) ;
  -- 用于缓存添加分区时候的sql
  DECLARE v_sql VARCHAR (5000) ;
  -- 分区名称定义
  DECLARE V_P_VALUE VARCHAR (100) DEFAULT CONCAT('P', REPLACE(_tenantId, '-', '')) ;
  DECLARE V_COUNT INT ;
  DECLARE V_LOONUM INT DEFAULT 0 ;
  DECLARE V_NUM INT DEFAULT 0 ;
  -- 定义游标,值是所有分区表的表名
  DECLARE curr CURSOR FOR 
  (SELECT 
    t.TABLE_NAME 
  FROM
    INFORMATION_SCHEMA.partitions t 
  WHERE TABLE_SCHEMA = SCHEMA() 
    AND t.partition_name IS NOT NULL 
  GROUP BY t.TABLE_NAME) ;
  -- 如果没影响的记录,程序也继续执行
  DECLARE CONTINUE HANDLER FOR NOT FOUND SET IS_FOUND=0;
  -- 获取上一步中的游标中获取到的表名的个数
  SELECT 
    COUNT(1) INTO V_LOONUM 
  FROM
    (SELECT 
      t.TABLE_NAME 
    FROM
      INFORMATION_SCHEMA.partitions t 
    WHERE TABLE_SCHEMA = SCHEMA() 
      AND t.partition_name IS NOT NULL 
    GROUP BY t.TABLE_NAME) A ;
  -- 只有在存在分区表的时候才打开游标
  IF V_LOONUM > 0 
  THEN -- 打开游标
  OPEN curr ;
  -- 循环
  read_loop :
  LOOP
    -- 声明结束的时候
    IF V_NUM >= V_LOONUM 
    THEN LEAVE read_loop ;
    END IF ;
    -- 取游标的值给变量
    FETCH curr INTO v_tablename ;
    -- 依次判断分区表是否存在改分区,如果不存在则添加分区
    SET V_NUM = V_NUM + 1 ;
    SELECT 
      COUNT(1) INTO V_COUNT 
    FROM
      INFORMATION_SCHEMA.partitions t 
    WHERE LOWER(T.TABLE_NAME) = LOWER(v_tablename) 
      AND T.PARTITION_NAME = V_P_VALUE 
      AND T.TABLE_SCHEMA = SCHEMA() ;
    IF V_COUNT <= 0 
    THEN SET v_sql = CONCAT(
      '  ALTER TABLE ',
      v_tablename,
      ' ADD PARTITION (PARTITION ',
      V_P_VALUE,
      ' VALUES IN(',
      _tenantId,
      ') ENGINE = INNODB) '
    ) ;
    SET @v_sql = v_sql ;
    -- 预处理需要执行的动态SQL,其中stmt是一个变量
    PREPARE stmt FROM @v_sql ;
    -- 执行SQL语句
    EXECUTE stmt ;
    -- 释放掉预处理段
    DEALLOCATE PREPARE stmt ;
    END IF ;
    -- 结束循环
  END LOOP read_loop;
  -- 关闭游标
  CLOSE curr ;
  END IF ;
END?

DELIMITER ;

проверка хранимой процедуры вызова

CALL add_table_partition (8) ;
  • Если таблица не является секционированной, при вызове хранимой процедуры будет сообщено о следующей ошибке:
错误代码: 1505
Partition management on a not partitioned table is not possible

Это переводится как: «Управление разделами невозможно на неразделенной таблице».

  • Может быть сообщено о следующей ошибке:
错误代码: 1329
No data - zero rows fetched, selected, or processed

Но если, запросив следующееinformation_schema.partitionsНет ошибки, то есть раздел успешно добавлен.

Это можно решить, добавив следующий метод после определения курсора и перед открытием курсора:

DECLARE CONTINUE HANDLER FOR NOT FOUND SET IS_FOUND=0;
SELECT 
  partition_name part,
  partition_expression expr,
  partition_description descr,
  table_rows 
FROM
  information_schema.partitions 
WHERE TABLE_SCHEMA = SCHEMA() 
  AND TABLE_NAME = 'sys_log' ;

пройти черезmybatisвызов хранимой процедуры

<select id="testProcedure" statementType="CALLABLE" useCache="false" parameterType="string">
        <![CDATA[
                call add_table_partition (
                        #{_tenantId,mode=IN,jdbcType=VARCHAR});
        ]]>
</select>

Внедрение простых разрешений данных

Нам может понадобиться это требование сценария

  1. Компания группы имеет несколько дочерних компаний, и компания группы, и каждая дочерняя компания являются арендаторами, но есть также дочерние компании в составе дочерней компании.
  2. Будь то группа компаний или ее дочерние компании, существуют соответствующие пользователи (t_user).
  3. Пользователям необходимо иметь разрешение на просмотр данных своей собственной компании и данных дочерних компаний, указанных ниже.

Из приведенных выше требований к сценарию мы знаем, чтоt_tenantСтол должен быть спроектирован в виде пня. Ниже мы тестируем.

изменить вышеуказанноеt_tenantТаблица:

CREATE TABLE `t_tenant` (
  `tenant_id` VARCHAR(40) NOT NULL DEFAULT '0' COMMENT '租户id',
  `path` VARCHAR(200) DEFAULT NOT NULL COMMENT '从根节点开始的id路径树,如:0-2-21-211-2111,通过"-"隔开,最末尾为自己id',
  `tenant_code` VARCHAR(100) DEFAULT NULL COMMENT '租户编码',
  `name` VARCHAR(50) DEFAULT NULL COMMENT '租户名称',
  `logo` VARCHAR(255) DEFAULT NULL COMMENT '公司logo地址',
  `status` SMALLINT(6) DEFAULT NULL COMMENT '状态1有效0无效',
  `create_by` VARCHAR(100) DEFAULT NULL COMMENT '创建者',
  `create_time` DATETIME DEFAULT NULL COMMENT '创建时间',
  `last_update_by` VARCHAR(100) DEFAULT NULL COMMENT '最后修改人',
  `last_update_time` DATETIME DEFAULT NULL COMMENT '最后修改时间',
  `street_address` VARCHAR(200) DEFAULT NULL COMMENT '街道楼号地址',
  PRIMARY KEY (`tenant_id`) USING BTREE
) ENGINE=INNODB DEFAULT CHARSET=utf8 COMMENT='租户的基本信息表'

Измененные места:

  • Для демонстрации некоторые поля, которые кажутся бесполезными, были удалены.
  • ДобавленpathПоля, реализующие древовидную структуру арендаторов и субарендаторов

Добавить тестовые данные

Добавьте информацию о арендаторе:

кэшируется по путиt_tenantдорожка дерева.

Создайте пользовательскую таблицу (t_user), добавьте тестового пользователя:

Идентификатор пользователя и tenant_id теста должны соответствовать

Создайте таблицу вложений (t_file), добавьте тестовые бизнес-данные:

Создано полем (create_by), связанный с пользовательской таблицей (t_user), также связанный с арендатором (tenant_id), чтобы указать, для какой дочерней компании данные.

провести тестирование

  • Проверятьtenant_idЭто информация об арендаторе «211» и информация о субарендаторе под ней.

    SELECT 
      tt.`tenant_id`,
      tt.path 
    FROM
      t_tenant tt 
    WHERE 
      (SELECT 
        INSTR(tt.path, "211")) ;
    
  • Проверятьtenant_idявляется вложением "211" и вложением информации субарендаторов под ним

    SELECT 
      * 
    FROM
      t_file tf 
    WHERE tf.`tenant_id` IN 
      (SELECT 
        tt.`tenant_id` 
      FROM
        t_tenant tt 
      WHERE 
        (SELECT 
          INSTR(tt.path, "211"))) ;
    

  • Проверятьtenant_idявляется вложением "2" и вложением информации о субарендаторах под ним

Реализовать разрешение данных для просмотра субарендатора через перехватчик mybatis

Напишите перехватчик:

package com.iee.orm.mybatis.common;

import com.baomidou.mybatisplus.core.toolkit.PluginUtils;
import com.iee.orm.mybatis.common.UserHelper;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.apache.ibatis.executor.statement.StatementHandler;
import org.apache.ibatis.mapping.MappedStatement;
import org.apache.ibatis.mapping.SqlCommandType;
import org.apache.ibatis.mapping.StatementType;
import org.apache.ibatis.plugin.*;
import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;
import org.springframework.context.annotation.Configuration;

import java.sql.Connection;
import java.util.Properties;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * 实现 拦截select语句,实现尾部拼接sql来查询本租户和子租户信息
 * @author longxiaonan@aliyun.com
 */
@Slf4j
@Configuration
@Intercepts({
        @Signature(type = StatementHandler.class, method = "prepare", args = {Connection.class, Integer.class})
})
public class SqlInterceptor implements Interceptor {
    @Override
    public Object intercept(Invocation invocation) throws Throwable {
        StatementHandler statementHandler = PluginUtils.realTarget(invocation.getTarget());
        MetaObject metaObject = SystemMetaObject.forObject(statementHandler);
        // 非select语句或者是存储过程 则跳过)
        MappedStatement mappedStatement = (MappedStatement) metaObject.getValue("delegate.mappedstatement");
        if (SqlCommandType.SELECT != mappedStatement.getSqlCommandType()
                || StatementType.CALLABLE == mappedStatement.getStatementType()) {
            return invocation.proceed();
        }
        // 拼接sql执行
        getSqlByInvocation(metaObject, invocation);
        return invocation.proceed();
    }

    /**
     * 拼接sql执行
     * @param metaObject
     * @param invocation
     * @return
     */
    private String getSqlByInvocation(MetaObject metaObject, Invocation invocation) throws NoSuchFieldException, IllegalAccessException {
        //在原始的sql中拼装sql,方式一
        String originalSql = (String) metaObject.getValue(PluginUtils.DELEGATE_BOUNDSQL_SQL);
        metaObject.setValue(PluginUtils.DELEGATE_BOUNDSQL_SQL, originalSql);
        String targetSql = addDataSql(originalSql);
        return targetSql;

        //在原始的sql中拼装sql,方式二
//        StatementHandler statementHandler = (StatementHandler) invocation.getTarget();
//        BoundSql boundSql = statementHandler.getBoundSql();
//        String sql = boundSql.getSql();
//        Field field = boundSql.getClass().getDeclaredField("sql");
//        field.setAccessible(true);
//        field.set(boundSql, addDataSql(sql));
//        return sql;
    }

    /**
     * 将原始sql进行拼接
     * @param sql
     * @return
     */
    static String addDataSql(String sql) {
        //需要去掉“;" 因为“;"表示sql结束,不去掉那么后面的拼接会受到影响
        sql = StringUtils.replace(sql, ";", "");
        StringBuilder sb = new StringBuilder(sql);
        String tenantId = UserHelper.getTenantId();

        //用于查看子租户信息的sql后缀
        String suffSql = " `tenant_id` IN " +
                "(SELECT " +
                "tt.`tenant_id` " +
                "FROM " +
                "t_tenant tt " +
                "WHERE " +
                "(SELECT " +
                "INSTR(tt.path," + tenantId + "))) ";

        String regex = "(.*)(where)(.*)";
        Pattern compile = Pattern.compile(regex);
        Matcher matcher = compile.matcher(sql);
        if (matcher.find()) {
            String whereLastSql = matcher.group(matcher.groupCount());
            //where 的条件存在 且是括号对的情况下,是不能再加“where”的, 但是需要添加“and”
            int left = StringUtils.countMatches(whereLastSql, "(");
            int right = StringUtils.countMatches(whereLastSql, ")");
            if(left == right){
                sb.append(" and ");
                sb.append(suffSql);
                log.info("数据权限替换后sql:--->" + sb.toString());
                return sb.toString();
            }
        }
        //其他情况下需要添加where
        sb.append(" where ");
        sb.append(suffSql);
        log.info("数据权限替换后sql:--->" + sb.toString());
        return sb.toString();
    }

    @Override
    public Object plugin(Object target) {
        return Plugin.wrap(target, new SqlInterceptor());
    }

    @Override
    public void setProperties(Properties properties) {
    }
}

Используется класс инструментов mybatis-plus

/*
 * Copyright (c) 2011-2020, baomidou (jobob@qq.com).
 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 * <p>
 * https://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */
package com.baomidou.mybatisplus.core.toolkit;

import org.apache.ibatis.reflection.MetaObject;
import org.apache.ibatis.reflection.SystemMetaObject;

import java.lang.reflect.Proxy;
import java.util.Properties;

/**
 * 插件工具类
 *
 * @author TaoYu , hubin
 * @since 2017-06-20
 */
public final class PluginUtils {
    public static final String DELEGATE_BOUNDSQL_SQL = "delegate.boundSql.sql";

    private PluginUtils() {
        // to do nothing
    }

    /**
     * 获得真正的处理对象,可能多层代理.
     */
    @SuppressWarnings("unchecked")
    public static <T> T realTarget(Object target) {
        if (Proxy.isProxyClass(target.getClass())) {
            MetaObject metaObject = SystemMetaObject.forObject(target);
            return realTarget(metaObject.getValue("h.target"));
        }
        return (T) target;
    }

    /**
     * 根据 key 获取 Properties 的值
     */
    public static String getProperty(Properties properties, String key) {
        String value = properties.getProperty(key);
        return StringUtils.isEmpty(value) ? null : value;
    }
}

В ходе тестирования было обнаружено, что пока используется оператор select, информация субарендатора будет сопоставляться и запрашиваться.

См. тестовый код:

GitHub.com/Лун Сяонань…

Ваша мотивация поощрять меня, пожалуйста, лайк и поддержку, спасибо!