GTLC全球技术领导力峰会·上海站,首批讲师正式上线! 了解详情
写点什么

信也科技数据库访问中间件 DAS 揭秘

2020 年 6 月 29 日

信也科技数据库访问中间件DAS揭秘

DAS 简介

DAS 是信也科技自研的数据库访问中间件,是集数据库管理,ORM,动态 SQL 构建和分库分表支持的一体化关系型数据库访问解决方案。


看到这里,你一定会说少年啊!已经有了那么多 ORM 框架和分库分表组件,像 Hibernate,Mybatis,mycat,sharding jdbc,还有我们最爱的携程 DAL 可供选择,干嘛还要重复造轮子?


答案很简单,这些工具都不好用!而 DAS 是我们最新发明的高科技轮子。


警告!前方高能!非资深开发人员尽快撤离。请握紧鼠标,抓牢键盘!


DAS 产品定位

你一定奇怪,既然要做数据库中间件,为什么不像其他产品那样,从 JDBC 或者数据库协议层入手,在传统数据库上面做分库分表或重新开发数据库引擎?那样多牛啊!现有程序不用改就可以无缝移植。为什么 DAS 还要提供 ORM 功能呢?


回答这个问题之前,让我们先简单回顾一下流行的数据库编程过程。这有助于理解 DAS 的产品定位。


mybatis,Hibernate 发明的年代还没有什么数据库垂直与水平扩容,分库分表之类的概念,自然也不会从设计上加以考虑。而现在随便一个互联网公司,每天产生的数据都是“天量”。因此一个正经的数据库项目往往会同时用到 ORM 工具和分库分表组件。无论是 ORM 还是分库分表组件,一般都需要繁琐的配置。区别只在于难度级别是受得了还是劝退。


以最流行的 mybatis+任意分库分表组件为例,如果你是一个资深的 CRUD boy,肯定非常熟悉下面的套路,在开始写下图中间最终实际的 DAO 代码之前,你需要先搞定另外四件事情:



当你手忙脚乱搞好这些配置,第一次测试时候,十有八九不会成功,这个时候千万不要气馁,因为更惨的还在后面,当在项目中使用独立的 ORM 和分库分表组件时,你会难过到流泪:


  • 见过在 XML 里面写代码的吗?mybatis 就是这样以 XML 形式存放表结构和动态 SQL 语句,与实际调试用的 Java 代码分离开,请告诉我如何在 XML 里面 debug?虽然 mybatis 的这种设计已经很过时了,但是它基于注解的新设计更挫,你见过上面足足十层奶油,底下却只有一层蛋糕的奶油蛋糕吗?mybatis 的注解看上去就是这种感觉

  • 想增加一个新方法,得先改 XML,再生成 DAO Interface,最后才能使用,虽然仪式感满满,但编程效率奇低。或者你可尝试在注解里面写 SQL,也蛮爽的。

  • 分库分表配置通常都比较复杂,而且基本上没有自动化工具支持,全靠手配。这体验跟蒙眼徒手在一个装满图钉的坛子里捉泥鳅一样刺激。我到现在还清楚的记得硬着头皮看了 3 遍还看不懂某个分库分表组件用户手册时的挫折感。

  • 最好的总在最后,你会发现不同环境的 Datasource 配置往往放在同一个项目中,通过 profile 或其他手段区分,很容易在打包阶段出错,而这个错误只有在部署阶段的时候才能发现,排查时得先下载再解包,极其麻烦。更别提生产数据库密码泄露的安全隐患,而随着库,表的增长,做这些事情的痛苦指数从痛苦向无限痛苦飞速发展。如果你觉得这没什么,那你一定是能享受福报的那一批人。


当最终克服万难代码能工作时,你会发现跟总体的配置和代码量相比,最终的 DAO 代码只占很少一部分。而就这一部分代码里面,真正有用的也只是极小一部分,不信你看:


public static void main (String[] args) throws IOException {    InputStream resourceAsstream = Resources.getResourceAsStream("cc/sq1MupConfig.xml");    SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(resourceAsstream);     SqlSession sqlSession = ssf.openSession();    //mapper 就是 UserMapper 接口的实现类    UserMapper mapper = sqlSession.getMapper(UserMapper.class);    User u = mapper.finduserById(10);    System.out.println(u);}
复制代码


上面这段典型的 mybatis 代码。除了倒数第二行代码,其他的都是什么玩意?只是抢个两分钱的红包,干嘛要“磕这么多响头”?刨去注释和无关代码,这里面真正有用的代码只占 1/5 而已。你不觉得这个比例荒谬到可笑吗?为交付这一点点代码,付出的代价如此之


“大”,是不是觉得哪里不对?



其实要查询,真正关键的信息就是数据库名和查询语句而已。评价一个设计的的好坏只要看实现一个需求在多大程度上只需要提供必要信息。额外步骤越多,设计越失败!作为参考,请思考餐厅用餐和自己买菜做饭的区别。


作为一个老程序员,我已经厌倦了使用破烂工具。人嘛,要对自己好一点,诚实一点。一个人性化的数据库访问框架应该是这个样子:


  • 具备简洁明了的 API,99%的操作都能一步搞定,使用起来如丝般顺滑。

  • 使用面向应用,面向数据对象这种高级抽象,而不是链接,事务这类极易用错的底层概念。

  • 提供基于 Java 的动态 SQL 生成器。在同一个上下文里写 SQL 和写代码,不用切来切去链接,事务之类的统统由框架搞定,不用操心资源的打开,关闭,泄露啥的,没有使用就没有伤害。

  • API 既简单又复杂。既能适应一般情况,又能处理特殊情况。别问,就要。

  • 内置分库分表能力,不用再单独集成第三方组件。分库分表配置必须极简单,连我都能学会,当然最好别学。如果你问一个研发人员这世界上什么最痛苦?他一定会回答你学习新技术最痛苦。如果要再上一个档次,那就是要学那种要花很多功夫学习却会隔很长时间才用一两次的技术,对,说的就是分库分表配置这种。

  • 不要让我手工编辑任何 XML 或配置文件,都已经 2020 年了,在几万块的 MAC 上写 XML,就像参加豪华晚宴却蹲在椅子上吃饭一样,像什么样子。如果实在免不了要配置,那必须提供能配得上我一指禅的最好的编辑器。

  • 只用写 DAO 相关代码,其他一概不管。


于是 2018 年在时任 CTO 的规划下,我们信也科技基础组件团队决定自己动手搞一套符合自己心意的数据库访问中间件,这就是


信也 DAS

DAS 是 Database Access Service 的缩写。DAS 的目标就是给研发人员提供一个一站式的数据库访问框架,让研发人员用最简单直接的方式开发数据库访问代码,实现上面所有“非分”的想法。



为实现这个目标,DAS 提供:


  • 同时具备 ORM 和分库分表能力的客户端 DAS Client ;

  • 基于 WEB 页面的数据库配置管理和代码生成工具 DAS Console ;

  • 可选的基于代理模式的 DAS Server。应用在直连和代理之间切换无需改代码,也不用知道;



但 DAS 的真正的核心优势不是这些组件,我们 build 了一个专业的团队,7*24 小时主动为程序员服务,帮大家搞定从原子到宇宙尺度的任何数据库问题。


在信也科技,研发人员发邮件告诉 DAS 团队各个环境的数据库配置和逻辑数据库信息,DAS 团队通过 DAS Console 配置好并自动同步到公司的配置中心后,用户只要在自己的项目里面引入 DAS Client 的依赖就可以开始直接写代码。对,你没有看错,直接开始写代码,无需任何的本地配置工作。我们把中间件产品的研发从交付组件提升到交付服务的层面。


这,才是我们成功的秘密![撒花]



DAS 核心设计初探

你心中一定冷笑,吹吧你!那让我们从技术角度看看 DAS 的核心 DAS Client 到底长什么样。



DAS Client 的设计遵从分层抽象原则,从上到下分为:


1. DAO 层,一个完整的 ORM 框架。关于编程所有美好的想象都在这里。


2. 分库分表层。抽象数据库操作差异,以统一的方式处理数据的路由与合并。


3. 执行层。操作底层数据库完成实际工作,封装数据源,链接与事务。


DAO 层是程序员使用最频繁的部分,今天会重点介绍这一部分,其他部分会在未来会逐一提供,请关注我们的“拍码场”公众号。


DAS ORM 简介:


DAS ORM 的主要由预定义 DAO 类 DasClient,SQL 创建工具类 SqlBuilder 和特殊操作指令类 Hints 组成。下面一一介绍。


DasClient

DAS ORM 的核心是 DasClient 类,来看看里面提供了啥方法:



DasClient 提供了几乎所有常见的 ORM 操作,开箱即用,不需要用户生成任何 DAO 接口或实现。



别跟我“扯犊子”,上代码!


OK!猜猜看用 DAS 实现一个查询操作需要几行代码?


Person pk = new Person();pk.setName("test");DasClient dao = DasClientFactory.getClient("logicDbName");List<Person> plist = dao.queryBySample(pk);
复制代码


客户端创建到使用,两行代码完事,是不是很简单粗暴?像我说的一样,如果你要完成一个查询,你需要提供就只是数据库名和 SQL,这里 SQL 用 sample data 表示。除此以外,没有多余动作。没有 session,没有事务,也没有 connection。只要写的代码足够少,BUG 就不会追上我。这就是传说中的极简编程风


通过这种预定义 API 的方式能节省多少代码呢?再以一个实际例子对比一下完成同样功能 mybatis 和 DAS 之间代码量:


Mybatis mapping:


<select id="selectByExample" parameterType="com.ppdai.xxxx.StrategyAccountDetailExample" resultMap="BaseResultMap">    select     <if test="distinct">      distinct    </if>    'false' as QUERYID,    <include refid="Base_Column_List" />    from strategyaccountdetail${tableSuffix}     <if test="_parameter != null">        <include refid="Example_Where_Clause" />    </if>    <if test="orderByClause != null">      order by ${orderByClause}    </if></select>
复制代码


DAS 对应代码:


public List<Strategyaccountdetail> selectByExample(Strategyaccountdetail detail) throws SQLException {     return client.queryBySample(detail);}
复制代码


看到区别了吗?在不需要写一行 XML 的情况下,DAS 用一行代码就可以搞定 mybatis 需要十几行,甚至几十行配置才能完成的功能。其实上面显示的还只是完成这个功能完整 mybatis 配置的一小部分配置,不过已经足够说明我并没有吹牛


SqlBuilder

你一定会想,按样例查询这个例子还是非常容易提供通用实现的,如果要根据各种条件生成复杂,动态的 SQL 怎么办?是不是要写很多 if-else 语句自己拼?图样!这时候就要 SqlBuilder 出马了。还是让我们看看实际的代码对比: Mybatis mapping:


<select id="selectListByUserIdExample" parameterType="java.util.Map" resultMap="BaseResultMap">    select * from (select ROW_NUMBER() OVER ( ORDER BY inserttime DESC ) rownum,  <include refid="Base_Column_List" />    from strategyaccountdetail${tableSuffix} WITH(NOLOCK)    where userid = #{userid,jdbcType=INTEGER}        <if test="strategyid != null and strategyid != ''">      and strategyid = #{strategyid,jdbcType=VARCHAR}        </if>    <if test="typeid != null">      and typeid = #{typeid,jdbcType=INTEGER}    </if>    <if test="beginInserttime != null">      and inserttime <![CDATA[>= ]]> #{beginInserttime,jdbcType=TIMESTAMP}    </if>    <if test="endInserttime != null">      and inserttime <![CDATA[<= ]]> #{endInserttime,jdbcType=TIMESTAMP}       </if>    AND isactive=1) tpage WHERE tpage.rownum BETWEEN ${startPage} AND ${pageSize}  </select>
复制代码


DAS 对应代码:


public List<Strategyaccountdetail> selectListByUserIdExample (Long userId, String strategyid, Integer typeId,        Date beginInserttime, Date endInserttime, Integer pageNum, Integer pageSize) throws SQLException {    SqlBuilder builder = SqlBuilder.selectAllFrom(definition).where()        .allOf(            definition.Userid.eq(userId),            definition.Isactive.eq(1),            definition.Strategyid.eq(strategyid).nullable(),            definition.Typeid.eq(typeId).nullable(),            definition.Inserttime.greaterThanOrEqual(beginInserttime).nullable(),            definition.Inserttime.lessThanOrEqual(endInserttime).nullable())        .orderBy(definition.Inserttime.desc())        .into(Strategyaccountdetail.class)        .offset(pageNum, pageSize).withLock();        return client.query(builder);}
复制代码


使用 SqlBuilder 的 DAS 的 code 是不是还是一样紧致光滑?有人会说最新的 mybatis 也有 SqlBuiler 嘛。那我们就也比一比,不要说我骗人: Mybatis Sql builder:


public string selectPersonLike(String id, String firstName, String lastName) {    return new SQL() {        {           SELECT("P.ID, P.USERNAIE, P.PASSWORD, P.FIRST_NANE, P.LAST_NAME");           FROM("PERSON P");                           if (id != **null**) {               WHERE(" P.ID like#{id}");           }                           if (firstlame != null) {               WHERE("P.FIRST_NANE like #{firstliase}");           }                 if (lastlame != null) {               WHERE("P.LAST_NAMIE like #{lastName}");           }            ORDER BY("P.LAST_NAMIE");         }     }.toString();}
复制代码


DAS SqlBuilder:


public SqlBuilder seletPersonLike (String id, String firstName, String lastName) {        Person.PersonDefinition P = Person.PERSON;               return sqlBuilder.selectAllFrom(p).where()            .allOf(                p.id.like(id).nullable(),                p.firstName.like(firstNane).nullable(),                p.lastName.1ike(lastName).nullab1e()            )            .orderBy(p.lastName);}
复制代码


明显还是 DAS 的 SqlBuilder 设计更出色!


Hints

一步到位的提供 API 会存在一个设计风险,那就是任何操作都会存在特殊情况。比如一个简单的插入操作,就存在很多变体:


1. 在存在自增 ID 的情况下生成自增 ID


2. 在存在自增 ID 的情况下使用自定义 ID


3. 在存在自增 ID 的情况下生成自增 ID 并将生成的 ID 设置到输入实体


4. 等等


普通的做法是为每种特殊做法提供 overload 的方法,有几种特殊情况就提供几个方法。按照这种思路发展下去,方法的数量很快就会多到失控。如何才能确保在一个精简的 API 集合上提供尽可能多的特殊操作呢?这就轮到 Hints 登场了。


你可能注意到 DasClient 的方法除了必要参数外,往往还会带一个 Hints。这个 Hints 要么是以可变参数存在,要么是作为必要参数的一个属性。DAS 利用 Hints 传递特殊指令,帮助用户处理灵活多变的场景。以插入单条记录为例,API 长这样:


public <T> int insert (T entity, Hints...hints) throws SQLException
复制代码


调用的时候既可以只传 entity:


dao.insert(p);
复制代码


也可以传最多一个 hints


dao.insert(p, hints.insertWithId());
复制代码


无论哪种情况,方法只有一个。 虽然 Hints 也算不上脑洞特别大的发明,但与 ORM 结合得如此之紧密自然,别无分号。这种设计带来的便利是巨大的。不信可以参考一下如果用独立的分库分表组件会怎样实现:


// Sharding database and table with using hintManager ,String sql = "SELECT * FROM t order";    try(HintManager hintManager = HintManager.getInstance();        Connectlon conn = dataSource.getConnection();        PreparedStatement preparedstatement conn.prepareStatement(sql)) {        hintManager.addDatabaseShardingValue("t_order", 1);        hintManager.addTableShardingValue("t_order", 2);                try (ResultSet rs = preparedStatement.executeQuery()) {              while (rs.next()) {                               ...            }        }}
复制代码


上面需要 3 行独立代码完成的 Hints 相关工作。倒不是说这个分库分表组件设计的不好。除了 TiDB 或 Amazon Aurora 这种真正的分布式数据库之外,绝大多数基于传统数据库之上的分库分表组件都难以做到完全的对应用代码透明。在特殊场景下都需要以某种方式传递特殊指令。如果依赖于现有 ORM 工具或基于 JDBC,就会存在类似上面这种很不自然的代码。


而 DAS 通过将 Hints 与 ORM 接口结合的方式,完美的解决了特殊与一般的矛盾。同样的事情 DAS 只需要一行:


List<Person) plist = dao.query(selectAllFrom(p). setHints(Hints.hints().shardValue(1).tableShardValue(2)));
复制代码


在推广过程中我们还发现一个有趣的事情。就是我们以为用户喜欢透明的分库分表,但事实上,出于各种原因,用户用的最多的反而是直接指定分库分表。当然利用 Hints 可以很简单的做到:


List<Person> plist = dao.query(selectAllFrom(p).setHints(Hints.hints(). inShard(1).inTableShard(2)));
复制代码


自研 ORM 还有一个额外的好处。那就是虽然从成本还有技术的方面来看,分库分表技术目前还有市场,但长远来看,这大概率是一种过渡性的技术。即使哪天人们完全解决了分布式数据库的性能和一致性问题,也还是需要某种面向应用的 ORM 技术来实现灵活多变的需求。这样 DAS 就可以继续发挥作用。从今天的标准来看,DAS ORM 的设计在易用性和灵活性上已经达到了能达到的极限。


总结

DAS 完美结合了 ORM 和分库分表功能,其产品定位是进可攻,退可守。根据公司内部实际使用效果来看,使用 DAS 能极大提高研发效率,减少代码量和出错概率,再也没有因配置导致的各种故障。


有一次偶尔路过听到一个总监和下面 tech leader 的对话,总监问如果技术输出,新的代码里面能否不用我们的 DAS,leader 微笑着但坚定的回答,不行,DAS 很好用的,我要用。


对我们做框架的程序员来说,还有什么比一句好用更高的评价吗?


是好东西就要拿出来大家一起用,DAS 已经开源,并提供了详尽的文档供大家参考,请大家尽情 star~


GitHub 地址:https://github.com/ppdaicorp/das


除了开源文档,我们还提供在线技术支持,有兴趣的朋友可以入群获得帮助或者更多活动信息。


最后说一句,不要重复造轮子是最广为人知的谬误。你不造,只是把机会让给别人。



欢迎入群交流


作者介绍

赫杰辉,信也科技基础组件部门主管、信也 DAS 产品负责人、布道师。图形化构建工具集 x-series 的作者。曾主持开发携程开源数据库访问框架 DAL。对应用开发效率提升和分布式数据库访问机制拥有有多年研究积累。


2020 年 6 月 29 日 07:001076
用户头像
刘燕 InfoQ记者

发布了 554 篇内容, 共 174.6 次阅读, 收获喜欢 1055 次。

关注

评论 4 条评论

发布
用户头像
不错 谢谢
2021 年 04 月 17 日 11:03
回复
用户头像
代码的排版,已没了兴致。
2020 年 07 月 01 日 23:52
回复
这文章发表的时候代码排版乱掉了。你可以百度搜一下同名的文章看看
2020 年 07 月 02 日 19:15
回复
代码格式已经改好了
2020 年 07 月 03 日 20:52
回复
没有更多了
发现更多内容

架构师训练营第十一周学习总结

文智

极客大学架构师训练营 架构师一期

Week7-性能优化-作业1

shuyaxx

架构师训练营 1 期 - 第 十一周总结(vaik)

行之

极客大学架构师训练营

Architecture Phase1 Week11:Summarize

phylony-lu

极客大学架构师训练营

架构师训练营第 1 期 -- 第十一周学习总结

发酵的死神

极客大学架构师训练营

架构师训练营第 7 周课后练习

菜青虫

极客大学架构师训练营

架构师训练营第 1 期 week11 总结

张建亮

极客大学架构师训练营

架构师训练营第七周作业

李日盛

性能测试

「架构师训练营第 1 期」第十一周作业

张国荣

第七周作业总结

hunk

极客大学架构师训练营

Week 11 學習總結

Christy LAW

什么样的股权,才算“到手”?| 法庭上的CTO(3)

赵新龙

股权 CTO 法庭上的CTO

第七周作业

hunk

极客大学架构师训练营

第七周-总结

jizhi7

极客大学架构师训练营

架构师训练营week11作业

FG佳

极客大学架构师训练营

架构1期 第十一周作业

haha

极客大学架构师训练营

性能优化 - 学习总结笔记

Xuenqlve

【架构师训练营第 1 期 11 周】 学习总结

Bear

极客大学架构师训练营

Architecture Phase1 Week11:HomeWork

phylony-lu

极客大学架构师训练营

Spock单元测试框架实战指南二-mock第三方依赖

Java老k

Java 单元测试 JUnit spock

第七周大作业

小兵

架构师训练营 1 期 - 第 十一周作业(vaik)

行之

极客大学架构师训练营

架构师训练营第十一周作业

文智

极客大学架构师训练营

第二周课后练习

Binary

极客大学架构师训练营

架构一期第十一周作业

Airs

Week7-性能优化-总结

shuyaxx

架构师训练营week11总结

FG佳

【架构师训练营第 1 期 11 周】 作业

Bear

极客大学架构师训练营

先从哪里开刀-组织形式还是制度安排

luojiahu

组织思考

第 7 周作业

Steven

极客大学架构师训练营

第 11 周 作业

Pyr0man1ac

DNSPod与开源应用专场

DNSPod与开源应用专场

信也科技数据库访问中间件DAS揭秘-InfoQ