背景
惯例还是说下背景,对于系统中的表单某些属性,例如,性别、合同类型、供应商,展示给用户看的,需要是文本内容,同时,系统往往需要根据这些属性进行逻辑功能的处理,如不同合同类型进行不同的业务操作,这时候就不能使用文本了,道理也很简单,文本是易变的,我们不能在程序中写if(contractType.equal(“销售合同”)){……}。
这时候,就需要编码或标识了。
大概分成两大类,一类通常称之为数据字典,如性别、证件类型,通常使用编码,如果使用标识,可读性会很差;另外一类就是业务实体的唯一性标识,通常对应数据库的主键,这类数据往往不需要在程序中进行逻辑比较,而是仅仅通过标识获取到业务实体的属性,如名称,如果特定业务场景下需要代码中进行逻辑处理,那么建议使用业务编码,如合同编号。与不能在代码中与中文文本比较一样,同样在代码中也不适合写if(contractId.equal(“227934324”)){……},运行没问题,但可读性为0。
分析
说完了背景,那么就存在一个客观需求了,就是如何将数据库中存储的编码,转换为文本后显示给用户。
问题的本质,也就是编码与文本之间转换工作,如何优雅的实现?
转换工作在哪一层实现最合理
首先,我们面临的一个问题,系统是分层的,转换工作在哪一层实现最合理。
系统目前常见的三层架构,数据流向从数据库到DAO层,再到Service层,然后通过Controller到前端。层与层之间,进行数据传递往往需要封装数据对象,也就通常说的vo、dto、po。数据对象在各层所处的位置,是这样的:前端-vo-控制器层-dto-服务层-po-数据库访问层-数据库。
持久化对象(po)对应实体层中的实体类,跟库表往往是一对一的。
数据传输对象(dto)有两个作用,一是控制器层,将前端传来的vo对象,转换为dto,传给服务层,二是服务层多个服务间的数据传递,特别是微服务架构中,多个后端微服务之间传递的dto对象。
视图对象(vo)则是前端与后端控制器层之间的交互的主要对象。
这些数据对象的存在,有利于降低各层之间的依赖和耦合,但这样也是有代价的,一方面,对象之间需要进行转换;另一方面,当实体属性调整后,往往需要同步调整vo、dto。
因此,在系统设计时,往往会根据实际情况做取舍。对于小型应用,往往只使用po,在各层中传递,简单暴力。对于前后端分离的系统,通常需要增加vo对象来实现前后端的解耦。对于大中型应用,会将dto也给实现出来,特别微服务架构下,微服务之间的调用,使用dto作为数据载体来传入和传出。
回到问题本身,也就是编码与文本之间转换工作,在哪一层来做?
这个问题看上去挺简单,放到控制器层,将服务层返回的实体数据对象,通过查询数据字典表的方式,将编码转换为显示内容,或者调用其他服务,通过标识获取对应的实体属性,例如显示人员信息的时候,通过部门服务,传入该用户的部门标识,拿到部门名称。
这样能解决大部分问题,不过也有一个小问题,就是如果多控制器要调用同一个服务,拿到数据显示给用户看,例如多个页面都要显示用户的性别,是不是意味着这几个控制器,都要进行性别的转换,也就是代码的重复?那从复用的角度考虑,是不是把转换工作,放到服务层来实现呢?
深入思考下,服务层的职责是拿到实体的数据,至于显示成什么样,应该是vo的职责,服务层输出的数据应当是唯一的,对应的前端可能是多个,常见的场景是几个系统公用用户基础数据,对于性别,服务层返回MALE/FEMALE,一些正式严肃的应用,将其展示为男/女,而另外一些社交类应用,则显示为帅哥/美女。即使是同一个业务系统,在移动端和PC端,显示也可能不完全相同,因此,转换处理放到服务层,弊远大于利,还是放在控制器层最合适。
如何优雅地实现
如何优雅地实现,要兼顾开发效率和运行性能综合考虑。
数据字典的处理
首先说下对数据字典的处理。数据字典属于使用高频,变动低频的数据。在一个业务实体中,往往同时有多个属性都是字典项,通过服务去查询库表转换,能走得通,但数据库读取的速度较低,过于频繁会影响性能,数据库压力也偏大,因此采用性能更佳、读取速度更快的缓存,如Redis来实现。在字典项维护的时候,就放到Redis缓存中,控制器直接从缓存里拿,进行转换。
关联实体的处理
其次说下对使用数据库唯一标识或业务编码的关联的实体数据,属于低频使用,但自身会随着用户维护而变化。这类数据,并不适合放到缓存里。通常来说有以下几种处理方案:
方案1:先取到列表数据,然后遍历,通过对应的服务,将标识或编码转换为显示名称。这种就是常说的1+N查询问题,显示某一业务实体列表结果,查询结果需要1次数据库读操作,转换工作,需要单次结果数量N次数据库读操作,客观地说,确实性能比较低,数据库负担比较重。MyBatis的关联表方式,其实就是这么实现的。在控制器层遍历实体,通过服务去拿数据转换,本质上也是执行了1+N次数据库读取操作。
方案2:直接使用表连接的方式,从数据库层面就把数据准备好。这种方式其实是将转换工作从控制器层换到的数据库层,只需要一次数据库读取操作,就能拿到实体列表显示的所有数据,不仅可以处理标识和编码的转换,也能把数据字典的转换一块处理了。从性能角度而言最佳,但对开发和运维上是有比较大的影响的,即需要编写和调试sql语句。单个字段简单,但多了就复杂了,常见的一条业务数据,包含了3-5个字典项,外加2-3个外部实体属性转换,这个sql要关联多次数据字典表,以及关联多个外部实体表,编写调试工作量实际不低,并且sql没有java那么友好和便利的代码提示。
方案3:获取实体列表,进行一次数据库读取操作,然后把外部实体的标识或编码,组装成集合,调用外部实体服务,拿到标识或编码和显示名称的集合(map),然后再对实体列表进行遍历,在内存中,将对应的标识或编码,转换为显示名称。这种方案实际是一种方案1和方案2折中的方案,兼顾了开发效率与运行效率。开发视角,不需要编写复杂的表关联sql语句,仍旧是单表操作。运行视角,数据库查询操作执行有限次,不会出现1+N过多的问题。
该方案的缺点是需要编写额外的java转换代码,重复工作量较大,不过,重复工作量也有对应的解决方案,可以使用代码生成器,来减少人工开发工作量。
总结
好了,到此为止,优雅的处理编码与文本之间转换工作的方案最终确定了。
对于数据字典,使用Redis缓存;对于外部实体,实体服务结合程序拼装查询处理,兼顾开发效率与运行效率。