In my DB schema I have a Post entity which can have a list of PostComment entities and every PostComment entity can have a list of PostCommentUpvote entities and a list of PostCommentDownvote entities (all self-explanatory I suppose).
Post:
public class Post {
...
private List<PostComment> postComments;
...
}
PostComment:
public class PostComment {
...
private List<PostCommentUpvote> postCommentUpvotes;
private List<PostCommentDownvote> postCommentDownvotes;
...
}
PostCommentUpvote and PostCommentDownvote have the same fields (but semantically are different):
public class PostCommentUpvote {
private Long id;
...
}
The end goal is to get all Post comments (List<PostComment>)
Target response DTO:
public class PostCommentsResponseDto {
private List<PostCommentResponseDto> comments;
private Integer count; // count is the size of the list of PostComments or PostCommentsResponseDto - they are semantically the same, PostCommentsResponseDto just has less fields
}
PostCommentResponseDto:
public class PostCommentResponseDto {
private Long id;
private String comment;
private String username;
private List<PostCommentUpvoteResponseDto> postCommentUpvotes;
private List<PostCommentDownvoteResponseDto> postCommentDownvotes;
private Timestamp createdAt;
private Timestamp updatedAt;
}
PostCommentUpvoteResponseDto and PostCommentDownvoteResponseDto are the same:
public class PostCommentUpvoteResponseDto {
private Long id;
}
So I'm basically doing mapping from Post to PostCommentsResponseDto.
PostMapper:
#Mapper(componentModel = "spring", uses = { PostCommentMapper.class })
public interface PostMapper {
#Named("postCommentsMapper")
default List<PostCommentResponseDto> postCommentsMapper(List<PostComment> postComments) {
// how to map List<PostComment> to List<PostCommentResponseDto> ?
}
#Named("postCommentsQuantityMapper")
default Integer postCommentsQuantityMapper(List<PostComment> postComments) {
return postComments.size();
}
#Mapping(source = "postComments", target = "comments", qualifiedByName = "postCommentsMapper")
#Mapping(source = "postComments", target = "count", qualifiedByName = "postCommentsQuantityMapper")
PostCommentsResponseDto postPostCommentsResponseDtoMapper(Post post);
}
PostCommentMapper:
#Mapper(componentModel = "spring", uses = { PostCommentUpvoteMapper.class, PostCommentDownvoteMapper.class })
public interface PostCommentMapper {
#Mapping(source = "user.username", target = "username")
public PostCommentResponseDto postCommentPostCommentResponseDtoMapper(PostComment postComment);
}
PostCommentUpvoteMapper and PostCommentDownvoteMapper are the same:
#Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.IGNORE)
public interface PostCommentUpvoteMapper {
PostCommentUpvoteResponseDto postcommentUpvotePostCommentUpvoteResponseDto(PostCommentUpvote postCommentUpvote);
}
The problem: How to map List<PostComment> to List<PostCommentResponseDto> which requires mapping User entity to just String username and more importantly mapping nested collections PostCommentUpvoteResponseDto and PostCommentDownvoteResponseDto? With the presented setup the final result is comments are null (which is bad because it should be empty array/list) and counter is 0 (which is okay because selected post doesn't have any comments)
Please ask if more clarification are needed.
hope you found solution, but if you are still need help, this is how I would handle it.
Basically, you should provide a mapper method for mapping an object and also method for mapping list of the same objects and MapStruct will do the rest.
#Mapping(source = "user.username", target = "username")
PostCommentResponseDto postCommentToPostCommentResponseDto(PostComment postComment);
List<PostCommentResponseDto> postCommentToPostCommentResponseDto(List<PostComment> postComment);
PostCommentUpvoteResponseDto postCommentUpvoteToPostCommentUpvoteResponseDto(PostCommentUpvote postCommentUpvote);
List<PostCommentUpvoteResponseDto> postCommentUpvoteToPostCommentUpvoteResponseDto(List<PostCommentUpvote> postCommentUpvote);
In example above MapStruct will automatically call method postCommentUpvoteToPostCommentUpvoteResponseDto inside the postCommentToPostCommentResponseDto method.
I have created small working example on GitHub so you can check it.
https://github.com/fpecek/MapstructDemo
Related
I want to create a unit test that will use reflection to find all missing fields in dto that implement BaseDto by their persistence entities. This is what I did.
#Slf4j
public class EntityAuditDtoTest {
#Test
public void find_MissingAndExtraFieldsThatUsedInAuditDtosByEntity_ReturnMissingAndExtraFields() throws ClassNotFoundException {
// Arrange
ClassPathScanningCandidateComponentProvider scanner = new ClassPathScanningCandidateComponentProvider(false);
scanner.addIncludeFilter(new AnnotationTypeFilter(AuditEntityType.class));
// Find all classes annotated with #AuditEntityType in the package com.example.dto
Set<BeanDefinition> auditDtoBeans = scanner.findCandidateComponents("com.example.dto");
// Act
for (BeanDefinition auditDtoBean : auditDtoBeans) {
Class<?> auditDtoClass = Class.forName(auditDtoBean.getBeanClassName());
// Make sure the DTO class implements BaseAuditDto
if (!BaseAuditDto.class.isAssignableFrom(auditDtoClass)) {
continue;
}
Class<?> entityClass = getEntityClassForDto(auditDtoClass);
Field[] dtoFields = auditDtoClass.getDeclaredFields();
Field[] entityFields = entityClass.getDeclaredFields();
List<String> missingFields = Arrays.stream(entityFields).map(Field::getName)
.filter(field -> Arrays.stream(dtoFields).noneMatch(f -> f.getName().equals(field))).toList();
if (!missingFields.isEmpty()) {
log.error("Missing fields in DTO class: {} \nfor entity class: {} : {}", auditDtoClass.getName(),
entityClass.getName(), missingFields);
}
List<String> extraFields = Arrays.stream(dtoFields).map(Field::getName)
.filter(field -> Arrays.stream(entityFields).noneMatch(f -> f.getName().equals(field))).toList();
if (!extraFields.isEmpty()) {
log.error("Extra fields in DTO class: {} \nfor entity class: {} : {}", auditDtoClass.getName(),
entityClass.getName(), extraFields);
}
}
}
}
But the problem is that the dto may have a field that is in the entity class, but the test will think that this is a missing field.
For example:
Dto class: ContractAudit has customerId field (customerId). And ContractEntity has public CustomerEntity customer.
This is the same fields. But of course for test they are different. I don't understand how to ignore them. I also don't want to hardcode filter that skip all endings with 'id' prefix.
#Data
#AuditEntityType("Contract")
public class ContractAudit implements BaseAuditDto {
private Long id;
private String ref;
private String status;
private Long customerId;
}
#Entity
#Table(name = "contract")
#Getter
#Setter
#ToString
#AllArgsConstructor
#NoArgsConstructor
#Builder
public class ContractEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
#ToString.Include
private Long id;
#Column(name = "ref", updatable = true)
#ToString.Include
private String ref;
#Column(name = "status")
#ToString.Include
#Enumerated(value = EnumType.STRING)
private ContractStatusEnum status;
#ManyToOne
#JoinColumn(name = "customer_id")
public CustomerEntity customer;
#Column(name = "deleted")
#ToString.Include
private boolean deleted;
#OneToMany(fetch = FetchType.LAZY)
#JoinColumn(name = "contract_id")
private List<ContractDocumentEntity> documents;
}
Output:
Missing fields in DTO class: ContractAudit for entity class: ContractEntity : [customer, deleted, documents]
Extra fields in DTO class: ContractAudit for entity class: ContractEntity : [customerId]
I want to have missing fields: [deleted, documents]
If you have any other ideas on how to do this, I'd love to hear it. I am not asking for implementation. Suggestions only)
Lol. I found solution for my case.
My previous approach was incorrect. Because it's impossible to find 'missing' and 'extra' fields by name correctly for every case. I decided to use:
assertThat(entityClass.getDeclaredFields()).hasSameSizeAs(auditDtoClass.getDeclaredFields());
So this code is checking if the entityClass and the DtoClass have the same number of fields (properties) declared. If not it fail test and print all fields from each classes. If anyone has better ideas I'll be happy to hear.
[Java, Spring Reactive, MongoDB]
I'm currently trying to learn Reactive programming by doing and I found a challenge.
I have db object CategoryDB which looks like this:
#NoArgsConstructor
#Getter
#Setter
#Document(collection = DBConstraints.CATEGORY_COLLECTION_NAME)
class CategoryDB {
#Id
private String id;
private String name;
private String details = "";
#Version
private Long version;
private String parentCategoryId;
private Set<String> childCategoriesIds = new HashSet<>();
}
In a service layer I want to use model object Category.
#Getter
#Builder
public class Category {
private String id;
private String name;
private String details;
private Long version;
private Category parentCategory;
#Builder.Default
private Set<Category> childCategories = new HashSet<>();
}
I want to create Service with method Mono<Category getById(String id). In this case I want to fetch just one level of childCategories and direct parent Category. By default repository deliver Mono findById(..) and Flux findAllById(..) which I could use, but I'm not sure what would be the best way to receive expected result. I would be grateful for either working example or directions where can I find solution for this problem.
I've spent some time to figure out solution for this problem, but as I'm learning I don't know if it's good way of solving problems.
Added some methods to Category:
#Getter
#Builder
public class Category {
private String id;
private String name;
private String details;
private Long version;
private Category parentCategory;
#Builder.Default
private Set<Category> childCategories = new HashSet<>();
public void addChildCategory(Category childCategory) {
childCategory.updateParentCategory(this);
this.childCategories.add(childCategory);
}
public void updateParentCategory(Category parentCategory) {
this.parentCategory = parentCategory;
}
}
Function inside service would look like this:
#Override
public Mono<Category> findById(String id) {
return categoryRepository.findById(id).flatMap(
categoryDB -> {
Category category = CategoryDBMapper.INSTANCE.toDomain(categoryDB);
Mono<CategoryDB> parentCategoryMono;
if(!categoryDB.getParentCategoryId().isBlank()){
parentCategoryMono = categoryRepository.findById(categoryDB.getParentCategoryId());
}
else {
parentCategoryMono = Mono.empty();
}
Mono<List<CategoryDB>> childCategoriesMono = categoryRepository.findAllById(categoryDB.getChildCategoriesIds()).collectList();
return Mono.zip(parentCategoryMono, childCategoriesMono, (parentCategoryDB, childCategoriesDB) -> {
Category parentCategory = CategoryDBMapper.INSTANCE.toDomain(parentCategoryDB);
category.updateParentCategory(parentCategory);
childCategoriesDB.forEach(childCategoryDB -> {
Category childCategory = CategoryDBMapper.INSTANCE.toDomain(childCategoryDB);
category.addChildCategory(childCategory);
});
return category;
});
}
);
}
Where mapper is used for just basic properties:
#Mapper
interface CategoryDBMapper {
CategoryDBMapper INSTANCE = Mappers.getMapper(CategoryDBMapper.class);
#Mappings({
#Mapping(target = "parentCategoryId", source = "parentCategory.id"),
#Mapping(target = "childCategoriesIds", ignore = true)
})
CategoryDB toDb(Category category);
#Mappings({
#Mapping(target = "parentCategory", ignore = true),
#Mapping(target = "childCategories", ignore = true)
})
Category toDomain(CategoryDB categoryDB);
}
As I said I don't know if it's correct way of solving the problem, but it seem to work. I would be grateful for review and directions.
A springboot project where I need to construct a DTO for a dashboard view using nominated fields from the parent and nominated fields from the newest of each of the children.
The entities are Plane which has a OneToMany relationship with Transponder, Maint Check and Transmitter.
Plane
#Entity
#Data
public class Plane {
#Id
#GeneratedValue(strategy = IDENTITY)
private Long id;
private String registration;
#JsonIgnore
#OneToMany(cascade = CascadeType.ALL, mappedBy = "plane")
private List<Transponder> listTransponder = new ArrayList<>();
#JsonIgnore
#OneToMany(cascade = CascadeType.ALL, mappedBy = "plane")
private List<Transmitter> listTransmitter = new ArrayList<>();
#JsonIgnore
#OneToMany(cascade = CascadeType.ALL, mappedBy = "plane")
private List<MaintCheck> listMaintCheck = new ArrayList<>();
Transponder
#Entity
#Data
#NoArgsConstructor
#AllArgsConstructor
public class Transponder {
#Id
#GeneratedValue(strategy = IDENTITY)
private Long id;
private String code;
private LocalDate dateInserted;
#ManyToOne(fetch = FetchType.LAZY, optional = true)
private Plane plane;
}
Maint Check and Transmitter have similar entities with a LocalDate field.
PlaneDTO looks liike
#Data
public class PlaneDTO {
private String registration;
private LocalDate maintCheck; // date of most recent Maint Check
private String transponderCode; // string of most recent Transponder code
private Integer channel; // Intger of most recent Transmitter Freq
}
I have attempted to consruct this PlaneDTO in the service layer, but I am manually doing much of the sorting of the lists of Transponder, Transmitter and Maint Check to get the most recent record from these lists.
//DRAFT METHOD CONSTRUCT DTO
#Override
public PlaneSummaryDTO getPlaneSummaryDTOById(Long id) {
Plane Plane = this.get(id);
PlaneSummaryDTO PlaneSummaryDTO = new PlaneSummaryDTO();
ModelMapper mapper = new ModelMapper();
PlaneSummaryDTO = modelMapper.map(get(id), PlaneSummaryDTO.class);
PlaneSummaryDTO.setTRANSPONDERCode(getNewestTRANSPONDERCode(Plane));
PlaneSummaryDTO.setLastMaintCheck(getNewestMaintCheckDate(Plane));
PlaneSummaryDTO.setChannel(getTransmitterCode(Plane));
PlaneSummaryDTO.setChannelOffset(getTransmitterOffset(Plane));
return PlaneSummaryDTO;
}
// RETURN NEWEST DATE OF MAINT CHECK BY CATCH DATE
public LocalDate getNewestMaintCheckDate(Plane Plane) {
List<MaintCheck> listMaintCheck = new ArrayList<>(Plane.getListMaintCheck());
MaintCheck newest = listMaintCheck.stream().max(Comparator.comparing(MaintCheck::getCatchDate)).get();
return newest.getCatchDate();
}
// RETURN NEWEST TRANSPONDER CODE FROM Plane BY DATE INSERTED
public String getNewestTransponderCode(Plane Plane) {
List<Transponder> listTransponder = new ArrayList<>(Plane.getListTransponder());
Transponder newest = listTransponder.stream().max(Comparator.comparing(Transponder::getDateInserted)).get();
return newest.getCode();
}
// OTHER METHODS TO GET MOST RECENT RECORD
QUESTION Is there a better way to calculate the most recent record of the child, using model mapper more efficiently (custom method?)
I am open to changing to MapStruct if it better supports getting the most recent child.
I briefly used ModelMapper in the past. I would suggest using mapstruct since I personaly find it easier to use. I know your mapping can be done there ;). In Mapstruct your Mapper could look something like this:
#MapperConfig(
componentModel = "spring",
builder = #Builder(disableBuilder = true)
)
public interface PlaneMapper {
#Mapping(target = "lastMaintCheck", ignore = true)
PlaneDTO planeToPlaneDTO(Plane plane);
#AfterMapping
default void customCodePlaneMapping(Plane source, #MappingTarget PlaneDTO target) {
target.setLastMaintCheck(source.getListMaintCheck.stream().max(Comparator.comparing(Transponder::getDateInserted)).get())
}
Your mapper call would then only be one line:
#Service
#RequiuredArgsConstructor
public class someService{
private final PlaneMapper planeMapper;
public void someMethod(){
....
PlaneDTO yourMappedPlaneDTO = planeMapper.planeToPlaneDTO(plane);
....
}
I did not fill in all values. But i hope the concept is clear.
EDIT:
You would also have to add the dependency of "mapstruct-processor" so that the MapperImpl classes can be gererated.
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
<scope>provided</scope>
<optional>true</optional>
</dependency>
So here's a different approach to the data model. I'll be using Transponder because the others are analog.
The target domain model could look like this:
#Data
class Plane {
Long id;
String registration;
Transponder activeTransponder;
}
#Data
class Transponder {
Long id;
Integer code; // this is a 4-digit octal number, why String? your call though
Long planeId;
Instant assignStart;
Instant assignEnd;
}
In the database, it would be sufficient to store the id and registration for the plane, because you can determine the current transponder with a proper query on the db entity, eg where transponder.planeId=id and transponder.assignEnd IS NULL. You can also store the transponderId of course, but then you'd need to take care to keep the data consistent between the tables.
If you want a history of all transponders - which to me seems like an entirely different use case to me, you can easily retrieve it in a separate service with a query getTransponderHistoryByPlane(long planeId) with a query like from transponders t where t.planeId=$planeId sorted by t.assignStart.
Again, this does depend on your use cases, and assumes that you usually only need one transponder for a given plane except in special cases, like from a different endpoint.
Anyway, this were my thoughts on the domain model, and you were aiming for your Dto; however, this is then easily mapped with mapstruct like (assuming you do the same for maintCheck and transmitter)
#Mapper
interface PlaneDtoMapper {
#Mapping(target = "transponderCode", source = "transponder.code")
#Mapping(target = "maintCheck", source = "maintCheck.date")
#Mapping(target = "channel", source = "transmitter.channel")
PlaneDTO fromPlane(Plane p);
}
You don't need the "registration" mapping because the fields in plane and dto are the same, and the #Mapping annotations tell mapstruct which subfields of which fields of plane to use.
I'm trying to use a mapstruct and I need to mapping Entity with a sub Entity list, I have relationship oneToMany and manyToOne and I need to mapping in both cases:
#Data
#Entity
public class EmailEntity {
private int id;
... // some fields
#ManyToOne
private DeliveredEmailInfoEntity deliveredEmailInfo;
}
.
#Data
#Entity
public class DeliveredEmailInfoEntity {
private int id;
... // some fields
#OneToMany
private List<EmailEntity> emails;
}
mapping to:
#Data
public class EmailDTO {
private int id;
... // some fields
private DeliveredEmailInfoDTO deliveredEmailInfo;
}
.
#Data
public class DeliveredEmailInfoDTO {
private int id;
... // some fields
private List<EmailDTO> emails;
}
How to do it in the best way ?
To avoid infinite cross setting of nested fields you should limit this dependency, for example on the second nested level, i.e. your root EmailDTO will have one nested DeliveredEmailInfoDTO object (many-to-one relationship), while your root DeliveredEmailInfoDTO will have the list of nested EmailDTO objects (one-to-many relationship) and nothing on the next nesting level:
#Mapper(uses = DeliveredEmailInfoMapper.class)
public interface EmailMapper {
#Mapping(target = "deliveredEmailInfo.emails", ignore = true)
EmailDTO toDTO(EmailEntity entity);
// other methods omitted
#Named("emailDTOList")
default List<EmailDTO> toEmailDTOList(List<EmailEntity> source) {
return source
.stream()
.map(this::toDTO)
.peek(dto -> dto.setDeliveredEmailInfo(null))
.collect(Collectors.toList());
}
}
#Mapper(uses = EmailMapper.class)
public interface DeliveredEmailInfoMapper {
#Mapping(target = "emails", source = "emails", qualifiedByName = "emailDTOList")
DeliveredEmailInfoDTO toDTO(DeliveredEmailInfoEntity entity);
// other methods omitted
}
(Also see other answer)
It should be straightforward, there is nothing challenging in your case:
#Mapper
public interface EmailInfoMapper {
EmailDTO entityToDTO(EmailEntity duration);
EmailEntity dtoToEntity(EmailDTO price);
DeliveredEmailInfoDTO entityToDTO(DeliveredEmailInfoEntity duration);
DeliveredEmailInfoEntity dtoToEntity(DeliveredEmailInfoDTO price);
}
You should include your mapper in your question and what the problem you have with it.
I'm using a JPA query that uses a specification to retrieve entities. When I execute the query, I'm getting the error:
org.springframework.data.mapping.PropertyReferenceException: No property name found for type Task!
I've looked at the answers to similar questions that have been asked on this site previously & tried to model my code to follow the patterns that were recommended but the code is still failing.
When I step through the code with a debugger, the expanded path in the criteria builder is returning the embedded ID class, but when the specification is actually used in the query it looks like the attribute is being applied to the base entity class.
Am I missing something obvious?
Here is the entity class:
#Entity
#Table(name = "TASKS")
public class Task implements Serializable {
#EmbeddedId
private TaskId id;
...more attributes, getters and setters
}
Here is the embedded ID entity class:
#Embeddable
public class TaskId implements Serializable {
#Column(name = "NAME", length = 100)
private String name;
...more attributes, getters and setters
}
Here is the specification builder that matches on the embedded id 'name' attribute:
public class HasTaskNameSpec {
private HasTaskNameSpec() {
}
public static Specification<Task> equals(String name) {
return (root, query, criteriaBuilder) -> {
return criteriaBuilder.equal(root.get("id").get("name"), taskName);
};
}
}
The query is executed on the repository as follows:
List<Task> results = taskRepository.findAll(HasTaskNameSpec.equals("foo"));
The repository itself is very simple:
public interface TaskRepository extends JpaRepository<Task, TaskId>, JpaSpecificationExecutor<Task> {
List<Task> findByIdName(String name);
Page<Task> findByIdName(String name, Pageable page);
}
** EDIT added methods to repository as was suggested below **
Ahh, the root cause was totally in our codebase. There was a sort order being specified on the page that didn't include the embedded "id" attribute. The above code works.
'root.get({embeddedIdName}).get({subPropertyName})' is used to query on embeddedId using specification.
#Embeddable
public class ProjectId implements Serializable{
private static final long serialVersionUID = 1L;
#Column(name = "PROJECT_NAME")
private String projectName;
#Column(name = "ORGANIZATION")
private String organization;
......
......
}
#Entity
#Table(name = "projects")
public class Project {
#EmbeddedId
private ProjectId projectId;
#Column(name = "STARTED_TIME")
private Timestamp startedTime;
#Column(name = "ACTIVE")
private String active;
#Column(name = "DESCRIPTION")
private String description;
......
......
}
In the above snippet, ProjectId is an embedded id. To query on projectName, we should use below snippet.
expression = root.get("projectId").get("projectName");
Demo application link.
Take a look at this link which has a similar query.
EmbbededId Lookup
The final answer suggests that you can add a method to your TaskRepository thus.
public interface TaskRepository extends JpaRepository<Task, TaskId>, JpaSpecificationExecutor<Task> {
public List<Task> findByIdName(String name);
}