概述
拍拍贷 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<String> locateDbShards(ShardingContext ctx);
Set<String> locateTableShards(TableShardingContext ctx);
}
复制代码
其中 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 的策略适用于更普遍的场景,对用户的限制更少,用法更灵活,更符合用户习惯。具体设计可以参考:
https://github.com/ppdaicorp/das/wiki/DasClient-%E5%88%86%E5%BA%93%E5%88%86%E8%A1%A8%E7%AD%96%E7%95%A5%E8%AE%BE%E8%AE%A1
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 的方式要自然和简洁得多。
例如:
PersonDefinition p = Person.PERSON;
p = p.inShard("0");
builder = SqlBuilder.selectAllFrom(p).where(p.Name.eq(name)).into(Person.class);
Person pk = dao.queryObject(builder);
复制代码
可以看到使用 DAS 的 entity 可以方便的获取表名,列名,创建表达式,指定表分片。
表达式方法除了全称,还有简写。例如 eq 和 equal 是等价的方法。下面是一个包含所有表达式全称与简写的例子:
query(selectAllFrom(p).where(p.PeopleID.eq(1)), i, 1);
query(selectAllFrom(p).where(p.PeopleID.equal(1)), i, 1);
query(selectAllFrom(p).where(p.PeopleID.neq(1)), i, 3);
query(selectAllFrom(p).where(p.PeopleID.notEqual(1)), i, 3);
query(selectAllFrom(p).where(p.PeopleID.greaterThan(1)), i, 3);
query(selectAllFrom(p).where(p.PeopleID.gteq(1)), i, 4);
query(selectAllFrom(p).where(p.PeopleID.greaterThanOrEqual(1)), i, 4);
query(selectAllFrom(p).where(p.PeopleID.lessThan(3)), i, 2);
query(selectAllFrom(p).where(p.PeopleID.lt(3)), i, 2);
query(selectAllFrom(p).where(p.PeopleID.lessThanOrEqual(3)), i, 3);
query(selectAllFrom(p).where(p.PeopleID.lteq(3)), i, 3);
query(selectAllFrom(p).where(p.PeopleID.between(1, 3)), i, 3);
query(selectAllFrom(p).where(p.PeopleID.notBetween(2, 3)), i, 2);
query(selectAllFrom(p).where(p.PeopleID.notBetween(2, 4)), i, 1);
query(selectAllFrom(p).where(p.PeopleID.in(pks)), i, 3);
query(selectAllFrom(p).where(p.PeopleID.notIn(pks)), i, 1);
query(selectAllFrom(p).where(p.Name.like("Te%")), i, 4);
query(selectAllFrom(p).where(p.Name.notLike("%s")), i, 4);
query(selectAllFrom(p).where(p.Name.isNull()), i, 0);
query(selectAllFrom(p).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(p).where(p.PeopleID.eq(j+1)).into(Person.class);
Person pk = dao.queryObject(builder);
builder = selectAllFrom(p).where(p.PeopleID.eq(j+1)).into(Person.class).withLock();
Person pk = dao.queryObject(builder);
SqlBuilder builder = select(p.Name).from(p).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(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));
复制代码
总结
本文主要介绍策略,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。对应用开发效率提升和分布式数据库访问机制拥有有多年研究积累。
评论