数据访问层详解
模块路径:
wemirr-platform-framework/db-spring-boot-starter核心技术: MyBatis-Plus + Dynamic-Datasource + Baomidou
核心组件
| 组件 | 文件 | 作用 |
|---|---|---|
DynamicDataSourceWebMvcAutoConfiguration | DynamicDataSourceWebMvcAutoConfiguration.java | 动态数据源切换拦截器 |
DynamicDataSourceHandler | DynamicDataSourceHandler.java | 数据源处理器 |
TenantHelper | TenantHelper.java | 租户工具类 |
DataScopeHandler | DataScopeHandler.java | 数据权限处理器 |
DataScopeSqlBuilder | DataScopeSqlBuilder.java | 数据权限 SQL 构建器 |
AuditInterceptor | AuditInterceptor.java | 审计日志拦截器 |
BaseMybatisConfiguration | BaseMybatisConfiguration.java | MyBatis 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" # 超级租户编码数据源名称映射:
| 租户编码 | 数据源名称 | 说明 |
|---|---|---|
0000 | master | 超级管理员使用主库 |
8888 | wemirr_tenant_8888 | 企业A独立库 |
9999 | wemirr_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 参数说明:
| 参数 | 类型 | 说明 | 默认值 |
|---|---|---|---|
alias | String | 表别名(多表 JOIN 时使用) | "" |
name | String | 数据库字段名 | "create_by" |
resource | DataResourceType | 资源类型 | DataResourceType.USER |
scopeType | DataScopeType | 权限范围类型 | 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 → 分页处理
↓
执行 SQL4.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