BeetlSQL 是如何支持多库的

 

BeetlSQL目标是代替传统的Hibernate,JPA,MyBatis。

  • 传统数据库:MySQL,MariaDB,Oralce,Postgres,DB2,SQL Server,H2,SQLite,Derby,神通,达梦,华为高斯,人大金仓,PolarDB,万里开源GreatSQL,南大通用GBase8s等
  • 大数据: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,这样会导致无法映射,因此有些情况,需要排除这个前缀

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章