精益求精-信也科技DAS與攜程DAL對比

1、概述

DAS是信也科技自研的數據庫訪問框架。DAS研發的目的是爲了解決當時日益嚴重的數據庫應用開發效率低下,數據庫配置管理混亂和數據庫難以水平擴展等問題。針對這些問題,DAS提供整合了數據庫配置管理portal,ORM框架和分庫分表引擎的一體化解決方案。一個DAS就能夠知足開發者全部的需求,無需花費大量的時間精力去整合各類框架和組件。在落地過程當中,DAS已經證實其能大幅提升研發效率,減低維護成本和避免生產事故。DAS已經開源,最新版本爲2.4.0。java

在DAS的研發初期,爲了實現快速交付,咱們基於攜程數據庫訪問框架DAL作了深度的定製化改造。在不斷的演化和重構中,DAL原有的代碼被大量替換掉,目前除了最底層的部分代碼外,DAS已是一個全新的產品。git

DAS與DAL的定位基本相同,站在使用者的角度看,DAS對DAL的改進主要體如今如下幾個方面:github

  1. 加強的分庫分表策略
  2. 簡潔高效的DAO設計
  3. 具有元數據的Entity
  4. 靈活方便的SqlBuilder

2、分庫分表策略改進

分庫分表策略是支持數據庫分片的數據庫訪問框架的核心。其做用是判斷用戶給出的SQL語句要在那些數據庫或表分片上執行。判斷SQL對應的分片範圍頗有技術挑戰。完美的解決方案應該是:算法

  1. 解析SQL,肯定全部的表達式,表達式包括但不限於如下>, >=, <, <=, <>, between,not between, in, not in (...), like, not like, is null, is not null,等等
  2. 計算每一個表達式對應的分片範圍
  3. 根據必定的規則合併各自的分片範圍來生成最終的集合。

分庫分表策略定義是否全面合理,決定了數據庫訪問框架的能力上限。sql

一、DAL策略設計

攜程DAL的策略接口核心定義以下:數據庫

public interface DalShardingStrategy {
    String locateDbShard(DalConfigure configure, String logicDbName, DalHints hints);

    String locateTableShard(DalConfigure configure, String logicDbName, String tabelName, DalHints hints);    
}

其中hints用於傳遞SQL中全部參數的集合,但不會傳遞參數對應的表達式的操做符(=,>,<之類)具體是什麼;同時接口的返回值定義爲單個String值編程

這種策略定義致使只有包含相等表達式或者賦值類操做的SQL才能準確的判斷分片範圍。而且每次調用策略算法僅能肯定最多一個分片。
該策略能夠支持以下所示包含相等判斷的語句:segmentfault

SELECT * FROM PERSON WHERE AGE = 18

因爲IN能夠看作是一系列相等操做,所以經過在hints中指定IN參數,也能夠變通的支持IN,因此下面的語句也支持:架構

SELECTE * FROM PERSON WHERE AGE IN (18,19,20)

可是用戶的SQL語句不只僅只是相等或者IN判斷,因此這種策略定義在實際使用中有較大限制。框架

二、DAS策略設計

接下來咱們看一下DAS策略接口的核心定義:

public interface ShardingStrategy {

    Set<String> locateDbShards(ShardingContext ctx);
 
    Set<String> locateTableShards(TableShardingContext ctx);    
}

其中ShardingContext參數中包含了ConditionList屬性。該屬性經過樹狀結構完整定義了語句中全部表達式的類型,數值以及表達式之間的關係(AND,OR,NOT)。同時策略的返回值容許是分片集合,而不是某個特定分片。

所以這種策略定義能夠支持幾乎全部的表達式,例如:

SELECT * FROM PERSON WHERE (AGE > 18 OR AGE <20) AND (AGE IN (18,19,20) OR AGE BETWEEN [0,100])

經過對比咱們能夠了解DAS的策略適用於更廣泛的場景,對用戶的限制更少,用法更靈活,更符合用戶習慣。

DAS策略的總體設計很是巧妙,花費了不少心思。對於但願提升本身設計能力的同窗來講也是個很好的參考。

具體設計在這裏:https://github.com/ppdaicorp/das/wiki

3、DAO改進

DAO是研發人員開發數據庫應用的打交道最多的編程接口。用戶對數據庫全部的增刪改查操做都要經過DAO完成,所以DAO設計的好壞直接影響了用戶的使用體驗。

一、DAL DAO設計

基於不要讓用戶寫本身寫哪怕一行DAO代碼的原則(錯誤假設),DAL有着較複雜的DAO類層次結構。要使用DAO,用戶須要先經過DAL console生成標準,構建和自定義DAO的代碼:

  1. 標準DAO包含了最經常使用的單表操做,與特定表相關聯。
  2. 構建DAO包含針對單表的自定義的操做,生成的時候會跟對應的同一表的標準DAO的代碼合併
  3. 自定義DAO包裝用戶提供的自定義SQL,用於跨表查詢,複雜語句或者數據庫特有語法的SQL

標準DAO和構建DAO基於基礎DAO類DalTableDao。自定義DAO基於基礎DAO類DalQueryDao。若是涉及到事務操做,須要調用底層接口DalClient。關係以下所示:

image.png
根據以前提到的原則,即便要完成最簡單的數據庫操做,用戶也須要先生成DAO。同時在某些特殊場景下還須要調用預約義的DAO。步驟委實有些繁瑣,我印象中,用戶多有吐槽。由於負責全團隊的DAO開發工做,有個用戶還曾經強烈要求咱們的DAO支持任意表,不然他要爲每張表都生成代碼,而這意味着開發幾百個DAO。咱們當時指導他直接使用DalTableDao,但他仍是罵罵咧咧不滿意。

二、DAS DAO設計

DAS對DAO作了大幅優化。將DalTableDao, DalQueryDao,DalClient的功能合併在DasClient一個類並暴露給用戶直接使用。項目添加DAS依賴後,用戶能夠直接使用DasClient作數據庫操做,再也無需先生成任何DAO代碼:
image.png
除了簡化DAO類設計,DAS還作了如下優化:

  1. 簡化API設計,下降學習成本。例如DAL中的DalTableDao和DalQueryDao一共有34個query方法,DasClient裏完成所有功能只用了7個
  2. 簡化Hints的用法,以在功能的靈活性,可理解性和系統複雜度方面取得平衡。基於經驗咱們去掉了DAL中不經常使用的hints,例如continueOnError,,asyncExecution等
  3. 加強DAS功能。例如從新設計了SqlBuilder類和表實體,可讓用戶相似寫原生SQL的方式建立動態SQL語句。下面的章節裏會專門介紹

在DAO設計上咱們下了不少功夫,作了不少的改進。與DAL相比,DAS的類層次更簡潔,API設計更合理,顯著下降了用戶上手門檻,用起來很順手。

還記得在在攜程咱們收到的用戶強烈但願DAO不要綁死在某張表上面的反饋嗎?咱們經過DAS DAO實現了這個想法。但在DAS落地過程當中,咱們卻收到用戶反饋說但願提供針對單表的DAO以方便繼承,同時還提出但願爲記錄邏輯刪除操做提供便利。因而咱們又增長了TableDao對DasClient作了簡單的封裝,將類型參數化從方法級別提高到類層次來知足用戶的自定義需求。並基於TableDao提供了LogicDeletionDao來支持邏輯刪除操做。

萬萬沒想到啊,一頓操做猛如虎以後發現貌似又回到了最初。真是用戶虐我千百遍,我待用戶如初戀
image.png

4、Entity改進

Entity是數據庫中的表或數據庫查詢結果的Java對應物,通常稱爲實體。其中表實體通常直接用於數據庫的增刪改查操做,查詢實體僅用於表示查詢結果。這兩種實體通常經過console生成。實體的主要結構是字段屬性,表類型的實體還會包含表名信息。

一、DAL表實體設計

DAL的表實體裏僅包含可賦值的了表字段,經過註解標明瞭對應的表字段結構。

@Entity(name="dal_client_test")
public class ClientTestModelJpa {
    @Id
    @Column(name="id")
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Type(value=Types.INTEGER)
    private Integer id;
    
    @Column(name="quantity")
    @Type(value=Types.INTEGER)
    private Integer quan;
...
    public Integer getId() {
        return id;
    }
 
    public void setId(Integer id) {
        this.id = id;
    }
 
    public Integer getQuantity() {
        return quan;
    }
 
    public void setQuantity(Integer quantity) {
        this.quan = quantity;
    }

這種結構的實體完成級別的基於對象實例的增刪改查沒問題。但除此以外沒有其餘用途。

二、DAS表實體設計

DAS擴充了DAL表實體的定義。在普通的屬性字段定義外,還新增了表結構元數據定義。下面的例子中,內部靜態類PersonDefinition定義person表結構的元數據,包括:

  1. 表名信息
  2. 字段元數據
  3. 分表操做
@Table
public class Person {
    public static final PersonDefinition PERSON = new PersonDefinition();
    
    public static class PersonDefinition extends TableDefinition {
        public final ColumnDefinition PeopleID;
        public final ColumnDefinition Name;
...
        public PersonDefinition as(String alias) {return _as(alias);}
        public PersonDefinition inShard(String shardId) {return _inShard(shardId);}
        public PersonDefinition shardBy(String shardValue) {return _shardBy(shardValue);}
 
        public PersonDefinition() {
            super("person");
            setColumnDefinitions(
                    PeopleID = column("PeopleID", JDBCType.INTEGER),
                    Name = column("Name", JDBCType.VARCHAR),
...
                    );
        }        
    }
    @Id
    @Column(name="PeopleID")
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Integer peopleID;
    
    @Column(name="Name")
    private String name;
....
    public Integer getPeopleID() {
        return peopleID;
    }
    public void setPeopleID(Integer peopleID) {
        this.peopleID = peopleID;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }

經過DAS的表實體元數據能夠方便的獲取表名,列名,指定表分片。而且基於這些元數據還能夠生成很是豐富和全面的表達式。與Sqlbuilder配合使用能夠很是方便直觀的構建動態SQL。例如:

import static com.ppdai.das.client.SqlBuilder.selectAllFrom;
private PersonDefinition p = Person.PERSON;
p = p.inShard("0");
builder = selectAllFrom(p).where(p.Name.eq(name)).into(Person.class);
Person pk = dao.queryObject(builder);

表達式方法除了全稱,還有簡寫。例如eq和equal是等價的方法。下面是一個全稱與簡寫的對比例子:

selectAllFrom(p).where(p.PeopleID.eq(1));
selectAllFrom(p).where(p.PeopleID.equal(1));
selectAllFrom(p).where(p.PeopleID.neq(1));
selectAllFrom(p).where(p.PeopleID.notEqual(1)));
selectAllFrom(p).where(p.PeopleID.greaterThan(1));
selectAllFrom(p).where(p.PeopleID.gteq(1));
selectAllFrom(p).where(p.PeopleID.greaterThanOrEqual(1));
selectAllFrom(p).where(p.PeopleID.lessThan(3));
selectAllFrom(p).where(p.PeopleID.lt(3));
selectAllFrom(p).where(p.PeopleID.lessThanOrEqual(3));
selectAllFrom(p).where(p.PeopleID.lteq(3));
selectAllFrom(p).where(p.PeopleID.between(1, 3));
selectAllFrom(p).where(p.PeopleID.notBetween(2, 3));
selectAllFrom(p).where(p.PeopleID.notBetween(2, 4));
selectAllFrom(p).where(p.PeopleID.in(pks));
selectAllFrom(p).where(p.PeopleID.notIn(pks));
selectAllFrom(p).where(p.Name.like("Te%"));
selectAllFrom(p).where(p.Name.notLike("%s"));
selectAllFrom(p).where(p.Name.isNull());
selectAllFrom(p).where(p.Name.isNotNull());

能夠看到這種構建SQL的方式很天然和緊湊。

5、SqlBuilder改進

除了直接基於表實體對象實例的增刪改查操做外,還有不少基於複雜SQL語句的需求場景。須要框架來提供建立動態SQL的功能。這個功能好很差用,也是區分框架設好壞的一個重要的衡量標準。

一、DAL SQL Builder設計

DAL的SqlBuilder比較複雜,分爲單表,多表和批處理三大類,共7種,與前面提到的各類DAO相對應:
image.png
直觀的感受是DAL裏面Builder類劃分過細了,一些常見的操做也要一個特定的builder來實現。下面是單表查詢builder的例子:

List<String> in = new ArrayList<String>();
    in.add("12");
    in.add("12");

    SelectSqlBuilder builder = new SelectSqlBuilder("People", DatabaseCategory.MySql, false);
     
    builder.select("PeopleID","Name","CityID");
     
    builder.equal("PeopleID", "1", Types.INTEGER);
    builder.and().in("Name", in, Types.INTEGER);
    builder.and().between("CityID", "wuhan", "shanghai", Types.INTEGER);
    builder.orderBy("PeopleID", false);

這裏的問題主要有如下幾個:

  1. 構建builder時需手工指定表名以及數據庫類型
  2. 建立表達式是須要手工指定列名,參數值以及參數類型
  3. 表達式調用的寫法與實際SQL語法相反。例如PeopleID = 1,要寫成equal("PeopleID", "1", Types.INTEGER)

手工操做太多很是容易出錯,並且在編譯階段沒法識別,出問題後要花不少時間逐行對比語句。感受過於酸爽。

二、DAS SQL Builder設計

在DAS中,上面全部的builder除了MultipleSqlBuilder外,在DAS裏都用一個SqlBuilder取代了。

在減小builder類數量的同時,爲了簡化和規範操做,DAS增長了專門用於批量查詢,更新的BatchQueryBuilder(對應以前的MultipleSqlBuilder),BatchUpdateBuilder以及專門用於存儲過程調用的CallBuilder和BatchCallBuilder。以下所示
image.png
DAL的4個單表操做SQL builder在DAS SqlBuilder中經過對應的靜態方法加以實現。與上一節提到的表實體一塊兒配合使用可讓用戶以基本符合SQL語法的方式建立動態SQL。示例以下:

import static com.ppdai.das.client.SqlBuilder.*;

//查詢
SqlBuilder builder = select(p.PeopleID, p.CountryID, p.CityID).from(p).where(p.PeopleID.eq(k+1)).into(Person.class);
Person pk = dao.queryObject(builder);
 
//插入
SqlBuilder builder = insertInto(p, p.Name, p.CountryID, p.CityID).values(p.Name.of("Jerry" + k), p.CountryID.of(k+100), p.CityID.of(k+200));
assertEquals(1, dao.update(builder));

//更新
SqlBuilder builder = update(Person.PERSON).set(p.Name.eq("Tom"), p.CountryID.eq(100), p.CityID.eq(200)).where(p.PeopleID.eq(k+1));
assertEquals(1, dao.update(builder));
 
//刪除
SqlBuilder builder = deleteFrom(p).where(p.PeopleID.eq(k+1));
assertEquals(1, dao.update(builder));

與DAL Builder相比,DAS SqlBuilder作到了如下改進:

  1. 能夠直接以SQL操做對應的靜態方法建立builder,無需指定表名,數據庫類型等參數
  2. 能夠直接從表實體對應的列上建立表達式,僅須要提供參數便可,無需指定列名和參數類型
  3. 表達式寫法與SQL語法一致。PeopleID = 1寫成p.PeopleID.eq(1)

DAS還定義了SegmentConstants類,裏面定義了經常使用SQL關鍵字和一些靜態方法,配合SqlBuilder使用,能夠給用戶飛通常的使用感受。

SqlBuilder builder = SqlBuilder.selectAllFrom(p).where(p.CityID.eq(1), OR, p.CountryID.eq(1), AND, p.Name.like("A"), OR, p.PeopleID.eq(1));

真是優秀!
image.png

6、總結

本文經過DAL與DAS在策略,DAO,entity和SqlBuilder等方面的對比,較深刻的剖析了DAS的設計思路和原理。

我曾經是攜程數據庫訪問框架DAL的產品負責人和Java客戶端主力開發。與團隊一塊兒打造了攜程DAL。 DAL目前還在繼續完善並做爲主力框架產品支撐着攜程天天億萬的數據庫請求。我爲個人團隊和產品感到萬分自豪。

在當年DAL的研發過程當中,因爲經驗不足和框架產品的特殊性,咱們很難大幅調整API來實現全部的優化。有時候權衡再三,最終仍是不得不放棄了一些很好的想法。這些遺憾在打造DAS的過程當中獲得了彌補。咱們將全部的好想法和經驗所有應用在了DAS的開發上並最終得到了用戶的承認和好評。所以這個對比也是一篇自我回顧,自我總結的文章。很有些「我殺了我」的感受😊。
image.png
爲了作出完美的設計,易用的功能,節省用戶每一步操做,咱們開發團隊付出了巨大的努力。DAS凝結了咱們全部的心血,在公司內部得到廣泛承認和好評。這麼好的框架你值得擁有。如今DAS已經貢獻給開源社區:

https://github.com/ppdaicorp/das

DAS除了客戶端外,還包括DAS Console和DAS Proxy Server。其中DAS Console的功能是管理數據庫配置和生成Entity類,功能很是強大。DAS Proxy Server能夠和DAS Client配合使用,透明的支持本地直連和基於代理的數據庫鏈接模式,容許用戶在數據庫不斷增加的狀況下平滑升級總體架構。關於這些的介紹請持續關注信也科技的拍碼場技術公衆號。

技術支持:
image.png


做者介紹

Hejiehui,信也科技基礎組件部門主管、信也DAS產品負責人、佈道師。圖形化構建工具集x-series的做者。曾主持開發攜程開源數據庫訪問框架DAL。對應用開發效率提高和分佈式數據庫訪問機制有多年的研究積累

相關文章
相關標籤/搜索