V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
devswork
V2EX  ›  Java

Java 中的 VO、DTO、PO、DO 是如何定义和互相转换的?

  •  
  •   devswork · 2022-04-28 14:54:20 +08:00 · 5458 次点击
    这是一个创建于 721 天前的主题,其中的信息可能已经有所发展或是发生改变。

    1.项目中 VO 、DTO 、PO 、DO 都是怎么定义(概念)?包括这些 O 在项目 package 路径里是怎么定义的( com.xxx.model.vo )? 2.这些领域对象都是如何转换的?手动 new set get 非常痛苦 3.针对查询参数传递到 mybatis ,是否需要单独的写 xxxxParam 来传参呢? 4.若不写 VO ,是不是 swagger 里就不能显示响应格式、请求参数(body)了?

    39 条回复    2022-08-22 14:30:03 +08:00
    wolfie
        1
    wolfie  
       2022-04-28 14:57:19 +08:00
    mapstruct (快) 或者 BeanUtil (慢)
    TuringHero
        2
    TuringHero  
       2022-04-28 15:04:23 +08:00
    MapStruct 正解
    sinnosong1
        3
    sinnosong1  
       2022-04-28 15:05:18 +08:00
    sinnosong1
        4
    sinnosong1  
       2022-04-28 15:06:56 +08:00
    手动转换痛苦可以用用类似 C#中的"AutoMapper"的库,java 也有只是一般是个人开源项目。
    kytrun
        5
    kytrun  
       2022-04-28 15:33:25 +08:00
    推荐 IDEA 的插件 vo2dto ,减轻工作量: https://plugins.jetbrains.com/plugin/18262-vo2dto
    VeryZero
        6
    VeryZero  
       2022-04-28 15:37:21 +08:00
    将常用转换操作封装成静态方法写在对应的 POJO 里。

    比如:
    class A {
    public Long aa;

    public static B2A(B b){
    var a = new A();
    a.aa = B.bb;
    return a;
    }
    }
    sujin190
        7
    sujin190  
       2022-04-28 15:41:30 +08:00
    lombok 为啥不搞个这种支持。。用的地方这么多了,mapstruct 或者 BeanUtil 都不能完美支持静态类型检查,否则重构修改的时候不运行就不知道有问题
    aguesuka
        8
    aguesuka  
       2022-04-28 16:09:31 +08:00
    @sujin190 Java 是 Nominal type system, 如果完美支持静态类型检查就是 Structural type system 了, 而两个系统是冲突的.

    当然可以通过改 IDE 插件来做一个穷人版的, 有时间我写一个
    sujin190
        9
    sujin190  
       2022-04-28 16:18:19 +08:00
    @aguesuka #8 我的完美只是说比如 PO 改了字段类型或者删掉了某个字段,如果有 PO 转 DTO 的操作那么编译时就应该有提示,BeanUtil 这种运行时反射显然不行吧,不管 Java 是啥类型系统,我手写 getter setter 肯定是会报错毫无疑问的吧

    IDE 插件似乎想做运行时反射类型检查应该不那么容易做吧,否则就是 generate 了,似乎还是不如 lombok 这种方便吧
    jellywong
        10
    jellywong  
       2022-04-28 16:23:04 +08:00
    mapstruct
    wolfie
        11
    wolfie  
       2022-04-28 16:30:43 +08:00
    @sujin190 #9
    mapstruct 编译期类型不一致会抛异常。一般类型会帮转换。
    aguesuka
        12
    aguesuka  
       2022-04-28 16:32:11 +08:00
    @sujin190 Structural type 是像 ts 那样, 是编译时不是运行时的. 如果 DO 和 DTO 字段完全一样, 那么它们就是一个类型, 如果少数字段不一样, 我们可以用解构语法, 并且是静态安全的.
    Oktfolio
        13
    Oktfolio  
       2022-04-28 16:37:17 +08:00
    AutoMapper 类似的库有 orika-mapper 和 DozerMapper ,但是好像都不如 AutoMapper 好用

    反正我不喜欢 MapStruct ,就像不喜欢 Lombok 一样
    sujin190
        14
    sujin190  
       2022-04-28 16:50:13 +08:00
    @wolfie #11 这货烦人的就是不能像 Lombok 加个注解就搞定,interface 也不想加,否则就用 idea 的 generate 了
    angryfish
        15
    angryfish  
       2022-04-28 16:58:06 +08:00
    1.项目中和数据库一致的,定义为 dto ,如果需要特别扩展属性的,加一个 vo 。其他什么 po,do 乱七八糟的一概不用。
    2.路径按照功能模块,房子啊 com.xxx.model
    3.mapstruct
    4.dto ,vo 混用。别介意。
    nothingistrue
        16
    nothingistrue  
       2022-04-28 17:11:33 +08:00   ❤️ 1
    这个已经过时了,除非你是在改当前项目,不建议再去深究了。

    现在,不管是 Hibernate 还是 Mybatis plus ,不管是 DDD 还是非 DDD ,Entity 都是一个特殊的对象类型,这个很好区分,他是跟数据库的表映射或者绑定的(如果是 DDD ,它还有行为方法)。

    DTO 在是一个重对象,它还会一直用下去,但是很少会使用。它的区分也很简单,它是一个重对象,自带数据转换逻辑,并且通常跟工厂一起使用。只有 Getter/Setter 的轻对象,不会是 DTO 的,国内有些人把上下层之间传输的参数一律叫做 DTO ,这是很大的误区。

    然后剩下的,VO 、PO 、DO 什么的,它们原本的定义是跟层绑定的,一个层使用一种 O ,禁止跨层使用(层与层要额外通过 DTO 来隔离,比如 VO 经 DTO 转换成 DO )或者只允许上层使用下层的。这些已经死翘翘了。前面已经说了,DTO 很重,没有人会采用“禁止跨层使用”的方式,都是采用“只允许上层使用下层”方式。然后,既然允许上层调用下层了,那为什么不直接调用 Entity 呢,所以最后全部都用 Entity 了。

    简单来说,除了 Entity 、DTO ,剩下的本质上都是 Data ,为了层解耦才定义了那么多 O 。随着垂直分层模式的崩溃,这些 O 也崩溃了。

    至于楼主的其他问题。2 ,如果你要负责任的话,那就必须手动转换,再痛苦也要转,业务太多变了,这玩意工具的作用很有限,当然有缓解的手段,对于一些纯内部使用的类,你可以考虑 lombok 的 chain 或 fluent 模式。3 ,是,专项专用,但是你可以通过合理规划查询 SQL ,使得多个 SQL 公用一个参数类。4.如果你要负责任的话,那必须给 Swagger ,或者说 Web 这一层,定义专门的数据对象( Swagger 叫做 model ,内部可定义为 value object VO ,也可以就叫做 Data )。
    pocketz
        17
    pocketz  
       2022-04-28 18:37:46 +08:00
    oneisall8955
        18
    oneisall8955  
       2022-04-28 19:05:03 +08:00
    @sujin190 #7 Mapstruct 编译的时候就生成代码的,会抛出异常
    qW7bo2FbzbC0
        19
    qW7bo2FbzbC0  
       2022-04-28 19:13:15 +08:00
    @nothingistrue #16 只有 getter setter 的后缀是什么? RD => Record?
    lessMonologue
        20
    lessMonologue  
       2022-04-28 19:17:02 +08:00
    @nothingistrue 大佬请问有没有什么比较标准的开源项目可以参考一下的
    MakHoCheung
        21
    MakHoCheung  
       2022-04-28 20:04:57 +08:00
    看了下评论,能否通俗点?
    1. DAO 返回给 Service 的是只返回 Entity ( PO )吗,如果涉及到连表的话是不是要返回 DTO ?
    2. DAO 返回的 Entity 需要在 Service 转成 DTO 然后再 Controller 层转成 VO 返回给前端吗,如果三者内容一致呢,是不是有多余重复的复制操作了


    C#、Go 开发 Web 有这么多规范吗
    KingOfUSA
        22
    KingOfUSA  
       2022-04-28 20:32:24 +08:00
    推荐这个库 https://github.com/ksprider/Surgical 至少在 controller 层可以无需转换成 vo ,也能将所需要的字段序列化掉,而不用新建一层 vo ,主要是这个 vo 复用性为 0 呐,
    aragakiyuii
        23
    aragakiyuii  
       2022-04-28 21:41:06 +08:00
    基本都是贫血模型搞这么多概念干嘛。。。需要去区分字段的时候定义一个新的就好了
    Leviathann
        24
    Leviathann  
       2022-04-28 21:47:20 +08:00
    一条垂直链路下来搞那么多纯属吃饱了撑的
    我们的项目只有 entity aggregate 输出输入专用对象 三种,如果算上部分字段查询结果的对象那就是四种
    逻辑都放在 aggregate 里
    leeg810312
        25
    leeg810312  
       2022-04-28 22:03:35 +08:00
    我同时用.net 和 Java 开发,所以行业内 2 个平台有各自的设计偏好我都有了解,现在实践中发现只用 2 种数据模型就足够了,Entity 用于数据表映射和 DTO 数据传输,太多模型分类都是过度设计,徒增无用的复杂性。ORM 获取数据都是 Entity ,DTO 顾名思义数据传输对象,那么不管转给中间服务层或是控制器都是 DTO ,不再分出其他的概念,不像楼上有人说会用得少。DTO 类的设计原则是根据业务需要,裁剪、增加、拼装 Entity 的属性,甚至可以增加一些辅助的方法,用平台常用的对象映射工具 AutoMapper/MapStruct 等进行 Entity 和 DTO 之间的转换。
    twing37
        26
    twing37  
       2022-04-28 22:50:32 +08:00
    1. VO 如果你写过脚本语言,如 php, 混在 V 层的模板对象就是 VO,
    2. DTO 前后端分类的,接口对象.
    > 注 1: 1 与 2 的区别在于, 举个例子, 如 性别字段, vo 里面是男, dto 里面是 1,
    3. 需要. 比如, http/grpc 的 json 与 proto --> service(use case)的逻辑层对象,
    4. 参见 1 与 2 的区别.
    > 注 2: 在足够简单的项目中,确定架构不会改变的情况下,请忽略这些.不管是 clean arch 还是 ddd 的六边形
    都不如梭哈. 我在之前的类似答案中也写过,在这个前提下,不如面条代码写来的更好.比如在 repo 下,只是简单的 curd,
    完全可以嵌入整个 store 逻辑,因为该逻辑很难再去复用.
    hingbong
        27
    hingbong  
       2022-04-29 00:07:54 +08:00 via Android
    我用 GitHub copilot 生成转换代码
    xuanbg
        28
    xuanbg  
       2022-04-29 06:59:11 +08:00
    我的做法是 DTO 用来接收数据,VO 返回数据。然后它们相同的字段从一个不带 XO 的同名类继承。相互转换很简单啊,就是 A 序列化后再反序列化成 B 就行了呀。效率虽低,但基本没有影响。谁又会在意一个接口的响应时间慢了几个微秒呢。
    MonkeyJon
        29
    MonkeyJon  
       2022-04-29 08:40:38 +08:00
    自己写个装换器或者 MapStruct
    securityCoding
        30
    securityCoding  
       2022-04-29 09:14:38 +08:00 via Android   ❤️ 1
    这套东西扔了吧,一群学院派搞出来的高大上概念
    nothingistrue
        31
    nothingistrue  
       2022-04-29 09:27:19 +08:00
    @hjahgdthab750 #19 后缀是由架构或规范决定的,没有特定规则。光凭“只有 getter setter ”这一个特点,是不知道这个类是干啥的,还要结合其他特点才能确切直到这个类是干啥的,之后才能命名。

    @lessMonologue #20 没有。就算有也不具备参考意义,因为这本来就是规范不是规则,是随团队变化的。

    @MakHoCheung #21 再严格一点,上下层之间不管调用还是返回都只能是 DTO ,DTO 负责对象的转换。例:Service 利用 DTO 的静态方法或者工厂方法将 BO 扔进 DTO ,再用这个 DTO 作为调用 DAO 的参数;反过来就是 DAO 将 Entity 利用工厂方法将扔进 DTO ,再返回 DTO 。这三者内容不可能一致的,Entity 是 ORM 的托管对象会带有 ORM 的信息,DTO 要负责数据转换会带有业务逻辑,BO 为 Service 服务会有业务逻辑的中间数据。然而,这随不是重复操作,但是属于杀鸡用牛刀的操作。

    @leeg810312 #25 你这个好,用 DTO 把数据转换和数据对象都给包了,就只剩下 Entity 和 DTO 两种,没有纯数据的 Data 了。
    notwaste
        32
    notwaste  
       2022-04-29 10:05:30 +08:00
    一路看下来都是经验之谈主观看法,那么我还是坚持我的想法跟着自身项目走就可以,本身就是为了规范而存在的
    LLaMA2
        33
    LLaMA2  
       2022-04-29 14:33:05 +08:00
    我一直想问各位大佬,mapstruct 中转换树形数据,不确定层级的嵌套数据有什么好办法,我用的时候每天被折磨的很难过,后来我用 TS 写代码,typeorm 就没有这个苦恼了
    Dlin
        34
    Dlin  
       2022-04-29 14:48:18 +08:00
    这些概念,往往都只是个概念,实践起来麻烦琐碎。
    issakchill
        35
    issakchill  
       2022-04-29 15:20:50 +08:00
    @ye4tar #33
    issakchill
        36
    issakchill  
       2022-04-29 15:23:59 +08:00
    @issakchill 错手回复了空内容.. 如果是树形转树形 mapstruct 好像是自动递归的
    cco
        37
    cco  
       2022-04-29 16:46:08 +08:00
    VO: 返回给前端,或者响应给请求者的对象。
    DTO: 请求参数、数据库查询列映射 负责数据传输。
    Entity: 和数据库表一一对应的实体对象。
    一般用这三个。这东西和接口与实现分包放一样,要不要都可以。对于数据库查询出来的数据,直接响应回去,那没必要 DTO (或者 Entity ) -> BO(可以理解为还是 DTO) -> VO ,直接返回即可。

    至于转换关系。mapstruct 、BeanUtil 都可以,不管什么都不可避免要手动加点进去。如果你查出来的列名和你要响应的字段都不一致呢?这些类库以及 idea 的插件也只是方便而已,不可能实现一键转换。除非你转换前和转换后几乎是一模一样的。
    kahlkn
        38
    kahlkn  
       2022-05-11 13:15:27 +08:00
    @sujin190 字段删掉的话,bean 就不 copy 字段,至于改字段类型,我是做了转换工具去转换得,总体上还是保留 不报错。 但是类型不一致是可以报错的,只是这些工具在实现的时候的考量可能是不报错。

    反射就能做到,用反射、Cglib 都封装过 bean copy 。
    generalfu
        39
    generalfu  
       2022-08-22 14:30:03 +08:00
    @kytrun 赞的很,好东西!
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   我们的愿景   ·   实用小工具   ·   5399 人在线   最高记录 6543   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 30ms · UTC 03:36 · PVG 11:36 · LAX 20:36 · JFK 23:36
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.