Spring boot Restful API: 使用 ModelMapper 将具有关系的 DTO 转换为实体?
Spring boot Restful API: DTO with relationships convert to entity using ModelMapper?
我现在对如何在 Rest API 和 Spring 中进行 CRUD 感到困惑。
让我解释一下,我有两条通往 POST 和 PUT 实体的路线。我为此创建了两个 DTO createPostRequest
和 updatePostRequest
。因为在添加时,属性不能为空,而在更新时可以(忽略空属性)。
问题一:
在我的前端,要求用户从数据库中选择一个标签列表 (multi select html)。这就是为什么 createPostRequest
有一个 tags
属性 类型 TagDTO
。但是,我如何使用 modelMapper 将 createPostRequest
映射到 Post
实体以确保标签存在于数据库中?
例如,如果用户尝试插入一个不存在的标签,我正在考虑做这样的事情:
postEntity.setTags(tagService.findAllByIds(postEntity.getTagsId()));
这在代码中造成了很多重复,因为在服务中我的实体的创建和更新方法之间,有很多相同的代码。
问题二:
基于我的问题 1,我怎样才能轻松地将我的两个 DTO 映射到同一个实体而不重复代码 2x?
代码示例- PostService
(见评论)
这是更新的示例,但 create
的代码几乎相同,我该如何继续?
@Transactional
public Post update(Integer postId, UpdatePostRequest request) {
return Optional.ofNullable(this.getById(postId)).map(post -> {
// here how to map non-null properties of my request
// into my post taking in consideration my comment above?
postDAO.save(post);
return post;
}).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
}
================================
更新:
根据要求,找到下面的代码。
控制器:
@RestController
@RequestMapping("/v1/posts")
public class PostController {
RequestMapping(method = RequestMethod.POST, consumes = "application/json", produces = "application/json; charset=UTF-8")
public ResponseEntity<Object> update(@Valid @RequestBody CreatePostRequest createPostRequest) {
Post post = postService.create(createPostRequest);
return new ApiResponseHandler(new PostDTO(post), HttpStatus.OK).response();
}
RequestMapping(value = "/{postId}", method = RequestMethod.PUT, consumes = "application/json", produces = "application/json; charset=UTF-8")
public ResponseEntity<Object> update(@Valid @RequestBody UpdatePostRequest updatePostRequest, @PathVariable Integer postId) {
Post post = postService.update(postId, updatePostRequest);
return new ApiResponseHandler(new PostDTO(post), HttpStatus.OK).response();
}
}
创建发布请求:
@Data
public class CreatePostRequest {
@NotNull
@Size(min = 10, max = 30)
private Sting title;
@NotNull
@Size(min = 50, max = 600)
private String description
@NotNull
@ValidDateString
private String expirationDate;
@NotNull
private List<TagDTO> tags;
public List<Integer> getTagIds() {
return this.getTags().stream().map(TagDTO::getId).collect(Collectors.toList());
}
}
更新发布请求:
@Data
public class UpdatePostRequest {
@Size(min = 10, max = 30)
private Sting title;
@Size(min = 50, max = 600)
private String description
@ValidDateString
private String expirationDate;
private List<TagDTO> tags;
public List<Integer> getTagIds() {
return this.getTags().stream().map(TagDTO::getId).collect(Collectors.toList());
}
}
服务:
@Service
@Transactional
public class PostService {
@Transactional
public Post create(CreatePostRequest request) {
ModelMapper modelMapper = new ModelMapper();
Post post = modelMapper.map(request, Post.class);
// map will not work for tags : how to check that tags exists in database ?
return postDAO.save(post);
}
@Transactional
public Post update(Integer postId, UpdatePostRequest request) {
return Optional.ofNullable(this.getById(postId)).map(post -> {
ModelMapper modelMapper = new ModelMapper();
modelMapper.getConfiguration().setSkipNullEnabled(true);
modelMapper.map(request, post);
// map will not work for tags : how to check that tags exists in database ?
postDAO.save(post);
return post;
}).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
}
}
为避免重复两个相似的 DTO,您可以使用 @Validated
组验证。这允许您主动设置要对每个 属性 进行哪些验证。您可以在以下在线资源 https://www.baeldung.com/spring-valid-vs-validated 中阅读更多相关信息。您将从创建两个市场界面开始:
interface OnCreate {}
interface OnUpdate {}
然后您可以将这些标记接口与您的通用 DTO 中的任何约束注释一起使用:
@Data
public class CreateOrUpdatePostRequest {
@NotNull(groups = OnCreate.class)
@Size(min = 10, max = 30, groups = {OnCreate.class, OnUpdate.class})
private Sting title;
@NotNull(groups = OnCreate.class)
@Size(min = 50, max = 600, groups = {OnCreate.class, OnUpdate.class})
private String description
@NotNull(groups = OnCreate.class)
@ValidDateString(groups = {OnCreate.class, OnUpdate.class})
private String expirationDate;
@NotNull(groups = OnCreate.class)
private List<TagDTO> tags;
public List<Integer> getTagIds() {
return this.getTags().stream().map(TagDTO::getId).collect(Collectors.toList());
}
}
最后,您只需要相应地在控制器中注释您的方法:
@RestController
@RequestMapping("/v1/posts")
@Validated
public class PostController {
@RequestMapping(method = RequestMethod.POST, consumes = "application/json", produces = "application/json; charset=UTF-8")
public ResponseEntity<Object> update(@Validated(OnCreate.class) @RequestBody CreateOrUpdatePostRequest createPostRequest) {
Post post = postService.create(createPostRequest);
return new ApiResponseHandler(new PostDTO(post), HttpStatus.OK).response();
}
@RequestMapping(value = "/{postId}", method = RequestMethod.PUT, consumes = "application/json", produces = "application/json; charset=UTF-8")
public ResponseEntity<Object> update(@Validated(OnUpdate.class) @RequestBody CreateOrUpdatePostRequest updatePostRequest, @PathVariable Integer postId) {
Post post = postService.update(postId, updatePostRequest);
return new ApiResponseHandler(new PostDTO(post), HttpStatus.OK).response();
}
}
有了这个,你就可以拥有单一的映射功能了。
不过请记住,考虑到我们混合了不同的关注点,使用验证组很容易成为一种反模式。对于验证组,经过验证的实体必须知道它所用的所有用例的验证规则。话虽如此,除非确实有必要,否则我通常会避免使用验证组。
关于 tags
我猜你唯一的选择是查询数据库。那些不存在的你应该创建它们(我猜),所以遵循以下几行:
List<Integer> tagsId = createOrUpdatePostRequest.getTagsId();
List<Tag> tags = tagService.findAllByIds(tagsId);
List<Integer> nonExistentTagsId = tagsId.stream().filter(id -> tags.stream().noneMatch(tag -> tag.getId().equals(id)));
if (!nonExistentTagsId.isEmpty()) {
// create Tags and add them to tags List
}
我现在对如何在 Rest API 和 Spring 中进行 CRUD 感到困惑。
让我解释一下,我有两条通往 POST 和 PUT 实体的路线。我为此创建了两个 DTO createPostRequest
和 updatePostRequest
。因为在添加时,属性不能为空,而在更新时可以(忽略空属性)。
问题一:
在我的前端,要求用户从数据库中选择一个标签列表 (multi select html)。这就是为什么 createPostRequest
有一个 tags
属性 类型 TagDTO
。但是,我如何使用 modelMapper 将 createPostRequest
映射到 Post
实体以确保标签存在于数据库中?
例如,如果用户尝试插入一个不存在的标签,我正在考虑做这样的事情:
postEntity.setTags(tagService.findAllByIds(postEntity.getTagsId()));
这在代码中造成了很多重复,因为在服务中我的实体的创建和更新方法之间,有很多相同的代码。
问题二:
基于我的问题 1,我怎样才能轻松地将我的两个 DTO 映射到同一个实体而不重复代码 2x?
代码示例- PostService
(见评论)
这是更新的示例,但 create
的代码几乎相同,我该如何继续?
@Transactional
public Post update(Integer postId, UpdatePostRequest request) {
return Optional.ofNullable(this.getById(postId)).map(post -> {
// here how to map non-null properties of my request
// into my post taking in consideration my comment above?
postDAO.save(post);
return post;
}).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
}
================================
更新:
根据要求,找到下面的代码。
控制器:
@RestController
@RequestMapping("/v1/posts")
public class PostController {
RequestMapping(method = RequestMethod.POST, consumes = "application/json", produces = "application/json; charset=UTF-8")
public ResponseEntity<Object> update(@Valid @RequestBody CreatePostRequest createPostRequest) {
Post post = postService.create(createPostRequest);
return new ApiResponseHandler(new PostDTO(post), HttpStatus.OK).response();
}
RequestMapping(value = "/{postId}", method = RequestMethod.PUT, consumes = "application/json", produces = "application/json; charset=UTF-8")
public ResponseEntity<Object> update(@Valid @RequestBody UpdatePostRequest updatePostRequest, @PathVariable Integer postId) {
Post post = postService.update(postId, updatePostRequest);
return new ApiResponseHandler(new PostDTO(post), HttpStatus.OK).response();
}
}
创建发布请求:
@Data
public class CreatePostRequest {
@NotNull
@Size(min = 10, max = 30)
private Sting title;
@NotNull
@Size(min = 50, max = 600)
private String description
@NotNull
@ValidDateString
private String expirationDate;
@NotNull
private List<TagDTO> tags;
public List<Integer> getTagIds() {
return this.getTags().stream().map(TagDTO::getId).collect(Collectors.toList());
}
}
更新发布请求:
@Data
public class UpdatePostRequest {
@Size(min = 10, max = 30)
private Sting title;
@Size(min = 50, max = 600)
private String description
@ValidDateString
private String expirationDate;
private List<TagDTO> tags;
public List<Integer> getTagIds() {
return this.getTags().stream().map(TagDTO::getId).collect(Collectors.toList());
}
}
服务:
@Service
@Transactional
public class PostService {
@Transactional
public Post create(CreatePostRequest request) {
ModelMapper modelMapper = new ModelMapper();
Post post = modelMapper.map(request, Post.class);
// map will not work for tags : how to check that tags exists in database ?
return postDAO.save(post);
}
@Transactional
public Post update(Integer postId, UpdatePostRequest request) {
return Optional.ofNullable(this.getById(postId)).map(post -> {
ModelMapper modelMapper = new ModelMapper();
modelMapper.getConfiguration().setSkipNullEnabled(true);
modelMapper.map(request, post);
// map will not work for tags : how to check that tags exists in database ?
postDAO.save(post);
return post;
}).orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
}
}
为避免重复两个相似的 DTO,您可以使用 @Validated
组验证。这允许您主动设置要对每个 属性 进行哪些验证。您可以在以下在线资源 https://www.baeldung.com/spring-valid-vs-validated 中阅读更多相关信息。您将从创建两个市场界面开始:
interface OnCreate {}
interface OnUpdate {}
然后您可以将这些标记接口与您的通用 DTO 中的任何约束注释一起使用:
@Data
public class CreateOrUpdatePostRequest {
@NotNull(groups = OnCreate.class)
@Size(min = 10, max = 30, groups = {OnCreate.class, OnUpdate.class})
private Sting title;
@NotNull(groups = OnCreate.class)
@Size(min = 50, max = 600, groups = {OnCreate.class, OnUpdate.class})
private String description
@NotNull(groups = OnCreate.class)
@ValidDateString(groups = {OnCreate.class, OnUpdate.class})
private String expirationDate;
@NotNull(groups = OnCreate.class)
private List<TagDTO> tags;
public List<Integer> getTagIds() {
return this.getTags().stream().map(TagDTO::getId).collect(Collectors.toList());
}
}
最后,您只需要相应地在控制器中注释您的方法:
@RestController
@RequestMapping("/v1/posts")
@Validated
public class PostController {
@RequestMapping(method = RequestMethod.POST, consumes = "application/json", produces = "application/json; charset=UTF-8")
public ResponseEntity<Object> update(@Validated(OnCreate.class) @RequestBody CreateOrUpdatePostRequest createPostRequest) {
Post post = postService.create(createPostRequest);
return new ApiResponseHandler(new PostDTO(post), HttpStatus.OK).response();
}
@RequestMapping(value = "/{postId}", method = RequestMethod.PUT, consumes = "application/json", produces = "application/json; charset=UTF-8")
public ResponseEntity<Object> update(@Validated(OnUpdate.class) @RequestBody CreateOrUpdatePostRequest updatePostRequest, @PathVariable Integer postId) {
Post post = postService.update(postId, updatePostRequest);
return new ApiResponseHandler(new PostDTO(post), HttpStatus.OK).response();
}
}
有了这个,你就可以拥有单一的映射功能了。
不过请记住,考虑到我们混合了不同的关注点,使用验证组很容易成为一种反模式。对于验证组,经过验证的实体必须知道它所用的所有用例的验证规则。话虽如此,除非确实有必要,否则我通常会避免使用验证组。
关于 tags
我猜你唯一的选择是查询数据库。那些不存在的你应该创建它们(我猜),所以遵循以下几行:
List<Integer> tagsId = createOrUpdatePostRequest.getTagsId();
List<Tag> tags = tagService.findAllByIds(tagsId);
List<Integer> nonExistentTagsId = tagsId.stream().filter(id -> tags.stream().noneMatch(tag -> tag.getId().equals(id)));
if (!nonExistentTagsId.isEmpty()) {
// create Tags and add them to tags List
}