Skip to content

数据访问层详解

模块路径: wemirr-platform-framework/db-spring-boot-starter 核心技术: MyBatis-Plus + Dynamic-Datasource + Baomidou


核心组件

组件文件作用
DynamicDataSourceWebMvcAutoConfigurationDynamicDataSourceWebMvcAutoConfiguration.java动态数据源切换拦截器
DynamicDataSourceHandlerDynamicDataSourceHandler.java数据源处理器
TenantHelperTenantHelper.java租户工具类
DataScopeHandlerDataScopeHandler.java数据权限处理器
DataScopeSqlBuilderDataScopeSqlBuilder.java数据权限 SQL 构建器
AuditInterceptorAuditInterceptor.java审计日志拦截器
BaseMybatisConfigurationBaseMybatisConfiguration.javaMyBatis Plus 配置

一、动态数据源切换

1.1 工作原理

DATASOURCE 模式下,每个租户使用独立的数据库:

┌─────────────────────────────────────────────────────────────┐
│                      多租户数据源架构                          │
└─────────────────────────────────────────────────────────────┘

┌──────────────┐    ┌──────────────┐    ┌──────────────┐
│  租户 0001   │    │  租户 8888   │    │  租户 9999   │
│  (平台管理)  │    │  (企业A)     │    │  (企业B)     │
└──────┬───────┘    └──────┬───────┘    └──────┬───────┘
       │                   │                   │
       │                   │                   │
   master DB        wemirr_tenant_8888  wemirr_tenant_9999
       │                   │                   │
       └───────────────────┴───────────────────┘

                    ┌───────▼────────┐
                    │  Tomcat 线程   │
                    │  HttpInterceptor│
                    └───────┬────────┘

                    DynamicDataSourceContextHolder
                    (ThreadLocal 存储当前数据源)

1.2 拦截器自动切换

代码位置: DynamicDataSourceWebMvcAutoConfiguration.java:52-97

java
@Override
public void preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
    var multiTenant = properties.getMultiTenant();
    
    if (context.anonymous()) {
        // 匿名用户使用默认数据源
        return true;
    }
    
    // 1. 从 AuthenticationContext 获取租户编码
    var tenantCode = context.tenantCode();
    
    // 2. 判断是否为超级租户
    if (multiTenant.isSuperTenant(tenantCode)) {
        // 超级租户使用主库
        DynamicDataSourceContextHolder.push(multiTenant.getDefaultDsName());
        return true;
    }
    
    // 3. 普通租户构建数据源名称
    String dsKey = multiTenant.getDsPrefix() + tenantCode;
    // 例如: wemirr_tenant_8888
    
    // 4. 切换数据源
    DynamicDataSourceContextHolder.push(dsKey);
    return true;
}

@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, 
                            Object handler, Exception ex) {
    // 请求结束清理数据源上下文
    DynamicDataSourceContextHolder.clear();
}

配置:

yaml
extend:
  mybatis-plus:
    multi-tenant:
      type: DATASOURCE        # 独立数据源模式
      default-ds-name: master  # 默认数据源(主库)
      ds-prefix: wemirr_tenant_  # 租户数据库前缀
      super-tenant-code: "0000"  # 超级租户编码

数据源名称映射:

租户编码数据源名称说明
0000master超级管理员使用主库
8888wemirr_tenant_8888企业A独立库
9999wemirr_tenant_9999企业B独立库

1.3 手动切换数据源

使用 TenantHelper 工具类:

java
// 使用主数据源执行(查询 tenant 表)
Tenant tenant = TenantHelper.executeWithMaster(() -> {
    return tenantMapper.selectById(1L);
    // SQL 在 master 库执行
});

// 使用指定租户数据源执行
User user = TenantHelper.executeWithTenantDb("8888", () -> {
    return userMapper.selectById(1L);
    // SQL 在 wemirr_tenant_8888 库执行
});

// 无返回值版本
TenantHelper.runWithMaster(() -> {
    tenantMapper.insert(tenant);
});

TenantHelper.runWithTenantDb("8888", () -> {
    userService.updateUser(user);
});

1.4 数据源动态创建

代码位置: DynamicDataSourceHandler.java

创建流程:

1. 接收数据源事件 (INIT/ADD/DEL)

2. 创建物理数据库 (CREATE DATABASE IF NOT EXISTS)

3. 创建数据源连接池 (HikariCP)

4. 注册到 DynamicRoutingDataSource

5. 执行初始化 SQL 脚本

二、数据权限(Data Scope)

2.1 工作原理

数据权限用于控制用户能看到哪些数据,常见的权限范围:

权限类型说明示例
ALL全部数据超级管理员查看所有
CUSTOM自定义数据根据数据权限规则配置
DEPT本部门数据部门经理查看本部门
DEPT_AND_SUB本部门及子部门区域经理查看区域
SELF仅本人数据普通员工只看自己的

2.2 使用注解配置

@DataScope - Mapper/方法级别注解

java
@DataScope(
    columns = {
        @DataColumn(name = "create_by", resource = DataResourceType.USER, scopeType = DataScopeType.SELF),
        @DataColumn(name = "department_id", resource = DataResourceType.DEPT, scopeType = DataScopeType.DEPT)
    }
)
public interface UserMapper extends BaseMapper<User> {
    // ...
}

@DataColumn 参数说明:

参数类型说明默认值
aliasString表别名(多表 JOIN 时使用)""
nameString数据库字段名"create_by"
resourceDataResourceType资源类型DataResourceType.USER
scopeTypeDataScopeType权限范围类型DataScopeType.IGNORE

DataResourceType - 资源类型:

java
public enum DataResourceType {
    USER("user", "用户"),
    DEPT("dept", "部门"),
    CUSTOM("custom", "自定义");
}

DataScopeType - 权限范围:

java
public enum DataScopeType {
    ALL("全部"),           // 查看所有数据
    CUSTOM("自定义"),       // 根据权限配置查看
    DEPT("本部门"),        // 只能看本部门
    DEPT_AND_SUB("本部门及子部门"), // 本部门及子部门
    SELF("仅本人"),        // 只能看自己创建的
    IGNORE("跟随系统配置"); // 跟随系统数据权限配置
}

2.3 SQL 追加示例

原始 SQL:

sql
SELECT * FROM sys_user WHERE status = 1

追加数据权限后(本人权限):

sql
SELECT * FROM sys_user 
WHERE status = 1 
  AND create_by = 1001  -- 自动追加

追加数据权限后(部门权限):

sql
SELECT * FROM sys_user 
WHERE status = 1 
  AND department_id IN (1, 2, 3)  -- 自动追加

2.4 两种 SQL 模式

代码位置: DataScopeSqlBuilder.java:210-216

模式1: SAME_DATABASE(同库模式)

使用 EXISTS 子查询,适合数据量大场景:

sql
SELECT * FROM sys_user u
WHERE status = 1
  AND EXISTS (
    SELECT 1 FROM sys_data_permission_ref ref
    WHERE ref.data_id = u.create_by
      AND ref.owner_id IN (1, 2, 3)  -- 当前用户的角色ID
      AND ref.owner_type = 'ROLE'
      AND ref.data_type = 'USER'
  )

优点:

  • 性能好(利用索引)
  • 无 IN 列表溢出风险

模式2: SEPARATE_DATABASE(分库模式)

使用 IN 列表,适合微服务/分库架构:

sql
SELECT * FROM sys_user u
WHERE status = 1
  AND u.create_by IN (1001, 1002, 1003, ...)  -- 权限用户ID列表

优点:

  • 实现简单
  • 适合跨库查询

配置:

yaml
extend:
  mybatis-plus:
    intercept:
      data-permission:
        enabled: true
        mode: SEPARATE_DATABASE  # SAME_DATABASE | SEPARATE_DATABASE
        in-threshold: 1000  # 超过此阈值使用 EXISTS 子查询

2.5 API 方式动态控制

java
@GetMapping("/users")
public List<User> listUsers() {
    // 方式1: 忽略数据权限
    return DataScope.withIgnore(() -> {
        return userMapper.selectList(null);
        // SQL 不会追加数据权限条件
    });
    
    // 方式2: 自定义数据权限规则
    return DataScope.with(
        DataScopeRule.builder()
            .column(DataScopeRule.Column.builder()
                .name("create_by")
                .scopeType(DataScopeType.SELF)
                .build())
            .build(),
        () -> userMapper.selectList(null)
    );
}

三、审计日志(Audit Log)

3.1 工作原理

审计日志拦截器会在执行 UPDATE 操作前,查询数据库中的旧数据,与新数据对比,记录变更内容。

流程:

1. 拦截 UPDATE 操作

2. 查询数据库中的旧数据

3. 对比新旧数据

4. 记录变更字段

5. 存储/输出审计日志

3.2 配置启用

application.yml:

yaml
extend:
  mybatis-plus:
    audit:
      enabled: true  # 启用审计日志
      
      # 需要记录审计日志的表
      include-tables:
        - sys_user
        - sys_role
        - biz_order
      
      # 全局忽略字段(这些字段的变更不会被记录)
      ignore-global-columns:
        - deleted
        - create_time
        - create_by
        - create_name
        - last_modify_time
        - last_modify_by
        - last_modify_name
      
      # 指定表忽略字段(特定表的字段不会被记录)
      ignore-table-columns:
        sys_user:
          - password  # 密码变更不记录
          - secret   # 密钥变更不记录

3.3 注解配置

@AuditColumn - 字段级别配置

java
@Data
@TableName("sys_user")
public class User {
    
    @Schema(description = "用户名")
    private String username;
    
    @AuditColumn(ignore = true)  // 忽略该字段
    @Schema(description = "密码")
    private String password;
    
    @AuditColumn(label = "手机号码")  // 自定义显示名称
    @Schema(description = "手机号")
    private String mobile;
}

3.4 审计日志输出

代码位置: AuditInterceptor.java:38-66

日志输出示例:

json
{
  "username": {
    "fieldName": "username",
    "label": "用户名",
    "oldValue": "admin",
    "newValue": "administrator",
    "formattedDescription": "字段 [用户名] 从 admin 修改至 administrator"
  },
  "mobile": {
    "fieldName": "mobile",
    "label": "手机号码",
    "oldValue": "13800138000",
    "newValue": "13900139000",
    "formattedDescription": "字段 [手机号码] 从 13800138000 修改至 13900139000"
  }
}

3.5 完整对比流程

代码位置: AuditInterceptor.java:77-117

java
private Map<String, AuditField> compareDifferences(TableInfo tableInfo, 
                                                     Object sourceObject, 
                                                     Object targetObject) {
    Map<String, AuditField> differences = new HashMap<>();
    
    for (TableFieldInfo fieldInfo : tableInfo.getFieldList()) {
        Field field = fieldInfo.getField();
        String column = fieldInfo.getColumn();
        
        // 1. 检查全局忽略
        if (audit.getIgnoreGlobalColumns().contains(column)) {
            continue;
        }
        
        // 2. 检查表级别忽略
        List<String> list = audit.getIgnoreTableColumns().get(tableName);
        if (list != null && list.contains(column)) {
            continue;
        }
        
        // 3. 检查注解忽略
        boolean ignore = field.getAnnotation(AuditColumn.class).ignore();
        if (ignore) {
            continue;
        }
        
        // 4. 对比新旧值
        Object oldValue = ReflectUtil.getFieldValue(sourceObject, field.getName());
        Object newValue = ReflectUtil.getFieldValue(targetObject, field.getName());
        
        if (!Objects.equals(oldValue, newValue)) {
            // 记录变更
            differences.put(fieldName, new AuditField(...));
        }
    }
    
    return differences;
}

四、MyBatis Plus 插件配置

4.1 插件执行顺序

代码位置: BaseMybatisConfiguration.java:74-136

java
public MybatisPlusInterceptor mybatisPlusInterceptor() {
    MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
    
    // 1. 多租户插件 (优先级最高)
    if (MultiTenantType.NONE != multiTenant.getType()) {
        interceptor.addInnerInterceptor(new TenantLineInnerInterceptor(...));
    }
    
    // 2. 数据权限插件
    if (properties.getDataPermission().isEnabled()) {
        interceptor.addInnerInterceptor(new DataPermissionInterceptor(...));
    }
    
    // 3. 防全表更新删除插件
    if (properties.getIntercept().isBlockAttack()) {
        interceptor.addInnerInterceptor(new BlockAttackInnerInterceptor());
    }
    
    // 4. 审计日志插件
    if (properties.getAudit().isEnabled()) {
        interceptor.addInnerInterceptor(new AuditInterceptor(...));
    }
    
    // 5. 分页插件 (优先级最低)
    interceptor.addInnerInterceptor(new PaginationInnerInterceptor(...));
    
    return interceptor;
}

执行顺序:

SQL 执行

1. TenantLineInnerInterceptor  → 追加租户条件

2. DataPermissionInterceptor     → 追加数据权限条件

3. BlockAttackInnerInterceptor    → 检查全表操作

4. AuditInterceptor              → 记录审计日志

5. PaginationInnerInterceptor     → 分页处理

执行 SQL

4.2 最终 SQL 示例

原始 SQL:

sql
SELECT * FROM sys_user 
WHERE status = 1

经过所有插件处理后:

sql
SELECT * FROM sys_user 
WHERE status = 1
  AND tenant_id = 1001              -- 多租户插件
  AND department_id IN (1, 2, 3)    -- 数据权限插件
LIMIT 10 OFFSET 0                    -- 分页插件

五、使用示例

5.1 数据源切换

java
@Service
@RequiredArgsConstructor
public class TenantService {
    
    private final TenantMapper tenantMapper;
    private final UserMapper userMapper;
    
    /**
     * 跨租户查询(从主库查询租户信息,然后切换到租户库查询用户)
     */
    public User getUserByTenant(String tenantCode, Long userId) {
        // 1. 从主库查询租户信息
        Tenant tenant = TenantHelper.executeWithMaster(() -> {
            return tenantMapper.selectOne(
                Wraps.<Tenant>lambdaQuery().eq(Tenant::getCode, tenantCode)
            );
        });
        
        // 2. 切换到租户库查询用户
        return TenantHelper.executeWithTenantDb(tenantCode, () -> {
            return userMapper.selectById(userId);
        });
    }
}

5.2 数据权限

java
// 方式1: 注解配置
@DataScope(
    columns = {
        @DataColumn(name = "create_by", scopeType = DataScopeType.SELF),
        @DataColumn(name = "department_id", scopeType = DataScopeType.DEPT)
    }
)
public interface UserMapper extends BaseMapper<User> {
    List<User> selectUsers(@Param("status") Integer status);
}

// 方式2: API 动态控制
@GetMapping("/users")
public List<User> listUsers(@RequestParam Integer status) {
    // 临时忽略数据权限
    return DataScope.withIgnore(() -> 
        userMapper.selectUsers(status)
    );
    
    // 自定义数据权限规则
    return DataScope.with(
        DataScopeRule.builder()
            .column(DataScopeRule.Column.builder()
                .name("department_id")
                .scopeType(DataScopeType.DEPT_AND_SUB)
                .build())
            .build(),
        () -> userMapper.selectUsers(status)
    );
}

5.3 审计日志

实体配置:

java
@Data
@TableName("biz_order")
public class Order {
    
    @TableId
    private Long id;
    
    @Schema(description = "订单号")
    private String orderNo;
    
    @AuditColumn(label = "订单金额")  // 记录变更
    @Schema(description = "金额")
    private BigDecimal amount;
    
    @AuditColumn(ignore = true)  // 忽略(敏感信息)
    @Schema(description = "支付密码")
    private String payPassword;
}

更新操作:

java
// 调用 updateById 更新订单金额
orderMapper.updateById(
    Order.builder()
        .id(1L)
        .amount(new BigDecimal("999.99"))
        .build()
);

// 审计日志会输出:
// {
//   "amount": {
//     "fieldName": "amount",
//     "label": "订单金额",
//     "oldValue": "100.00",
//     "newValue": "999.99",
//     "formattedDescription": "字段 [订单金额] 从 100.00 修改至 999.99"
//   }
// }

六、配置总结

yaml
extend:
  mybatis-plus:
    # 多租户配置
    multi-tenant:
      type: COLUMN  # NONE | COLUMN | DATASOURCE | SCHEMA
      include-tables:
        - sys_user
        - biz_order
      tenant-id-column: tenant_id
      tenant-code-column: Tenant-Code
      super-tenant-code: "0000"
      default-ds-name: master
      ds-prefix: wemirr_tenant_
    
    # 数据权限配置
    intercept:
      data-permission:
        enabled: true
        mode: SEPARATE_DATABASE  # SAME_DATABASE | SEPARATE_DATABASE
        in-threshold: 1000
    
    # 审计日志配置
    audit:
      enabled: true
      include-tables:
        - sys_user
        - sys_role
      ignore-global-columns:
        - deleted
        - create_time
        - last_modify_time
      ignore-table-columns:
        sys_user:
          - password
          - secret
    
    # 分页配置
    intercept:
      pagination:
        db-type: mysql
        overflow: false

相关文档