# 扩展BeetlSQL3 BeetlSQL是高度可扩展的数据系统访问框架,能容易的支持不同的数据库系统,在《新库支持》作为专门章节说明 本章说明其他扩展BeetlSQL3的地方,有些内容在前面章节也提到过,这里再详细说明 如下图说明,除了SQLManager,ClassAnnotations类,其他在BeetlSQL3所见的都类都是可以扩展和定制的。SQLManager也可以派生子类完成特定功能,比如ConditionalSQLManager > 本章说明的内容偏向源码,介意在属性BeetlSQL3用法后,在看一章 ![](/Users/xiandafu/books/beetlsql3_guide/images/component.png) ## BaseMapper定制 Mapper 是BeetlSQL3推荐的的使用方法,你可以编写接口继承BaseMapper,能得到很多内置的CRUD方法,你可以编写自己的BaseMapper. @AutoMapper注解适合作为你的BaseMapper方法,@AutoMapper指明了mapper方法的执行类,如下insert方法的执行类是InsertAMI ```java public interface BaseMapper<T> { /** * 通用插入,插入一个实体对象到数据库,所以字段将参与操作,除非你使用ColumnIgnore注解 *SqlResource * @param entity */ @AutoMapper(InsertAMI.class) void insert(T entity); } ``` InsertAMI非常简单,它是一个MapperInvoke子类,需要实现call方法 ```java public class InsertAMI extends MapperInvoke { @Override public Object call(SQLManager sm, Class entityClass, Method m, Object[] args) { int ret = sm.insert(args[0]); return ret; } } ``` 参数entityClass代表了Mapper的泛型类,参数m代表了被调用的mapper方法,参数args是调用的实际参数 Mapper提供了很多注解,比如@Sql,@Template,@SpringData,@SqlProvider等等,你也许想定义自己的注解来解释mapper执行 这是可行的。以@SpringData为例子, ```java @Target({java.lang.annotation.ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Builder(SpringDataBuilder.class) public @interface SpringData { } ``` 我们可以到此注解被@Builder注解标注,那么所有被@SpringData标注的Mapper方法,都通过SpringDataBuilder类来解释执行。SpringDataBuilder类实现了MapperExtBuilder,职责是构造一个MapperInvoke方法供BeetSQL调用 ```java public class SpringDataBuilder implements MapperExtBuilder { @Override public MapperInvoke parse(Class entity, Method m) { //解析method的方法,得出MapperInvoke } } ``` MapperInvoke会在Mapper方法调用时候被BeetlSQL3调用,MapperInvoke对应Mapper的每一个方法,在InsertAMI中我们已经看到一个实现。 总结一下实现Mapper的自定义注解,是需要定义任何一个注解,并用@Builder(XXX.class) 说明执行类是XXX.class,此执行类需要实现MapperExtBuilder接口,能根据标注的方法,返回一个MapperInvoke执行类 ![](/Users/xiandafu/books/beetlsql3_guide/images/mapper.png) ## SQLExecutor定制 SQLExecutor是核心类, 赋值底层的JDBC 执行,SQLManager会调用SQLExecutor得到执行结果,SQLExecutor有来三个类 * BaseSQLExecutor是核心类,执行JDBC操作 * BaseStatementOnlySQLExecutor 对于不支持PreparedStatement的数据库引擎使用此 * QuerySQLExecutor,只支持查询不支持更新的SQL查询引擎使用,所有更新API都会抛出异常 SQLExecutor是在SQLManager中每次调用通过dbStyle.buildExecutor创建一个新的实例,因此可以扩展DBStyle来 可以扩展如上任何一个子类 ```java public interface DBStyle { SQLExecutor buildExecutor(ExecuteContext executeContext); } ``` 如下是一个BaseSQLExecutor扩展,假设对某些查询,需要设置JDBC Fetch Size ```java public static class MyExecutor extends BaseSQLExecutor{ public MyExecutor(ExecuteContext executeContext) { super(executeContext); } @Override protected ResultSetHolder dbQuery(Connection conn, String sql, List<SQLParameter> jdbcPara) throws SQLException { if(this.getExecuteContext().target!= UserEntity.class){ return super.dbQuery(conn,sql,jdbcPara); } //对于UserEntity对象查询,考虑使用特殊设置 PreparedStatement ps = conn.prepareStatement(sql,ResultSet.TYPE_FORWARD_ONLY, ResultSet.CONCUR_READ_ONLY); ps.setFetchSize(100000); this.setPreparedStatementPara(ps, jdbcPara); ResultSet rs = ps.executeQuery(); return new ResultSetHolder(ps, rs); } } ``` 设计一个新的DBStyle ```java public static class XXXStylePlus extends MySqlStyle { @Override public SQLExecutor buildExecutor(ExecuteContext executeContext){ return new MyExecutor(executeContext); } } ``` ## MetadataManager MetadataManager负责数据库元信息维护,BeetlSQL3自带的SchemaMetadataManager能读取目前按照JDBC规范,读取所有的数据库信息(但是,每个库对JDBC规范,仍然有不同) `initMetadataManager`方法是DBStyle初始化的时候调用,返回MetaManager类,用于管理数据库的metadata信息, ```java public interface DBStyle { MetadataManager initMetadataManager(ConnectionSource cs); } ``` 定义如下 ```java public interface MetadataManager { boolean existTable(String tableName); TableDesc getTable(String name); Set<String> allTable(); void addTableVirtuals(String realTable,String virtual); } ``` 通常,可以根据JDBC规范直接调用DatabaseMetaData获取数据库表信息,参考代码`SchemaMetadataManager`,如果有些库不支持metadata,譬如drill,查询文件,不提供metadata,你需要使用其`NoSchemaMetaDataManager`,此类能接受多个POJO类,根据POJO的定义,解析成MetaData信息,有点类似Hibenrate那样根据POJO类生成数据库 ```java public NoSchemaMetaDataManager(List<Class> beans){ beans.forEach(bean->parseBean(bean)); } public void addBean(Class bean){ parseBean(bean); } protected void parseBean(Class bean){ } } ``` parseBean会解析bean,得出目标数据库表的信息 ## ExecuteContext `ExecuteContext`代表了BeetlSQL的执行上下文信息,SQLExecutor.getExecuteContext 返回一个实现。通常这个类不需要扩展 ```java public class ExecuteContext { /** * sqlId */ public SqlId sqlId ; /** * select 语句需要映射的对象,有可能没有,比如update语句 */ public Class target; /** * 原始参数 */ public Object inputParas; /** * sql模板 */ public SQLSource sqlSource; /** * ViewType类型,如果viewType不为null */ public Class viewClass = null; /** * 行映射类,与resultMapper只能二选一存在 */ public RowMapper rowMapper = null; /** * Bean映射类 */ public ResultSetMapper resultMapper = null; /** * 用来负责将ResultSet映射到对象上,如果此不为null,则使用此类负责映射 * 否则,参考RowMapper或者ResultSetMapper,如果也为null,则使用SQLManager的默认的BeanProcessor */ public BeanProcessor beanProcessor = null; public SQLManager sqlManager; /** * sql模板渲染后的sql语句和参数 */ public SQLResult sqlResult = new SQLResult(); /** * Executor执行结果,非convert,fetch扩展操作结果 */ public Object executeResult; /** * 在执行过程中的产生控制 */ public Map<String,Object> contextParas; } ``` ​ 在BeetlSQL执行过程中,BeetlSQL依据context里提供的信息可以进一步扩展,比如根据rowmapper或者resultMapper进行映射,这俩个类可以在执行过程中改变,比如在sql模板语句中修改rowMapper实现类,以实现个性化映射。默认情况下,这俩个类为空,关于映射,参考下一章个性化映射 ## 数据库表到Java对象 BeetlSQL提供多种方式实现数据库映射,包括 * 约定习俗,指定NameConversion * 通过@Table和@Column注解 * 通过ViewType 只映射一部分结果集 * 通过RowMapper自定义行映射,想在如上映射结果基础上,在定制映射结果 * 通过ResultSetMapper 自定义结果集映射,这有包含了@JsonMapper 的实现JsonConfigMapper和AutoJsonMapper俩种复杂结果集映射,类似MyBatis通过XML配置映射 映射完毕后,可以通过AttributeConvert或者BeanConvert再次对映射结果处理。比如加密字段的解密,或者字符串变成json操作 在返回结果集前,BeetlSQL还会查看是否有@Fetch标签,进行额外数据的抓取 ### NameConversion NameConversion 定义了如何把Java名字转化为数据库名字,或者相反 ```java public abstract String getTableName(Class<?> c); public abstract String getColName(Class<?> c,String attrName); public abstract String getPropertyName(Class<?> c,String colName); ``` NameConversion 的子类内置了DefaultNameConversion,即不做任何改变。UnderlinedNameConversion,把下划线去掉,其后字母大写。最为常用,也符合数据库设计规范,使用UnderlinedNameConversion 重写NameConversion需要考虑读取@Table和@Cloumn注解,可以复用NameConversion.getAnnotationColName,getAnnotationAttrName和getAnnotationTableName,如下是UnderlinedNameConversion的实现 ```java @Override public String getTableName(Class<?> c) { String name = getAnnotationTableName(c); if(name!=null){ return name; } return StringKit.enCodeUnderlined(c.getSimpleName()); } @Override public String getColName(Class<?> c,String attrName) { String col = super.getAnnotationColName(c,attrName); if(col!=null){ return col; } return StringKit.enCodeUnderlined(attrName); } @Override public String getPropertyName(Class<?> c,String colName) { String attrName = super.getAnnotationAttrName(c,colName); if(attrName!=null){ return attrName; } return StringKit.deCodeUnderlined(colName.toLowerCase()); } ``` ### ViewType ViewType 类似Jackson的@View注解,在BeetlSQL查询过程中,查询被VIewType申明的字段,如下TestUser,属性myId和myName被@View注解标注,因此sqlManager指定viewType为KeyInfo.class的时候,仅仅查询此俩列 ```java TestUser keyInfo = sqlManager.viewType(TestUser.KeyInfo.class).unique(TestUser.class, 1); @Data public static class TestUser { public static interface KeyInfo { } @Column("id") @AutoID @View(KeyInfo.class) Integer myId; @Column("name") @View(KeyInfo.class) String myName; Integer departmentId; } ``` VIewType会影响代码生成,因此对于TestUser对象来说,根据主键查询会有俩条内置sql语句生成,参考代码AbstractDBStyle ```java public SQLSource genSelectByIds(Class<?> cls,Class viewType) { ConcatContext concatContext = this.createConcatContext(); Select select = concatContext.select(); appendIdsCondition(cls,select); select.from(cls); if(viewType!=null){ select.all(cls,viewType); }else{ select.all(); } return new SQLTableSource(select.toSql()); } ``` 对于普通的sql'语句,也可以只映射部分查询结果,而不需要映射所有结果集,比如某些大字段(TODO,未完成) ### RowMapper RowMapper 可以在BeetlSQL默认的映射规则基础上,添加用户自定义映射,RowMapper可以通过SQLManager传入,或者通过POJO上的注解来申明,比如 ```java @RowProvider(MyRowMapper.class) public static class TestUser2 extends TestUser { } ``` 所有查询结果映射到TestUser2后,还需要执行MyRowMapper接口 @RowProvider注解定义如下 ```java @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) public @interface RowProvider { Class<? extends RowMapper> value(); } ``` ### ResultSetMapper ResultSetMapper对象相当于告诉BeetlSQL,不需要BeetlSQL来映射,交给ResultSetMapper来实现,比如一个select join结果需要映射到复杂的对象上(比如一个用户有多个角色,属于多个组织),BeetlSQL自带了JsonConfigMapper实现,用json来申明如何映射,类似MyBatis用xml来申明如何映射 ```java String sql = "select d.id id,d.name name ,u.id u_id,u.name u_name " + " from department d join beetlSQLSysUser u on d.id=u.department_id where d.id=?"; Integer deptId = 1; SQLReady ready = new SQLReady(sql,new Object[]{deptId}); List<DepartmentInfo> list = sqlManager.execute(ready,DepartmentInfo.class); @Data @ResultProvider(JsonConfigMapper.class) @JsonMapper( "{'id':'id','name':'name','users':{'id':'u_id','name':'u_name'}}") public static class DepartmentInfo { Integer id; String name; List<UserInfo> users; } ``` 注解ResultProvider提供了一个ResultSetMapper实现类,@JsonMapper是一个配置注解,与ResultProvider搭档,提供额外配置,JsonMapper支持配置在java代码里,或者通过文件配置 > Pojo类上所有注解都在`ClassAnnotation`类上存放,ResultProvider和JsonMapper 被缓存在ClassAnnotation类里,因为JsonMapper注解被`ProviderConfig`注解所申明,所以他俩是一对一 > > ``` > @ProviderConfig() > public @interface JsonMapper { > String value() default ""; > String resource() default ""; > } > > ``` > > ClassAnnotation 不仅仅寻找ResultProvider注解,也寻找使用了@ProviderConfig()的注解,并作为配置注解放在一起。BeetlSQL大量使用这种注解的注解,来提供扩展机制 JsonConfigMapper定义如下 ```java public class JsonConfigMapper extends ConfigJoinMapper { protected AttrNode parse(ExecuteContext ctx, Class target, ResultSetMetaData rsmd, Annotation config){ } } ``` ConfigJoinMapper 是基类,他会根据AttrNode描述来做映射,因此JsonConfigMapper只需要读取config注解申明的配置,然后转化成AttrNode即可,如果你想让配置是yml或者xml,可以实现parse方法即可 ### AttributeConvert AttributeConvert用于属性转化,定义如下 ```java public default Object toAttr(ExecuteContext ctx, Class cls,String name, ResultSet rs, int index) throws SQLException { return rs.getObject(index); } public default Object toDb(ExecuteContext ctx, Class cls,String name, Object dbValue) { return dbValue; } ``` toAttr用于把数据库转化成属性值,比如数据库字符串转成Java的json对象,toDb则是把属性值在存入数据库之前转成合适的值,比如json对象转成字符串 在定义了AttributeConvert类后,需要在定义一个注解,这样,beetlsql遇到此注解,将按照上述机制执行,注解的注解仍然使用`@Builder` 来完成,Builder接受一个AttributeConvert子类 ```java @Retention(RetentionPolicy.RUNTIME) @Target(value = {ElementType.METHOD, ElementType.FIELD}) @Builder(Base64Convert.class) public static @interface Base64 { } ``` 因此,可以自pojo上使用此注解 ```java @Table(name="beetlSQLSysUser") @Data public static class UserData{ @AutoID Integer id; @Base64 String name; } ``` > 所有关于pojo的注解都在`ClassAnnotation`里维护 一个简单的实现Base64注解实现如下,这样保证name字段存入数据库是经过base64加密,取出是base64解密 ```java public static class Base64Convert implements AttributeConvert { Charset utf8 = Charset.forName("UTF-8"); public Object toDb(ExecuteContext ctx, Class cls, String name, Object dbValue) { String value= (String) BeanKit.getBeanProperty(dbValue,name); byte[] bs = java.util.Base64.getEncoder().encode(value.getBytes(utf8)); return new String(bs,utf8); } public Object toAttr(ExecuteContext ctx, Class cls, String name, ResultSet rs, int index) throws SQLException { String value = rs.getString(index); return new String(java.util.Base64.getDecoder().decode(value),utf8); } } ``` ### BeanConvert BeanConvert同AttributeConvert类似,但用于整个Bean,BeanConvert定义如下 ```java public interface BeanConvert { public default Object before(ExecuteContext ctx, Object obj, Annotation an){ return obj; } public default Object after(ExecuteContext ctx, Object obj, Annotation an){ return obj; } } ``` ### Fetch BeetlSQL提供了默认的3个注解用来获取额外的对象,@FetchOne,@FetchMany,和@FetchSql。 开发者可以自定自己的Fetch注解,以FetchSql为例子 ```java public class CustomerOrder2 { @AutoID Integer id; String name; Integer customerId; @FetchSql("select * from sys_customer where id =#{customerId}") Customer customer; } ``` @FetchSql必须使用@Builder注解指明实现类 ```java @Retention(RetentionPolicy.RUNTIME) @Target(value = {ElementType.METHOD, ElementType.FIELD}) @Builder(FetchSqlAction.class) public @interface FetchSql { /** * sql模板语句 * @return */ String value(); } ``` FetchSqlAction实现了FetchAction接口,推荐继承`AbstractFetchAction` ```java public interface FetchAction { public void execute(ExecuteContext ctx, List list); public void init(Class owner, Class target,Annotation config, PropertyDescriptor pd); public Annotation getAnnotation(); public PropertyDescriptor getOriginProperty(); } ``` execute方法是加载额外数据,list是sqlManager查询的结果,Fetch操作需要遍历list,获取每一个数据库查询结果,然后根据init方法的配置,知道需要额外加载哪些属性,比如CustomerOrder2对象的customer 实现FetchSql 需要考虑关键一点,就是确保如果对象已经被处理过,不需要加载额外数据了,免得无限嵌套循环,比如a加载b,b又加载a。AbstractFetchAction的queryFromCache会查询FetchContext(一个上下文)是否已经处理过此对象,AbstractFetchAction的containAttribute方法则会从FetchContext查询此对象的此属性是否已经处理 ## 代码生成 BeetlSQL提供了弹性的代码生机制,可以根据数据库的定义生成java代码,文档,业务代码,BeetlSQL自身提供了一套生成模板,用户可以复用或者修改这些模板,或者提供新的模板,比如生成Controller代码 `SourceConfig`是核心类,接收参数SQLManager以及一组SourceBuilder,前者用于获取metadata,后者是具体执行生成过程 ```java public SourceConfig(SQLManager sqlManager,List<SourceBuilder> sourceBuilder) { this(sqlManager,false); this.sourceBuilder = sourceBuilder; } public void gen(String tableName, BaseProject project){} public void genAll(BaseProject project, SourceFilter sourceFilter){} ``` gen或者genAll方法用于开始执行,每个tableName都会传递给sourceBuilder集合,一次运行 SourceBuilder 需要实现如下方法,执行具体的生成动作 ```java public abstract void generate(BaseProject project, SourceConfig config,Entity entity); ``` 参数BaseProject提供了生成的基本信息,比如BaseProject的子类ConsoleOnlyProject,会提供一个基于System.out的输出流,因此生成的代码都打印在控制台,而SimpleMavenProject 则提供一个文件路径,代码会按照包名保存到相应的工程文件里 Entity 对象则是目标表或者视图的描述,包含了java属性,类型,数据库的列名,类型,备注等信息,足够生成一个java类 用户通常需要完成SourceBuilder来实现代码生成,生成到文件,控制台,还是其他地方则是通过BaseProject决定 如下是一个根据表生成数据库表的描述文档 ```java public class MDDocBuilder extends BaseTemplateSourceBuilder { public static String mapperTemplate="doc.btl.md"; public MDDocBuilder() { super("doc"); } @Override public void generate(BaseProject project, SourceConfig config, Entity entity) { //BeetlSQl中的配置 Beetl beetl = ((BeetlTemplateEngine)config.getSqlManager().getSqlTemplateEngine()).getBeetl(); //模板 Template template = groupTemplate.getTemplate(mapperTemplate); template.binding("tableName", entity.getTableName()); template.binding("comment", entity.getComment()); template.binding("colsMap", entity.getTableDesc().getColsDetail()); template.binding("table", entity.getTableDesc()); String mdFileName = StringKit.toLowerCaseFirstOne(entity.getName())+".doc..md"; Writer writer = project.getWriterByName(this.name,mdFileName); template.renderTo(writer); } ``` 对应的模板doc.btl.md 如下 ```markdown ## ${tableName} **说明** ${isEmpty(comment)?"无注释":comment} **表信息** <% var ids = table.idNames; %> * 主键 ${ids} * 表注释 | 名称 | 数据类型 | 长度 | 说明 | | :--: | :--- | :------: | :----: | <% for(col in colsMap){ var name = col.key; if(@ids.contains(name)){ name="*"+name; } var detail = col.value; var dbType =@org.beetl.sql.clazz.kit.JavaType.jdbcTypeId2Names.get(detail.sqlType); %> |${name} | ${dbType}| ${detail.size} | ${detail.remark} | <%}%> ```