精益求精-信也科技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。对应用开发效率提高和分布式数据库访问机制有多年的研究积累

相关文章
相关标签/搜索