写点什么

DAS 易用性设计

作者:赫杰辉
  • 2021 年 11 月 24 日
  • 本文字数:8970 字

    阅读完需:约 29 分钟

前言工欲善其事必先利其器,对于研发人员来说,拥有一款趁手的工具来高效工作是一件梦寐以求的事情。特别是每天都要频繁用到的框架类的工具,其易用性的好坏显得尤为重要。易用性一般从学习难度,集成难度,使用难度等几个方面衡量。本文将以信也数据库访问框架 DAS 为例,具体介绍易用性设计如何落实到框架类产品设计上面。本文可以作为框架类软件的设计参考,希望能为大家带来些启发。DAS 简介 DAS 是信业科技自研的数据库访问中间件,支持数据库配置管理,ORM,动态 SQL 构建和分库分表支持等功能。DAS 的研发目标是最大化研发人员的工作效率,最小化数据库开发成本,从而支持研发人员将精力主要放在业务逻辑层面。使用案例我们先通过一个简单的例子来体会一下 DAS 的便利性。

DasClient dao = DasClientFactory.getClient("logicDbName");Person pk = new Person();pk.setPeopleID (10);Person p = dao.queryByPk(pk);
复制代码


这段代码很好理解,几乎就是大白话:


  1. 按照给定的数据库名字从工厂类中获取 dao 实例

  2. 构建一个表实例,为主键赋值

  3. 调用 dao 实例完成按照主键值的查询并返回符合条件的实例。


貌似平平无奇?我们来对比一下类似操作用 mybatis 长啥样:

InputStream resourceAsstream = Resources.getResourceAsStream("cc/sq1MupConfig.xml");SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(resourceAsstream);SqlSession sqlSession = ssf.openSession();UserMapper mapper = sqlSession.getMapper(UserMapper.class);User u = mapper.finduserById(10);
复制代码


相信大部分人对 mybatis 都比较熟悉,我这里就不逐行解释。这里的主要工作首先是创建一个工厂类,其次根据 dao 的类型获得 mapper 实例,最后调用实例方法


例子很直观显示 DAS 的代码更少,更好理解。这只是初步的印象,接下来我们会通过深入的剖析来详细介绍 DAS 的易用性设计。信息最少原则信息最少原则是贯穿 DAS 设计始终的最重要原则。信息最少原则的含义是完成一件工作需要用户提供的信息越少越好。我们首先用创建 dao 实例为大家做介绍。


不知道大家有没有思考过完成一件工作的抽象过程应该是怎样的?完成一件工作首先需要提供必要外部信息,其次基于这些信息通过一系列步骤达成的效果。对应于程序就是方法定义。如果把代码复杂度定义为 F(C)= P * N,其中 P 为完全由外部提供的参数个数,N 为代码行数,可以看到需要的参数和执行的步骤越少,则该工作的难度越小,体现在易用性方面就越高。


我们回顾一下之前的例子,其中 DAS 创建 dao 实例仅需提供数据库名称,全部创建过程只需要一行代码:

DasClient dao = DasClientFactory.getClient("logicDbName");
复制代码

从道理上讲,要获得操作一个特定数据库的 dao,最基本所需要的信息只有数据库名称。从 dao 实例的创建过程来看,DAS 仅需要提供数据库名称通过一步操作完成,已经是理论上最简化的做法。


而 mybatis dao 实例的创建首先需要提供 xml 配置文件路径创建流资源,其次创建工厂实例和会话实例,最后提供特定的类名称创建 dao 实例,需要提供两个信息,前后一共 4 行代码:

InputStream resourceAsstream = Resources.getResourceAsStream("cc/sq1MupConfig.xml");SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(resourceAsstream);SqlSession sqlSession = ssf.openSession();UserMapper mapper = sqlSession.getMapper(UserMapper.class);
复制代码


按照之前的复杂度公式 DAS 的复杂度为 1 * 1,mybatis 的复杂度为 2 * 4 = 8,两者相差 8 倍。如果单纯以代码行数衡量,两者效率相差 4 倍。如果按照 F(C)= P + N 来算,也相差 3 倍。虽然因为产品设计理念不同,不能完全 1 比 1 类比,但是 mybatis 的做法在达到类似目的所需要的信息和步骤更多,并且其中还包含与数据库无关的中间步骤。很明显遵循信息最少原则的 DAS 的设计更加简洁和直观。


其实这还不是真正的效率差距,因为 DAS 创建的 dao 是面向给定数据库的通用的 dao,所有该数据库的操作都可以使用这个 dao。而 mybatis 创建的 dao 其实是基于给定 mapper 定义的 dao,能完成的操作仅限于 mapper 中定义的范围,同时定义 mapper 也需要花费成本。总体而言 DAS 以更少的代码创建了能力更广泛的实例。


事务处理也能很好的体现 DAS 设计理念中的信息最少原则。常规事务处理需要开启事务,结束前提交事务,出错时回滚事务,一般会是一个巨大的 try-catch-final,事务处理代码和业务逻辑代码混在一起。其实事务处理里真正的核心就是数据库操作逻辑,除此以外的所有信息或步骤都是附加的,可以隐藏掉。DAS 事务处理秉持这种原则,仅需要用户提供业务逻辑,其他事情交给框架处理:


PersonDefinition p = Person.PERSON;dao.execute(() -> {    SqlBuilder builder = selectAllFrom(p).where(p.PeopleID.gt(0)).orderBy(p.PeopleID.asc()).into(Person.class);    List<Person> plist = dao.query(builder);    assertArrayEquals(new int[] {1, 1, 1, 1}, dao.batchDelete(plist));});            
复制代码


上例中,仅 dao 需要通过数据库名获取,剩下所有代码都是用户自己的业务逻辑。DAS 还提供注解的方式支持事务,仅需在方法声明上提供事务标签即可。

@DasTransactional(logicDbName = "logicDBName")public void execute() {    //DAS的操作}
复制代码

恰如《给加西亚的信》一样,运用信息最少原则,DAS 可以让研发人员集中精力在做什么,而不是怎么做上面。操作通用原则虽然数据库操作主要就是就是 CRUD,但由于用户的表结构千差万别,如果不能在框架操作层面消除这种差异,那么必然会外溢到应用层面从而导致自定义方法过多的情况。为避免这种情况,DAS 在设计上采用了操作通用化原则,通过对方法的精心设计,简化了学习与使用成本。


比如按照主键查询大概是用得最多的数据库操作,在最开始的例子里,DAS 的做法是:

Person pk = new Person();pk.setPeopleID (10);Person p = dao.queryByPk(pk);
复制代码

Mybatis 的做法是:

User u = mapper.finduserById(10);
复制代码

乍一看似乎 DAS 的更麻烦。但这只是因为这个例子里面包含了构造 pk 的过程,这一过程其实是应用逻辑的一部分,pk 完全可以作为参数传进来,直接与 dao 相关的代码只有第三行:

Person p = dao.queryByPk(pk);
复制代码

虽然看起来 das 和 mybatis 完成按主键查询都是一行代码,但 mybatis 这个是用户自定义专用的,只能查询定义中指定的表。而 das 的方法却允许用户查询任意表。作为通用方法,DAS 的可以在任意应用中使用,完全无需重复定义。


有人可能会问为什么 DAS 用表实体作为参数,而不是像 mybatis 这样只用主键值。这么做主要出于以下几点:


  1. 最小化参数传递。通过 DAS 自带的工具可以方便的生成包括元信息的表实体。作为通用查询,das 需要知道表名,主键名和返回的表实体类型作为参数,这些元信息都已经包含在表实体定义里。如果这些元信息不作为参数提供,则必须在系统某处预先定义,这种信息的离散会降低系统的可理解性,或者说透明度,从而导致易用性降低。

  2. 支持联合主键场景。主键并不一定只包含一个字段,虽然现在主流的数据库规范都要求使用单一主键,即使是联合表或者子表也这样要求。但是本质上这是规范,并不是标准,因此 DAS 必须支持多主键查询。要处理多主键的情况,要么单独定义一个主键类,包含所有主键字段;要么在方法参数定义中要求传多个参数。前一种情况会导致每个表对应的类定义增加一倍,如果非主键字段较少,那么主键类和实体类将非常相像,差别仅在几个字段上,单独定义的费效比很差,利用继承来复用又会造成可读性较低;后一种情况会增加参数数量,顺序,名字等复杂度。综合考虑复用表实体来传递主键取值更加合理。

  3. 支持分片数据传输。DAS 支持分库分表,用于分片的字段值可以通过表实体一起传给 DAS,从而避免其以参数或者其他方式额外传输的成本。


关于通用性再举一个例子。按照部分字段取值对单表进行查询是非常常见的操作。DAS 通过 queryBySample 来直接支持这种做法,只需要一行代码即可实现:

DasClient dao = DasClientFactory.getClient("logicDbName");Person sample = new Person();sample.setName(userName);sample. setCityID (cityId);List<Person> p = dao.queryBySample(sample);
复制代码

类似的还有 countBySample,deleteBySample 方法,方法名字已经解释一切。同样的功能使用 mybatis 只能在 XML 中为每个表写一个包含字段判断的复杂 mapper,如果表结构变了还得修改 XML,搞过的自然懂。


利用通用性原则,DAS 仅用有限的预定义方法就满足了绝大部分复杂的数据库操作功能要求。能操作一个表就能操作所有表,避免了由于用户表结构等因素可能导致的方法爆炸。少而精的方法既可以降低学习难度,还提高了框架的易用性。操作直观原则操作是不是简洁直观,能不能直接的实现开发者的意图是框架易用性的直接体现。常见的数据库操作按动作类型来说包括增删改查,也即 insert,delete,update,select。从操作方式来说分为单步操作,批处理。从是否包含在事务角度来说分为普通操作和事务操作。当然还有现在已经很少见的调用存储过程。如何支持上述所有功能并同时保持易用性是一个很大的挑战。为达成这一目标,DAS 在通过直观的方法设计,让研发人员通过方法名字就能毫不费力的选择最合适的方式完成工作。让我们用案例说话。


比如想要插入一条数据,对应的 SQL 命令是 insert,那么对应的 DAS 命令就是 insert:

Person p = new Person();p.setName("jerry");p.setCountryID(k);p.setCityID(k);assertEquals(1, dao.insert(p));
复制代码

如果想要同时插入几条,只要调用重载的 insert 命令即可,DAS 会使用 INSERT INTO VALUES 语法执行:

List<Person> pl = new ArrayList<>();for(int k = 0; k < 4;k++) {    Person p = new Person();    p.setName("jerry");    p.setCountryID(k);    p.setCityID(k);    pl.add(p);}assertEquals(4, dao.insert(pl));
复制代码

想要 update,设置好记录的主键和想要更新的字段,直接调用 dao 的 update 方法即可,为空的字段会自动忽略:Person pk = new Person();pk.setPeopleID(1);pk.setName("Tom");pk.setCountryID(100);pk.setCityID(200);assertEquals(1, dao.update(pk));


delete 操作我们也可以依葫芦画瓢,但为了与 deleteBySample 相区别,名字是 deleteByPk:Person pk = new Person();pk.setPeopleID(1);assertEquals(1, dao.deleteByPk(pk));


如果想要利用 SQL 的批量功能,可以直接调用 batchInsert,batchDelete,batchUpdate:

    int[] ret = dao.batchInsert(pl);        int[] ret = dao.batchDelete(pl);        int[] ret = dao.batchUpdate(pl);
复制代码

很明显只要具备基本的 SQL 知识就可以利用关键字找到对应方法,轻松的实现数据库操作。


可能这么介绍比较没有说服力,我们对比一下 mybatis 插入多条数据需要在 xml 里面怎么做:

<insert id="insertAuthor" useGeneratedKeys="true"keyProperty="id">    insert into Author (username, password, email, bio) values    <foreach item="item" collection="list" separator=",">        #{item.username}, #{item.password}, #{item.email}, #{item.bio})    </foreach></insert>
复制代码

通过提供直观的操作,DAS 的设计让研发人员更多的聚焦在做什么,而不是怎么做上面,从而提高了易用性。研发人员能干净利落的实现自己的意图,没有额外的接口定义,没有语句映射,没有模板编写。兼顾特殊与一般虽然 DAS 通用直观的 API 可以覆盖大部分场景,但是用户还是会有些特殊场景,这种情况下,DAS 通过在原方法上提供操作 hints 来实现用户意图。我们以 insert 方法做例子来说明 DAS 通用方法的定义形式:

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

insert 方法定义了可变参数 Hints,普通操作的时候用户可以不提供该参数,需要的情况下用户可以提供最多一个 hints 实例。每个 hints 实例可以用链式的方法指定多个提示。既可以 hints.xxx().yyy().zzz()。


对于 ID 自增长的表来说,插入操作会忽略实体自带的主键值,但如果用户希望插入的时候由应用指定主键,则可以:

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

再比如利用主从库实现读写分离时,查询一般缺省路由到读库。但是由于同步延迟,读库可能并没有及时和主库同步,在必须保障获取最新数据的场景下,利用 DAS 可以很方便的修改缺省行为,要求读必须走主库:

dao.queryBySample(sample, hints().masterOnly());
复制代码

如果觉得上面指定的方式太机械,还可以指定从库的新鲜度来达到灵活性。如果从库新鲜度小于此值,则读从库,反之读主库:

dao.queryBySample(sample, hints().freshness(5));
复制代码

利用可变参数 Hints,既保障了一般操作的简单直接,又保障了特殊操作的机动灵活。而这都是在一个简洁的操作集合之上完成的,避免了采用重载可能带来的方法膨胀。最小化代码生成研发人员要完成各种 CRUD 操作,主要通过 dao 和实体类,这部分代码可以由用户自己创建,也可以通过工具生成。很显然通过工具生成更省事,质量也有保障。使用是不是方便,生成的代码是不是精练,适用范围是不是广泛都是易用性的表现。DAS 通过最小化代码生成来提高框架的易用性。


首先从设计上减少必须生成的代码种类。由于 DAS 已经缺省提供了通用直观的 dao,因此这部分代码不用自动生成。用户只需要考虑用于参数和返回值的实体类如何创建。实体类分为表实体类和查询实体类,分别对应单表的 CRUD 操作和复杂查询结果集表示。


其次提供方便的实体类生成手段。登录 DAS 控制台后,只能看到自己有权限访问的库表,选好表后就可以生成表实体类。要生成查询实体类仅需要提供一段样例查询语句,DAS 就会根据查询结果生成代码。


最后通过一物多用来尽可能扩大生成代码的使用范围。DAS 生成的表实体类不仅仅可用于一般的 CRUD 操作,还可以用于自定义 SQL 的创建。这是由于表实体里面不但包括对应表结构的属性字段,还包括能直接使用的表名,字段等表的元信息。通过这些信息可以让研发人员以类似直接写 SQL 的方式创建自定义的 SQL 语句。


为方便大家理解其结构,以下是表实体类的代码片段:

@Tablepublic class Person {    public static final PersonDefinition PERSON = new PersonDefinition();    public static class PersonDefinition extends TableDefinition {        public final ColumnDefinition PeopleID;        public final ColumnDefinition Name;        public final ColumnDefinition CityID;        public final ColumnDefinition ProvinceID;        public final ColumnDefinition CountryID;        public final ColumnDefinition DataChange_LastTime;        public PersonDefinition as(String alias) {return _as(alias);}        。。。        }    }
@Id @Column(name="PeopleID") @GeneratedValue(strategy = GenerationType.AUTO) private Integer peopleID; @Column(name="Name") private String name; 。。。
复制代码


可以看到表实体内部包含一个跟该表对应,用于存储元信息的表定义类,本例为 PersonDefinition。定义类里包含了可直接引用的列定义,如 PeopleID 等。有了表定义类,列定义等元信息,在构建 SQL 的时候就可以利用 IDE 工具的代码自动填充功能和编译校验功能。在减轻记忆负担的同时防范直接用字符串拼接时容易犯的拼写错误。


其实数据库操作所需的信息主要就是表结构,而表结构可以从数据库获得,因此理论上讲,最少需要生成的代码就是表结构相关的实体类。当然为支持复杂查询场景,DAS 也提供查询类。DAS 的代码生成通过逼近理论上的最小极限。从而减少研发工作量,提高可理解性与易用性。灵活高效的 DSL 尽管绝大部分数据库操作都可以通过 dao 与实体类配合完成,但是还是有部分操作,例如复杂的查询语句免不了自定义。DAS 借鉴 DSL 技术来降低用户创建自定义语句的难度。


在进一步介绍之前,我们先了解一下创建自定义 SQL 的难点何在。由于 Java 与 SQL 是两种语法结构完全不一样的语言,创建自定义 SQL 一般的做法是在代码或配置里面根据情况手动拼接包含 SQL 语句片段的字符串和参数占位符。这些 SQL 片段在 Java IDE 里面仅被当做普通的字符串。这导致三种问题。


首先是纯写文本的工作量大,非常容易写错;其次只有在程序运行起来后才能发现语法错误,并且根据出错信息难以定位。最后在拼接过程中往往会根据业务逻辑包含或者排除某些部分,在代码里包含条件判断语句,损害了代码可读性。整体来说,拼字符串的方式自定义 SQL 相当于在一种语言里面,用支离破碎的方式编写另一种语言的代码,是对研发人员非常不友好的方式。


DAS 通过 SqlBuilder 降低自定义 SQL 的创建难度。SqlBuilder 基于 SQL 命令,语法和关键字定义了一系列链式方法,让研发人员在 Java 编程环境中以符合 SQL 语法和使用习惯的方式创建自定义 SQL 语句。


首先是数据库操作的静态创建方式。普通的 CRUD 命令以静态方法的方式提供,可以在 Java8 及以上的版本中通过 static import 的方式在代码中直接引用,配合表实体类中的元数据可以达到直接写 SQL 的效果。以 select 为例:

PersonDefinition p = Person.PERSON;SqlBuilder builder = selectAll().from(p).where(p.PeopleID.between(1, 3)).    orderBy(p.PeopleID.asc()).into(Person.class);List<Person> pl = dao.query(builder);
复制代码


对于常用的语句,例如 SELECT TOP,SELECT DISTINCT 可以直接调用同名方法:

SqlBuilder builder = selectTop(3, p.PeopleID, p.Name).from(p).    orderBy(p.PeopleID.asc()).into(Person.class);
SqlBuilder builder = selectDistinct(p.Name).from(p). orderBy(p.Name.asc()).intoObject();
复制代码

除了查询,INSERT,DELTE,UPDATE 也可以依葫芦画瓢,以 INSERT 为例:

PersonDefinition p = Person.PERSON;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 还提供简洁的条件拼接,避免代码中出现大量的 if-else,使代码读起来更加的顺畅。比如只有在条件满足时才拼接指定语句,可以这样写:

builder.appendWhen(true, "ABC", "DEF");assertEquals("ABC DEF", builder.build(ctx));
复制代码

还支持参数为 null 时自动忽略对应条件的场景。如果用户条件参数为 null,DAS 会自动判断并在消除参数所在的表达式片段,同时调整逻辑判断操作,必要时还调整括号来保证整体表达式语法的准确性。例如下例参数 strategyid 为 null 时,会自动忽略对 Strategyid 字段的相等判断:

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 和表实体类,可以按照原生 SQL 甚至更简洁的方式创建自定义语句,且不用担心语法错误或者拼写错误。SqlBuilder 还有很多巧妙的设计来减轻工作量,是整个 DAS 中非常出彩的部分。例如使用 AND 连接多个表达式时,可以直接使用 allOf 方法,可以消除大量的 and 操作符,同理还有 anyOf,限于篇幅这里就不再一一介绍。集成诊断机制开发时能否快速调试程序也是易用性的一部分。查看 SQL 语句,执行时间等信息是开发数据库应用的刚需,DAS 通过扩展点与第三方监控系统相集成方便用户查看,但这需要登录第三方系统进行查看,要切换工作环境,不直接。为了提高本地调试的便利性与准确性,DAS 允许用户利用 Hints.diagnose()方法指定哪些操作需要记录详细步骤信息。不仅可以在调试时看到生成的 SQL 语句,还可以看到对应数据库信息,分库分表时每个分片的执行详情等。

Hints hints = Hints.hints();pk = dao.queryByPk(pk, hints.diagnose());System.out.println(hints.getDiagnose());
复制代码

如果觉得通过 hints 的做法略显麻烦,还可以直接通过启动参数

-Ddas.client.debug=true
复制代码

开启 Debug 模式。这样在出错时会自动打印诊断信息到 System.err。集成最简化原则对研发人员来说,要不要使用一个框架需要很慎重的下决定。因为即使功能强大到无所不能,但如果配置繁琐,文档过时,集成时问题花样百出,都可以轻易的让人打退堂鼓。作为研发人员,DAS 团队深深的理解大家的顾虑,将集成 DAS 的全部动作简化到只需要引入一个简单的内部组件依赖。没有本地配置文件,没有环境参数,没有 POM 中诡异的 profile,仅仅是一个 jar 包,不需要任何额外操作。而对于需要研发人员参与的部分,例如配置数据库,也只需要通过自助平台完成,本地无需做任何改动。DAS 通过覆盖率超过 90%的 3000 多个单元测试保证自身的质量,让研发人员能够放心使用。使用反馈 DAS 在信也科技内部已经使用快 4 年,接入 400 个应用,获得用户的一致好评:

  • 开发环境配置顺利,省去了 mybatis 的 xml 操作

  • 上手方便,直接 DasClient dao = DasClientFactory.getClient(逻辑数据库名);即可

  • 缺省提供的 API 可以满足大部分数据操作需求

  • 复杂查询可以使用 SqlBuilder,链式接口功能强大

  • 可以使用 hints 进行特殊操作和调试

  • 集成了应用监控和配置中心,免去了客户端配置总结为什么要强调易用性?


因为框架类产品会被无数人使用,即使是一点点缺陷,一点点麻烦都会放大无数倍,无形中推高研发成本。框架产品应该易于集成,易于学习,易于使用,易于调试,低调的支撑业务研发工作。DAS 看起来平凡,用起来顺手,能让研发人员能集中全部精力做业务,而不必为如何使用 DAS 分心。


为了提供一款易用性极高的数据库访问框架,节约每一行代码,消除每一个多余动作,避免每一个潜在风险,DAS 研发团队默默的付出了大量的努力。相比与常见的 ORM 框架搭配分库分表中间件的方式,使用 DAS 完成同样功能所需代码和工作量会大大减少,开发效率和代码质量显著提高。


希望 DAS 的易用性设计能为大家带来一些启发和鼓励,在编程之道上共同精进。


作者简介赫杰辉,信也科技架构研究员,DAS 产品负责人。对架构设计,中间件,低代码等方面有较深入研究。参考资料https://github.com/ppdaicorp/das

用户头像

赫杰辉

关注

开源可视化系统构建工具x-series作者 2020.06.09 加入

还未添加个人简介

评论

发布
暂无评论
DAS易用性设计