Getting nulls while using ModelMapper - java

I'm trying to utilize the ModelMapper in my convertion process. What I need to do is to convert the Sample entity to SampleDTO object.
I have the Sample entity like the following:
#Entity
#Table(name = "sample", schema = "sample_schema")
#Data
#NoArgsConstructor
public class Sample {
private static final String SEQUENCE = "SAMPLE_SEQUENCE";
#Id
#SequenceGenerator(sequenceName = SEQUENCE, name = SEQUENCE, allocationSize = 1)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = SEQUENCE)
private Long id;
#Column(name = "name")
private String name;
#Column
private String surname;
#OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
#JoinColumn(name = "id_deetails")
private Details details;
}
Which holds the Details one:
#Entity
#Table(name = "details", schema = "sample_schema")
#Data
#NoArgsConstructor
public class Details {
private static final String SEQUENCE = "DETAILS_SEQUENCE";
#Id
#SequenceGenerator(sequenceName = SEQUENCE, name = SEQUENCE, allocationSize = 1)
#GeneratedValue(strategy = GenerationType.SEQUENCE, generator = SEQUENCE)
private Long id;
#Column(name = "street_name")
private String streetName;
#Column
private String city;
}
I'd like the DTO to be this format:
#NoArgsConstructor
#AllArgsConstructor
#Data
public class SampleDTO {
private Long id;
private String name;
private String surname;
private String streetName;
private String city;
}
I also made a ModelMapper bean like:
#Bean
public ModelMapper modelMapper() {
return new ModelMapper();
}
And I made a converter component:
#Component
public class EntityDtoConverter {
private final ModelMapper modelMapper;
#Autowired
public EntityDtoConverter(ModelMapper modelMapper) {
this.modelMapper = modelMapper;
}
public SampleDTO sampleToDto(Sample entity) {
return modelMapper.map(entity, SampleDTO.class);
}
}
The problem is
when I try to use this mapper converter in my service
#Service
public class SampleService {
private final SampleRepository sampleRepository;
private final EntityDtoConverter entityDtoConverter;
#Autowired
public SampleService(SampleRepository sampleRepository, EntityDtoConverter entityDtoConverter) {
this.sampleRepository = sampleRepository;
this.entityDtoConverter = entityDtoConverter;
}
public List<SampleDTO> getSamples() {
List<SampleDTO> samples = sampleRepository.findAll()
.map(entityDtoConverter::sampleToDto);
return new List<SampleDTO>(samplesPage);
}
}
I get nulls in places of Details fields.
I have followed Baeldung's tutorial about model-to-dto conversion with ModelMapper and the documentation of it as well but the least wasn't much of help. There is something I'm missing and I have no idea what it is.
I'm working on:
Java 11
Spring Boot 2.3.0
ModelMapper 2.3.8

Try:
modelMapper.getConfiguration().setPropertyCondition(Conditions.isNotNull());
Also check: Modelmapper: How to apply custom mapping when source object is null?

Related

Error when mapping classes with attribute that has foreign key

I have the Hardware entity, HardwareDtoRequest and HardwareDtoResponse classes, where I'm using the modelMapper to map them. In the Hardware table, there is a foreign key to the Provider table.
The problem is that I am not able to map this attribute to HardwareDtoRequest, when I call the POST method in Postman passing only the provider_id in the request body it saves only one record with that particular ID, when trying to save again another record with the same ID it updates the old one.
How do I map this foreign key attribute to the DtoRequest and save?
Hardware.java
#Getter
#Setter
#Entity
public class Hardware {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(nullable = false, length = 100)
private String name;
#ManyToOne
#JoinColumn(name = "provider_id")
private Provider provider;
}
Provider.java
#Getter
#Setter
#Entity
public class Provider {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(nullable = false, length = 100)
private String name;
}
HardwareDtoRequest.java
#Getter
#Setter
#ToString
#AllArgsConstructor
#NoArgsConstructor
public class HardwareDtoRequest {
#NotNull(message = "required field")
private String name;
#NotNull(message = "required field")
private Long providerId;
}
HardwareDtoResponse.java
#Getter
#Setter
#ToString
#AllArgsConstructor
#NoArgsConstructor
public class HardwareDtoResponse {
private Long id;
private String name;
private ProviderDtoResponse provider;
}
HardwareMapper.java
public HardwareDtoResponse toHardwareDtoResponse(Hardware hardware) {
return mapper.map(hardware, HardwareDtoResponse.class);
}
public Hardware toHardware(HardwareDtoRequest hardwareDtoRequest) {
return mapper.map(hardwareDtoRequest, Hardware.class);
}
HardwareService.java
#Transactional
public HardwareDtoResponse save(HardwareDtoRequest hardwareDtoRequest) {
Hardware hardware = mapper.toHardware(hardwareDtoRequest);
Hardware saveHardware = hardwareRepository.save(hardware);
return mapper.toHardwareDtoResponse(saveHardware);
}
HardwareController.java
#PostMapping
public ResponseEntity<HardwareDtoResponse> save(#Valid #RequestBody HardwareDtoRequest hardwareDtoRequest) {
log.info("saving hardware: {}", hardwareDtoRequest);
HardwareDtoResponse hardware = hardwareService.save(hardwareDtoRequest);
return new ResponseEntity<>(hardware, HttpStatus.CREATED);
}
I managed to solve it, for those who have the same problem of mapping dtos with modelMapper, I use the following snippet in ModelMapperConfig:
#Configuration
public class ModelMapperConfig {
#Bean
public ModelMapper mapper() {
ModelMapper modelMapper = new ModelMapper();
modelMapper.getConfiguration().setMatchingStrategy(MatchingStrategies.STRICT);
return modelMapper;
}
}
You can try to set provider manually. Like this:
public Hardware toHardware(HardwareDtoRequest hardwareDtoRequest) {
Hardware hardware = mapper.map(hardwareDtoRequest, Hardware.class);
Provider provider = providerRepository.findById(hardwareDtoRequest.providerId);
hardware.setProvider(provider);
return hardware;
}

One to Many Relation mapping using org.mapstruct framework

How can I map the one to many relationship using org.mapstruct framework?
DTO classes:
#Data
public class ScheduledJobDTO {
private String jobName;
private String jobGroup;
private String jobClass;
private String cronExpression;
private Boolean cronJob;
private Long repeatTime;
private Integer repeatCount;
private Set<ScheduledJobParamsDTO> paramtersDTOs;
}
#Data
#EqualsAndHashCode
public class ScheduledJobParamsDTO {
String name;
String value;
}
Domain Classes -
#Data
#Entity
#Table(name = "scheduled_job")
public class ScheduledJob {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
#Column(name = "job_name")
private String jobName;
#Column(name = "job_group")
private String jobGroup;
#Column(name = "job_class")
private String jobClass;
#Column(name = "cron_expression")
private String cronExpression;
#Column(name = "is_cron_job")
private Boolean cronJob;
#Column(name = "repeat_time")
private Long repeatTime;
#Column(name = "repeat_count")
private Integer repeatCount;
#Column(name = "trigger_start_date")
private LocalDate triggerStartDate;
#Column(name = "trigger_end_date")
private LocalDate triggerEndDate;
#Column(name = "created_at")
private LocalDate createdAt;
#Column(name = "modified_at")
private LocalDate modifiedAt;
#Column(name = "is_active")
private Boolean active;
#OneToMany(mappedBy = "scheduledJob")
private Set<ScheduledJobParams> parameters;
}
#Data
#Entity
#Table(name = "scheduled_job_params")
#EqualsAndHashCode
public class ScheduledJobParams {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
Long id;
#ManyToOne(fetch = FetchType.LAZY, optional = false)
#JoinColumn(name = "scheduled_job_id", nullable = false)
ScheduledJob scheduledJob;
String name;
String value;
}
Mapper Class -
#Mapping(source = ".", target = ".")
#Mapping(source = "paramtersDTOs", target = "parameters")
ScheduledJob mapToDomain(ScheduledJobDTO scheduledJobDTO);
Now, the above mapper is mapping the ScheduledJob & ScheduledJobParams, but the ScheduledJobParams has reference of ScheduledJob.
How can I map the reference ScheduledJob to ScheduledJobParams?
You can achieve that through #AfterMapping with #MappedTarget. This is described in the reference documentation: 12.2. Mapping customization with before-mapping and after-mapping methods.
// Java 8+ otherwise you need to use an abstract class and a for-loop instead
#Mapper(componentModel = "spring")
public interface ScheduledJobMapper {
#Mapping(target = "parameters", source = "paramtersDTOs")
ScheduledJob mapToDomain(ScheduledJobDTO dto);
#AfterMapping
default void after(#MappingTarget ScheduledJob domain, ScheduledJobDTO dto) {
domain.getParameters().forEach(scheduledJobParams -> {
scheduledJobParams.setScheduledJob(domain);
});
}
}
However, I am sure you don't need to fill the bidirectional relationship when you map back from the DTO into the entity (this is what I understand as you refer to "domain"). Note printing out or serializing such object i.e. into JSON or XML throws java.lang.StackOverflowError if not properly handled.

Mapping object's related entities by other field than ID

I am using the following entities:
#Entity
#Table(name = "books")
public class Book {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
#ManyToOne
private User user;
private UUID uuid = UUID.randomUUID();
}
and
#Entity
#Table(name = "users")
public class User {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String email;
private String firstName;
private String lastName;
private String password;
#OneToMany(mappedBy = "user")
private List<Book> books;
private UUID uuid = UUID.randomUUID();
}
and the following DTO:
public class NewBookRequest {
private UUID userUuid;
}
and the converter:
#Component
public class NewBookRequestToEntityConverter implements Converter<NewBookRequest, Book> {
private final ModelMapper modelMapper;
public NewBookRequestToEntityConverter(final ModelMapper modelMapper) {
this.modelMapper = modelMapper;
}
#Override
public Book convert(#NotNull final NewBookRequest source) {
return modelMapper.map(source, Book.class);
}
}
and there is part of service code:
public void addBook(final NewBookRequest newBookRequest) {
final Book newBook = newBookRequestToEntityConverter.convert(newBookRequest);
bookRepository.save(newBook);
}
When I try to save the converted Book entity I get the ConstraintViolationException exception, as newBook.user.id is null. However, newBook.user.uuid is correctly assigned. Is there any way to automatically map newBook's user by its uuid? Or the only solution is to do something like this:
add new method to converter:
#Component
public class NewBookRequestToEntityConverter implements Converter<NewBookRequest, Book> {
private final ModelMapper modelMapper;
public NewBookRequestToEntityConverter(final ModelMapper modelMapper) {
this.modelMapper = modelMapper;
}
#Override
public Book convert(#NotNull final NewBookRequest source) {
return modelMapper.map(source, Book.class);
}
public Book convert(#NotNull final NewBookRequest source, final User user) {
Book target = modelMapper.map(source, Book.class);
target.setUser(user);
return target;
}
}
and modify service's code:
public void addBook(final NewBookRequest newBookRequest) {
final User user = userRepository.getUserByUuid(newBookRequest.getUserUuid());
final Book newBook = newBookRequestToEntityConverter.convert(newBookRequest, user);
bookRepository.save(newBook);
}
? Thanks for any help!
In Book or User entity you could use UUID as primary key. Did you try that?
#Id
#GeneratedValue(generator = "UUID")
#GenericGenerator(
name = "UUID",
strategy = "org.hibernate.id.UUIDGenerator",
)
#Column(name = "id", updatable = false, nullable = false)
private UUID id;
However on some databases there can be performance issue when using indexes based on UUID if there is high load.
I think problem is your converter does not initialize the primary key of User.
You may either change the type of id field in the User class due to answer https://stackoverflow.com/a/64141178/14225495, or add the annotation
#JoinColumn(name = "user_uuid", referencedColumnName = "uuid")
to the user field in the Book class. In this case you should also add column "user_uuid" in the books table, make sure the users.uuid column is unique and not nullable to create foreign key.

Not exposing the path of entities that have a composite primary key to the front end when using the Springframework Page object

I'm working on an API endpoint that returns a Springframework Page response. I want the front end to be able to sort the data but I can't expect the front end to know that the column they want to sort on is actually inside a composite primary key.
In the example below (a simplified version of what I'm working on) you can see that the startDate column is inside a RouteEntityPk class, which is linked to the RouteEntity class with the #EmbeddedId annotation. To Sort on that column the front end would need to add ?sort=pk.startdate,asc to the request. I want the front end to only have to provide ?sort=startdate,asc.
Is there a way - using Spring magic - of having the repository know that startdate == pk.startdate, or will I have to write a translator which will remove the pk when showing the sort column to the front end, and add it where necessary when reading it from the request?
Controller:
#GetMapping(value = "routes/{routeId}", produces = APPLICATION_JSON_UTF8_VALUE)
public ResponseEntity<Page<Route>> getRouteByRouteId(#PathVariable(value = "routeId") final String routeId,
#PageableDefault(size = 20) #SortDefault.SortDefaults({
#SortDefault(sort = "order", direction = Sort.Direction.DESC),
#SortDefault(sort = "endDate", direction = Sort.Direction.DESC)
}) final Pageable pageable) {
return ResponseEntity.ok(routeService.getRouteByRouteId(routeId, pageable));
}
Service:
public Page<Route> getRouteByRouteId(String routeId, Pageable pageable) {
Page<RouteEntity> routeEntities = routeRepository.findByRouteId(routeId, pageable);
return new PageImpl<>(
Collections.singletonList(routeTransformer.toRoute(routeId, routeEntities)),
pageable,
routeEntities.getContent().size()
);
}
Repository:
#Repository
public interface RouteRepository extends JpaRepository<RouteEntity, RouteEntityPk> {
#Query(value = " SELECT re FROM RouteEntity re"
+ " AND re.pk.routeId = :routeId")
Page<RouteEntity> findByRouteId(#Param("routeId") final String routeId,
Pageable pageable);
}
Entities:
Route:
#Data
#Entity
#Builder(toBuilder = true)
#NoArgsConstructor
#AllArgsConstructor
#Table(name = "ROUTE", schema = "NAV")
public class RouteEntity {
#EmbeddedId
private RouteEntityPk pk;
#Column(name = "NAME")
private String name;
#Column(name = "ORDER")
private Integer order;
#Column(name = "END_DTE")
private LocalDate endDate;
}
RoutePk:
#Data
#Builder(toBuilder = true)
#Embeddable
#NoArgsConstructor
#AllArgsConstructor
public class RouteEntityPk implements Serializable {
private static final long serialVersionUID = 1L;
#Column(name = "ROUTE_ID")
private String routeId;
#Column(name = "STRT_DTE")
private LocalDate startDate;
}
Models:
Route:
#Data
#Builder
public class Route {
public String name;
public String routeId;
public List<RouteItem> items;
}
Item:
#Data
#Builder
public class Item {
public Integer order;
public LocalDate startDate;
public LocalDate endDate;
}
Transformer:
public Route toRoute(String routeId, Page<RouteEntity> routeEntities) {
return Route.builder()
.name(getRouteName(routeEntities))
.routeId(routeId)
.items(routeEntities.getContent().stream()
.map(this::toRouteItem)
.collect(Collectors.toList()))
.build();
}
private Item toRouteItem(RouteEntity item) {
return ParcelshopDrop.builder()
.order(item.getOrder())
.startDate(item.getStartDate())
.endDate(item.getEndDate())
.build();
}
So it looks like the way to do this is to use the other way you can deal with composite primary key's in JPA, the annotation #IdClass. This way you can put the fields in the main entity and refer to them as such.
Below is a link to the baeldung article I followed and the changes to the entities I posted above that make this work:
https://www.baeldung.com/jpa-composite-primary-keys
Entities:
Route:
#Data
#Entity
#Builder(toBuilder = true)
#NoArgsConstructor
#AllArgsConstructor
#IdClass(RouteEntityPk.class)
#Table(name = "ROUTE", schema = "NAV")
public class RouteEntity {
#Id
#Column(name = "ROUTE_ID")
private String routeId;
#Id
#Column(name = "STRT_DTE")
private LocalDate startDate;
#Column(name = "NAME")
private String name;
#Column(name = "ORDER")
private Integer order;
#Column(name = "END_DTE")
private LocalDate endDate;
}
RoutePk:
#Data
#Builder(toBuilder = true)
#NoArgsConstructor
#AllArgsConstructor
public class RouteEntityPk implements Serializable {
private static final long serialVersionUID = 1L;
private String routeId;
private LocalDate startDate;
}
This is one solution, probably not the best, but you can transform Pageable object in order to replace the field name like this :
In your controller getRouteByRouteId method :
List<Order> orders = pageable.getSort().stream().map(o -> o.getProperty().equals("startdate") ? new Order(o.getDirection(), "pk.startdate"): o).collect(Collectors.toList());
Then you can call the service with the modified object :
return ResponseEntity.ok(routeService.getRouteByRouteId(routeId, PageRequest.of(pageable.getPageNumber(), pageable.getPageSize(), Sort.by(orders))));

Java ModelMapper -Failed to convert java.util.List to java.util.List

I am using the Java ModelMapper library to map DTOs to ORM #Entity objects. I have the following test set up:
public class MapperTest {
#Autowired
private ModelMapper mapper;
#Autowired
private TicketRepository ticketRepo;
#Test
public void testTicket() {
Ticket ticket = ticketRepo.findById(4).get();
TicketDTO dto = mapper.map(ticket, TicketDTO.class);
assertThat(dto.getEquipmenDescription()).isEqualTo(ticket.getEquipment().getDescription());
assertThat(dto.getEquipmentNotes()).isEqualTo(ticket.getEquipmentNotes());
assertThat(dto.getId()).isEqualTo(ticket.getId());
assertThat(dto.getNotes()).isEqualTo(ticket.getNotes());
assertThat(dto.getOracleID()).isEqualTo(ticket.getOracleID());
assertThat(dto.getPropertyID()).isEqualTo(ticket.getPropertyID());
assertThat(dto.getNotes().size()).isEqualTo(ticket.getNotes().size());
for (TicketNoteDTO note : dto.getNotes()) {
assertThat(note.getId()).isNotEqualTo(0);
assertThat(note.getIssueDate()).isNotNull();
assertThat(note.getUserUserName()).isNotEmpty();
}
}
}
This fails with the following error:
org.modelmapper.MappingException: ModelMapper mapping errors:
1) Converter org.modelmapper.internal.converter.CollectionConverter#501c6dba failed to convert java.util.List to java.util.List.
The following are my Entity and corresponding DTOs. Getters and setters are omitted for brevity.
Ticket
#Entity
#Table(name = "ticket")
public class Ticket {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private int id;
#Column(name = "equipment_notes")
private String equipmentNotes;
#Column(name = "is_open")
private boolean isOpen;
#Column(name = "oracle_id")
private String oracleID;
#ManyToOne
#JoinColumn(name = "equipment_id")
private EquipmentCategory equipment;
#Column(name = "property_id")
private String propertyID;
#OneToMany(mappedBy = "ticket")
private List<TicketNote> notes = new ArrayList<>();
}
TicketNote (getters and Setters omitted for brevity)
#Entity
#Table(name = "ticket_note")
public class TicketNote {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id")
private int id;
#ManyToOne
#JoinColumn(name = "ticket_id")
private Ticket ticket;
#ManyToOne
#JoinColumn(name = "user_id")
private AppUser user;
#Column(name = "issue_date")
private LocalDate issueDate;
#Column(name = "oracle_contact")
private String oracleContact;
#Column(name = "issue_resolved")
private boolean issueResolved;
}
TicketDTO
public class TicketDTO {
private int id;
private String equipmentNotes;
private boolean isOpen;
private String oracleID;
private String equipmenDescription;
private String propertyID;
private List<TicketNoteDTO> notes = new ArrayList<>();
}
TicketNoteDTO
public class TicketNoteDTO {
private int id;
private String userUserName;
private LocalDate issueDate;
private String oracleContact;
private boolean issueResolved;
}
I have some experience with the ModelMapper library, but I am not sure what the issue is. Any advice is appreciated.
Thanks.

Categories