# 多库使用
一个系统随着规模增加,会从单一数据库变成多数据库,一种是业务库变成主从库,或者是分库分表。还有一种是业务拆分多个库,不如拆分成订单库,商品库
BeetlSQL3 应对这种数据库扩展有简单的应对规则。如果是同一种业务库,则使用一个SQLManager管理,无论是分库还是分表,都在底层路由到不同的数据库。 如果是多个业务库,则使用多个SQLMananger。 多个SQLManager可以使用一个ConditionalSQLManager来管理,方便开发
>有数据库中间件支持分库分表,如果你使用了了数据库中间件,那么只需要给BeetlSQL配置一个数据源即可,数据源指向了数据库中间件,中间件将完成分库分表
>
>BeetlSQL3对于数据库分库分表后的汇总查询无能为力,这需要使用SQL查询引擎实现。BeetlSQL支持很多SQL查询引擎
本章例子来源于源码 S6MoreDatabase或者单元MoreDatabaseTest
## 主从库
SQLManager使用数据源,如果只提供一个数据源,则认为读写均操作此数据源,如果提供多个,则默认第一个为写库,其他为读库。用户在开发代码的时候,无需关心操作的是哪个数据库,因为调用SQLMnager 的 select相关api的时候,总是去读取从库,add/update/delete 的时候,总是读取主库(如下是主从实现原理,大部分情况下无需关心如何实现)
```java
sqlManager.insert(User.class,user) // 操作主库,如果只配置了一个数据源,则无所谓主从
sqlManager.unique(id,User.class) //读取从库
```
主从库的逻辑是由ConnectionSource来决定的,如下DefaultConnectionSource 的逻辑
```java
@Override
public Connection getConn(ExecuteContext ctx,boolean isUpdate){
if(this.slaves==null||this.slaves.length==0) return this.getWriteConn(ctx);
if(isUpdate) return this.getWriteConn(ctx);
else return this.getReadConn(ctx);
}
```
如果你有一主多从数据源,可以通过ConnectionSourceHelper 构造一个支持主从的ConnectionSource
```java
//为了简单起见,主从库都走同一个数据库
DataSource master = SampleHelper.datasource();
DataSource slave1 = SampleHelper.datasource();
DataSource slave2 = SampleHelper.datasource();
ConnectionSource source = ConnectionSourceHelper.getMasterSlave(master,new DataSource[]{slave1,slave2});
SQLManagerBuilder builder = new SQLManagerBuilder(source);
builder.setNc(new UnderlinedNameConversion());
builder.setInters(new Interceptor[]{new DebugInterceptor()});
builder.setDbStyle(new MySqlStyle());
SQLManager sqlManager = builder.build();
```
如果你是Spring或者SpringBoot框架,则可以简单指定多个数据源,以SpringBoot为例子
```properties
beetlsql = sqlManager1
beetlsql.sqlManager1.ds=ds1,salve1,salve2
beetlsql.sqlManager1.basePackage=org.beetl.sql.springboot.simple
```
这里ds1和slave1,slave2 是数据源的名称。
如果是Spring。配置如下
```xml
<bean id="sqlManagerFactoryBean" class="org.beetl.sql.ext.spring.SqlManagerFactoryBean">
<property name="cs" >
<bean class="org.beetl.sql.ext.spring.SpringConnectionSource">
<property name="masterSource" ref="dataSource-master"></property>
<property name="slaveSource">
<list>
<ref bean="dataSource-slave-1"></ref>
<ref bean="dataSource-slave-2"></ref>
</list>
</property>
</bean>
</property>
<!-- 其他配置 -->
</bean>
```
对于不同的ConnectionSource 完成逻辑不一样,对于spring,jfinal这样的框架,如果sqlManager在事务环境里,总是操作主数据库,如果是只读事务环境 则操作从数据库。如果没有事务环境,则根据sql是查询还是更新来决定。
如下是SpringConnectionSource 提供的主从逻辑
```java
@Override
public Connection getConn(ExecuteContext ctx, boolean isUpdate){
if (this.slaves == null || this.slaves.length == 0)
return this.getWriteConn(ctx);
//如果是更新语句,也得走master
if (isUpdate){
return this.getWriteConn(ctx);
}
//在事物里都用master,除了readonly事物
boolean inTrans = TransactionSynchronizationManager.isActualTransactionActive();
if (inTrans) {
boolean isReadOnly = TransactionSynchronizationManager.isCurrentTransactionReadOnly();
if (!isReadOnly) {
return this.getWriteConn(ctx);
}
}
return this.getReadConn(ctx);
}
```
注意,对于使用者来说,无需关心上面说的细节,仅供要定制主从逻辑的架构师
## 多个SQLManager
如果系统有拆分成多个业务系统,则需要为系统配置多个SQLManager,每个SQLManager关联一个业务系统的数据库,或者主从库
如下是Spring Boot 配置orderSqlManager和productSqlManager
```java
beetlsql = orderSqlManager,productSqlManager
beetlsql.orderSqlManager.ds=orderDs1,orderDs2-slave,orderDs3-slave
beetlsql.productSqlManager.ds=productDs
```
## ConditionalSQLManager
在系统中使用ConditionalSQLManager,可以把多个SQLManager合并成一个。如下例子来自源码S6MoreDatabase.conditional
```java
SQLManager a = SampleHelper.init();
SQLManager b = SampleHelper.init();
Map<String, SQLManager> map = new HashMap<>();
map.put("a", a);
map.put("b", b);
SQLManager sqlManager = new ConditionalSQLManager(a, map);
//不同用户,用不同sqlManager操作,存入不同的数据库
UserData user = new UserData();
user.setName("hello");
user.setDepartmentId(2);
sqlManager.insert(user);
DepartmentData dept = new DepartmentData();
dept.setName("dept");
sqlManager.insert(dept);
```
ConditionalSQLManager 默认是通过操作POJO上的@TargetSQLManager注解来区分
```java
@Data
@Table(name = "sys_user")
@TargetSQLManager("a")
public static class UserData {
@Auto
private Integer id;
private String name;
private Integer departmentId;
}
@Data
@Table(name = "department")
@TargetSQLManager("b")
public static class DepartmentData {
@Auto
private Integer id;
private String name;
}
```
Conditional接口决定了使用何种数据库,以ConditionalSQLManager.DefaultConditional为例子,会返回真正的SQLManager
```java
public static class DefaultConditional implements Conditional{
@Override
public SQLManager decide(Class pojo,SQLManager defaultSQLManager, Map<String, SQLManager> sqlManagerMap) {
TargetSQLManager an = (TargetSQLManager)pojo.getAnnotation(TargetSQLManager.class);
if(an==null){
return defaultSQLManager;
}
String sqlManagerName = an.value();
SQLManager target = sqlManagerMap.get(sqlManagerName);
if(target==null){
throw new IllegalArgumentException("未发现目标sqlManager "+sqlManagerName+" from "+sqlManagerMap.keySet());
}
return target;
}
}
```
## 单表多租户
多租户有多种实现方式,最简单是单表里通过tenant_id字段区分当前租户。因此每个数据库操作都必须带上此租户值。有时候为了安全考虑,交给程序员每次设置租户id会容易出错,BeetlSQL3 可以使用TargetAdditional为每次SQL请求自动带上额外的参数,而不需要在代码里每次提供
```java
public interface TargetAdditional {
public Map<String,Object> getAdditional(ExecuteContext ctx, Annotation an);
}
```
TargetAdditional 实现类提供额外的sql参数,比如租户id
```java
public class TenantContext implements TargetAdditional {
public static ThreadLocal<Integer> tenantLocals = new ThreadLocal<>();
@Override
public Map<String, Object> getAdditional(ExecuteContext ctx, Annotation an) {
Map map = new HashMap();
Integer tenant = tenantLocals.get();
if(tenant==null){
throw new IllegalStateException("缺少租户信息");
}
map.put("tenantId",tenant);
}
}
```
定义@Tenant注解,这里使用@Builder指示执行类是TenantContext.class
```java
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.TYPE})
@Builder(TenantContext.class)
public @interface Tenant {
}
```
任何使用@Tenant的类,在BeetlSQL执行过程中,都会具有额外的参数tenantId
```java
@Data
@Table(name="sys_user")
@Tenant
public static class TenantUser{
@Auto
private Integer id;
@Column("name")
private String name;
private String tenantId
}
```
注意,并不是赋值TenantUser.tenantId属性,而是在BeetlSQL执行参数上下文里提供。BeetlSQL取值的时候会从上下文里得到tenantId的参数,如下sql模板,并未提供tenantId,而是通过注解@Tenant实现
```java
String sql = "select * from sys_user where department_id=#{tenantId}";
List<TenantUser> list = sqlManager.execute(sql,TenantUser.class,new HashMap());
System.out.println(list.get(0));
```
## 每个租户一个表
> 参考代码在源码 S6MoreDatabase.multipleTables
可以为每个租户设置一个表。BeetlSQL为了实现此特性,在@Table 或者 sql模板中指定表名的时候使用动态表名
```java
static final String USER_TABLE="${toTable('sys_user')}";
@Data
@Table(name = USER_TABLE)
public class MyUser {
@AssignID
private Integer id;
private String name;
}
```
我们的表名使用了`${toTable('sys_user')}` ,toTable是待会需要定义的一个函数名,输入是sys_user,输出可能是以tenant为后缀的表名,比如sys_user_abc1,sys_user_cd
为了使得BeetlSQL能识别动态表明${toTable('sys_user')},还需要配置SQLManager
```java
protected SQLManager getSQLManager4MultipleTables(){
SQLManager sqlManager = SampleHelper.getSqlManager();
//告诉sqlManager遇到USER_TABLE这个不存在的表不报错,它是个虚表,真实表是sys_user_xx
sqlManager.addVirtualTable("sys_user",USER_TABLE);
BeetlTemplateEngine templateEngine = (BeetlTemplateEngine)sqlManager.getSqlTemplateEngine();
// 注册一个方法来实现映射到多表的逻辑
templateEngine.getBeetl().getGroupTemplate().registerFunction("toTable", new Function(){
@Override
public Object call(Object[] paras, Context ctx) {
String tableName = (String)paras[0];
String tenantId = TenantLocalContext.get();
return tableName+"_"+tenantId
}
});
return sqlManager;
}
```
* addVirtualTable ,接受俩个第一个参数,第一个是对应数据库的真实的表,第二个参数是也是一个表名,但不存在数据库,是个虚拟的表,当BeetlSQL需要知道虚拟表的metadata信息的时候,就从对应的真实表里查找。这常用分表环境中,比如数据库有bbs_topic_01到 bbs_topic_10, BeetSQL可以自定表名为@Table(name="bbs_topic"),但考虑系统并未有bbs_topic表,所以可以addVirtualTable("bbs_topic_01","bbs_topic"), 以方便如果需要知道"bbs_topic“表的信息,可以从bbs_topic_01中找到。在如上的虚拟表是个函数`${toTable('sys_user')}` 对应的表信息可以从sys_user中获取
* BeetlSQL默认使用Beetl引擎,因此自定义了个toTable函数,会接受一个表名,然后根据上下文(TenantLocalContext)来取得当前租户信息,代码如下
```JAVA
String tableName = (String)paras[0];
String tenantId = TenantLocalContext.get();
return tableName+"_"+tenantId
```
有了如上配置后,可以使用多表
```java
TenantLocalContext.set(teantId);
sqlManger.insert(MyUser.class);
String sql = "select * from ${toTable('sys_user')} where department_id=#{tenantId}";
List<TenantUser> list = sqlManager.execute(sql,TenantUser.class,new HashMap());
```
## 多库多租户
如果每个库一个租户,则需要动态路由到指定的数据源,可以借助ConditionalConnectionSource类,类似ConditionalSQLManager,可以代理ConnectionSource,根据适当条件使用返回不同数据源的数据库连接
> 参考S6MoreDatabase.multipleDataBaseAndTables
```java
public class ConditionalConnectionSource implements ConnectionSource {
Policy policy;
Map<String,ConnectionSource> all;
ConnectionSource defaultCs;
/**
*
* @param policy 选择数据源的策略
* @param all 所有备选的数据源表
*/
public ConditionalConnectionSource( Policy policy,Map<String,ConnectionSource> all){
this.all = all;
String defaultName = policy.getMasterName();
defaultCs = all.get(defaultName);
if(defaultCs==null){
throw new IllegalArgumentException("根据 "+defaultName+" 找不到对应的ConnectionSource");
}
this.policy = policy;
}
//忽略其他代码
}
```
构造ConditionalConnectionSource 需要一个策略类以及多个ConnectionSource构成的Map。策略类决定了使用哪个ConnectionSource。定义如下
```java
@Override
public Connection getConn(ExecuteContext ctx, boolean isUpdate) {
String name = policy.getConnectionSourceName(ctx,isUpdate);
ConnectionSource cs = all.get(name);
if(cs==null){
throw new IllegalArgumentException("根据 "+name+" 找不到对应的ConnectionSource");
}
return cs.getConn(ctx,isUpdate);
}
```
策略类可以根据山下文,或者ExecuteContext提供的参数来决定返回哪一个ConnectionSouce,这个跟ConditionalSQLManager原来类似。Policy定义如下
```java
public static interface Policy{
String getConnectionSourceName(ExecuteContext ctx, boolean isUpdate);
String getMasterName();
}
```