1.VO与数据实体的使用问题
最终方案:对于同一个实体信息的访问,大致的思路是,参数VO,返回调用方的VO以及数据实体。 调用方将参数VO传递到Controller, Contorller调用具体的servcie来获取数据模型, 最后将数据模型实例转化成返回值VO. 做两次解耦的原因如下,1.对于参数VO和数据模型的解耦,可以减少不必要数据属性的传递, 数据实体的属性信息也不用全部暴露给调用者。2.对于返回信息与数据模型的解耦,便于做权限控制,防止把多余的信息暴露给调用方。
*在分页查询时,增加对应的列表信息VO, 该VO专门用于列表信息的展示。
2. VO的分类,今天自己思考了VO的分类,最后觉得对于基础操作CRUD 可以按照按操作权限去分。例如,对于实体entity, 可以创建对应entityUpdateSaveVO,对应于保存和更新操作, 因为往往更新和保存操作可编辑的字段对于用户来说是比较一致的(如果差异很大建议分成两个独立VO)。另外,对于查看单个实体详情操作,可以单独建立entityDetailVO,这个VO里包含所有可以被调用方查看的信息。
结合1中所说,对于实体entity, 一般来说常用的VO有
-entityUpdateSaveVO: 包含用户可编辑的字段
-entityDetailVO:包含用户可查看的字段
-entityListVO: 包含列表信息字段(往往是entity的一少部分)
-entityListQueryParamVO:包含分页查询条件字段,根据条件分页查询满足条件的entity
3. 核心服务与具体业务扩展遇到的问题
在我们这个项目中,核心服务是指针对抽象核心业务实体提供的一些最基本的服务,目的就是可以让不同的具体业务都能使用这些核心服务,从而实现低成本的业务扩展和高复用的服务调用。当然这期间肯定会遇到各种中问题,这一小节专门记录。
3.1 核心业务术语与具体业务不一致的问题
提供的核心业务服务相当于一套抽象的业务描述,在编写这些服务的时候我们会自定义一套业务术语(或者取自使用最多的具体业务), 所以对应的数据实体,展示层实体的类名,属性名对应到具体业务中可能都会有变化。例如,在核心业务服务中,我们把一个业务实体一个属性叫做 coreField1, 但是在具体的业务中,同样的实体属性,它的名称可能是businessField1。 那么,对于各个模型类中的属性名,我们都需要做相应的转化。具体转化方式有如下几种:
1)调用核心服务后,new出具体业务模型类,然后getter/setter来转化。此种情况,只适合与类属性很少的情况,如果类的数量以及类属性的数量很多的话,就得在所有调用核心服务的地方写一大堆getter,setter.
2) 具体业务也使用和核心业务相同的数据模型,即类名属性名都相同,从而可以使用BeanUtil实现转化。这种做法虽然方便,但是会降低代码的可读性,即程序员在具体业务中编写代码时,都要在脑袋里做一次转化,这样很容易导致术语理解混淆不清晰。另外,后续的代码维护会出现同样的问题。
3) 注解的方式。使用自定义注解,在具体业务类属性上标注出核心业务类对应属性的名字,然后自己编写两个类相互转化的方法。
4) 继承的方式。具体业务模型类,继承核心的模型类。然后在自定义属性的getter/setter调用父类属性的getter/setter。 优点:简单易实现。缺点:要修改核心业务数据模型的字段属性(private -> protected)。
5) 组合的方式。 在具体业务模型类中声明一个核心业务类。然后再具体属性getter/setter时使用核心业务属性。本来个人认为这是最好的方式,所以给出实现说明如下。向比于3)这种方式简单直白,并且不会有很高的转化成本, 但是写完发现有个很严重的问题,就是每个相关的模型类都需要手动的去写这些getter/setter,类多字段多的时候真的会怀疑人生, 而且这些
1 //具体业务模型类 2 class BusinessEntity { 3 4 //对应核心业务模型类 5 private CoreEntity coreEntity; 6 //具体业务属性 7 private String BusinessField1; 8 9 public String getBusinessField1{ 10 return coreEntity.getCoreField1; 11 } 12 13 public void setBusinessField1(String businessField1){ 14 this.coreEntity.setCoreField1 = businessField1; 15 } 16 }
4. 多表连接条件查询--Mybatis Plus 的深坑
4.1 其实单纯说是Mybatis Plus(MP)的坑也不全面。事情是这样的,多表连接查询(如一对多关系)其实mybatis有很清楚的配置,就是在结果映射<ResultMap>里面配置好<Collection>或者<Association>就行了。但是这种方式,不适合可以根据条件去筛选多端实体的情况。比如,一个班级有多个学生,那么可能会有如下定义:
1 class ClassOne { 2 private String classId; 3 private String className; 4 //该班级里的学生 5 private List<Student> studentList; 6 }
如果只是根据ClassOne的本生的属性去去分页查班级信息和学生信息,那MP可以完美支持,只要配置好Collection属性,然后在select班级语句的后面加上对应的模糊查询判断即可。但是要根据学生的信息,来查询班级的情况,那就不好使了,比如要查询‘李’姓学生的所在班级情况,如果把查询条件通过<Collection>标签中的column属性带到查询学生的语句中去(比如,column(classId=classId,李=studentName)我也是在网上看到的,神奇的写法),条件是可以带过去的,但是如果所有班级都没有李姓同学,那么MP仍然会返回所有的班级实体,只是所有的studentList均为null。 显然这种情况不是我们需要的,我们需要的是整体结果返回为null. 所以这种情况还是老老实实写left join然后在最后加上所有可能的判断条件吧。
4.2 另一个MP和github pageHelper分页的神坑,就是在写left join的时候,在选择条件里写的select t1.fieldA, t2.fieldA from t1 left join t2 on t1.id = t2.id.会报“未明确定义列”的错误。但是这句语句在PL/SQL里面是可以执行的。后面检查发现,pageHelper分页会嵌套一层sql在select语句外面,从而导致t1, t2标识符失效,从而导致最终的sql 里面出现两个fieldA,然后触发了“未明确定义列”的错误。所以解决方法也很简单,起别名就好了,把select语句改成select t1.fieldA as t1Field, t2.fieldA as t2Field from t1 left join t2 on t1.id = t2.id. 问题就解决了,当然对应实体属性映射关系也要记得改一下!
4.3 MP和pageHelper的坑又来了, 问题大概是这样的, 因为有一对多关联查询,实体类对应关系大概就是如下
1 class Entity1{ 2 private String field1; 3 private List<Entity2> entity2List; 4 }
在查询的配置里配置了<Collection> 但是来关联list2,但是用left join的方式在分页查询的时候查出3条记录,但是如果其中有两个id相同的Entity1类型的关联记录,则MP会自动将其合并成两条记录,从而导致最终返回的记录数为2而不是3,从而导致分页混乱。因为时间问题,这里还是沿用两个框架(MP和PageHelper),解决方法是,查两次,第一次根据条件查询出Entity1实体信息与其关联的Entity2的id信息,然后再根据id信息单独查询出所有满足条件的entity2实例,并塞到对应的entity2List中。当然,可能会有人说可以现查出满足条件的Entity1, 然后再查关联的Entity2,可以这样做的场景是,查询条件不包括任何与Entity2实体相关的字段, 否则最终查出的记录数可能还是无法满足实际页数需求(手头的项目就是要根据Entity1和Entity2两个实体的条件来查询的)。这个方案虽然可行,但是肯定还有待优化。
*从遇到这个问题,我也想过,可能一对多关联分页查询不太适合用这种List来封装多端对象的情况(或者说当如果是一对多分页查询,就不太适合把Entity2中的内容也作为查询条件),如果查询条件只针对Entity1那是没问题的,但是如果同样涉及到Entity2,那么,可能直接如下组合会比较方便进行关联分页查询,当然,这样的话,当关联多个Entity2时会造成Entity1的数据冗余,但是也确实有些业务分页界面是冗余显示的,这个就得看具体需求了。不过这种组合方式只需要查一次库,所以效率肯定会更高。
1 class Entity1{ 2 private String field1; 3 private Entity2 entity; 4 }
4.4 Mybatis Plus 对于xml文件,会将值为false的boolean类型的变量自动转义成空字符,所以尽量不要传递到boolean类型的值到xml文件当中,更好的方案是用有意义的字符串。对于null值也一样,本次项目中有的值是让前端传过来null表示查询全部,但是这并不是一个好的实践。
7)经验教训:需求不明确对象的关系时,不要写代码。事情是这样的,项目中有两个实体对象的关系不明确,不知道是一对一,一对多还是多对多,后来,负责人说先按一对多来写,结果,写完一堆多之后,确定了是多对多关系,这真是日了狗了。后来加表,该关联,真是没事找事,以后一定记住,不清楚需求的就不要写!!
8)项目经验,初始阶段可以根据UI把字典值确定好,不要等到前后台对接的时候再确定。(会被催)
9)沙雕Maven, 自己打了一个jar包放到公司的Maven库上,然后引入Pom文件就是找不到,结果,把本地库中对应的引入文件夹除开jar之外的文件都删除才解决问题。
10)当查询某个时间段的数据记录时(具体到天),需要注意的是,对于时间的结束范围,需要将其设置成当天的23:59:59,如果不设置,XML文件传入生成的sql默认是当天的零点,显然是不正确的。
11) 业务编码的顺序,最好是insert -> update -> queryDetail -> queryByPage 一般来说实现难度会是递增的,然后新建保存的时候会对业务和业务实体有一个更好的了解,在之后写业务性比较强的查询代码时,会有一个更好的理解,可以避免很多麻烦。
12) 设计数据库表时,对于字典值属性,对照UI设计,或者根据经验,如果字段会需要支持模糊查询的话,最好把字典值的翻译也作为字段存在数据表中,否则,模糊查询会很麻烦。<经验>
13) Oracle字符相关
varchar2 最多存4000字节,1.当字符集为GBK时(2字节对应一个字符),可以存2000汉字;2.当字符集为UTF-8时,最多存1333个汉字,此时,varchar2(1333 char) 等价于 varchar(4000 char)
14) 关于批量删除接口,如果需要实现批量删除,注意处理删除失败时的逻辑;如果删除中发生异常导致部分实体删除失败, 一般处理可以分为两种,1.,操作失败,删除结果为操作前的结果。2.操作部分失败,以删除成功的实体被删除,未被删除的实体信息返回给用户。
15)对于任何设计,或者实现方案,应当综合考虑风险,风险出现的概率以及后果,来加以比较抉择。