# BeetlSQL 多数据库支持 BeetlSQL的目标是提供开发高效,维护高效,运行高效的数据库访问框架,在一个系统多个库的情况下,提供一致的编写代码方式。支持如下数据平台 * 传统数据库:MySQL,MariaDB,Oralce,Postgres,DB2,SQL Server,H2,SQLite,Derby,神通,达梦,华为高斯,人大金仓,PolarDB等 * 大数据:HBase,ClickHouse,Cassandar,Hive * 物联网时序数据库:Machbase,TD-Engine,IotDB * SQL查询引擎:Drill,Presto,Druid * 内存数据库:ignite,CouchBase BeetlSQL 不仅仅是简单的类似MyBatis或者是Hibernate,或者是俩着的综合,BeetlSQL目的是对标甚至超越Spring Data,是实现数据访问统一的框架,无论是传统数据库,还是大数据,还是查询引擎或者时序库,内存数据库。 BeetlSQL提供了接口来抽象不同的数据库或者SQL查询引擎,新的数据库只要实现这些接口,便能作为插件作为BeetlSQL使用 ## 多库之间的不同 可能你会疑惑,JDBC已经规范访问数据库的方式,为什么还需要BeetlSQL来规范。这是因为不同数据库,对JBDC的实现并不完全一样,而且,对SQL的的实现也不一定一样。在完成数据库集成的时候,需要考虑如下问题 * 数据库的jdbc是否支持PreparedStatement,大部分数据库支持,但有的数据库只支持Statement,比如Drill,Druid,Presto,因此,需要BeetlSQL在这些情况下,使用Statement来作为底层执行接口 * 数据库是否支持Metadata,如果支持,数据库框架可以得到数据库和表定义,大部分都支持。Drill 不支持(比如查询目标是个文件),TD-Engine是支持的,但目前版本获取Metadata会报错,也认为不支持。因此,需要BeetlSQL提供接口添加metadata信息 * 数据库支持序列,但使用方式不一样,比如,Oralce是xxx..nextval,而Postgres是nextval('xxxx') * 数据库是否支持update操作,SQL查询引擎是不支持的,因此需要屏蔽内置的更新SQL语句 * 数据库的翻页语句是否一样,大部分都不相同,都需要实现Range接口,然而,有些数据库是类似的,可以重用,比如OffsetLimitRange作为Range的实现类,可以为Mysql,大梦,TD-Engine,H2,Clickhouse,SqlLite使用 * 数据库JDBC驱动对日期字段是否支持,由于Java的日期类型比较多,传统数据可能会兼容java.util.Date,以及JDK后的LocalDate,LocalDateTime, 但也可能不兼容,BeelSQL框架提供了TypeHandler来负责实现这种转化 * 数据库JDBC对特殊字段是否支持,比如JSON,XML等,由于这两种类型并不是java规范,比如json实现有fastjson、jackson,因此需要TypeHandler来实现这种转化,把这些类型转化为数据库对应的类型 * 数据库对主键支持情况。越来越多的应用使用uuid、snowflake等分布式id来作为数据表主键,也有传统应用使用自增主键和数据库序列,比如Mysql自增,DB2和Postgres或者同时兼容两种。 * SQL查询引擎,如Presto,不支持insert,update语句 ## 跨库支持实现 如果BeetlSQL 目前不支持你所用的数据库,可以自己轻松扩展实现。主要的核心类是 * DbStyle * BeanProcessor 首先,先看看数据库跟哪些数据库比较接近或者兼容,比如很多云数据库都有传统数据库的影子,因此,你可以尝试使用传统数据库的DBStyle,比如阿里云云数据库兼容MySQL。因此,完全可以使用MySqlStyle,华为开源高斯数据库类似Postgres。 其次,新兴的数据库都有传统数据库的影子,比如翻页,大部分都是`limit ${offset}, ${limit}` , 比如mysql,因此可以用复用类`OffsetLimitRange` ;有的数据库则是`limit ${limit} offset ${offset}` ,比如apache drill,couchbase,apache ignite,还有国产TD-Engine, 这时候可以复用`LimitWithOffsetRange`。 有的数据库翻页类似Oralce,因此可以复用`RowNumRange`,比如国产数据库达梦 实现XXX数据库基本上只要是实现XXXStyle,继承AbstractDbStyle,AbstractDbStyle的一些核心方法是BeetlSQL解决不同数据库差异的主要类,这些方法将在本章后面详细简介,现在简单说明如下 ```java @Override public String getName() { return "mysql"; } @Override public int getDBType() { return DBType.DB_MYSQL; } ``` getName 返回数据库名称,getDBType则返回一个唯一标识,可以返回1000以外。数据库名称可以用于各种特殊处理。数据库sql文件也可以存放在以数据库名称作为目录名下,以实现跨数据库操作。 ```java @Override public RangeSql getRangeSql() { return this.rangeSql; } ``` 返回一个翻页辅助类,这将在后面详细讲解。这也是大部分数据库的差异点 对于NOSQL或者查询引擎来说,还有需要考虑的地方,以Presto为例子 ```java @Override public boolean isNoSql(){ return true; } public boolean preparedStatementSupport(){ return false; } @Override public String wrapStatementValue(Object value){ return super.wrapStatementValue(value); } @Override public SQLExecutor buildExecutor(ExecuteContext executeContext){ return new QuerySQLExecutor(executeContext); } ``` `isNoSql` 返回true,表示是非传统数据库。 `preparedStatementSupport` 返回false,表示数据库jdbc 不支持预编译,因此BeetlSQL将使用Statement而不是PreparedStatement,并会调用`wrapStatementValue`来动态构造sql buildExecutor 实际上构造了BeetlSQL的执行核心,这里返回QuerySQLExecutor而不是默认的BaseSQLExecutor,因为QuerySQLExecutor只保留了查询支持 有些数据库对MetaData支持不够友好,比如某些查询数据库查询文件,因此需要代码添加对“表”的描述,DBStyle需要重载initMetadataManager ```java @Override public MetadataManager initMetadataManager(ConnectionSource cs){ metadataManager = new NoSchemaMetaDataManager(); return metadataManager; } ``` NoSchemaMetaDataManager 类提供了addBean方法用于通过POJO提供一个表描述,这样才能保证BeetlSQL的代码能执行。 AbstractStyle 还支持config(SQLManager sqlManager),有机会配置sqlManager ```java @Override public void config(SQLManager sqlManager){ } ``` ## DBStyle DBStyle 是提供一致使用方式的关键,抽象类AbstractDBStyle是其子类,实现了大多数方法。不同数据库Style可以继承AbstractDBStyle,覆盖其特定实现,下面会以传统数据库Mysql和大数据库Clickhouse 为例来做说明 ### MySqlStyle 例子 ```java public class MySqlStyle extends AbstractDBStyle { RangeSql rangeSql = null; public MySqlStyle() { rangeSql = new OffsetLimitRange(this); } @Override public String getName() { return "mysql"; } @Override public int getDBType() { return DBType.DB_MYSQL; } @Override public RangeSql getRangeSql() { return this.rangeSql; } @Override public int getIdType(Class c,String idProperty) { List<Annotation> ans = BeanKit.getAllAnnotation(c, idProperty); int idType = DBType.ID_AUTO; //默认是自增长 for (Annotation an : ans) { if (an instanceof AutoID) { idType = DBType.ID_AUTO; break;// 优先 } else if (an instanceof SeqID) { //my sql not support } else if (an instanceof AssignID) { idType = DBType.ID_ASSIGN; } } return idType; } ``` 对于传统的数据库,需要重写的方法较少,主要是 * getIdType ,选择id的主键类型,mysql既可以是是@AutoId,也可以是@AssingId,这取决于其主键属性上的注解,如果同时有@AutoId或者@AssingId,则优先使用AutoId * getName ,返回数据库名字,如mysql,sqlserver2010,sqlserver2015等 * getDBType ,返回任意一个数字类型,默认的都在DBType类里 * rangeSql,用来实现翻页的,输入是jdbc sql,或者是模板sql,输出是一个翻页语句,本例子实现类是OffsetLimitRange,定义如下 ```java public class OffsetLimitRange implements RangeSql { AbstractDBStyle sqlStyle = null; public OffsetLimitRange(AbstractDBStyle style){ this.sqlStyle = style; } @Override public String toRange(String jdbcSql, Object objOffset , Long limit) { Long offset = ((Number)objOffset).longValue(); offset = PageParamKit.mysqlOffset(sqlStyle.offsetStartZero, offset); StringBuilder builder = new StringBuilder(jdbcSql); builder.append(" limit ").append(offset).append(" , ").append(limit); return builder.toString(); } @Override public String toTemplateRange(Class mapping,String template) { return template + sqlStyle.getOrderBy() + " \nlimit " + sqlStyle.appendExpress( DBAutoGeneratedSql.OFFSET ) + " , " + sqlStyle.appendExpress(DBAutoGeneratedSql.PAGE_SIZE); } @Override public void addTemplateRangeParas(Map<String, Object> paras, Object objOffset, long size) { Long offset = (Long)objOffset; paras.put(DBAutoGeneratedSql.OFFSET, offset - (sqlStyle.offsetStartZero ? 0 : 1)); paras.put(DBAutoGeneratedSql.PAGE_SIZE, size); } } ``` * toRange,返回一个JDBC的翻页SQL,对于MySQL,H2等支持limit&offset的来说,非常简单,后面添加limit offsetXXX,limitXX即可 * toTemplateRange, 针对模板sql翻页语句,类似toRange方法,但使用的是俩个变量,变量名的定义是DBAutoGeneratedSql.OFFSET,DBAutoGeneratedSql.PAGE_SIZE * addTemplateRangeParas, 这个是同toTemplateRange匹配,提供了DBAutoGeneratedSql.OFFSET的值,以及DBAutoGeneratedSql.PAGE_SIZE的值 ### H2Style例子 H2同Mysql很类似,唯一不同的是H2还支持序列,需要覆盖getSeqValue方法,得到一个在H2数据库里,序列求值的表达式 ```java @Override public String getSeqValue(String seqName) { return "NEXT VALUE FOR "+seqName; } ``` ### ClickHouseStyle例子 ```java public class ClickHouseStyle extends AbstractDBStyle { RangeSql rangeSql = null; public ClickHouseStyle() { super(); rangeSql = new OffsetLimitRange(this); } @Override public int getIdType(Class c,String idProperty) { //只支持 return DBType.ID_ASSIGN; } @Override public boolean isNoSql(){ return true; } @Override public String getName() { return "clickhouse"; } @Override public int getDBType() { return DBType.DB_CLICKHOUSE; } @Override public RangeSql getRangeSql() { return rangeSql; } @Override protected void checkId(Collection colsId, Collection attrsId, String clsName) { // 不检测主键 return ; } @Override public void config(SQLManager sqlManager){ Map<Class, JavaSqlTypeHandler> handlerMap = sqlManager.getDefaultBeanProcessors().getHandlers(); handlerMap.put(java.util.Date.class,new UtilDateTypeHandler() ); } } ``` 由于Clickhouse的翻页风格类似MySQL,因此rangeSql重用了OffsetLimitRange类 * getIdType,由于clickhouse不支持序列和自增主键,因此,这里直接使用DBType.ID_ASSIGN * isNoSql 返回true * checkId方法,不检查主键,因为clickhouse实际上并没有唯一主键的概念 ### HBaseStyle例子 ```java public class HBaseStyle extends AbstractDBStyle { RangeSql rangeSql = null; public HBaseStyle() { super(); rangeSql = new HbaseRange(this); } @Override public int getIdType(Class c,String idProperty) { return DBType.ID_ASSIGN; } @Override public boolean isNoSql(){ return true; } @Override public String getName() { return "hbase"; } @Override public int getDBType() { return DBType.DB_HBASE; } @Override public RangeSql getRangeSql() { return rangeSql; } @Override protected SQLSource generalInsert(Class<?> cls,boolean template){ SQLSource sqlSource = super.generalInsert(cls,template); String upsert = sqlSource.template.replaceFirst("insert","UPSERT"); sqlSource.template = upsert; return sqlSource; } @Override public SQLSource genUpdateById(Class<?> cls) { return this.generalInsert(cls,false); } } ``` * getIdType 跟clickhouse一样,没有自增和序列主键,因此设定为ID_ASSIGN * rangeSql,返回一个HbaseRange实例,Hbase翻页跟MySql类似但略有不同 * generalInsert,此方法是根据实体生成内置insert语句,因为hbase使用upsert,而不是insert,因此修改了AbtractStyle.generalInsert返还默认的SQL * genUpdateById,同样根据id修改对象,也采用UPSERT方式 ### DruidStyle例子 druid是查询引擎,不支SQL预编译,也不支持数据更改操作,也不支持翻页 ```java @Override public boolean preparedStatementSupport() { return false; } public RangeSql getRangeSql(){ throw new UnsupportedOperationException("druid 不支持offset"); } @Override public SQLExecutor buildExecutor(ExecuteContext executeContext){ return new QuerySQLExecutor(executeContext); } ``` druid的翻页因此在BeetlSQL中不支持 ## MetadataManager 此类定义了数据库的Metadata,类似JDBC的DatabaseMetaData。但考虑到有些数据库可能没有metadata,比如文件系统,因此 MetadataManager有如下子类 * SchemaMetadataManager: 大部分数据库,大数据使用,这些数据库都有严格的schema * NoSchemaMetaDataManager,无schema,如drill使用文件系统,这时候需要调用addBean方法通过POJO定义反向得到一个模拟的Schema * SchemaLessMetaDataManager,综合上面俩种情况 ```java public interface MetadataManager { boolean existTable(String tableName); TableDesc getTable(String name); Set<String> allTable(); public void addTableVirtuals(String realTable,String virtual); } ``` * existTable 用于检测表是否存在 * getTable,返回TableDesc ,表的详细描述,如主键,列,备注等 * allTable 返回所有表名 * addTableVirtuals, 建立一个真实不要和虚拟表的映射,因此当beetlsql 通过getTable,传入虚拟表的时候,实际得到的是真实表的TableDesc,比如在分表场景下,有user_001,user_002,但表定义都是user表 对于NoSchemaMetaDataManager,还有如下方法 * addBean 传入一个POJO,通过POJO的定义可以反向得到表定义 比如TD-Engine的JDBC目前不支持,因此DbStyle定义如下 ```java @Override public MetadataManager initMetadataManager(ConnectionSource cs){ metadataManager = new NoSchemaMetaDataManager(); return metadataManager; } ``` 然后在代码里手工添加定义 ```java NoSchemaMetaDataManager metaDataManager = (NoSchemaMetaDataManager)sqlManager.getMetaDataManager(); metaDataManager.addBean(Data.class); //Data是一个POJO,描述了个表t,有字段ts和a @Table(name="t") @lombok.Data public class Data { @Column("ts") Timestamp ts; @Column("a") Integer a; } ``` ## BeanProcessor BeanProcessor是非常底层一个类,紧密跟JDBC 规范打交道,因此许多个性化扩展都可以通过实现BeanProcessor的某些方法来完成,比如,在前面例子中展示的让Clickhouse的结果集能映射java.util.Date上,这是最常用的情况,BeanProcessor已经内置如下类型转化,你的数据库可以重新实现或者新增类型转化 ```java static BigDecimalTypeHandler bigDecimalHandler = new BigDecimalTypeHandler(); static BooleanTypeHandler booleanDecimalHandler = new BooleanTypeHandler(); static ByteArrayTypeHandler byteArrayTypeHandler = new ByteArrayTypeHandler(); static ByteTypeHandler byteTypeHandler = new ByteTypeHandler(); static CharArrayTypeHandler charArrayTypeHandler = new CharArrayTypeHandler(); static DateTypeHandler dateTypeHandler = new DateTypeHandler(); static DoubleTypeHandler doubleTypeHandler = new DoubleTypeHandler(); static FloatTypeHandler floatTypeHandler = new FloatTypeHandler(); static IntegerTypeHandler integerTypeHandler = new IntegerTypeHandler(); static LongTypeHandler longTypeHandler = new LongTypeHandler(); static ShortTypeHandler shortTypeHandler = new ShortTypeHandler(); static SqlDateTypeHandler sqlDateTypeHandler = new SqlDateTypeHandler(); static SqlXMLTypeHandler sqlXMLTypeHandler = new SqlXMLTypeHandler(); static StringTypeHandler stringTypeHandler = new StringTypeHandler(); static TimestampTypeHandler timestampTypeHandler = new TimestampTypeHandler(); static TimeTypeHandler timeTypeHandler = new TimeTypeHandler(); static CLobJavaSqlTypeHandler clobTypeHandler = new CLobJavaSqlTypeHandler(); static BlobJavaSqlTypeHandler blobTypeHandler = new BlobJavaSqlTypeHandler(); static LocalDateTimeTypeHandler localDateTimeHandler = new LocalDateTimeTypeHandler(); static LocalDateTypeHandler localDateHandler = new LocalDateTypeHandler(); ``` 如果考虑到某个类的所有子类都采用指定的Handler,那需要调用addAcceptType方法,指明,比如JsonNode类都使用JsonNodeTypeHandler ```java JsonNodeTypeHandler typeHandler = new JsonNodeTypeHandler(); sqlManager.getDefaultBeanProcessors().addAcceptType( new BeanProcessor.InheritedAcceptType( JsonNode.class,typeHandler)); ``` 另外一个扩展方法可能是setPreparedStatementPara,这是给PreparedStatement赋值,如果有需要特殊处理逻辑,也可以扩展此处。 还有一个很少用的扩展地方是getColName方法,他是根据ResultSet结果集,返回结果集的列名称,在Hive中,就重新实现了此方法,因为Hive会把SQL的子查询的前缀也传递到Java侧,比如 ``` select * from (select id from user) t ``` 在JDBC返回结果中,列名是t.id,而不是id,这样会导致无法映射,因此有些情况,需要排除这个前缀