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

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

  •  
  •   devswork · 67 天前 · 3457 次点击
    这是一个创建于 67 天前的主题,其中的信息可能已经有所发展或是发生改变。

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

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

    比如:
    class A {
    public Long aa;

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

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

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

    反正我不喜欢 MapStruct ,就像不喜欢 Lombok 一样
    sujin190
        14
    sujin190  
       66 天前
    @wolfie #11 这货烦人的就是不能像 Lombok 加个注解就搞定,interface 也不想加,否则就用 idea 的 generate 了
    angryfish
        15
    angryfish  
       66 天前
    1.项目中和数据库一致的,定义为 dto ,如果需要特别扩展属性的,加一个 vo 。其他什么 po,do 乱七八糟的一概不用。
    2.路径按照功能模块,房子啊 com.xxx.model
    3.mapstruct
    4.dto ,vo 混用。别介意。
    nothingistrue
        16
    nothingistrue  
       66 天前   ❤️ 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  
       66 天前
    oneisall8955
        18
    oneisall8955  
       66 天前
    @sujin190 #7 Mapstruct 编译的时候就生成代码的,会抛出异常
    qW7bo2FbzbC0
        19
    qW7bo2FbzbC0  
       66 天前
    @nothingistrue #16 只有 getter setter 的后缀是什么? RD => Record?
    lessMonologue
        20
    lessMonologue  
       66 天前
    @nothingistrue 大佬请问有没有什么比较标准的开源项目可以参考一下的
    MakHoCheung
        21
    MakHoCheung  
       66 天前
    看了下评论,能否通俗点?
    1. DAO 返回给 Service 的是只返回 Entity ( PO )吗,如果涉及到连表的话是不是要返回 DTO ?
    2. DAO 返回的 Entity 需要在 Service 转成 DTO 然后再 Controller 层转成 VO 返回给前端吗,如果三者内容一致呢,是不是有多余重复的复制操作了


    C#、Go 开发 Web 有这么多规范吗
    KingOfUSA
        22
    KingOfUSA  
       66 天前
    推荐这个库 https://github.com/ksprider/Surgical 至少在 controller 层可以无需转换成 vo ,也能将所需要的字段序列化掉,而不用新建一层 vo ,主要是这个 vo 复用性为 0 呐,
    aragakiyuii
        23
    aragakiyuii  
       66 天前
    基本都是贫血模型搞这么多概念干嘛。。。需要去区分字段的时候定义一个新的就好了
    Leviathann
        24
    Leviathann  
       66 天前
    一条垂直链路下来搞那么多纯属吃饱了撑的
    我们的项目只有 entity aggregate 输出输入专用对象 三种,如果算上部分字段查询结果的对象那就是四种
    逻辑都放在 aggregate 里
    leeg810312
        25
    leeg810312  
       66 天前
    我同时用.net 和 Java 开发,所以行业内 2 个平台有各自的设计偏好我都有了解,现在实践中发现只用 2 种数据模型就足够了,Entity 用于数据表映射和 DTO 数据传输,太多模型分类都是过度设计,徒增无用的复杂性。ORM 获取数据都是 Entity ,DTO 顾名思义数据传输对象,那么不管转给中间服务层或是控制器都是 DTO ,不再分出其他的概念,不像楼上有人说会用得少。DTO 类的设计原则是根据业务需要,裁剪、增加、拼装 Entity 的属性,甚至可以增加一些辅助的方法,用平台常用的对象映射工具 AutoMapper/MapStruct 等进行 Entity 和 DTO 之间的转换。
    twing37
        26
    twing37  
       66 天前
    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  
       66 天前 via Android
    我用 GitHub copilot 生成转换代码
    xuanbg
        28
    xuanbg  
       66 天前
    我的做法是 DTO 用来接收数据,VO 返回数据。然后它们相同的字段从一个不带 XO 的同名类继承。相互转换很简单啊,就是 A 序列化后再反序列化成 B 就行了呀。效率虽低,但基本没有影响。谁又会在意一个接口的响应时间慢了几个微秒呢。
    MonkeyJon
        29
    MonkeyJon  
       66 天前
    自己写个装换器或者 MapStruct
    securityCoding
        30
    securityCoding  
       66 天前 via Android   ❤️ 1
    这套东西扔了吧,一群学院派搞出来的高大上概念
    nothingistrue
        31
    nothingistrue  
       66 天前
    @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  
       66 天前
    一路看下来都是经验之谈主观看法,那么我还是坚持我的想法跟着自身项目走就可以,本身就是为了规范而存在的
    ye4tar
        33
    ye4tar  
       66 天前
    我一直想问各位大佬,mapstruct 中转换树形数据,不确定层级的嵌套数据有什么好办法,我用的时候每天被折磨的很难过,后来我用 TS 写代码,typeorm 就没有这个苦恼了
    Dlin
        34
    Dlin  
       66 天前
    这些概念,往往都只是个概念,实践起来麻烦琐碎。
    issakchill
        35
    issakchill  
       66 天前
    @ye4tar #33
    issakchill
        36
    issakchill  
       66 天前
    @issakchill 错手回复了空内容.. 如果是树形转树形 mapstruct 好像是自动递归的
    cco
        37
    cco  
       65 天前
    VO: 返回给前端,或者响应给请求者的对象。
    DTO: 请求参数、数据库查询列映射 负责数据传输。
    Entity: 和数据库表一一对应的实体对象。
    一般用这三个。这东西和接口与实现分包放一样,要不要都可以。对于数据库查询出来的数据,直接响应回去,那没必要 DTO (或者 Entity ) -> BO(可以理解为还是 DTO) -> VO ,直接返回即可。

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

    反射就能做到,用反射、Cglib 都封装过 bean copy 。
    关于   ·   帮助文档   ·   API   ·   FAQ   ·   我们的愿景   ·   广告投放   ·   感谢   ·   实用小工具   ·   4220 人在线   最高记录 5497   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 08:17 · PVG 16:17 · LAX 01:17 · JFK 04:17
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.