概述
拍拍貸DAS是拍拍貸自研的數據庫訪問框架,支持數據庫管理,ORM,並內置了分庫分表引擎。爲了快速交付能力,DAS在研發初期並沒有從頭開發新產品,而是以攜程DAL框架爲基礎做深入的定製化改造。DAS在不斷的演化升級中逐漸重構和替換掉攜程DAL原有代碼,目前除了客戶端最底層的部分代碼外,DAS已經是一個全新的產品。
DAS與DAL的定位基本相同,站在使用者的角度看,DAS對DAL的改進主要體現在以下幾個方面:
- 增強的分庫分表策略
- 簡潔高效的DAO設計
- 具備元數據的Entity
- 靈活方便的SqlBuilder
由於DAL也是我參與開發的項目,因此這個對比也是一篇自我回顧,自我總結的文章。頗有些我“殺”了我的感覺。
分庫分表策略改進
分庫分表策略定義
分庫分表策略是支持數據庫分片的數據庫訪問框架的核心。其作用是判斷用戶給出的SQL語句要在那些數據庫或表分片上執行。判斷SQL對應的分片範圍很有技術挑戰。完美的解決方案應該是:
1. 解析SQL,確定所有的表達式,表達式包括但不限於以下>, >=, <, <=, <>, between,not between, in, not in (…), like, not like, is null, is not null,等等。
2. 計算每個表達式對應的分片範圍。
3. 根據一定的規則合併各自的分片範圍來生成最終的集合。
分庫分表策略定義是否全面合理,直接決定了數據庫訪問框架的能力上限。接下來我們來對比DAL和DAS各自的策略定義。
攜程DAL的策略接口核心定義如下:
public interface DalShardingStrategy {
…
String locateDbShard(DalConfigure configure, String logicDbName, DalHints hints);
String locateTableShard(DalConfigure configure, String logicDbName, String tabelName, DalHints hints);
}
其中hints參數會傳遞表達式中參數的集合,但不會傳遞參數對應的表達式的操作符(=,>,<之類)具體是什麼;同時接口的返回值定義爲String,因此而返回值僅能指定最多一個分片。
這種策略定義導致只有包含相等表達式或者賦值類操作的SQL才能準確的判斷分片範圍。
該策略可以支持的語句如下:
SELECTE * FROM PERSON WHERE AGE = 18
當然由於IN可以看做是一系列相等操作,因此經過變通也可以支持IN,所以下面的語句也支持:
SELECTE * FROM PERSON WHERE AGE IN (18,19,20)
但是用戶的SQL語句不僅僅只是相等或者IN判斷,所以這種策略定義在實際使用中有較大限制。
接下來我們看一下DAS策略接口的核心定義:
public interface ShardingStrategy {
…
Set
Set
}
其中ShardingContext參數中包含了ConditionList屬性。該屬性定義了表達式集合,以及表達式之間的關係(AND,OR,NOT)。同時策略的返回值允許是分片集合,而不是某個特定分片。
這種策略定義可以支持幾乎所有的表達式。可以處理表達式間的與或非關係,以及括號和嵌套括號。例如:
SELECTE * FROM PERSON WHERE (AGE > 18 OR AGE <20) AND (AGE IN (18,19,20) OR AGE BETWEEN [0,100])
通過對比我們可以瞭解DAS的策略適用於更普遍的場景,對用戶的限制更少,用法更靈活,更符合用戶習慣。具體設計可以參考:
DAO改進
DAO是用戶使用數據庫訪問框架的主要途徑,通過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。關係如下所示:
即使要完成最簡單的數據庫操作,用戶也需要先生成DAO。同時在某些特殊場景下還需要調用預定義的DAO,步驟繁瑣,學習成本高。我印象中,用戶多有吐槽。
DAS對DAO做了大幅優化。將DalTableDao, DalQueryDao,DalClient的功能合併在DasClient一個類並暴露給用戶直接使用。要做數據庫操作時,用戶可以直接使用DasClient,再也無需先生成任何DAO代碼:
除了簡化DAO類設計,DAS還做了以下優化:
1. 簡化API設計,降低學習成本。例如DAL中的DalTableDao和DalQueryDao一共有34個query方法,DasClient裏完成全部功能只用了7個。
2. 簡化Hints的用法,去掉了DAL中不常用的hints,例如continueOnError、asyncExecution等等,以在功能的靈活性,可理解性和系統複雜度方面取得平衡。
3. 增強DAS功能。例如重新設計了SqlBuilder類和表實體,可以讓用戶類似寫原生SQL的方式自定義SQL語句。下面的章節裏會專門介紹
DAS在DAO設計上相比DAL有很顯著的改進。與DAL相比,DAS的類層次更簡潔,API設計更合理,顯著降低了用戶上手門檻,用起來很順手。
在DAL的落地中我們原來收到的反饋是用戶強烈希望DAO不要綁死在某張表上面,因此我們將DAL的DAO簡化爲DAS的形式。但在DAS落地過程中,卻有用戶反饋希望提供針對單表的DAO以方便繼承,還提出希望爲記錄邏輯刪除操作提供便利。於是我們又增加了TableDao對DasClient做了簡單的封裝,參數化了實體類型來滿足用戶自定義需求。並基於TableDao提供了LogicDeletionDao來支持邏輯刪除操作。一頓操作猛如虎之後發現貌似又回到了開頭,真是萬萬沒想到啊。
Entity改進
Entity是數據庫中的表或數據庫查詢結果的Java對應物,一般稱爲實體。其中表實體可以直接用於數據庫的CRUD操作,查詢實體僅用於表示查詢結果。這兩種實體一般通過console生成。實體的主要結構是字段屬性,表類型的實體還會包含表名信息。
DAL表實體
DAL的entity裏僅包含可賦值的了表字段,通過註解標明瞭對應的表字段結構。
@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表實體的定義。在普通的屬性字段定義外,還新增了表結構元數據定義。下面的例子中,Person實體代碼裏面的PersonDefinition定義表結構的元數據,其包含的PeopleID等定義表字段元數據。
@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;
}
這些元數據提供了可以生成非常豐富和全面的表達式的API。基於這些API,用戶可以按照符合SQL語法的方式通過Sqlbuilder構建動態SQL。比DAL構建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);
可以看到使用DAS的entity可以方便的獲取表名,列名,創建表達式,指定表分片。
表達式方法除了全稱,還有簡寫。例如eq和equal是等價的方法。下面是一個包含所有表達式全稱與簡寫的例子:
query(selectAllFrom§.where(p.PeopleID.eq(1)), i, 1);
query(selectAllFrom§.where(p.PeopleID.equal(1)), i, 1);
query(selectAllFrom§.where(p.PeopleID.neq(1)), i, 3);
query(selectAllFrom§.where(p.PeopleID.notEqual(1)), i, 3);
query(selectAllFrom§.where(p.PeopleID.greaterThan(1)), i, 3);
query(selectAllFrom§.where(p.PeopleID.gteq(1)), i, 4);
query(selectAllFrom§.where(p.PeopleID.greaterThanOrEqual(1)), i, 4);
query(selectAllFrom§.where(p.PeopleID.lessThan(3)), i, 2);
query(selectAllFrom§.where(p.PeopleID.lt(3)), i, 2);
query(selectAllFrom§.where(p.PeopleID.lessThanOrEqual(3)), i, 3);
query(selectAllFrom§.where(p.PeopleID.lteq(3)), i, 3);
query(selectAllFrom§.where(p.PeopleID.between(1, 3)), i, 3);
query(selectAllFrom§.where(p.PeopleID.notBetween(2, 3)), i, 2);
query(selectAllFrom§.where(p.PeopleID.notBetween(2, 4)), i, 1);
query(selectAllFrom§.where(p.PeopleID.in(pks)), i, 3);
query(selectAllFrom§.where(p.PeopleID.notIn(pks)), i, 1);
query(selectAllFrom§.where(p.Name.like(“Te%”)), i, 4);
query(selectAllFrom§.where(p.Name.notLike("%s")), i, 4);
query(selectAllFrom§.where(p.Name.isNull()), i, 0);
query(selectAllFrom§.where(p.Name.isNotNull()), i, 4);
SqlBuilder改進
DAL的SqlBuilder比較複雜,分爲單表,多表和批處理三大類,共7種:
可以DAL裏面Builder類劃分過細。
在DAS中,上面所有的builder除了MultipleSqlBuilder外,在DAS裏都用一個SqlBuilder取代了。
同時爲了簡化和規範操作,DAS增加了專門用於批量查詢,更新的BatchQueryBuilder,BatchUpdateBuilder以及專門用於存儲過程調用的CallBuilder和BatchCallBuilder。如下所示:
此外,SqlBuilder增加了生成語句的靜態方法,可以讓用戶以符合SQL語法的方式寫創建動態SQL。
示例如下:
import static com.ppdai.das.client.SqlBuilder.*;
//查詢
SqlBuilder builder = selectAllFrom§.where(p.PeopleID.eq(j+1)).into(Person.class);
Person pk = dao.queryObject(builder);
builder = selectAllFrom§.where(p.PeopleID.eq(j+1)).into(Person.class).withLock();
Person pk = dao.queryObject(builder);
SqlBuilder builder = select(p.Name).from§.where().allOf(p.PeopleID.eq(k+1), p.Name.eq(“test”)).into(String.class);
String name = dao.queryObject(builder);
SqlBuilder builder = select(p.PeopleID, p.CountryID, p.CityID).from§.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§.where(p.PeopleID.eq(k+1));
assertEquals(1, dao.update(builder));
可以看到DAS重新設計了SqlBuilder,相對DAL既有簡化,又有增強。
總結
本文主要介紹策略,DAO,entity和SqlBuilder的對比。
今天寫這個DAS和DAL的對比讓我非常感慨。我在2013年到2018年作爲產品負責人和Java客戶端主力開發,與團隊一起打造了攜程DAL。 DAL目前還在繼續完善並作爲主力框架產品支撐着攜程每天億萬的數據庫請求。我爲我的團隊和產品感到萬分自豪。
但在DAL的研發中,也有很多遺憾。因爲是第一次開發如此複雜,使用量如此大的產品,同時由於經驗不足,我們有些使用場景假設是錯誤的,一些設計也存在考慮不周的情況。在使用中,我們不斷收到用戶的反饋。雖然盡心盡力的改進着,但由於框架產品的特殊性,一旦發佈就會被所有上游代碼所依賴,我們很難調整API來實現所有改進,有時候權衡再三,最終還是不得不放棄了一些想法。
這些遺憾在打造信業框架DAS框架的時候得到了彌補。幾乎是奇蹟般的,我有一個全新的能將所有好的想法,用戶的反饋和積累的全部經驗付諸實施的機會。爲了做出完美的設計,易用的功能,節省用戶每一步操作,我們開發團隊付出了巨大的努力。DAS凝結了我們所有的心血,在公司內部獲得普遍認可和好評。現在公司將其貢獻給開源社區回報社會。願大家用起來順手之餘,心滿意足的star我們的產品:
DAS除了客戶端外,還包括DAS Console和DAS Proxy Server。其中DAS Console的功能是管理數據庫配置和生成Entity類。DAS Proxy Server可以和DAS Client配合使用,透明的支持本地直連和基於代理的數據庫連接模式,允許用戶在數據庫不斷增長的情況下平滑升級整體架構。關於這些的介紹請持續關注信也科技的拍碼場技術公衆號。
作者介紹
赫傑輝,信也科技基礎組件部門主管、信也DAS產品負責人、佈道師。圖形化構建工具集x-series的作者。曾主持開發攜程開源數據庫訪問框架DAL。對應用開發效率提升和分佈式數據庫訪問機制擁有有多年研究積累。