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