一、概述

DAS是信也科技自研的数据库访问框架。DAS研发的目的是为了解决当时日益严重的数据库应用开发效率低下,数据库配置管理混乱和数据库难以水平扩展等问题。针对这些问题,DAS提供整合了数据库配置管理portal,ORM框架和分库分表引擎的一体化解决方案。一个DAS就可以满足开发者所有的需求,无需花费大量的时间精力去整合各种框架和组件。在落地过程中,DAS已经证明其能大幅提高研发效率,减低维护成本和避免生产事故。DAS已经开源,最新版本为2.4.0。

在DAS的研发初期,为了实现快速交付,我们基于携程数据库访问框架DAL做了深度的定制化改造。在不断的演化和重构中,DAL原有的代码被大量替换掉,目前除了最底层的部分代码外,DAS已经是一个全新的产品。

DAS与DAL的定位基本相同,站在使用者的角度看,DAS对DAL的改进主要体现在以下几个方面:

  1. 增强的分库分表策略
  2. 简洁高效的DAO设计
  3. 具备元数据的Entity
  4. 灵活方便的SqlBuilder

二、分库分表策略改进

分库分表策略是支持数据库分片的数据库访问框架的核心。其作用是判断用户给出的SQL语句要在那些数据库或表分片上执行。判断SQL对应的分片范围很有技术挑战。完美的解决方案应该是:

  1. 解析SQL,确定所有的表达式,表达式包括但不限于以下>, >=, <, <=, <>, between,not between, in, not in (...), like, not like, is null, is not null,等等
  2. 计算每个表达式对应的分片范围
  3. 根据一定的规则合并各自的分片范围来生成最终的集合。

分库分表策略定义是否全面合理,决定了数据库访问框架的能力上限。

1、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才能准确的判断分片范围。并且每次调用策略算法仅能确定最多一个分片。
该策略可以支持如下所示包含相等判断的语句:

SELECT * FROM PERSON WHERE AGE = 18

由于IN可以看做是一系列相等操作,因此通过在hints中指定IN参数,也可以变通的支持IN,所以下面的语句也支持:

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

但是用户的SQL语句不仅仅只是相等或者IN判断,所以这种策略定义在实际使用中有较大限制。

2、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

三、DAO改进

DAO是研发人员开发数据库应用的打交道最多的编程接口。用户对数据库所有的增删改查操作都要通过DAO完成,因此DAO设计的好坏直接影响了用户的使用体验。

1、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,但他还是骂骂咧咧不满意。

2、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

四、Entity改进

Entity是数据库中的表或数据库查询结果的Java对应物,一般称为实体。其中表实体一般直接用于数据库的增删改查操作,查询实体仅用于表示查询结果。这两种实体一般通过console生成。实体的主要结构是字段属性,表类型的实体还会包含表名信息。

1、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;
    }

这种结构的实体完成级别的基于对象实例的增删改查没问题。但除此之外没有其他用途。

2、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的方式很自然和紧凑。

五、SqlBuilder改进

除了直接基于表实体对象实例的增删改查操作外,还有很多基于复杂SQL语句的需求场景。需要框架来提供创建动态SQL的功能。这个功能好不好用,也是区分框架设好坏的一个重要的衡量标准。

1、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)

手工操作太多非常容易出错,而且在编译阶段无法识别,出问题后要花很多时间逐行对比语句。感觉过于酸爽。

2、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

六、总结

本文通过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。对应用开发效率提升和分布式数据库访问机制有多年的研究积累


信也科技布道师
12 声望10 粉丝