【问题标题】:How to separate the dto mapping logic如何分离dto映射逻辑
【发布时间】:2020-09-28 01:40:48
【问题描述】:

我在代码中组织映射方法时遇到了一些问题。我正在控制器层中执行映射逻辑,但某些实体需要对每个操作(插入、更新和删除)使用不同的 dto。

我创建了一个具有 2 种类型的通用控制器:原始实体和 dto 类型。但是在这种情况下,这会导致它使用多个 dto 表示。我不确定创建 3 个泛型类型是否是处理此问题的好方法。

另一个问题是我的控制器层随着许多映射方法变得越来越大。即使使用 ModelMapper 作为自动映射器,在某些情况下我更喜欢自己做而不是创建复杂的转换器类。

如何组织我的 dto 映射代码,并且不让控制器使用大量映射方法过载?

P.S: 我的项目是一个带有 jax-rs、cdi 和 jpa 的 rest api

【问题讨论】:

  • 这个问题有点宽泛,但我的一个建议是使用 MapStruct 而不是 ModelMapper;根据我的经验,它更易于管理和定制(而且速度更快)。
  • 我还推荐 MapStruct,映射器在编译时而不是运行时生成,并且 MapStruct 映射器可通过 CDI 注入。

标签: java jakarta-ee design-patterns architecture mapping


【解决方案1】:

我建议你遵循以下规则:

  • 将提供 API 的 DTO 与其他域类分开,并使用命名约定来快速识别它(例如 XxxAPI)。您可以在资源专用包中组织:控制器、DTO 和映射器类。
  • 不要害怕编写大量代码,尤其是在映射器类中,您可以使用一些 IDE 技巧来生成和测试它们。
  • 小心使用 automapper,过多的魔法是危险的,您可以考虑使用 Builder 模式来促进您的 DTO/域映射

问候。

【讨论】:

    【解决方案2】:

    一种常见的方法是将 DTO 转换逻辑拆分为自己的类。根据项目的大小,创建存储库类也可能很有用。这给我们留下了三个类:

    1. 控制器:执行 REST 操作
    2. 存储库:从数据库或数据访问对象 (DAO) 中获取 DTO
    3. DTO 映射器:将 DTO 转换为域对象

    存储库允许我们对控制器完全隐藏 DTO。相反,控制器将只处理域对象,并且根本不知道发生了从 DTO 到域对象的转换。

    存储库(假设从FooDto 对象创建的Foo 域对象)是:

    public Foo {
    
        private long id;
        private String name;
    
        // ...getters & setters...
    }
    
    public interface FooRepository {
        public List<Foo> findAll();
        public Optional<Foo> findById(long id);
        public Foo create(long id, String name);
        public Foo update(long id, String name);
        public void delete(long id);
    }
    

    DTO 转换逻辑是:

    public class FooDto {
    
        private long id;
        private String name;
    
        // ...getters & setters...
    }
    
    public class FooDtoMapper {
    
        public Foo fromDto(FooDto dto) {
    
            Foo foo = new Foo();
            foo.setId(dto.getId();
            foo.setName(dto.getName();
    
            return foo;
        }
    
        public FooDto toDto(Foo foo) {
    
            FooDto dto = new FooDto();
            dto.setId(foo.getId();
            dto.setName(foo.getName();
    
            return dto;
        }
    }
    

    创建FooDtoMapper 后,我们可以创建FooRepository 实现:

    public class DatabaseFooRepository implements FooRepository {
    
        @Inject
        private DatabaseConnection dbConnection;
    
        @Inject
        private FooDtoMapper mapper;
    
        @Override
        public List<Foo> findAll() {
    
            return dbConnection.getAllFromCollection("FOO", FooDto.class)
                .stream()
                .map(mapper::fromDto)
                .collect(Collectors.toList());
        }
    
        // ...implement other methods
    }
    

    dbConnection 对象是从中提取 DTO 的数据库的抽象。在此示例中,我们可以假设 getAllFromCollection("FOO", FooDto.class) 返回一个 List&lt;FooDto&gt;,然后我们使用 FooDtoMapper 对象 (mapper) 将其流式传输并转换为 List&lt;Foo&gt;。在您的项目中,这可能会被替换为 JPA 特定的代码,但原理仍然相同:从 JPA 接口获取 DTO,并使用 mapper 对象将它们转换为域对象。

    这导致以下控制器逻辑:

    @Path("foo")
    @Controller
    public class FooController {
    
        @Inject
        private FooRepository repository;
    
        @GET
        public Response findAll() {
            List<Foo> foos = repository.findAll();
            Response.ok(foos);
        }
    
        // ...other controller methods...
    }
    

    使用这种模式,我们将 DTO 转换为自己的类的逻辑抽象出来,控制器只负责处理域对象。

    一般来说,最好有许多简单的类来做一件事,而不是将所有逻辑放在一个类中(如您的原始控制器)以希望减少类的数量。例如,FooDtoMapper 只负责将FooDto 对象转换为Foo 对象,反之亦然。 DatabaseFooRepository 仅负责从数据库中获取 DTO 并使用 FooDtoMapper 将 DTO 转换为域对象(即从数据库中获取域对象)。最后,FooController 只负责从FooRepository 获取域对象(即运行时的DatabaseFooRepository)并提供必要的REST API 元数据(即HTTP 状态OK)。

    请注意,在这种情况下,FooFooDto 对象是相同的,没有太多理由将 FooFooDto 对象分开(即,为什么不只存储 Foo数据库中的对象而不是 FooDto 对象?),但情况并非总是如此。通常,域对象和 DTO 会有所不同。例如,域对象可能具有必须转换为 String 或其他可以存储在数据库中的数据结构的货币金额或日期(DTO 将具有此 String 字段,而域对象将具有实际字段,例如货币或日期)。在本例中,为了简单起见,我将域对象和 DTO 设置为相同。

    【讨论】:

    • 您的 FooDto 是一种 bean 形式,因为它具有公共变量和 getter/setter,而不是实际的 DTO,后者是一种具有公共变量但没有函数的数据结构。对吗?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-11-23
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多