# 多库使用 一个系统随着规模增加,会从单一数据库变成多数据库,一种是业务库变成主从库,或者是分库分表。还有一种是业务拆分多个库,不如拆分成订单库,商品库 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(); } ```