扩展BeetlSQL3
About 3693 wordsAbout 12 min
1984-01-24
BeetlSQL是高度可扩展的数据系统访问框架,能容易的支持不同的数据库系统,在《新库支持》作为专门章节说明
本章说明其他扩展BeetlSQL3的地方,有些内容在前面章节也提到过,这里再详细说明
如下图说明,除了SQLManager,ClassAnnotations类,其他在BeetlSQL3所见的都类都是可以扩展和定制的。SQLManager也可以派生子类或者自定义注解完成特定功能,比如ConditionalSQLManager,@SpringData,@SubQuery
本章说明的内容偏向源码,介意在属性BeetlSQL3用法后,在看一章
BaseMapper定制
Mapper 是BeetlSQL3推荐的的使用方法,你可以编写接口继承BaseMapper,能得到很多内置的CRUD方法,你可以编写自己的BaseMapper.
@AutoMapper注解适合作为你的BaseMapper方法,@AutoMapper指明了mapper方法的执行类,如下insert方法的执行类是InsertAMI
public interface BaseMapper<T> {
/**
* 通用插入,插入一个实体对象到数据库,所以字段将参与操作,除非你使用ColumnIgnore注解
*SqlResource
* @param entity
*/
@AutoMapper(InsertAMI.class)
void insert(T entity);
}
InsertAMI非常简单,它是一个MapperInvoke子类,需要实现call方法
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为例子,
@Target({java.lang.annotation.ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Builder(SpringDataBuilder.class)
public @interface SpringData {
}
我们可以到此注解被@Builder注解标注,那么所有被@SpringData标注的Mapper方法,都通过SpringDataBuilder类来解释执行。SpringDataBuilder类实现了MapperExtBuilder,职责是构造一个MapperInvoke方法供BeetSQL调用
public class SpringDataBuilder implements MapperExtBuilder {
@Override
public MapperInvoke parse(Class entity, Method m) {
//解析method的方法,得出MapperInvoke
}
}
MapperInvoke会在Mapper方法调用时候被BeetlSQL3调用,MapperInvoke对应Mapper的每一个方法,在InsertAMI中我们已经看到一个实现。
与@SpringData类似的是@SubQuery,定义如下,有兴趣的可以看看SubQueryBuilder的实现,非常简单
@Target({java.lang.annotation.ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Builder(SubQueryBuilder.class)
public @interface SubQuery {
}
总结一下实现Mapper的自定义注解,是需要定义任何一个注解,并用@Builder(XXX.class) 说明执行类是XXX.class,此执行类需要实现MapperExtBuilder接口,能根据标注的方法,返回一个MapperInvoke执行类
SQLExecutor定制
SQLExecutor是核心类, 负责底层的JDBC 执行,SQLManager会调用SQLExecutor得到执行结果,SQLExecutor有来三个类
- BaseSQLExecutor是核心类,执行JDBC操作
- BaseStatementOnlySQLExecutor 对于不支持PreparedStatement的数据库引擎使用此
- QuerySQLExecutor,只支持查询不支持更新的SQL查询引擎使用,所有更新API都会抛出异常
SQLExecutor是在SQLManager中每次调用通过dbStyle.buildExecutor创建一个新的实例,因此可以扩展DBStyle来 可以扩展如上任何一个子类
public interface DBStyle {
SQLExecutor buildExecutor(ExecuteContext executeContext);
}
如下是一个BaseSQLExecutor扩展,假设对某些查询,需要设置JDBC Fetch Size
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
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信息,
public interface DBStyle {
MetadataManager initMetadataManager(ConnectionSource cs);
}
定义如下
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类生成数据库
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 返回一个实现。通常这个类不需要扩展
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名字转化为数据库名字,或者相反
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的实现
@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的时候,仅仅查询此俩列
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
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上的注解来申明,比如
@RowProvider(MyRowMapper.class)
public static class TestUser2 extends TestUser {
}
所有查询结果映射到TestUser2后,还需要执行MyRowMapper接口
@RowProvider注解定义如下
@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来申明如何映射
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定义如下
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用于属性转化,定义如下
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子类
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD, ElementType.FIELD})
@Builder(Base64Convert.class)
public static @interface Base64 {
}
因此,可以自pojo上使用此注解
@Table(name="beetlSQLSysUser")
@Data
public static class UserData{
@AutoID
Integer id;
@Base64
String name;
}
所有关于pojo的注解都在
ClassAnnotation
里维护
一个简单的实现Base64注解实现如下,这样保证name字段存入数据库是经过base64加密,取出是base64解密
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定义如下
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为例子
public class CustomerOrder2 {
@AutoID
Integer id;
String name;
Integer customerId;
@FetchSql("select * from sys_customer where id =#{customerId}")
Customer customer;
}
@FetchSql必须使用@Builder注解指明实现类
@Retention(RetentionPolicy.RUNTIME)
@Target(value = {ElementType.METHOD, ElementType.FIELD})
@Builder(FetchSqlAction.class)
public @interface FetchSql {
/**
* sql模板语句
* @return
*/
String value();
}
FetchSqlAction实现了FetchAction接口,推荐继承AbstractFetchAction
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,后者是具体执行生成过程
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 需要实现如下方法,执行具体的生成动作
public abstract void generate(BaseProject project, SourceConfig config,Entity entity);
参数BaseProject提供了生成的基本信息,比如BaseProject的子类ConsoleOnlyProject,会提供一个基于System.out的输出流,因此生成的代码都打印在控制台,而SimpleMavenProject 则提供一个文件路径,代码会按照包名保存到相应的工程文件里
Entity 对象则是目标表或者视图的描述,包含了java属性,类型,数据库的列名,类型,备注等信息,足够生成一个java类
用户通常需要完成SourceBuilder来实现代码生成,生成到文件,控制台,还是其他地方则是通过BaseProject决定
如下是一个根据表生成数据库表的描述文档
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 如下
## ${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} |
<%}%>