分类
REST spring

Spring REST实体(entity)与数据传输对象(dto)间的转换

Entity To DTO Conversion for a Spring REST API

资源列表:

在正式开始前,可以点以下链接获取一份与本文相同的初始化代码。

1. 概述

在实际的开发中,直接将内部的实体返回给客户端并不是明智的选择。这不仅直接暴露了项目的数据结构,还需要使用JsonView定制返回的数据字段,最主要的一旦实体结构发生变更,将直接影响发布的API文档。

本文将阐述如何将Spring内部实体转换为外部数据传输对象(DTOs ---- Data Transfer Objects)的方法,使用该方法返回数据很好的规避上述问题。

2. Model Mapper

我们将使用一款名为ModelMapper的entity-DTO转换库来完成这一功能。

首先打开pom.xml并添加如下依赖:

<dependency>
    <groupId>org.modelmapper</groupId>
    <artifactId>modelmapper</artifactId>
    <version>2.3.5</version>
</dependency>

如果你想用最新的版本的ModelMapper,请点击查看

接下来在Spring配置类中如下定义ModelMapper bean。

@Bean
public ModelMapper modelMapper() {
    return new ModelMapper();
}

3. DTO

接下来让我们建立第一个DTO类 -- Post DTO

public class PostDto {
    private static final SimpleDateFormat dateFormat
      = new SimpleDateFormat("yyyy-MM-dd HH:mm");
 
    private Long id;
 
    private String title;
 
    private String url;
 
    private String date;
 
    private UserDto user;
 
    public Date getSubmissionDateConverted(String timezone) throws ParseException {
        dateFormat.setTimeZone(TimeZone.getTimeZone(timezone));
        return dateFormat.parse(this.date);
    }
 
    public void setSubmissionDate(Date date, String timezone) {
        dateFormat.setTimeZone(TimeZone.getTimeZone(timezone));
        this.date = dateFormat.format(date);
    }
 
    // 以下省略标准的getters与setters
}

在上述代码中我们使用了两个时间相关的方法来对Date日期类型进行转换(将Date类型的日期转换为String类型的日期),该方法为服务端的日期格式(Date类型的日期)与客户端的日期格式(String类的日期)搭起一做转换的桥梁。

  • getSubmissionDateConverted()方法将String类型的日期根据时区转换为Date类型的日期。该方法将用于持久化Post实体时。
  • setSubmissionDate()方法将Date类型的日期依据时区转换为String类型的日期。

4. 服务层

服务层(比如PostServiceImpl.java)的示例代码如下:

@Service
public class PostServiceImpl implements PostService {
    @Autowired
    PostRepository postRepository;
    @Override
    public List<Post> findAll() {
        return (List<Post>) this.postRepository.findAll();
    }

    @Override
    public Post save(Post post) {
        return this.postRepository.save(post);
    }

    @Override
    public Post findById(Long id) {
        return this.postRepository.findById(id).get();
    }
}

接下来,我们将重点放在调用服务层(service layer)的控制器层(controller layer),这是因为实际的数据转换将发生在控制器层。

5. 控制器层

下面,让我们看一个标准的控制器实现,为Post资源暴露一个简单的REST接口。

以下代码中将展示一些简单的CRUD操作:创建、更新、获取某条数据以及获取全部数据。CRUD是常规的操作,所以在阅读以下代码时,可以重点关注实体与数据传输对象的转换过程(Entity-DTO conversion):

@RestController
public class PostRestController {

    @Autowired
    PostService postService;
    @Autowired
    ModelMapper modelMapper;

    @PostMapping
    public PostDto save(@RequestBody PostDto postDto) throws ParseException {
        Post post = this.convertToEntity(postDto);
        return this.convertToDto(this.postService.save(post));
    }

    @GetMapping("/{id}")
    public PostDto getById(@PathVariable Long id) {
        return this.convertToDto(this.postService.findById(id));
    }

    @GetMapping
    public List<PostDto> getAll() {
        return this.postService.findAll()
                               .stream().map(this::convertToDto)
                               .collect(Collectors.toList());
    }
}

以下代码展示了实体Post向数据传输对象PostDto的转换过程:

    private PostDto convertToDto(Post post) {
        PostDto postDto = modelMapper.map(post, PostDto.class);
        postDto.setSubmissionDate(post.getSubmissionDate(), "GMT+8:00");
        return postDto;
    }

以下代码展示了数据传输对象PostDto像实体Post的转换过程:

    private Post convertToEntity(PostDto postDto) throws ParseException {
        Post post = modelMapper.map(postDto, Post.class);
        post.setSubmissionDate(postDto.getSubmissionDateConverted(
                "GMT+8:00"));

        return post;
    }

从以上代码不难看出,ModelMapper使得实体与数据传输对象(DTO)之间的转换变得即快又简单 ---- 我们使用了ModelMappermap接口,无需编写任何转换的代码便实现了数据转换。

6. 单元测试

最后,让我们做个简单的测试以保障本文前面涉及代码的正确性:

public class PostDtoUnitTest {
 
    private ModelMapper modelMapper = new ModelMapper();
 
    @Test
    public void whenConvertPostEntityToPostDto_thenCorrect() {
        Post post = new Post();
        post.setId(1L);
        post.setTitle(randomAlphabetic(6));
        post.setUrl("www.test.com");
 
        PostDto postDto = modelMapper.map(post, PostDto.class);
        assertEquals(post.getId(), postDto.getId());
        assertEquals(post.getTitle(), postDto.getTitle());
        assertEquals(post.getUrl(), postDto.getUrl());
    }
 
    @Test
    public void whenConvertPostDtoToPostEntity_thenCorrect() {
        PostDto postDto = new PostDto();
        postDto.setId(1L);
        postDto.setTitle(randomAlphabetic(6));
        postDto.setUrl("www.test.com");
 
        Post post = modelMapper.map(postDto, Post.class);
        assertEquals(postDto.getId(), post.getId());
        assertEquals(postDto.getTitle(), post.getTitle());
        assertEquals(postDto.getUrl(), post.getUrl());
    }
}

7. 总结

本文展示了使用ModelMapper来完成实体与数据传输对象(DTO) 之间的快速转换方法,相较于传统的转换方法,在使用该方法进行Entity-DTO转换时可以大幅减少代码的书写量。

尺有所短、寸有所长,本文展示的方法并不能很好的适应实体字段名称、实体结构变更的情况,在使用本方法的过程中还应该结合良好的控制器单元测试来保障项目的开发质量。