MapStruct generic Map and map combined list of children objects - java

I have as parent class : User.java , and 2 classes : FacebookUser.java and TwitterUser.java they are entities that returned depends on the type column in database using DiscriminatorColumn, I want to write correct mapper to map User that could be instance of FacebookUser or TwitterUser. I have the following mapper that seems not works as intended, only Mapping the User parent not the children:
#Mapper
public interface UserMapper {
public static UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
User map(UserDTO userDTO);
#InheritInverseConfiguration
UserDTO map(User user);
List<UserDTO> map(List<User> users);
FacebookUser map(FacebookUserDTO userDTO);
#InheritInverseConfiguration
FacebookUserDTO map(FacebookUser user);
TwitterUser map(TwitterUserDTO userDTO);
#InheritInverseConfiguration
TwitterUserDTO map(TwitterUser user);
}
Then I use :
UserDTO userDto = UserMapper.INSTANCE.map(user);
Classes to map:
#Entity
#Table(name = "users")
#Inheritance(strategy = InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(name = "type", discriminatorType = DiscriminatorType.STRING, length = 10)
#DiscriminatorValue(value = "Local")
public class User {
#Column
private String firstName;
#Column
private String lastName;
///... setters and getters
}
#Entity
#DiscriminatorValue(value = "Facebook")
public class FacebookUser extends User {
#Column
private String userId;
///... setters and getters
}
#Entity
#DiscriminatorValue(value = "Twitter")
public class TwitterUser extends User {
#Column
private String screenName;
///... setters and getters
}
The DTOs:
public class UserDTO {
private String firstName;
private String lastName;
///... setters and getters
}
public class FacebookUserDTO extends UserDTO {
private String userId;
///... setters and getters
}
public class TwitterUserDTO extends UserDTO {
private String screenName;
///... setters and getters
}
Also if I have list of users that mixed with Facebook users and Twitter users, or basic user:
Lets say I have the following users:
User user = new User ("firstName","lastName");
User fbUser = new FacebookUser ("firstName","lastName","userId");
User twUser = new TwitterUser ("firstName","lastName","screenName");
List<User> users = new ArrayList<>();
users.add(user);
users.add(fbUser);
users.add(twUser);
//Then:
List<UserDTO> dtos = UserMapper.INSTANCE.map(users);
I get only firstName and lastName but not screenName or userId.
Any solution for this?

Currently, it seems it's not available yet as a feature for mapstruct : Support for Type-Refinement mapping (or Downcast Mapping)
I asked the question in their google group: https://groups.google.com/forum/?fromgroups#!topic/mapstruct-users/PqB-g1SBTPg
and found that I need to do manual mapping using default method inside interface (for java 8).
And got another issue for mapping parent that was almost not applicable so I write one more empty class that child of parent class called LocalUserDTO:
So the code becomes like the following:
#Mapper
public interface UserMapper {
public static UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
LocalUser map(LocalUserDTO userDTO);
#InheritInverseConfiguration
LocalUserDTO map(LocalUser user);
List<UserDTO> map(List<User> users);
FacebookUser map(FacebookUserDTO userDTO);
#InheritInverseConfiguration
FacebookUserDTO map(FacebookUser user);
TwitterUser map(TwitterUserDTO userDTO);
#InheritInverseConfiguration
TwitterUserDTO map(TwitterUser user);
default UserDTO map(User user) {
if (user instanceof FacebookUser) {
return this.map((FacebookUser) user);
} else if (user instanceof TwitterUser) {
return this.map((TwitterUser) user);
} else {
return this.map((LocalUser) user);
}
}
#InheritInverseConfiguration
default User map(UserDTO userDTO) {
if (userDTO instanceof FacebookUserDTO) {
return this.map((FacebookUserDTO) userDTO);
} else if (userDTO instanceof TwitterUserDTO) {
return this.map((TwitterUserDTO) userDTO);
} else {
return this.map((LocalUserDTO) userDTO);
}
}
}

Was googling about this exact issue yesterday, but couldn't find anything on internet, so let's leave this here.
As the issue mentioned in the former answer has been solved earlier this year, Mapstruct now officially supports downcasting by #SubclassMapping.
public interface UserMapper {
#Named("UserToDTO")
#SubclassMapping(source = FacebookUser.class, target = FacebookUserDTO.class)
#SubclassMapping(source = TwitterUser.class, target = TwitterUserDTO.class)
UserDTO toDTO(User source);
}
However you can't put parameterized class i.e. List<User> in that annotation, and Mapstruct isn't actually using this specification above for Lists. Hence, one more function is needed.
public interface UserMapper {
#Named("UserToDTOList")
#IterableMapping(qualifiedByName = "UserToDTO")
List<UserDTO> toDTO(List<User> source);
#Named("UserToDTO")
#SubclassMapping(source = FacebookUser.class, target = FacebookUserDTO.class)
#SubclassMapping(source = TwitterUser.class, target = TwitterUserDTO.class)
UserDTO toDTO(User source);
}
This also applies to field in a class. For my case, we have a list field like that in a class. At first I only added #SubclassMapping, but after checking the code Mapstruct generated, I found that it isn't using the method for list converting. Adding #IterableMapping fixed that.
Consider a UsersWrapper class with a list field in it.
public class UsersWrapper {
List<User> list;
}
And a UsersWrapperDTO.
public class UsersWrapperDTO {
List<UserDTO> list;
}
Then, we can use one more method in the mapper.
public interface UserMapper {
#Mapping(source = "source.list", target = "list", qualifiedByName = "UserToDTOList")
UsersWrapperDTO toDTO(UsersWrapper source);
#Named("UserToDTOList")
#IterableMapping(qualifiedByName = "UserToDTO")
List<UserDTO> toDTO(List<User> source);
#Named("UserToDTO")
#SubclassMapping(source = FacebookUser.class, target = FacebookUserDTO.class)
#SubclassMapping(source = TwitterUser.class, target = TwitterUserDTO.class)
UserDTO toDTO(User source);
}
Mapstruct actually generates very readable codes, so if you find it doesn't work like you thought, checking there first is also an option.
Ain't familiar with Mapstruct so took me a whole afternoon to put the pieces together! Though, it does seem simple and make sense that I need to combine these two annotations together. Hopefully this post will save people some time.

Related

Ignore DTO field using MapStruct (Spring boot)

I created a couple of DTO's and a MapStruct interface for getting the User data:
public class UserDto {
private Long id;
private CountryDto country;
}
public class CountryDto {
private Long id;
private String name;
private List<TimeZoneDto> timeZones = new ArrayList<TimeZoneDto>();
}
#Mapper
public interface UserMapper {
UserMapper INSTANCE = Mappers.getMapper(UserMapper.class);
// TODO exclude country timezones
UserDto mapToDto(User entity);
}
I would like to modify UserMapper so the CountryDto timezones list are excluded
{
"id":1,
"country": {
"id": 182,
"name":"Australia"
}
}
I finally found a solution for this, just adding the line below in UserMapper did the trick:
#Mapping(target = "country.timeZones", ignore = true)
UserDto mapToDto(User entity);

Post Method using DTO

I want to use DTO to communicate with the Angular, but actually it doesn't work. I want to create POST request to add data from my application to the database using Dto model.
You can see my errors on the picture:
My class Customer:
#Entity
#Table(name = "customer")
public class Customer {
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private int id;
#Column(name = "name")
private String name;
#OneToMany
private List<Ticket> ticket;
...
Class CustomerDto:
public class CustomerDto {
private String name;
private List<TicketDto> ticket;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public List<TicketDto> getTicket() {
return ticket;
}
public void setTicket(List<TicketDto> ticket) {
this.ticket = ticket;
}
}
Class CustomerController:
#Autowired
CustomerService customerService;
#PostMapping(value = "/customers/create")
public Customer postCustomer(#RequestBody CustomerDto customerDto, List<TicketDto> ticketDtos) {
//ArrayList<TicketDto> tickets = new ArrayList<>();
ticketDtos.add(customerDto.getName());
ticketDtos.add(customerDto.getTicket());
Customer _customer = customerService.save(new Customer(customerDto.getName(), ticketDtos ));
return _customer;
}
CustomerService:
public interface CustomerService {
void save(CustomerDto customerDto, List<TicketDto> ticketDtos);
}
CustomerServiceImpl:
#Service
public class CustomerServiceImpl implements CustomerService {
#Autowired
CustomerRepository repository;
#Override
public void save(CustomerDto customerDto, List<TicketDto> ticketDtos) {
Customer customer = new Customer();
customer.setName(customerDto.getName());
customer.setTicket(customerDto.getTicket());
List<Ticket> tickets = new ArrayList<>();
for (TicketDto ticketDto : ticketDtos) {
Ticket ticket = new Ticket();
ticket.setDestinationCity(ticketDto.getDepartureCity());
ticket.setDestinationCity(ticketDto.getDestinationCity());
tickets.add(ticket);
}
}
Since you CustomerServiceImpl is taking CustomerDto and list of TicketDtos, you need to change your method call on controller as below:
Class CustomerController:
#Autowired
CustomerService customerService;
#PostMapping(value = "/customers/create")
public Customer postCustomer(#RequestBody CustomerDto customerDto) {
Customer _customer = customerService.save(customerDto));
return _customer;
}
And update CustomerServiceImpl as:
#Service
public class CustomerServiceImpl implements CustomerService {
#Autowired
CustomerRepository repository;
// change save to return saved customer
#Override
public Customer save(CustomerDto customerDto) {
Customer customer = new Customer();
customer.setName(customerDto.getName());
// customer.setTicket(customerDto.getTicket()); // remove this
List<Ticket> tickets = new ArrayList<>();
for (TicketDto ticketDto : customerDto.getTicketDtos) {
Ticket ticket = new Ticket();
ticket.setDestinationCity(ticketDto.getDepartureCity());
ticket.setDestinationCity(ticketDto.getDestinationCity());
tickets.add(ticket);
}
customer.setTickets(tickets); // add this to set tickets on customer
return repository.save(customer);
}
Obviously, you need to change your interface as well:
public interface CustomerService {
Customer save(CustomerDto customerDto);
}
For entity-DTO conversion, we need to use ModelMapper or mapstruct library.
With the help of these libraries, we can easily convert from Dto to entity and entity to dto object. After adding any of the dependency, We are able to use it.
How can we use, Let see...
Define modelMapper bean in spring configuration.
#Bean
public ModelMapper modelMapper() {
return new ModelMapper();
}
Suppose we need to convert List to List obj then we can perform simply like that :
List<TicketDto> ticketDtos = .... //Suppose It is holding some data
List<Ticket> tickets = ticketDtos.stream()
.map(tkt-> mappper.map(tkt, ticket.class))
.collect(Collectors.toList());
It is very simple to use like mappper.map(targetClass, DestinationClass.class)
I used Java8 code here but you can use anyone. I hope It would be very helpful to you.

Spring boot issue with mapping entity to entityDTO

I am using spring-boot-1.5.6 and modelmapper-1.1.0. I would like to map the entity object to OrderDto
but don't know how to do it via modelMapper. Please find the below code
Order.Java
public class Order {
private String orderUid;
private Invoice invoice;
private List<Item> items;
//Getter & setter
}
Item.java
public class Item {
private String itemId;
private String itemName;
private String itemUid;
private String isbn;
//other details of item
//Getter & setter
}
OrderDTO.java
public class OrderDTO {
private String orderUid;
private Invoice invoice;
private String itemId;
private String itemName;
private String itemUid;
private String isbn;
//other details of item
//Getter & setter
}
I would like to return OrderDTO with the item based on the itemID we are getting from the client(FrontEnd)
public Page<OrderDTO> convertOrderEntityToDTO (Page<Order> orderList,String itemId) {
ModelMapper modelMapper = new ModelMapper();
Type listType = new TypeToken<Page<OrderDTO>>() {}.getType();
modelMapper.addConverter((MappingContext<Order, OrderDTO> context) -> {
Item item = context.getSource().getItems().stream()
.filter(item -> equalsIgnoreCase(item.getItemId,itemId))
.findAny().orElse(null);
if (item != null) {
OrderDTO orderDTO = context.getDestination();
orderDTO.setItemId(item.getItemId());
orderDTO.setItemName(item.getItemName());
orderDTO.setItemUid(item.getItemUid());
orderDTO.setIsbn(item.getIsbn());
return orderDTO;
}
return null;
});
Page<OrderDTO> addonServices = modelMapper.map(orderList, listType);
}
In the above method, converter was never called(may be because of incorrect TypePair of modelMapper) and the item related attributes
in OrderDTO is always null. I would like to get the Item value based on ItemId.
Any help or hint would be appreciable. Any suggestion with or without modelMapper would be really appreciable.
As far as I understand, you use Page class of org.springframework.data or something like that. Anyway, this generic Page class contains generic List with your data. I would say, that this construction is just "too generic" for modelMapper. You'd better get list of your data, convert it and create a new Page.
Furthermore
You should create a typeMap to register a new converter
context.getDestination() returns not null if you provide new destination object by modelMapper.map(..). In your case you'll get null.
This is my vision of your convertOrderEntityToDTO method:
public Page<OrderDTO> convertOrderEntityToDTO(Page<Order> orderList, String itemId) {
ModelMapper modelMapper = new ModelMapper();
modelMapper.createTypeMap(Order.class, OrderDTO.class)
.setConverter(context -> context.getSource().getItems().stream()
.filter(o -> equalsIgnoreCase(o.getItemId(), itemId))
.findAny().map(o -> {
OrderDTO orderDTO = new OrderDTO();
orderDTO.setItemId(o.getItemId());
orderDTO.setItemName(o.getItemName());
orderDTO.setItemUid(o.getItemUid());
orderDTO.setIsbn(o.getIsbn());
return orderDTO;
}).orElse(null));
List<OrderDTO> orderDtoList = orderList.getContent().stream()
.map(o -> modelMapper.map(o, OrderDTO.class))
.collect(Collectors.toList());
return new PageImpl<>(orderDtoList, orderList.getPageable(), orderList.getTotalElements());
}
If you are using Spring Data JpaRepositories to retrieve data from the database you can make a repository method that creates a DTO for you. Such a method would look something like this:
#Query("SELECT new full.path.OrderDTO(s.itemId, s.itemName, s.itemUid) FROM Item s WHERE s.itemId = :id")
public OrderDTO getOrderDTOByItemId(#Param("id") long id);
The method should go in the jparepository class you use to retrieve Item instances from the database with. To make this work your OrderDTO needs to have a constructor with this parameter list. ( long itemId, String itemName, String itemUid). The parameters need to be in the same order as (s.itemId, s.itemName, s.itemUid). Also always make sure you create a default constructor as well when you create a constructor with parameters.

Update multiple users by passing list of User ids

I am trying to update some user information by passing List of User-Ids as parameter
i want to update isActive field of User fo which i am passing the user ids.
Below is my controller
#PutMapping
#ResponseStatus(HttpStatus.OK)
#RequestMapping("/UserUpdate")
public ResponseEntity<?> updateUsers(List<Long> userIds) {
**userService.updateUsers(userIds);**
return ResponseEntity.ok(200);
}
updateUsers() is a method in my Service where i have to write the logic
I tried something like below but it's not working
public void updateUsers(List<Long> userIds) {
List<Users> userList= userRepository.findAll();
for (Long i : userIds) {
for ( Users user : userList)
{
if(userRepository.findById(i) != null)
{
user.setIsActive(9L);
user.setUserName("Update Test");
}
my dto
public class UserDto {
private List<Users> userList;
private String appName="Users Project";
// getters and setters removed for brevity
And my Users entity class
#Entity
#Table(name="USERS")
public class Users {
#Id
#Column(name="USER_ID")
#GeneratedValue(strategy = GenerationType.AUTO)
private Long userId;
#Column(name="NAME")
private String userName;
#Column(name="ACTIVE")
private Long isActive;
// getters and setters removed for brevity
Alternatively you can use the following code
#Modifying
#Query("update Users u set u.isActive = ?1, u.userName = ?2 where u.userId in ?3")
void updateUsers(Long isActive, String userName, List<Long> userId);
Add this code in your userRepository and use the method.
public void updateUsers(List<Long> userIds) {
for (Long i : userIds) {
User user = userRepository.findById(i);
if(user != null){
user.setIsActive(9L);
user.setUserName("Update Test");
// call your update method here (this is not stated in your code)
}
}
}

Spring-Boot Data MongoDB - How to getting a specific nested object for a super specific object

I have the following data model, and I want to get a specific object in the sub list objects, I know it's possible to get the entire list and go through each object and compare with what the search id, but I wonder if it is possible use MongoRepository to do this.
#Document
public class Host {
#Id
private String id;
#NotNull
private String name;
#DBRef
private List<Vouchers> listVoucher;
public Host() {
}
//Getters and Setters
}
And..
#Document
public class Vouchers {
#Id
private String id;
#NotNull
private int codeId;
public Vouchers() {
}
//Getters and Setters
}
The Repository Class:
public interface HostRepository extends MongoRepository<Host, String> {
List<Host> findAll();
Host findById(String id);
Host findByName(String name);
//How to build the correct query ??????????
List<Vouchers> findVouchersAll();
Vouchers findByVouchersById(String hostId, String voucherId);
}
The Controller Class:
#RestController
#RequestMapping(value = "api/v1/host")
public class VoucherController {
#Inject
HostRepository hostRepository;
#RequestMapping(value = "/{hostId}/voucher",method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
#ResponseBody
public List<Vouchers> list() {
return hostRepository.findVouchersAll();
}
#RequestMapping(value = "/{hostId}/voucher/{voucherId}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
#ResponseBody
public Vouchers getOneVoucher(#PathVariable String hostId, #PathVariable String voucherId) {
Vouchers voucher = hostRepository.findByVouchersById(hostId, voucherId);
if (voucher != null) {
return voucher;
} else {
throw new VoucherNotFoundException(String.format("There is no voucher with id=%s", voucherId));
}
}
}
Thanks in Advance!
I think there is a way to do this although I have not tried this myself but maybe I can shed some light in how I would do it.
Firstly, I would rather use the more flexible way of querying mongodb by using MongoTemplate. MongoTemplate is already included in the Spring Boot Mongodb data library and it looks like you are already using the library so it is not an additional library that you will have to use. In Spring there is a way to #Autowired your MongoTemplate up so it is quick and easy to get the object for completing this task.
With mongoTemplate, you would do something like this:
Query query = new Query();
query.addCriteria(Criteria.where("listVouchers.id").is("1234"));
List<Host> host = mongoTemplate.find(query, Host.class);
Please see docs here: https://docs.mongodb.org/manual/tutorial/query-documents/

Categories