vlambda博客
学习文章列表

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

作者 | Hejiehui
编辑 | Ken Zheng


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 {
Inputstrean resourceAsstream Resources ogetResourceasstrean("cc/sq1MupConfig.xml");
sq1SessionFactory ssfenew sq1Session actoryBullder() .build(resourceAsstream);
///mapper就是UserMapper接口的实现类
UserMapper mapper = sqlSession. getMapper(UserMapper.class);
User u = mapper.finduserById(10);
system.out.print1n();
}

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

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

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

  • 具备简洁明了的API,99%的操作都能一步搞定,使用起来如丝般顺滑
  • 使用面向应用,面向数据对象这种高级抽象,而不是链接,事务这类极易用错的底层概念
  • 提供基于Java的动态SQL生成器。在同一个上下文里写SQL和写代码,不用切来切去
  • 链接,事务之类的统统由框架搞定,不用操心资源的打开,关闭,泄露啥的,没有使用就没有伤害
  • API既简单又复杂。既能适应一般情况,又能处理特殊情况。别问,就要
  • 内置分库分表能力,不用再单独集成第三方组件。分库分表配置必须极简单,连我都能学会,当然最好别学。如果你问一个研发人员这世界上什么最痛苦?他一定会回答你学习新技术最痛苦。如果要再上一个档次,那就是要学那种要花很多功夫学习却会隔很长时间才用一两次的技术,对,说的就是分库分表配置这种
  • 不要让我手工编辑任何XML或配置文件,都已经2020年了,在几万块的MAC上写XML,就像参加豪华晚宴却蹲在椅子上吃饭一样,像什么样子。如果实在免不了要配置,那必须提供能配得上我一指禅的最好的编辑器。
  • 只用写DAO相关代码,其他一概不管

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

信也DAS


DAS是Database Access Service的缩写。DAS的目标就是给研发人员提供一个一站式的数据库访问框架,让研发人员用最简单直接的方式开发数据库访问代码,实现上面所有非分之想信也科技数据库访问中间件DAS揭秘

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

  • 同时具备ORM和分库分表能力的客户端 DAS Client
  • 基于WEB页面的数据库配置管理和代码生成工具 DAS Console
  • 可选的基于代理模式的 DAS Server。应用在直连和代理之间切换无需改代码,也不用知道
信也科技数据库访问中间件DAS揭秘

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

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

这,才是我们成功的秘密![撒花]信也科技数据库访问中间件DAS揭秘

DAS核心设计初探

你心中一定冷笑,吹吧你!那让我们从技术角度看看 DAS的核心DAS Client 到底长什么样信也科技数据库访问中间件DAS揭秘

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

  1. DAO层,一个完整的ORM框架。关于编程所有美好的想象都在这里
  2. 分库分表层。抽象数据库操作差异,以统一的方式处理数据的路由与合并
  3. 执行层。操作底层数据库完成实际工作,封装数据源,链接与事务

DAS ORM简介

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

DasClient

DAS ORM的核心是DasClient类,来看看里面提供了啥方法:信也科技数据库访问中间件DAS揭秘

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.xxxxxxxxxxxxxxxxxxx.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(final String id, final String firstName, final string lastlame)
return new SQL()
{
{
SELECT("P. ID, P.USERNAIE, P.PASSHORD, P.FIRST _NANE, P.LAST NAME");
FROM("PERSON P")
if (id != null) {
WHERE("P.ID like#{id}");
}
if (firstlame != null) {
WHERE("P.FIRST MAE like #{firstliase}");
}
if (lastlame != null) {
WHERE("P.LAST NAMIE like #{lastName}");
}
ORDER BY("P.LAST. NAME");
}
}.toString();
}

DAS SqlBuilder:

    public SqlBuilder seletPersonLike(final string id, final String firstlane, final string lastName) {
Person.PersonDefinition P = Person.PERSON;
return sqlBuilder.selectAllFrom(p) where().
allOf(
p.d.like(id).nullable(),
p.firstName.like(firstNane).nullable(),
p. lastNare .1ike(iastName).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(sq1)) {
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

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

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


作者介绍

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