Appearance
数据库相关的配置全部位于database.yml
yaml
acuity:
mysql: # mysql 常用配置
ip: 127.0.0.1
port: 3306
driverClassName: com.mysql.cj.jdbc.Driver
database: acuity_pro_defaults
username: 'root'
password: 'root'
database: # 字段介绍参考 DatabaseProperties
multiTenantType: DATASOURCE
# COLUMN模式中隔离 租户 的列名
tenantIdColumn: create_org_id
initDatabasePrefix: # DATASOURCE 和 DATASOURCE_COLUMN模式 租户库 前缀
- acuity_ds_c_base
ignoreTable: # 在执行sql时,忽略 租户插件自动拼接租户编码的表 (仅 COLUMN 和 DATASOURCE_COLUMN 模式有效)
- def_xxx
ignoreTablePrefix: # 在执行sql时,忽略 租户插件自动拼接租户编码的表 前缀 (仅 COLUMN 和 DATASOURCE_COLUMN 模式有效)
- def_xxx
# 是否不允许写入数据 WriteInterceptor
isNotWrite: false
# 是否启用 sql性能规范插件
isBlockAttack: false
# 是否启用 sql性能规范插件
isIllegalSql: false
# 是否启用分布式事务
isSeata: false
# 是否在控制台打印详细的SQL语句 生产环境请设置 p6spy = false
p6spy: true
# 是否启用数据权限
isDataScope: true
# 分页相关start
# 分页大小限制
maxLimit: -1
# 数据库类型
dbType: MYSQL
# 溢出总页数后是否进行处理
overflow: true
# 生成 countSql 优化掉 join 现在只支持 left join
optimizeJoin: true
# 分页相关end
# id 生成策略开始
# id生成策略 支持 HU_TOOL、CACHE、DEFAULTS
id-type: HU_TOOL
hutoolId:
workerId: 0
dataCenterId: 0
defaultId:
time-bits: 31
worker-bits: 22
seq-bits: 10
epochStr: '2020-09-15'
cache-id:
time-bits: 31
worker-bits: 22
seq-bits: 10
epochStr: '2020-09-15'
boost-power: 3
padding-factor: 50
# id 生成策略结束
# mysql 通用配置
spring:
autoconfigure:
exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
datasource:
dynamic:
enabled: false # 这里一定要写成false,无论是什么模式
druid:
enable: true
# 从这里开始(druid),中间的这段配置用于 acuity.database.multiTenantType != DATASOURCE 时
username: ${acuity.mysql.username}
password: ${acuity.mysql.password}
driver-class-name: ${acuity.mysql.driverClassName}
url: jdbc:mysql://${acuity.mysql.ip}:${acuity.mysql.port}/${acuity.mysql.database}?serverTimezone=CTT&characterEncoding=utf8&useUnicode=true&useSSL=false&autoReconnect=true&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&nullCatalogMeansCurrent=true
db-type: mysql
initialSize: 10 #初始的数据源链接数
minIdle: 10 # 最小连接池数量
maxActive: 200 # 最大连接池数量
max-wait: 60000 # 获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。
pool-prepared-statements: true # 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。
max-pool-prepared-statement-per-connection-size: 20 # 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100
validation-query: SELECT 'x' # 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
test-on-borrow: false # 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
test-on-return: false # 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能
test-while-idle: true # 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
time-between-eviction-runs-millis: 60000 #配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
min-evictable-idle-time-millis: 300000 #配置一个连接在池中最小生存的时间,单位是毫秒
filters: stat,wall # 属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有: 监控统计用的filter:stat 、 日志用的filter:log4j 、 防御sql注入的filter:wall
filter:
wall:
enabled: true
config:
commentAllow: true
multiStatementAllow: true
noneBaseStatementAllow: true
# 从这里结束(druid),中间的这段配置用于 acuity.database.multiTenantType != DATASOURCE 时
# 以下的2段配置,同时适用于所有模式
web-stat-filter: # WebStatFilter配置,说明请参考Druid Wiki,配置_配置WebStatFilter
enabled: true
url-pattern: /*
exclusions: "*.js , *.gif ,*.jpg ,*.png ,*.css ,*.ico , /druid/*"
session-stat-max-count: 1000
profile-enable: true
session-stat-enable: false
stat-view-servlet: #展示Druid的统计信息,StatViewServlet的用途包括:1.提供监控信息展示的html页面2.提供监控信息的JSON API
enabled: true
url-pattern: /druid/* #根据配置中的url-pattern来访问内置监控页面,如果是上面的配置,内置监控页面的首页是/druid/index.html例如:http://127.0.0.1:9000/druid/index.html
reset-enable: true #允许清空统计数据
login-username: acuity
login-password: acuity
allow: ''
acuity:
mysql: # mysql 常用配置
ip: 127.0.0.1
port: 3306
driverClassName: com.mysql.cj.jdbc.Driver
database: acuity_pro_defaults
username: 'root'
password: 'root'
database: # 字段介绍参考 DatabaseProperties
multiTenantType: DATASOURCE
# COLUMN模式中隔离 租户 的列名
tenantIdColumn: create_org_id
initDatabasePrefix: # DATASOURCE 和 DATASOURCE_COLUMN模式 租户库 前缀
- acuity_ds_c_base
ignoreTable: # 在执行sql时,忽略 租户插件自动拼接租户编码的表 (仅 COLUMN 和 DATASOURCE_COLUMN 模式有效)
- def_xxx
ignoreTablePrefix: # 在执行sql时,忽略 租户插件自动拼接租户编码的表 前缀 (仅 COLUMN 和 DATASOURCE_COLUMN 模式有效)
- def_xxx
# 是否不允许写入数据 WriteInterceptor
isNotWrite: false
# 是否启用 sql性能规范插件
isBlockAttack: false
# 是否启用 sql性能规范插件
isIllegalSql: false
# 是否启用分布式事务
isSeata: false
# 是否在控制台打印详细的SQL语句 生产环境请设置 p6spy = false
p6spy: true
# 是否启用数据权限
isDataScope: true
# 分页相关start
# 分页大小限制
maxLimit: -1
# 数据库类型
dbType: MYSQL
# 溢出总页数后是否进行处理
overflow: true
# 生成 countSql 优化掉 join 现在只支持 left join
optimizeJoin: true
# 分页相关end
# id 生成策略开始
# id生成策略 支持 HU_TOOL、CACHE、DEFAULTS
id-type: HU_TOOL
hutoolId:
workerId: 0
dataCenterId: 0
defaultId:
time-bits: 31
worker-bits: 22
seq-bits: 10
epochStr: '2020-09-15'
cache-id:
time-bits: 31
worker-bits: 22
seq-bits: 10
epochStr: '2020-09-15'
boost-power: 3
padding-factor: 50
# id 生成策略结束
# mysql 通用配置
spring:
autoconfigure:
exclude: org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration
datasource:
dynamic:
enabled: false # 这里一定要写成false,无论是什么模式
druid:
enable: true
# 从这里开始(druid),中间的这段配置用于 acuity.database.multiTenantType != DATASOURCE 时
username: ${acuity.mysql.username}
password: ${acuity.mysql.password}
driver-class-name: ${acuity.mysql.driverClassName}
url: jdbc:mysql://${acuity.mysql.ip}:${acuity.mysql.port}/${acuity.mysql.database}?serverTimezone=CTT&characterEncoding=utf8&useUnicode=true&useSSL=false&autoReconnect=true&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&nullCatalogMeansCurrent=true
db-type: mysql
initialSize: 10 #初始的数据源链接数
minIdle: 10 # 最小连接池数量
maxActive: 200 # 最大连接池数量
max-wait: 60000 # 获取连接时最大等待时间,单位毫秒。配置了maxWait之后,缺省启用公平锁,并发效率会有所下降,如果需要可以通过配置useUnfairLock属性为true使用非公平锁。
pool-prepared-statements: true # 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。
max-pool-prepared-statement-per-connection-size: 20 # 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100
validation-query: SELECT 'x' # 用来检测连接是否有效的sql,要求是一个查询语句,常用select 'x'。如果validationQuery为null,testOnBorrow、testOnReturn、testWhileIdle都不会起作用。
test-on-borrow: false # 申请连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能。
test-on-return: false # 归还连接时执行validationQuery检测连接是否有效,做了这个配置会降低性能
test-while-idle: true # 建议配置为true,不影响性能,并且保证安全性。申请连接的时候检测,如果空闲时间大于timeBetweenEvictionRunsMillis,执行validationQuery检测连接是否有效。
time-between-eviction-runs-millis: 60000 #配置间隔多久才进行一次检测,检测需要关闭的空闲连接,单位是毫秒
min-evictable-idle-time-millis: 300000 #配置一个连接在池中最小生存的时间,单位是毫秒
filters: stat,wall # 属性类型是字符串,通过别名的方式配置扩展插件,常用的插件有: 监控统计用的filter:stat 、 日志用的filter:log4j 、 防御sql注入的filter:wall
filter:
wall:
enabled: true
config:
commentAllow: true
multiStatementAllow: true
noneBaseStatementAllow: true
# 从这里结束(druid),中间的这段配置用于 acuity.database.multiTenantType != DATASOURCE 时
# 以下的2段配置,同时适用于所有模式
web-stat-filter: # WebStatFilter配置,说明请参考Druid Wiki,配置_配置WebStatFilter
enabled: true
url-pattern: /*
exclusions: "*.js , *.gif ,*.jpg ,*.png ,*.css ,*.ico , /druid/*"
session-stat-max-count: 1000
profile-enable: true
session-stat-enable: false
stat-view-servlet: #展示Druid的统计信息,StatViewServlet的用途包括:1.提供监控信息展示的html页面2.提供监控信息的JSON API
enabled: true
url-pattern: /druid/* #根据配置中的url-pattern来访问内置监控页面,如果是上面的配置,内置监控页面的首页是/druid/index.html例如:http://127.0.0.1:9000/druid/index.html
reset-enable: true #允许清空统计数据
login-username: acuity
login-password: acuity
allow: ''
禁止在控制台打印sql
yaml
acuity:
database:
# 生产环境请设置p6spy = false
p6spy: false
# p6spy 输出的sql格式是 TenantP6SpyLogger 控制的
acuity:
database:
# 生产环境请设置p6spy = false
p6spy: false
# p6spy 输出的sql格式是 TenantP6SpyLogger 控制的
调整在控制台打印的sql参数或格式
java
public class TenantP6SpyLogger implements MessageFormattingStrategy {
public static final String REGX = "[\\s]+";
@Override
public String formatMessage(int connectionId, String now, long elapsed, String category,
String prepared, String sql, String url) {
return StringUtils.isNotBlank(sql) ?
StrUtil.format(" tenant: {} userId: {} \n Consume Time:{} ms {} \n url: {} \n Execute SQL:{} \n",
ContextUtil.getTenant(), ContextUtil.getUserId(), elapsed, now, url, sql.replaceAll(REGX, StringPool.SPACE)) :
StringPool.EMPTY;
}
}
public class TenantP6SpyLogger implements MessageFormattingStrategy {
public static final String REGX = "[\\s]+";
@Override
public String formatMessage(int connectionId, String now, long elapsed, String category,
String prepared, String sql, String url) {
return StringUtils.isNotBlank(sql) ?
StrUtil.format(" tenant: {} userId: {} \n Consume Time:{} ms {} \n url: {} \n Execute SQL:{} \n",
ContextUtil.getTenant(), ContextUtil.getUserId(), elapsed, now, url, sql.replaceAll(REGX, StringPool.SPACE)) :
StringPool.EMPTY;
}
}
使用自定义sql时,如何优雅的写模糊查询参数
- xml中自定义sql:
sql
select * from user where 1=1
and name like #{keyword, typeHandler=fullLike}
and account like #{keyword, typeHandler=leftLike}
and describe like #{keyword, typeHandler=rightLike}
select * from user where 1=1
and name like #{keyword, typeHandler=fullLike}
and account like #{keyword, typeHandler=leftLike}
and describe like #{keyword, typeHandler=rightLike}
- 实际sql:
sql
select * from user where 1=1 and name like '%张三%' and account like '%acuity' and describe like '工作%'
select * from user where 1=1 and name like '%张三%' and account like '%acuity' and describe like '工作%'
- 原理: FullLikeTypeHandler、LeftLikeTypeHandler、RightLikeTypeHandler
id、createTime、createdBy、updateTime、updatedBy 等字段自动填充
实现元对象处理器接口:top.acuity.commons.database.datasource.AcuityMetaObjectHandler
- insert 方法,自动填充 id, createdTime, updatedTime, createdBy, updatedBy 字段,字段为NULL则自动填充,不为空则使用传递进来的
- update 方法,自动填充 id, updatedTime, updatedBy 字段,字段为NULL则自动填充,不为空则使用传递进来的
注解填充字段
@TableField(.. fill = FieldFill.INSERT)
生成器策略部分也可以配置!javapublic class User { // 注意!这里需要标记为填充字段 @TableField(.. fill = FieldFill.INSERT) private String fillField; .... }
public class User { // 注意!这里需要标记为填充字段 @TableField(.. fill = FieldFill.INSERT) private String fillField; .... }
集群部署如何配置雪花id规则? 为什么id为重复?
yaml
acuity:
database:
id-type: HU_TOOL # HU_TOOL、 DEFAULT、CACHE
hutoolId:
workerId: 0
dataCenterId: 0
cache-id:
time-bits: 31
worker-bits: 22
seq-bits: 10
epochStr: '2020-09-15'
boost-power: 3
padding-factor: 50
acuity:
database:
id-type: HU_TOOL # HU_TOOL、 DEFAULT、CACHE
hutoolId:
workerId: 0
dataCenterId: 0
cache-id:
time-bits: 31
worker-bits: 22
seq-bits: 10
epochStr: '2020-09-15'
boost-power: 3
padding-factor: 50
雪花id生成类型有3种: HU_TOOL、DEFAULT、CACHE
- HU_TOOL: 使用hutool 提供的雪花生成算法,集群部署时,需要保证每个节点读取到的
acuity.database.hutoolId.workerId
或acuity.database.hutoolId.dataCenterId
不一样,否则可能会造成id重复问题 。 - DEFAULT:UidGenerator通过借用未来时间来解决sequence天然存在的并发限制。
- CACHE( 推荐使用 ):采用RingBuffer来缓存已生成的UID, 并行化UID的生产和消费, 同时对CacheLine补齐,避免了由RingBuffer带来的硬件级「伪共享」问题 。
其中 DEFAULT、CACHE 依赖于 worker_node
表, 工作原理是: 每次启动项目时,自增一条数据,使用自增id 作为雪花id的 workerId ,避免了集群部署时,动态扩展和缩减节点,需要手动配置不同的workerId 的问题。 而CACHE模式,是在DEFAULT模式的基础上做了缓存,性能更好。 所以集群部署,尤其是需要动态扩容时,强烈建议使用 CACHE 模式。
如何在 SuperMapper 中,增加方法
在SuperMapper中定义方法接口,如:
java/** * 全量修改所有字段 * * @param entity 实体 * @return 修改数量 */ int updateAllById(@Param(Constants.ENTITY) T entity); /** * 批量插入所有字段 * <p> * 只测试过MySQL!只测试过MySQL!只测试过MySQL! * * @param entityList 实体集合 * @return 插入数量 */ int insertBatchSomeColumn(List<T> entityList);
/** * 全量修改所有字段 * * @param entity 实体 * @return 修改数量 */ int updateAllById(@Param(Constants.ENTITY) T entity); /** * 批量插入所有字段 * <p> * 只测试过MySQL!只测试过MySQL!只测试过MySQL! * * @param entityList 实体集合 * @return 插入数量 */ int insertBatchSomeColumn(List<T> entityList);
定义 updateAllById 方法的动态sql. (具体的实现代码位于: AlwaysUpdateSomeColumnById )
java@NoArgsConstructor public class UpdateAllById extends AlwaysUpdateSomeColumnById { public UpdateAllById(Predicate<TableFieldInfo> predicate) { super(predicate); } @Override public String getMethod(SqlMethod sqlMethod) { // 自定义 mapper 方法名 return "updateAllById"; } }
@NoArgsConstructor public class UpdateAllById extends AlwaysUpdateSomeColumnById { public UpdateAllById(Predicate<TableFieldInfo> predicate) { super(predicate); } @Override public String getMethod(SqlMethod sqlMethod) { // 自定义 mapper 方法名 return "updateAllById"; } }
注入 UpdateAllById
javapublic class AcuitySqlInjector extends DefaultSqlInjector { @Override public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) { List<AbstractMethod> methodList = super.getMethodList(mapperClass, tableInfo); methodList.add(new UpdateAllById(field -> !ArrayUtil.containsAny(new String[]{ SuperEntity.CREATE_TIME_COLUMN, SuperEntity.CREATED_BY_COLUMN }, field.getColumn()))); return methodList; } }
public class AcuitySqlInjector extends DefaultSqlInjector { @Override public List<AbstractMethod> getMethodList(Class<?> mapperClass, TableInfo tableInfo) { List<AbstractMethod> methodList = super.getMethodList(mapperClass, tableInfo); methodList.add(new UpdateAllById(field -> !ArrayUtil.containsAny(new String[]{ SuperEntity.CREATE_TIME_COLUMN, SuperEntity.CREATED_BY_COLUMN }, field.getColumn()))); return methodList; } }
配置 AcuitySqlInjector
java@Bean @ConditionalOnMissingBean public AcuitySqlInjector getMySqlInjector() { return new AcuitySqlInjector(); }
@Bean @ConditionalOnMissingBean public AcuitySqlInjector getMySqlInjector() { return new AcuitySqlInjector(); }
如何修改datasource模式的 租户数据库名
修改 database.yml
yamlacuity: mysql: database: test_defaults # 启动时连接的默认库 database: initDatabasePrefix: - test_base # 启动时连接的租户库前缀 务必与 ContextConstants.TENANT_BASE_POOL_NAME_HEADER 保持一致
acuity: mysql: database: test_defaults # 启动时连接的默认库 database: initDatabasePrefix: - test_base # 启动时连接的租户库前缀 务必与 ContextConstants.TENANT_BASE_POOL_NAME_HEADER 保持一致
修改 ContextConstants
javapublic final class ContextConstants { // 切换数据源时租户前缀 务必与 acuity.database.initDatabasePrefix 保持一致 public static final String TENANT_BASE_POOL_NAME_HEADER = "test_base"; }
public final class ContextConstants { // 切换数据源时租户前缀 务必与 acuity.database.initDatabasePrefix 保持一致 public static final String TENANT_BASE_POOL_NAME_HEADER = "test_base"; }
服务运行时,如何链接、查询多个租户库?
- 修改 database.yml
yaml
acuity:
database:
initDatabasePrefix:
- test_base # 启动时连接的租户库前缀 务必与TENANT_BASE_POOL_NAME_HEADER保持一致
- test_extend # 启动时连接的租户库前缀2 务必与TENANT_EXTEND_POOL_NAME_HEADER保持一致
- test_xxx # 启动时连接的租户库前缀3 需要在ContextConstants中新增一个常量: TENANT_XXX_POOL_NAME_HEADER
acuity:
database:
initDatabasePrefix:
- test_base # 启动时连接的租户库前缀 务必与TENANT_BASE_POOL_NAME_HEADER保持一致
- test_extend # 启动时连接的租户库前缀2 务必与TENANT_EXTEND_POOL_NAME_HEADER保持一致
- test_xxx # 启动时连接的租户库前缀3 需要在ContextConstants中新增一个常量: TENANT_XXX_POOL_NAME_HEADER
- 修改 ContextConstants
java
public final class ContextConstants {
// (内置)切换数据源时租户前缀
public static final String TENANT_BASE_POOL_NAME_HEADER = "test_base";
// (内置)
public static final String TENANT_EXTEND_POOL_NAME_HEADER = "test_extend";
// (自行新增)
public static final String TENANT_XXX_POOL_NAME_HEADER = "test_xxx";
}
public final class ContextConstants {
// (内置)切换数据源时租户前缀
public static final String TENANT_BASE_POOL_NAME_HEADER = "test_base";
// (内置)
public static final String TENANT_EXTEND_POOL_NAME_HEADER = "test_extend";
// (自行新增)
public static final String TENANT_XXX_POOL_NAME_HEADER = "test_xxx";
}
- 修改 ContextUtil
java
public final class ContextUtil {
public static void setTenantId(Object tenantId) {
set(ContextConstants.TENANT_ID_HEADER, tenantId);
setTenantBasePoolName(tenantId);
setTenantExtendPoolName(tenantId);
// 自行新增
setTenantXxxPoolName(tenantId);
}
/**
* 切换xxx库
*
* @param tenantId
*/
public static void setTenantXxxPoolName(Object tenantId) {
set(ContextConstants.TENANT_XXX_POOL_NAME_HEADER, tenantId);
}
}
public final class ContextUtil {
public static void setTenantId(Object tenantId) {
set(ContextConstants.TENANT_ID_HEADER, tenantId);
setTenantBasePoolName(tenantId);
setTenantExtendPoolName(tenantId);
// 自行新增
setTenantXxxPoolName(tenantId);
}
/**
* 切换xxx库
*
* @param tenantId
*/
public static void setTenantXxxPoolName(Object tenantId) {
set(ContextConstants.TENANT_XXX_POOL_NAME_HEADER, tenantId);
}
}
- 修改 DsConstant
java
public interface DsConstant {
/**
* 默认数据源
*/
String DEFAULTS = "0";
/**
* 动态租户数据源
*/
String BASE_TENANT = "#thread." + TENANT_BASE_POOL_NAME_HEADER;
String EXTEND_TENANT = "#thread." + TENANT_EXTEND_POOL_NAME_HEADER;
// 自行新增
String XXX_TENANT = "#thread." + ContextConstants.TENANT_XXX_POOL_NAME_HEADER;
}
public interface DsConstant {
/**
* 默认数据源
*/
String DEFAULTS = "0";
/**
* 动态租户数据源
*/
String BASE_TENANT = "#thread." + TENANT_BASE_POOL_NAME_HEADER;
String EXTEND_TENANT = "#thread." + TENANT_EXTEND_POOL_NAME_HEADER;
// 自行新增
String XXX_TENANT = "#thread." + ContextConstants.TENANT_XXX_POOL_NAME_HEADER;
}
- 编写ServiceImpl代码
java
@DS(DsConstant.BASE_TENANT)
public class ServiceImpl {
public void test1() {
// CRUD 操作 test_base 库
}
@DS(DsConstant.DEFAULTS)
public void test2() {
// CRUD 操作 test_defaults 库
}
@DS(DsConstant.EXTEND_TENANT)
public void test3() {
// CRUD 操作 test_extend 库
}
@DS(DsConstant.XXX_TENANT)
public void test4() {
// CRUD 操作 test_xxx 库
}
}
@DS(DsConstant.BASE_TENANT)
public class ServiceImpl {
public void test1() {
// CRUD 操作 test_base 库
}
@DS(DsConstant.DEFAULTS)
public void test2() {
// CRUD 操作 test_defaults 库
}
@DS(DsConstant.EXTEND_TENANT)
public void test3() {
// CRUD 操作 test_extend 库
}
@DS(DsConstant.XXX_TENANT)
public void test4() {
// CRUD 操作 test_xxx 库
}
}