DTO/DO等POJO对象的使用场景和 orika-mapper 框架的使用
对于项目而言, 我们一般会有DAO->Service->Controller分层设计, 这些层次体现了每层的作用, 而层次之间的数据传递对象设计很少被提及, 下面是一个相对完整的数据转换过程:
Table层--(DO对象)-->DAO层--(DO对象)-->Service层--(DTO对象)-->Controller层--(VO对象)-->Web Template层
DO(domain object) 领域对象, 也有一种叫法是 entity object, 个人不推荐使用 entity object这个叫法, 因为Relationship 表也可以有DO 类.
1. 一般DO类和数据表是一一对应, 属性和字段也是一一对应的.
2. 对于 relationship 表, 一般也需要有一个对应的 DO 类.
3. DO 类中不应该包含复杂的逻辑, 仅仅是一些简单的 setter() 和 getter() 方法.
4. 比如 user 表有一个 deptId 字段, user entity 类必须有一个 deptId 属性, 不推荐有一个对应的 dept Entity 对象, 当然更不应该直接包含 dept entity 对象的属性, 比如 deptName 等.
5. 多个 DO 类之间的关系和数据模型一样, 是满足第三范式.
6. DO类名: 以DO作为后缀, 或者不加DO这样的后缀.
DTO(Data Transfer Object) 数据传输对象:
1. 用于"跨进程或远程"传输, 对象的序列化/反序列化的网络开销较大, 这时就需要加入 DTO 对象, 典型的使用场景是用来封装Rest API接口. 如果是一个单体应用, 专门维护一套DTO类, 成本和收益相比, 引入DTO意义就不大了.
2. DTO 是一个贫血对象, 它不应包含"业务"处理逻辑, 主要是一些属性和getter和setter访问器, 也可包含一些属性重组逻辑.
3. 多个 DTO 类之间的关系一般不再满足第三范式, 而是反范式.
4. DTO类名: 应该以DTO作为后缀.
5. 用于解耦实体对象的存储层和上层, 这包含下面的好处:
(1): 隐藏部分底层表的属性, 以减少网络传输的代价.
(2): 在存储层和上层增加一个隔离, 屏蔽相互之间的影响.
(3): 用来可封装多个DTO对象, 比如user DTO对象可以包含一个Dept DTO对象; 或者将Dept DTO某些常用属性直接flatten到User DTO对象上, 方便上层的使用. 所以, 一般情况下DTO类的数量要比DO类要少.
(4): 如果没有DTO, 很多时候 Controller 层不得不直接最低层的 Entity 类, 跨层数据对象依赖将使得分层设计大打折扣.
VO(View Object/Value object)视图对象:
专门用于展现层(比如页面展现等).
对于一般的项目, VO 和 DTO 属性基本一致, 没有必要再维护一套VO类.
在前后端分离的大背景下, 即使是大型项目, 也没有必要再维护一套VO类.
Pojo(plain old java object) 普通java对象:
上述的DO/DTO/VO对象都属于Pojo对象. 阿里巴巴规范中有如下要求:
1. Pojo 类属性必须使用包装数据类型, 而不是基本数据类型, 理由是: 数据库中由可能是null值, 如果使用基本类型, 由可能导致自动拆箱异常.
2. 不能为任何属性设定默认值. 理由是: 强制使用者在使用时显式赋值, 任何NPE问题都应由使用者来保证.
3. Pojo 类都必须实现 toString() 方法, 可以使用 IDE 的 source/generate toString() 功能
4. Pojo 类都必须实现 Serializable 接口.
下图能形象展现 DO 类和 DTO 类的区别:
下图以基于角色的权限管理模块为例, 展现了 table/DO对象/DAO层/Service层 对应关系:
(1) table 是采用范式建模, 共三个实体表, 两个关系表.
(2) pojo和table直接对应,
(3) 在Dao层重点关注实体的操作, 所以就只有三个dao对象, 关系表的维护退化到实体操作中.
(4) 在service层, 是面向顶层使用, 重点考虑的是使用如何方便, 一般也都和Dao的类数量一致.
=====================================
object-object mapping framework 清单
=====================================
正如ORM过程, 我们可以手写代码, 也可以使用MyBatis这样的ORM 工具, 对于DO/DTO的转换也是一样, 可以手写代码转换, 也可以使用object-object mapping framework完成自动转换, 当然也能用在对象之间需要深度拷贝的时候.
当前活跃的框架清单:
https://github.com/mapstruct/mapstruct
https://github.com/DozerMapper/dozer
https://github.com/orika-mapper/orika
https://github.com/modelmapper/modelmapper
性能对比:
https://github.com/arey/java-object-mapper-benchmark
https://www.baeldung.com/java-performance-mapping-frameworks
综合考虑性能/流行度/项目活跃程度, mapstruct 明显领先, 不过我这里更推荐使用 orika-mapper, 原因是:
1. 使用 mapstruct的话, IDE中需要引入 mapstruct 编译插件, 我们自己负责编写mapper interface, mapstruct 负责生成mapper实现类的class文件, 工程化成本较高, 可控性较差.
2. orika-mapper 相对更容易推广, mapping 逻辑可控性较强, 性能也算不错.
=====================================
orika-mapper 使用
=====================================
pom.xml 引入依赖
<dependency> <groupId>ma.glasnost.orika</groupId> <artifactId>orika-core</artifactId> <version>${orika.version}</version> </dependency>
--------------------------------
简单示例:
--------------------------------
Orika 要求 Pojo 类必须是public级别,否则会在运行时报错 java.lang.IllegalAccessError.
只要src/target属性名一致, Orika 能自动完成所有的属性的深拷贝, 甚至是 List<T> 这样的属性. 需要说明的是, 默认情况下定义的映射规则是双向的, 也就是说, 如果从 target object 也能得到 src object.
//准备 src 对象 PersonSource source = new PersonSource(); //生成 MapperFactory 对象, 然后使用 MapperFactory 对象注册 src/target 映射关系, 本例 src/target 的属性名完全一致, 所以不需要再设置映射关系. MapperFactory mapperFactory = new DefaultMapperFactory.Builder().build(); // MapperFacade 对象负责对象之间的映射 MapperFacade mapper = mapperFactory.getMapperFacade(); //完成DO->DTO的映射 PersonDest destination = mapper.map(source, PersonDest.class);
--------------------------------
更复杂的示例:
--------------------------------
如果src/target属性名不一致, 或者要重组DO属性, 这时需要自定义src/target的映射规则.
//自定义 src/target class的映射规则 mapperFactory.classMap( Source.class, Destination.class) .field(......) //src/target 双向映射 .fieldAToB(......) //src->target的单向映射 .fieldBToA(......) //target->src的单向映射 .exclude(......) //不考虑指定的属性 .byDefault() .register();
mapperFactory 除了负责注册src/target 映射规则, 还可以 mapperFactory.getConverterFactory().registerConverter()来注册自定义类型转换器, 比如要将 Date 类型转换为 String类型, 实际项目中, 一般不需要自定义类型转换器, 因为 json 框架也有这样的功能.
//定义一个类型转换器 MyConverter public class MyConverter extends CustomConverter<Date,MyDate>{} //注册类型转换器 mapperFactory.getConverterFactory().registerConverter(new MyConverter()); // 然后仍然通过 mapper 完成src/target对象的深度拷贝. MapperFacade mapper = mapperFactory.getMapperFacade();
=====================================
参考
=====================================
https://cloud.tencent.com/developer/article/1110666
http://tech.dianwoda.com/2017/11/04/gao-xing-neng-te-xing-feng-fu-de-beanying-she-gong-ju-orika/?utmsource=tuicool&utmmedium=referral