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,這樣會導致無法映射,因此有些情況,需要排除這個前綴

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