我应该使用模型类还是有效载荷类来序列化json响应

时间:2018-09-08 13:42:02

标签: java spring spring-boot spring-data-jpa

我将Spring Boot与mysql一起使用来创建Restful API。这是我如何返回json响应的一个示例。

首先我有一个模型:

@Entity
public class Movie extends DateAudit {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    private Date releaseDate;
    private Time runtime;
    private Float rating;
    private String storyline;
    private String poster;
    private String rated;

    @OneToMany(mappedBy = "movie", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<MovieMedia> movieMedia = new ArrayList<>();

    @OneToMany(mappedBy = "movie", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<MovieReview> movieReviews = new ArrayList<>();

    @OneToMany(mappedBy = "movie", cascade = CascadeType.ALL, orphanRemoval = true)
    private List<MovieCelebrity> movieCelebrities = new ArrayList<>();

    // Setters & Getters
}

并对应存储库:

@Repository
public interface MovieRepository extends JpaRepository<Movie, Long> {
}

我还有一个有效载荷类 MovieResponse ,它代表电影而不是电影模型,例如,如果我需要额外的字段或需要返回特定的字段。

public class MovieResponse {

    private Long id;
    private String name;
    private Date releaseDate;
    private Time runtime;
    private Float rating;
    private String storyline;
    private String poster;
    private String rated;
    private List<MovieCelebrityResponse> cast = new ArrayList<>();
    private List<MovieCelebrityResponse> writers = new ArrayList<>();
    private List<MovieCelebrityResponse> directors = new ArrayList<>();

    // Constructors, getters and setters

    public void setCelebrityRoles(List<MovieCelebrityResponse> movieCelebrities) {
        this.setCast(movieCelebrities.stream().filter(movieCelebrity -> movieCelebrity.getRole().equals(CelebrityRole.ACTOR)).collect(Collectors.toList()));
        this.setDirectors(movieCelebrities.stream().filter(movieCelebrity -> movieCelebrity.getRole().equals(CelebrityRole.DIRECTOR)).collect(Collectors.toList()));
        this.setWriters(movieCelebrities.stream().filter(movieCelebrity -> movieCelebrity.getRole().equals(CelebrityRole.WRITER)).collect(Collectors.toList()));
    }
}

如您所见,我将movieCelebrities列表分为3个列表(广播,Directos和作家)

然后将电影映射到 MovieResponse ,我正在使用ModelMapper类:

public class ModelMapper {

    public static MovieResponse mapMovieToMovieResponse(Movie movie) {

        // Create a new MovieResponse and Assign the Movie data to MovieResponse
        MovieResponse movieResponse = new MovieResponse(movie.getId(), movie.getName(), movie.getReleaseDate(),
                movie.getRuntime(),movie.getRating(), movie.getStoryline(), movie.getPoster(), movie.getRated());

        // Get MovieCelebrities for current Movie
        List<MovieCelebrityResponse> movieCelebrityResponses = movie.getMovieCelebrities().stream().map(movieCelebrity -> {

            // Get Celebrity for current MovieCelebrities
            CelebrityResponse celebrityResponse = new CelebrityResponse(movieCelebrity.getCelebrity().getId(),
                    movieCelebrity.getCelebrity().getName(), movieCelebrity.getCelebrity().getPicture(),
                    movieCelebrity.getCelebrity().getDateOfBirth(), movieCelebrity.getCelebrity().getBiography(), null);

            return new MovieCelebrityResponse(movieCelebrity.getId(), movieCelebrity.getRole(),movieCelebrity.getCharacterName(), null, celebrityResponse);
        }).collect(Collectors.toList());

        // Assign movieCelebrityResponse to movieResponse
        movieResponse.setCelebrityRoles(movieCelebrityResponses);
        return movieResponse;
    }
}

最后是我在控制器中调用的我的 MovieService 服务:

@Service
public class MovieServiceImpl implements MovieService {

    private MovieRepository movieRepository;

    @Autowired
    public void setMovieRepository(MovieRepository movieRepository) {
        this.movieRepository = movieRepository;
    }

    public PagedResponse<MovieResponse> getAllMovies(Pageable pageable) {

        Page<Movie> movies = movieRepository.findAll(pageable);

        if(movies.getNumberOfElements() == 0) {
            return new PagedResponse<>(Collections.emptyList(), movies.getNumber(),
                    movies.getSize(), movies.getTotalElements(), movies.getTotalPages(), movies.isLast());
        }

        List<MovieResponse> movieResponses = movies.map(ModelMapper::mapMovieToMovieResponse).getContent();
        return new PagedResponse<>(movieResponses, movies.getNumber(),
                movies.getSize(), movies.getTotalElements(), movies.getTotalPages(), movies.isLast());
    }
}

所以这里的问题是:对于每个模型都可以使用,我有一个用于json序列化的有效负载类吗?还是有更好的方法。
伙计们id我的代码有任何问题,请随时发表评论。

7 个答案:

答案 0 :(得分:1)

我们应该使每一层彼此分开。与您的情况一样,您已经定义了实体和响应类。这是分离事物的正确方法,我们永远不要在响应中发送实体。即使是要求的东西,我们也应该上课。

如果我们发送实体而不是响应dto,会出现什么问题。

  • 不可用于修改它们,因为我们已经向客户端公开了它

  • 有时我们不想序列化某些字段并作为响应发送。

存在一些将请求转换为域,将实体转换为域等的开销。但是可以保持组织性更好。 ModelMapper是实现翻译目的的最佳选择。

尝试使用构造注入而不是setter来实现任务依赖。

答案 1 :(得分:1)

始终建议将DTO和实体分开。 即使的结构,实体也应与 DB / ORM 进行交互,并且 DTO 应与客户端层(用于请求和响应的层)进行交互实体 DTO 相同。

此处的实体为Movie, DTO为MovieResponse

使用您现有的课程MovieResponse进行请求和响应。
切勿使用Movie类进行请求和响应。 并且类MovieServiceImpl应该包含用于将实体转换为 DTO 的业务逻辑,或者您可以使用 Dozer API进行自动转换。

分离的原因:

  • 如果您需要在“请求/响应”中添加/删除新元素,则无需更改太多代码
  • 如果2个实体具有2种方式映射(例如,一对多/多对多关系),则 如果对象具有嵌套数据,则无法创建JSON对象,这将在序列化
  • 时引发错误
  • 如果数据库或实体中的任何内容发生更改,那么(大多数情况下)不会影响 JSON响应。
  • 代码将清晰易维护。

答案 2 :(得分:1)

DTO是一种设计模式,它解决了从服务中获取最大有用数据的问题。

在像您这样的简单应用程序的情况下,DTO往往类似于Entity类。但是,对于某些复杂的应用程序,可以扩展DTO以合并来自各种实体的数据,从而避免对服务器的多次请求,从而节省了宝贵的资源和请求响应时间。

我建议不要在像这样的简单情况下重复代码,并使用模型类来响应API。将单独的响应类用作DTO不会解决任何目的,只会使代码维护变得困难。

答案 3 :(得分:1)

一方面,您应该将它们分开,因为有时在模型中使用的某些JPA注释不能与json处理器注释配合使用。是的,您应该将事情分开。

如果您以后决定更改数据层怎么办?您是否需要重写所有客户端?

另一方面,存在映射问题。为此,您可以使用性能损失较小的库。

答案 4 :(得分:1)

不久以前我就遇到了这个难题,这是我的思考过程。我在这里https://stackoverflow.com/questions/44572188/microservices-restful-api-dtos-or-not

仅公开域对象的优点

  1. 编写的代码越少,产生的错误越少。

    • 尽管我们的代码库中包含大量(可争论的)测试用例,但由于将域从域复制到DTO或从域复制到域而错失了错误,所以我遇到了错误。
  2. 可维护性-更少的样板代码。

    • 如果我必须添加一个新属性,则不必添加Domain,DTO,Mapper和测试用例。不要告诉我这可以通过使用反射beanCopy utils(如dozer或mapStruct)来实现,它违背了整个目的。
    • 龙目岛,Groovy,科特林,我知道,但这只会让我省却吸气塞特的头痛。
  3. 性能
    • 我知道这属于“过早的性能优化是万恶之源”的范畴。但这仍然可以节省一些CPU周期,因为不必每次请求至少创建一个对象(至少以后要进行垃圾收集)

缺点

  1. 从长远来看,DTO将为您提供更大的灵活性

    • 如果我需要那种灵活性。至少,到目前为止,我遇到的都是对HTTP的CRUD操作,我可以使用几个@JsonIgnores来进行管理。或者,如果有一个或两个字段需要使用杰克逊注释无法完成的转​​换,正如我之前所说,我可以编写自定义逻辑来处理该问题。
  2. 域对象因注释而肿。

    • 这是一个有效的问题。如果我使用JPA或MyBatis作为持久性框架,则域对象可能具有这些注释,那么也将有Jackson注释。如果您使用的是Spring Boot,则可以使用应用程序范围的属性,例如mybatis.configuration.map-underscore-to-camel-case: truespring.jackson.property-naming-strategy: SNAKE_CASE

短篇小说,至少就我而言,缺点并没有超过优点,因此以新的POJO作为DTO来重复自己毫无意义。更少的代码,更少的错误机会。因此,继续公开Domain对象,没有单独的“ view”对象。

免责声明:这可能适用于您的用例,也可能不适用。这个观察是我的用例(基本上是具有15个端点的CRUD api)

答案 5 :(得分:0)

尽管大多数人回答了使用DTO对象的利弊,但我想给我2美分。在我的情况下,DTO是必需的,因为并非从用户捕获数据库中所有持久化的字段。有一些字段是根据(其他字段的)用户输入来计算的,并且不会向用户公开。而且,它还可以减小有效负载的大小,从而在这种情况下可以提高性能。

答案 6 :(得分:0)

我主张将“Payload”或“Data”对象与“Model”或“Display”对象分开。几乎总是。这只会让事情更容易管理。

这是一个例子: 假设您需要访问一个 API,该 API 可为您提供有关待售猫的数据。然后将数据解析为猫模型对象并填充猫的列表,然后将其显示给用户。很酷。

但是现在您想要集成另一个 API 并从 2 个数据库中拉猫。但是你遇到了一个问题。一个 API 返回 furColor 表示颜色,而新 API 返回 catColor 表示颜色。

如果您使用相同的对象也显示信息,您有一些选择:

  • furColorcatColor 都添加到模型对象中,使它们都是可选的,并执行某种计算属性来检查设置了哪个并使用那个来显示颜色
    • 实际上,这很少是一种选择,因为响应通常与这样的一个值大不相同,因此无论如何您可能都需要一个全新的解析器
  • 添加一个新的数据对象,然后添加一个新的适配器,然后必须进行某种检查以了解何时使用哪个适配器
  • 其他仍然不漂亮或不好玩的东西

但是,如果您创建一个捕获响应的数据对象,然后创建一个仅包含填充列表所需信息的显示对象,这将变得非常简单:

  • 您有一个数据对象可以捕获来自第一个 API 的响应
  • 现在创建一个数据对象来捕获来自第二个 API 的响应
  • 现在您只需要某种简单的映射器来将响应映射到显示对象
  • 现在两者都将转换为一个通用的简单显示对象,并且可以使用相同的适配器来显示新的猫而无需额外工作

这也将使在本地存储数据更加干净。