How can I use another mapping from different class in mapstruct - java

I would like to mapping a model object to dto model. I already have mapper for one of the object.
How can I reuse this mapper in another mapper which is in another class?
I have below as model
#Getter
#AllArgsConstructor
#ToString
public class History {
#JsonProperty("identifier")
private final Identifier identifier;
#JsonProperty("submitTime")
private final ZonedDateTime submitTime;
#JsonProperty("method")
private final String method;
#JsonProperty("reason")
private final String reason;
#JsonProperty("dataList")
private final List<Data> dataList;
}
#DynamoDBTable(tableName = "history")
#Data
#NoArgsConstructor
public class HistoryDynamo {
#DynamoDBRangeKey(attributeName = "submitTime")
#DynamoDBTypeConverted(converter = ZonedDateTimeType.Converter.class)
private ZonedDateTime submitTime;
#DynamoDBAttribute(attributeName = "identifier")
#NonNull
private Identifier identifier;
#DynamoDBAttribute(attributeName = "method")
private String method;
#DynamoDBAttribute(attributeName = "reason")
private String reason;
#DynamoDBAttribute(attributeName = "dataList")
private List<Data> dataList;
}
#Data
#DynamoDBDocument
#NoArgsConstructor
public class Identifier implements Serializable {
#DynamoDBAttribute(attributeName = "number")
private String number;
#DynamoDBAttribute(attributeName = "cityCode")
#NonNull
private String cityCode;
#DynamoDBAttribute(attributeName = "countryCode")
#NonNull
private String countryCode;
#DynamoDBTypeConverted(converter = LocalDateType.Converter.class)
private LocalDate mydate;
}
#Data
#EqualsAndHashCode
#NoArgsConstructor
#RequiredArgsConstructor
#JsonInclude(JsonInclude.Include.NON_NULL)
public class Identifier implements Serializable {
#NonNull
#lombok.NonNull
#NotNull
private String number;
#NonNull
#lombok.NonNull
#NotNull
private City city;
#NonNull
#lombok.NonNull
#NotNull
private Country country;
#JsonDeserialize(using = LocalDateDeserializer.class)
#JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd'Z'")
#DateTimeFormat(pattern = "yyyy-MM-dd'Z'")
#NonNull
#lombok.NonNull
#NotNull
private LocalDate mydate;
}
And here is my mapping
#Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.WARN, injectionStrategy = InjectionStrategy.CONSTRUCTOR, nullValueMappingStrategy = NullValueMappingStrategy.RETURN_NULL)
public interface IdentifierMapper {
IdentifierMapper MAPPER = Mappers.getMapper(IdentifierMapper.class);
#Mappings({#Mapping(source = "identifier.number", target = "number"),
#Mapping(source = "identifier.city.code", target = "cityCode"),
#Mapping(source = "identifier.country.code", target = "countryCode"),
#Mapping(source = "identifier.mydate", target = "mydate")})
#Named("toIdentifierDynamo")
myproject.entity.dynamo.Identifier toIdentifierDynamo(myproject.model.Identifier identifier);
}
#Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.WARN, injectionStrategy = InjectionStrategy.CONSTRUCTOR,
nullValueMappingStrategy = NullValueMappingStrategy.RETURN_NULL, uses = {IdentifierMapper.class})
public interface HistoryMapper {
HistoryMapper MAPPER = Mappers.getMapper(HistoryMapper.class);
#Mappings({#Mapping(source = "identifier", target = "identifier", qualifiedByName = "toIdentifierDynamo"),
#Mapping(source = "method", target = "method"),
#Mapping(source = "reason", target = "reason"),
#Mapping(source = "timestamp", target = "timestamp")})
HistoryDynamo toHistoryDynamo(History history);
}
I would like to map History to HistoryDynamo and reuse IdentifierMapper to map one of the object in HistoryDynamo.
How can I use toIdentifierDynamo in toHistoryDynamo?

First of all you don't have to create instance in Spring. You could
just Autowire your Mapper.
Second of all you don't have to provide #Mapping annotation for
each field if it has the same name. Mapstruct will do it for you.
Your issue could be done using uses parameter of MapStruct mapper
HistoryMapper could have in #Mapper annotation parameter uses = IdentifierMapper.class. It will autowire IdentifierMapper into
HistoryMapper. By default it will do via field. You could change it
also in parameters: injectionStrategy = InjectionStrategy.CONSTRUCTOR and probably it will be enough as you
have the same name of field (identifier) and MapStruct should realize
that should be use IdentifierMapper

Using spring dependency can be injected easily as
private final HistoryMapper
historyMapper;
Also for fields with same name in target and source no need to use #Mapping, so in above case below mapper definition is enough to acheive desired result.
#Mapper(
componentModel = "spring",
injectionStrategy = InjectionStrategy.CONSTRUCTOR,
uses = {IdentifierMapper.class})
public interface HistoryMapper {
HistoryDynamo toHistoryDynamo(History history);
}
Refer github sample here https://github.com/rakesh-singh-samples/map-struct-samples/tree/stack-question-60523230/src/sample/mapstruct/mapper

Related

Mapstruct in Spring Boot Sets all fields to null

I have Spring Boot application (v3.0.2, Java 17), and in it, a simple entity ActivityType and corresponding ActivityDto.
//Entity (uses Lombok 1.18.24)...
#Getter
#Setter
#Entity
public class ActivityType {
#Id
#Column(name = "ActivityTypeId", nullable = false)
private Integer id;
#Column(name = "ActivityName", nullable = false, length = 30)
private String activityName;
#Column(name = "ActivityDescription")
private String activityDescription;
}
//DTO...
public record ActivityTypeDto(
Integer id,
String activityName,
String activityDescription) implements Serializable {
}
I'm using IntelliJ Idea (v2022.2.4) and JPA Buddy (v2022.5.4-222) to generate the Mapper Interface (MapStruct v1.5.3.Final). When I build the Mapper implementation, in the generated code, both the toEntity and toDto methods are incorrect.
#Component public class ActivityTypeMapperImpl implements ActivityTypeMapper {
#Override
public ActivityType toEntity(ActivityTypeDto activityTypeDto) {
if ( activityTypeDto == null ) {
return null;
}
ActivityType activityType = new ActivityType();
return activityType;
}
#Override
public ActivityTypeDto toDto(ActivityType activityType) {
if ( activityType == null ) {
return null;
}
// What's this all about?? Why not activityType.id, etc??
Integer id = null;
String activityName = null;
String activityDescription = null;
ActivityTypeDto activityTypeDto = new ActivityTypeDto( id, activityName, activityDescription );
return activityTypeDto;
}
#Override
public ActivityType partialUpdate(ActivityTypeDto activityTypeDto, ActivityType activityType) {
if ( activityTypeDto == null ) {
return activityType;
}
return activityType;
}
I've tried various alternatives, including using a class for the DTO instead of a record, but no success. Looks like I've missed something, but not sure what.
Update:
I can fix this by not using Lombok for the Entity getters/setters, which leads me on to final question, is there a setting on the MapStruct plugin to take Lomboz into account?
please define you entity like this,
#Entity
#Data
#AllArgsConstructor
#NoArgsConstructor
public class ActivityType {
#Id
#Column(name = "ActivityTypeId", nullable = false)
private Integer id;
#Column(name = "ActivityName", nullable = false, length = 30)
private String activityName;
#Column(name = "ActivityDescription")
private String activityDescription;
}
then define ActivityTypeDTO like this,
#Data
public class ActivityTypeDTO {
#JsonProperty("id")
private Integer id;
#JsonProperty("ActivityName")
private String ActivityName;
#JsonProperty("activityDescription")
private String activityDescription;
best practice to use MapStruct is like this,
#Mapper(componentModel = "spring", uses = {})
public interface ActivityMapper extends EntityMapper<ActivityTypeDTO, ActivityType> {
ActivityTypeDTO toDto(ActivityType activityType);
ActivityType toEntity(ActivityTypeDTO activityTypeDTO);
}
and EntityMApper in Mapper should be like this,
public interface EntityMapper<D, E> {
E toEntity(D dto);
D toDto(E entity);
}
Now I am sure you mapper work correctly.

Mapping nested collections of entities - Mapstruct

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

Java Spring Reactive, returning one Mono<..> from many multiple requests

[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.

How to make Json result using JPA inheritance (spring boot)

I have an error about "findAll" when I use JPA inheritage tables.
I what make the Json result like this ["asdf" : "adf", "asdf" : "asdf"]
but the return values are like [com.example.model.AccountEntity#57af674a]
Controller
#RequestMapping(value = "/getMyInfoall", produces = MediaType.APPLICATION_JSON_VALUE)
public String getMemberall(#RequestBody JSONObject sendInfo) throws IOException {
List user = UserService.findAll();
JSONObject result = new JSONObject();
result.put("data", user);
return result.toJSONString();
}
Service
public List findAll() {
List users = UserRepository.findAll();
return users;
}
Repository
#Repository
public interface UserRepository extends JpaRepository<UserEntity, Long> {
}
Entity
#Entity(name = "Users")
#Inheritance(strategy = InheritanceType.JOINED)
public class UserEntity {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private int userkey;
#Column(nullable = false, unique = true)
private String id;
#Column(nullable = false, length = 50)
private String name;
#Column(nullable = false)
private String password;
#Column(nullable = true)
private String email;
}
#Entity(name = "Account")
public class AccountEntity extends UserEntity{
#Column(nullable = false, unique = true)
private String accountno;
#Column(nullable = true)
private String accountname;
#Column(nullable = false)
private int accountpw;
#Column(nullable = false)
private long balance;
}```
I would highly recommend to use Spring's default HTTPMessageConverters, e.g. Jackson for JSON.
Building a JSON-array from a List
But you can also use JSON.org's light-weight library like guided on JSON-java README:
convert the List to an array, e.g. UserEntity[]
create a JSONArray from this Java array
return this JSON-array representation formatted as String, using method toString()
List<UserEntity> userList = // a list returned from your database/repo
UserEntity[] myArr = userList.toArray(new UserEntity[userList.size()]); // convert this to an array
// here simply follow the guide on JSON
JSONArray jArr = new JSONArray(myArr);
// return the JSON-array as string
return jArr.toString();
You should convert your UserEntity objects to a UserDto DTO that would then be returned in your Controller. Rely on Jackson instead of JSONObject managed and created by you.
public class UserDto {
private String id;
private String name;
}
You Service should do the mapping:
public List<UserDto> findAll() {
List<UserEntity> users = UserRepository.findAll();
return users.stream().map(user -> // your mapping logic to UserDto object);
}
And your Controller just needs to return it:
#RequestMapping(value = "/getMyInfoall", produces = MediaType.APPLICATION_JSON_VALUE)
public List<UserDto> getMemberall(#RequestBody JSONObject sendInfo) throws IOException {
return UserService.findAll();
}
You can do a similar thing with JSONObject sendInfo and replace it with an object of your own.

Validation error of type SubSelectionRequired: Sub selection required for type Timestamp of field

I use GraphQL-SPQR Library
The problem is "Validation error of type SubSelectionRequired: Sub selection required for type Timestamp"
Maybe there is expression in query for timestamp
or format in Entity
{"query":
"{findUserPointByUserId(userId:73){rowNum userAccountPointUserId totalPoint pointTypeDescription point userAccountCreatedDate} findUserAccountImgByUserId(userId:73){imageId,userId,presentImgNum}}"
}
Error
{
"errors": [
{
"message": "Validation error of type SubSelectionRequired: Sub selection required for type Timestamp of field userAccountCreatedDate",
"locations": [
{
"line": 1,
"column": 103
}
]
}
]
}
Entity
#NoArgsConstructor
#AllArgsConstructor
#Getter
#Setter
#Entity
#Table(name = "view_user_account_point", schema = "public", catalog = "corus")
public class ViewUserAccountPoint {
#Id
#Basic
#GraphQLQuery(name = "rowNum")
#Column(name = "row_num", nullable = true)
private Long rowNum;
#Basic
#Column(name = "user_account_point_userid", nullable = true)
#GraphQLQuery(name = "userAccountPointUserId")
private Integer userAccountPointUserId;
#Basic
#Column(name = "subject_id", nullable = true)
#GraphQLQuery(name = "subjectId")
private Integer subjectId;
#Basic
#Column(name = "point", nullable = true)
#GraphQLQuery(name = "point")
private Integer point;
#Basic
#Column(name = "user_account_point_typeid", nullable = true)
#GraphQLQuery(name = "userAccountPointTypeId")
private Integer userAccountPointTypeId;
#Basic
#Column(name = "date_created", nullable = true)
#GraphQLQuery(name = "userAccountCreatedDate")
private Timestamp userAccountCreatedDate;
Service
public List<ViewUserAccountPoint> findUserPointByUserId(#GraphQLArgument(name = "userId") Integer userId){
return viewUserAccountPointRepository.findByUserAccountPointUserIdOrderByUserAccountCreatedDateDesc(userId);
}
Controller
private final GraphQL graphQL;
public UserController(UserAccountService userAccountService) {
GraphQLSchema schema = new GraphQLSchemaGenerator()
.withResolverBuilders(
//Resolve by annotations
new AnnotatedResolverBuilder())
.withOperationsFromSingleton(userAccountService,UserAccountService.class)
.withValueMapperFactory(new JacksonValueMapperFactory())
.generate();
graphQL = GraphQL.newGraphQL(schema).build();
}
#PostMapping(value = "/graphql", consumes = MediaType.APPLICATION_JSON_UTF8_VALUE, produces = MediaType.APPLICATION_JSON_UTF8_VALUE)
#ResponseBody
public Map<String, Object> graphql(#RequestBody Map<String, String> request, HttpServletRequest raw) {
ExecutionResult executionResult = graphQL.execute(ExecutionInput.newExecutionInput()
.query(request.get("query"))
.operationName(request.get("operationName"))
.context(raw)
.build());
return executionResult.toSpecification();
}
I search through all query timestamp format
However, i couldn't find
i hope to hear the solution.
thank you
For one reason or another, Timestamp got mapped incorrectly. It ended up being an object and not a scalar.
As mentioned in the issue you opened, it's unclear where is Timestamp in your code coming from.
java.sql.Timestamp is supported out of the box in recent versions of GraphQL SPQR, so you might be on an older version.
If that's not the case, it would mean Timestamp is some other than java.sql.Timestamp, and you'd need to register a custom mapper for it.
public class TimestampMapper implements TypeMapper {
// Define the scalar as needed, see io.leangen.graphql.util.Scalars for inspiration
private static final GraphQLScalarType TIMESTAMP = ...;
#Override
public GraphQLOutputType toGraphQLType(AnnotatedType javaType, OperationMapper operationMapper, Set<Class<? extends TypeMapper>> mappersToSkip, BuildContext buildContext) {
return TIMESTAMP; //it's important to always return the same instance
}
#Override
public GraphQLInputType toGraphQLInputType(AnnotatedType javaType, OperationMapper operationMapper, Set<Class<? extends TypeMapper>> mappersToSkip, BuildContext buildContext) {
return TIMESTAMP; //same as above
}
#Override
public boolean supports(AnnotatedType type) {
return ClassUtils.isSuperClass(Timestamp.class, type);
}
}
Then register your mapper:
generator.withTypeMappers(new TimestampMapper())
It's incorrect query body for my case, make sure you have the right one.

Categories

Resources