BeetlSQL 多数据库支持
About 3191 wordsAbout 11 min
1984-01-24
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解决不同数据库差异的主要类,这些方法将在本章后面详细简介,现在简单说明如下
@Override
public String getName() {
return "mysql";
}
@Override
public int getDBType() {
return DBType.DB_MYSQL;
}
getName 返回数据库名称,getDBType则返回一个唯一标识,可以返回1000以外。数据库名称可以用于各种特殊处理。数据库sql文件也可以存放在以数据库名称作为目录名下,以实现跨数据库操作。
@Override
public RangeSql getRangeSql() {
return this.rangeSql;
}
返回一个翻页辅助类,这将在后面详细讲解。这也是大部分数据库的差异点
对于NOSQL或者查询引擎来说,还有需要考虑的地方,以Presto为例子
@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
@Override
public MetadataManager initMetadataManager(ConnectionSource cs){
metadataManager = new NoSchemaMetaDataManager();
return metadataManager;
}
NoSchemaMetaDataManager 类提供了addBean方法用于通过POJO提供一个表描述,这样才能保证BeetlSQL的代码能执行。
AbstractStyle 还支持config(SQLManager sqlManager),有机会配置sqlManager
@Override
public void config(SQLManager sqlManager){
}
DBStyle
DBStyle 是提供一致使用方式的关键,抽象类AbstractDBStyle是其子类,实现了大多数方法。不同数据库Style可以继承AbstractDBStyle,覆盖其特定实现,下面会以传统数据库Mysql和大数据库Clickhouse 为例来做说明
MySqlStyle 例子
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,定义如下
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数据库里,序列求值的表达式
@Override
public String getSeqValue(String seqName) {
return "NEXT VALUE FOR "+seqName;
}
ClickHouseStyle例子
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例子
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预编译,也不支持数据更改操作,也不支持翻页
@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,综合上面俩种情况
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定义如下
@Override
public MetadataManager initMetadataManager(ConnectionSource cs){
metadataManager = new NoSchemaMetaDataManager();
return metadataManager;
}
然后在代码里手工添加定义
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已经内置如下类型转化,你的数据库可以重新实现或者新增类型转化
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
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,这样会导致无法映射,因此有些情况,需要排除这个前缀