MapStruct: How to map to existing target? - java

I have a method in a service that updates an entity. It accepts object that has the data to update the entity. Dto object has less fields than entity but the fields have the same names.
Could it be possible to use mapstruct for that routine job by passing an existing target object?
class Entity {
id
name
date
country
by
... //hell of the fields
}
class UpdateEntity {
name
country
... //less but still a lot
}
class EntityService {
update(UpdateEntity u) {
Entity e = // get from storage
mapstructMapper.mapFromTo(u, e)
}
}

Yes, all you need to do is define a Mapper with an update method, something like:
import org.mapstruct.Mapper;
import org.mapstruct.MappingTarget;
#Mapper
public interface EntityMapper {
void update(#MappingTarget Entity entity, UpdateEntity updateEntity);
}
Please, review the relevant documentation.
By default, Mapstruct will map every matching property in the source object to the target one.

Related

How to ignore field/column only when return response from spring boot

I need to ignore the field when return the response from spring boot. Pls find below info,
I have one pojo called Student as below
Student {
id,
name,
lastName
}
i am getting a body for as PostRequest as below
{
id:"1",
name:"Test",
lname:"Test"
}
i want get all the data from frontEnd (id,name,Lname) But i just want to return the same pojo class without id as below,
{
name:"Test",
lName:"Test"
}
I have tried #JsonIgnore for column id, But it makes the id column as null(id=null -it is coming like this even when i send data to id field from postman) when i get the data from frontEnd.
I would like to use only one pojo to get the data with proper data(withoud getting id as Null), and need to send back the data by ignoring the id column.
Is there any way to achieve it instead of using another pojo?
You just need to use #JsonInclude(JsonInclude.Include.NON_NULL) at class level and it will be helpful for ignore all your null fields.
For example :
#JsonInclude(JsonInclude.Include.NON_NULL)
public class Test {
// Fields
// Constructors
// Getters - setters
}
As of now you are using only one POJO it's not good practice because it's your main entity into your project, so good practice is always make DTO for the same.
This is possible via the #JsonView annotation that is part of Jackson. Spring can leverage it to define the views used on the controller.
You'd define your DTO class like this:
class User {
User(String internalId, String externalId, String name) {
this.internalId = internalId;
this.externalId = externalId;
this.name = name;
}
#JsonView(User.Views.Internal.class)
String internalId;
#JsonView(User.Views.Public.class)
String externalId;
#JsonView(User.Views.Public.class)
String name;
static class Views {
static class Public {
}
static class Internal extends Public {
}
}
}
The Views internal class acts as a marker to jackson, in order to tell it which fields to include in which configuration. It does not need to be an inner class, but that makes for a shorter code snippet to paste here. Since Internal extends Public, all fields marked with Public are also included when the Internal view is selected.
You can then define a controller like this:
#RestController
class UserController {
#GetMapping("/user/internal")
#JsonView(User.Views.Internal.class)
User getPublicUser() {
return new User("internal", "external", "john");
}
#GetMapping("/user/public")
#JsonView(User.Views.Public.class)
User getPrivateUser() {
return new User("internal", "external", "john");
}
}
Since Spring is aware of the JsonView annotations, the JSON returned by the /public endpoint will contain only externalId and name, and the /internal endpoint will additionally include the internalId field.
Note that fields with no annotation will not be included if you enable any view. This behaviour can be controlled by MapperFeature.DEFAULT_VIEW_INCLUSION, which was false in the default Spring ObjectMapper when I used this for the last time.
You can also annotate your #RequestBody parameters to controller methods with JsonView, to allow/disallow certain parameters on input objects, and then use a different set of parameters for output objects.

LazyInitializationException with Mapstruct because of cyclic issue

I have a development project using Spring Data JPA and MapStruct to map between Entities and DTOs. Last week I decided it was time to address the FetchType.EAGER vs LAZY issue I have postponed for some time. I choose to use #NamedEntityGraph and #EntityGraph to load properties when needed. However I am stuck with this LazyInitializationExeption problem when doing the mapping from entity to dto. I think I know where this happens but I do not know how to get passed it.
The code
#NamedEntityGraph(name="Employee.full", ...)
#Entity
public class Employee {
private Set<Role> roles = new HashSet<>();
}
#Entity
public class Role {
private Set<Employee> employees = new HashSet<>();
}
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
#EntityGraph(value = "Employee.full")
#Override
Page<Employee> findAll(Pageable pageable);
}
#Service
public class EmployeeService {
public Page<EmployeeDTO> findAll(PageRequest pageRequest) {
Page<Employee> employees = repository.findAll(pageRequest); // ok
Page<EmployeeDTO> dtos = employees.map(emp -> mapper.toDTO(emp, new CycleAvoidMappingContext()); // this is where the exception happens
return dtos;
}
}
// also there is EmployeeDTO and RoleDTO classes mirroring the entity classes
// and there is a simple interface EmployeeMapper loaded as a spring component
// without any special mappings. However CycleAvoidingMappingContext is used.
I have tracked down the LazyInitializationException to happen when the mapper tries to map the roles dependency. The Role object do have Set<Employee> and therefore there is a cyclic reference.
When using FetchType.EAGER new CycleAvoidingMappingContext() solved this problem, but with LAZY this no longer works.
Does anybody know how I can avoid the exception and at the same time get my DTOs mapped correctly?
The problem is that when the code returns from findAll the entities are not managed anymore. So you have a LazyInitializationException because you are trying, outside of the scope of the session, to access a collection that hasn't been initialized already.
Adding eager make it works because it makes sure that the collection has been already initialized.
You have two alternatives:
Using an EAGER fetch;
Make sure that the entities are still managed when you return from the findAll. Adding a #Transactional to the method should work:
#Service
public class EmployeeService {
#Transactional
public Page<EmployeeDTO> findAll(PageRequest pageRequest) {
Page<Employee> employees = repository.findAll(pageRequest);
Page<EmployeeDTO> dtos = employees.map(emp -> mapper.toDTO(emp, new CycleAvoidMappingContext());
return dtos;
}
}
I would say that if you need the collection initialized, fetching it eagerly (with an entity graph or a query) makes sense.
Check this article for more details on entities states in Hibernate ORM.
UPDATE: It seems that this error happens because Mapstruct is converting the collection even if you don't need it in the DTO.
In this case, you have different options:
Remove the field roles from the DTO. Mapstruct will ignore the field in the entity because the DTO doesn't have a field with the same name;
Create a different DTO class for this specific case without the field roles;
Use the #Mapping annotation to ignore the field in the entity:
#Mapping(target = "roles", ignore = true)
void toDTO(...)
or, if you need the toDTO method sometimes
#Mapping(target = "roles", ignore = true)
void toSkipRolesDTO(...) // same signature as toDTO

MapStruct Is there a way to define defaut enum strategy for unknown value at class level?

I'm looking to define at mapper class level, a strategy that said if an enum value is not mapped, to map it to null.
Just like i would write at method level:
#ValueMapping( source = MappingConstants.ANY_REMAINING, target = MappingConstants.NULL )
So that it applies to all sub mapping method automaticaly generated by mapstruct, without forcing me to declare all of them just to add this line.
Create a 'class' mapper for your enum and then use it in other mapper with the uses attribute of the #Mapper annotation.
For example :
public interface MyEnumMapper {
#ValueMapping( source = MappingConstants.ANY_REMAINING, target = MappingConstants.NULL )
public String asString(MyEnum myenum);
}
and then to use it
#Mapper(uses=MyEnumMapper.class)
public class CarMapper {
CarDto carToCarDto(Car car);
}
(assuming Car object has an attribute of type MyEnum)
see the full documentation here : https://mapstruct.org/documentation/stable/reference/html/#invoking-other-mappers

Ambiguous mapping methods found for mapping property

I have two domain entities:
class Identity {
Long id;
Set<Business> businesses;
}
class Business {
Long id;
String name;
}
I then have two DTOs that extend a base DTO:
class BaseDto {
String id;
}
class IdentityDto extends BaseDto {
Set<BaseDto> businesses;
}
class BusinessDto extends BaseDto {
String name;
}
Then I created a mapper that maps a list of my domain entities to either a Set of the specific dto, or a set of the more generic base dto. This is because when I am getting a list of businesses, I want the full business dto, but when I get an identity, I just what the base info in it's list of businesses.
But when I try to create the mapper for the identity I get the following error:
Ambiguous mapping methods found for mapping property
"Set<Business> businesses" to Set<BaseDto>:
Set<BusinessDto> BusinessMapper.toSet(Set<Business> businesses),
Set<BaseDto> BusinessMapper.toBaseSet(Set<Business> businesses).
I thought that mapstruct used the most specific method, so should know to use the toIdentifierSet method in this case.
How do I make mapstruct know which method to use?
There is no most specific method here as you are trying to map into Set<BaseDto>.
You can use Mapping method selection based on qualifiers.
You can define some annotations:
#Qualifier
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.CLASS)
public #interface BaseInfo {
}
Then in your BusinessMapper
#Mapper
public interface BusinessMapper {
Set<BusinessDto> toSet(Set<Business> businesses);
#BaseInfo
Set<BaseDto> toBaseSet(Set<Business> businesses);
}
Then in your identifier
#Mapper
public interface IdentifierMapper {
#Mapping(target = "businesses", qualifiedBy = BaseInfo.class)
IdentityDto map(Identity source);
}
In case you want to explicitly pick always you can add another annotation BusinessInfo and then annotate the other method. Then you would need to pick a method each time.

how to save an entity with a DiscriminatorValue

I have two entities User and Candidat, where Candidat extends the class User, as following :
User entity :
#Entity
#Table(name="users")
#Inheritance(strategy=InheritanceType.SINGLE_TABLE)
#DiscriminatorColumn(name="TYPE_USER",discriminatorType=DiscriminatorType.STRING,length=2)
#JsonTypeInfo(use=JsonTypeInfo.Id.NAME,include=JsonTypeInfo.As.PROPERTY,property="type")
#JsonSubTypes({
#Type(name="UC",value=Candidat.class)
})
#XmlSeeAlso({Candidat.class})
public class User implements Serializable {
#GeneratedValue(strategy=GenerationType.IDENTITY)
private Long codeUser;
//other code ...
Candidat entity :
#Entity
#DiscriminatorValue("UC")
#XmlType(name = "UC")
public class Candidat extends User {
private String codeMassar;
and to save a new Candidat I call this repository method :
candidatRepository.save()
from :
public interface CandidatRepository extends JpaRepository<Candidat, String> {
}
This is my rest service that calls the save method:
#RequestMapping(value = "/candidats", method = RequestMethod.POST)
public Candidat saveCandidat(#RequestBody Candidat candidat) throws Exception {
return candidatMetier.saveCandidat(candidat);
}
The problem is when I want to save a new Candidat as following :
{
"username": "User",
"password": "123456",
"email": "user#gmail.com"
}
I get an error saying :
Failed to read HTTP message:
org.springframework.http.converter.HttpMessageNotReadableException:
Could not read document: Unexpected token (END_OBJECT), expected
FIELD_NAME: missing property 'type' that is to contain type id (for
class org.capvalue.fme.domain.Candidat)
What I understand from it that I have to specify the type in the JSON object I'm sending, but I don't think that's necessary because I save a new Candidat which has the #DiscriminatorValue("UC"), so when its save in the User table it will be saved with type='UC' automatically.
how can I solve this ?
what I understand from it that I have to specify the type in the json
object I'm sending
Since you've added the #JsonTypeInfo, you should specify the actual class of object instances using the type field. For instance, if you set the type field to UC, Jackson will create an instance of Candidat class.
but I dont think that's necessary because I save a new Candidat which
has the #DiscriminatorValue("UC") so when its save in the User table
it will be saved with type='UC' automatically.
#DiscriminatorValue is going to be handled by your JPA provider. On the contrary, #JsonTypeInfo is a Jackson concept, so No! You can't expect your JPA #DiscriminatorValue helps Jackson to determine the actual type of the object instances.
You should either send type information in your JSON representation or remove #JsonTypeInfo and #JsonSubTypes from your User class. I guess removing the Jackson type annotations is the better approach, since you're using the actual subclasses in:
public Candidat saveCandidat(#RequestBody Candidat candidat) { ... }
Also, try to define some DTOs and return them as your REST endpoint return values. One advantage of this approach is that your Jackson and JPA metadatas are separated, hence you would avoid these problems.

Categories

Resources