I am facing an issue with one of my Mapstruct mappers not using another mapper with #Mapper(uses =
Our ValidationSupportNeedMapper maps from entities to DTOs. One ValidationSupportNeedEntity contains an ActivityEntity property and I am trying to map from this property to an Activity DTO.
The issue is therefore with a nested object i.e. ActivityEntity to Activity.
Here is the source code:
From ValidationSupportNeedMapper.java:
#Mapper(uses = {LifecycleErrorMessagesMapper.class, ActivityMapper.class})
public interface ValidationSupportNeedMapper {
ValidationSupportNeed toValidationSupportNeed(ValidationSupportNeedEntity source);
...
From ActivityMapper.java:
#Component
public class ActivityMapper {
public Activity toActivity(ActivityEntity activity) {
//Implementation
}
public ActivityEntity toActivityEntity(Activity activity) {
//Implementation
}
}
From ValidationSupportNeedEntity.java (Entity)
public class ValidationSupportNeedEntity {
private ActivityEntity activityEntity;
From ValidationSupportNeed.java (DTO)
public class ValidationSupportNeed implements AutoValidated {
private Activity validationActivity;
However Mapstruct seems to ignore the uses= attribute on the #Mapper annotation and goes ahead and generates its own mapping method as follows:
#Override
public ValidationSupportNeed toValidationSupportNeed(ValidationSupportNeedEntity source) {
if ( source == null ) {
return null;
}
ValidationSupportNeed validationSupportNeed = new ValidationSupportNeed();
validationSupportNeed.setValidationActivity( validationSupportNeedEntityToActivity( source ) );
...
}
protected Activity validationSupportNeedEntityToActivity(ValidationSupportNeedEntity validationSupportNeedEntity) {
if ( validationSupportNeedEntity == null ) {
return null;
}
Activity activity = new Activity();
activity.setCode( validationSupportNeedEntity.getValidationActivityCode() );
return activity;
}
What am I missing? Can someone please help?
edit: ActivityMapper is not autowired into the ValidationSupportNeedMapper implementation.
Adding a mapping annotation sorted the issue:
#Mapping(source = "activityEntity", target = "validationActivity")
ValidationSupportNeed toValidationSupportNeed(ValidationSupportNeedEntity source);
Notice the names of the attributes are different.
Related
i have a custom case that some of my dto's have a field of type X, and i need to map this class to Y by using a spring service method call(i do a transactional db operation and return an instance of Y). But in this scenario i need to use existing value of Y field. Let me explain it by example.
// DTO
public class AnnualLeaveRequest {
private FileInfoDTO annualLeaveFile;
}
//ENTITY
public class AnnualLeave {
#OneToOne
private FileReference annualLeaveFile;
}
#Mapper
public abstract class FileMapper {
#Autowired
private FileReferenceService fileReferenceService;
public FileReference toFileReference(#MappingTarget FileReference fileReference, FileInfoDTO fileInfoDTO) {
return fileReferenceService.updateFile(fileInfoDTO, fileReference);
}
}
//ACTUAL ENTITY MAPPER
#Mapper(uses = {FileMapper.class})
public interface AnnualLeaveMapper {
void updateEntity(#MappingTarget AnnualLeave entity, AnnualLeaveRequest dto);
}
// WHAT IM TRYING TO ACHIEVE
#Component
public class MazeretIzinMapperImpl implements tr.gov.hmb.ikys.personel.izinbilgisi.mazeretizin.mapper.MazeretIzinMapper {
#Autowired
private FileMapper fileMapper;
#Override
public void updateEntity(AnnualLeave entity, AnnualLeaveUpdateRequest dto) {
entity.setAnnualLeaveFile(fileMapper.toFileReference(dto.getAnnualLeaveFile(), entity.getAnnualLeaveFile()));
}
}
But mapstruct ignores the result of "FileReference toFileReference(#MappingTarget FileReference fileReference, FileInfoDTO fileInfoDTO) " and does not map the result of it to the actual entity's FileReference field. Do you have any idea for resolving this problem?
Question
How do I replace the annualLeaveFile property while updating the AnnualLeave entity?
Answer
You can use expression to get this result. For example:
#Autowired
FileMapper fileMapper;
#Mapping( target = "annualLeaveFile", expression = "java(fileMapper.toFileReference(entity.getAnnualLeaveFile(), dto.getAnnualLeaveFile()))" )
abstract void updateEntity(#MappingTarget AnnualLeave entity, AnnualLeaveRequest dto);
MapStruct does not support this without expression usage. See the end of the Old analysis for why.
Alternative without expression
Instead of fixing it in the location where FileMapper is used, we fix it inside the FileMapper itself.
#Mapper
public abstract class FileMapper {
#Autowired
private FileReferenceService fileReferenceService;
public void toFileReference(#MappingTarget FileReference fileReference, FileInfoDTO fileInfoDTO) {
FileReference wanted = fileReferenceService.updateFile(fileInfoDTO, fileReference);
updateFileReference(fileReference, wanted);
}
// used to copy the content of the service one to the mapstruct one.
abstract void updateFileReference(#MappingTarget FileReference fileReferenceTarget, FileReference fileReferenceFromService);
}
Old analysis
The following is what I notice:
(Optional) your FileMapper class is not a MapStruct mapper. This can just be a normal class annotated with #Component, since it does not have any unimplemented abstract methods. (Does not affect code generation of the MazeretIzinMapper implementation)
(Optional, since you have this project wide configured) you do not have componentModel="spring" in your #Mapper definition, maybe you have this configured project wide, but that is not mentioned. (required for the #Autowired annotation, and #Component on implementations)
Without changing anything I already get a working result as you want it to be, but for non-update methods (not listed in your question, but was visible on the gitter page where you also requested help) the FileMapper as is will not be used. It requires an additional method that takes only 1 argument: public FileReference toFileReference(FileInfoDTO fileInfoDTO)
(Edit) to get rid of the else statement with null value handling you can add nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE to the #Mapper annotation.
I've run a test and with 1.5.0.Beta2 and 1.4.2.Final I get the following result with the thereafter listed FileMapper and MazeretIzinMapper classes.
Generated mapper implementation
#Generated(
value = "org.mapstruct.ap.MappingProcessor",
date = "2022-03-11T18:01:30+0100",
comments = "version: 1.4.2.Final, compiler: Eclipse JDT (IDE) 1.4.50.v20210914-1429, environment: Java 17.0.1 (Azul Systems, Inc.)"
)
#Component
public class MazeretIzinMapperImpl implements MazeretIzinMapper {
#Autowired
private FileMapper fileMapper;
#Override
public AnnualLeave toEntity(AnnualLeaveRequest dto) {
if ( dto == null ) {
return null;
}
AnnualLeave annualLeave = new AnnualLeave();
annualLeave.setAnnualLeaveFile( fileMapper.toFileReference( dto.getAnnualLeaveFile() ) );
return annualLeave;
}
#Override
public void updateEntity(AnnualLeave entity, AnnualLeaveRequest dto) {
if ( dto == null ) {
return;
}
if ( dto.getAnnualLeaveFile() != null ) {
if ( entity.getAnnualLeaveFile() == null ) {
entity.setAnnualLeaveFile( new FileReference() );
}
fileMapper.toFileReference( entity.getAnnualLeaveFile(), dto.getAnnualLeaveFile() );
}
}
}
Source classes
Mapper
#Mapper( componentModel = "spring", uses = { FileMapper.class }, nullValuePropertyMappingStrategy = NullValuePropertyMappingStrategy.IGNORE )
public interface MazeretIzinMapper {
AnnualLeave toEntity(AnnualLeaveRequest dto);
void updateEntity(#MappingTarget AnnualLeave entity, AnnualLeaveRequest dto);
}
FileMapper component
#Mapper
public abstract class FileMapper {
#Autowired
private FileReferenceService fileReferenceService;
public FileReference toFileReference(#MappingTarget FileReference fileReference, FileInfoDTO fileInfoDTO) {
return fileReferenceService.updateFile( fileInfoDTO, fileReference );
}
public FileReference toFileReference(FileInfoDTO fileInfoDTO) {
return toFileReference( new FileReference(), fileInfoDTO );
}
// other abstract methods for MapStruct mapper generation.
}
Why the exact wanted code will not be generated
When generating the mapping code MapStruct will use the most generic way to do this.
An update mapper has the following criteria:
The #MappingTarget annotated argument will always be updated.
It is allowed to have no return type.
the generic way to update a field is then as follows:
// check if source has the value.
if (source.getProperty() != null) {
// Since it is allowed to have a void method for update mappings the following steps are needed:
// check if the property exists in the target.
if (target.getProperty() == null) {
// if it does not have the value then create it.
target.setProperty( new TypeOfProperty() );
}
// now we know that target has the property so we can call the update method.
propertyUpdateMappingMethod( target.getProperty(), source.getProperty() );
// The arguments will match the order as specified in the other update method. in this case the #MappingTarget annotated argument is the first one.
} else {
// default behavior is to set the target property to null, you can influence this with nullValuePropertyMappingStrategy.
target.setProperty( null );
}
I have a #MapperConfig looks like this.
#MapperConfig(componentModel = "spring")
public interface SomeEntityTypeMapperConfig {
#Mapping(target = PROPERTY_NAME_ENTITY)
#Mapping(source = SomeEntity.ATTRIBUTE_NAME_ID, target = SomeEntityType.PROPERTY_NAME_ID)
#Mapping(source = SomeEntity.PROPERTY_NAME_CREATED_AT, target = SomeEntityType.PROPERTY_NAME_CREATED_AT)
#Mapping(source = SomeEntity.PROPERTY_NAME_UPDATED_AT, target = SomeEntityType.PROPERTY_NAME_UPDATED_AT)
#Mapping(source = SomeEntity.PROPERTY_NAME_CREATED_BY, target = SomeEntityType.PROPERTY_NAME_CREATED_BY)
#Mapping(source = SomeEntity.PROPERTY_NAME_UPDATED_BY, target = SomeEntityType.PROPERTY_NAME_UPDATED_BY)
SomeEntityType<?, ?> fromEntity(SomeEntity entity);
// No #Mapping
void toEntity(SomeEntityType<?, ?> type, #MappingTarget SomeEntity entity);
}
Here comes my base mapper interface.
public interface SomeEntityTypeMapper<T extends SomeEntityType<?, U>, U extends SomeEntity> {
T fromEntity(U entity);
void toEntity(T type, #MappingTarget U entity);
}
And here comes my real mapper.
#Mapper(config = SomeEntityTypeMapperConfig.class)
public interface UserTypeMapper extends SomeEntityTypeMapper<UserType, User> {
#Mapping(source = User.ATTRIBUTE_NAME_NAME, target = UserType.PROPERTY_NAME_NAME)
#Override
UserType fromEntity(User entity);
#Mapping(source = UserType.PROPERTY_NAME_NAME, target = User.ATTRIBUTE_NAME_NAME)
#Override
void toEntity(UserType type, #MappingTarget User entity);
}
And MapStruct generates following impl class with unwanted mappings in it.
public class UserTypeMapperImpl implements UserTypeMapper {
#Override
public UserType fromEntity(User entity) {
if ( entity == null ) {
return null;
}
UserType userType = new UserType();
userType.setName( entity.getName() ); // explicitly configured
userType.setId( entity.getId() ); // inherited from the config
userType.setCreatedAt( entity.getCreatedAt() ); // inherited from the config
userType.setUpdatedAt( entity.getUpdatedAt() ); // inherited from the config
userType.setCreatedBy( entity.getCreatedBy() ); // inherited from the config
userType.setUpdatedBy( entity.getUpdatedBy() ); // inherited from the config
return userType;
}
#Override
public void toEntity(UserType type, User entity) {
if ( type == null ) {
return;
}
entity.setName( type.getName() ); // explicitly configured
entity.setCreatedAt( type.getCreatedAt() ); // UNWANTED!!!
entity.setUpdatedAt( type.getUpdatedAt() ); // UNWANTED!!!
entity.setUpdatedBy( type.getUpdatedBy() ); // UNWANTED!!!
entity.setId( type.getId() ); // UNWANTED!!!
entity.setCreatedBy( type.getCreatedBy() ); // UNWANTED!!!
}
}
What did I do wrong and How can I fix it?
What you are referring as unwanted reverse mapping without any annotations is actually the normal way that MapStruct generates mappings. If the source and target beans have the same property (which they do in your case) MapStruct would create a mapping for it.
In case you don't want to map some properties you can either ignore those one by one or use #BeanMapping( ignoreByDefault = true). With the second option MapStruct would only create mappings for the defined #Mapping.
I'm sharing what I found.
I needed to annotate the method with #BeanMapping(ignoreByDefault = true).
Interestingly the annotation must be located with leaf mapper interfaces.
#BeanMapping(ignoreByDefault = true) // WORKS!!!
#Mapping(source = UserType.PROPERTY_NAME_NAME, target = User.ATTRIBUTE_NAME_NAME)
#Override
void toEntity(UserType type, #MappingTarget User entity);
Didn't work with configuration nor parent interface.
public interface SomeEntityTypeMapperConfig {
#BeanMapping(ignoreByDefault = true) // Doesn't work!
void toEntity(SomeEntityType<?, ?> type, #MappingTarget SomeEntity entity);
}
public interface SomeEntityTypeMapper<T extends SomeEntityType<?, U>, U extends SomeEntity> {
#BeanMapping(ignoreByDefault = true) // Doesn't work!
void toEntity(T type, #MappingTarget U entity);
}
In mapstruct 1.3.0.Final we have dependency injection via constructor. Documentation says:
The generated mapper will inject all classes defined in the uses
attribute
(...)
For abstract classes or decorators setter injection should be
used.
I have following example:
#Mapper
public abstract class VehicleMapper {
#Autowired
private CarMapper carMapper;
#Autowired
private BikeMapper bikeMapper;
#Override
public VehicleDTO toDto(final Vehicle source) {
if (source instanceof Car) {
return carMapper.toDto((Car) source);
} else if (source instanceof Bike) {
return bikeMapper.toDto((Bike) source);
} else {
throw new IllegalArgumentException();
}
}
(...)
So in my case it should look like this (componentModel defined in maven):
#Mapper
public abstract class VehicleMapper {
private CarMapper carMapper;
private BikeMapper bikeMapper;
#Autowired
public void setCarMapper(final CarMapper carMapper) {
this.carMapper = carMapper;
}
#Autowired
public void setBikeMapper(final BikeMapper bikeMapper) {
this.bikeMapper = bikeMapper;
}
#Override
public VehicleDTO toDto(final Vehicle source) {
if (source instanceof Car) {
return carMapper.toDto((Car) source);
} else if (source instanceof Bike) {
return bikeMapper.toDto((Bike) source);
} else {
throw new IllegalArgumentException();
}
}
(...)
Question:
So it is not possible to inject carMapper and bikeMapper via constructor ? does injectionStrategy = CONSTRUCTOR works only for classes declared in #Mapper(uses = {}) ?
I think that injectionStrategy = CONSTRUCTOR works on the interface that has the #Mapper annotation. I don't think it works with abstract classes. I'm sure it will not work when you define your own fields (instance variables). How would MapStruct know what user defined fields to initialise in the constructor?
I am testing DynamoDB tables and want to set up different table names for prod and dev environment using the keyword"dev" for development and prod for production.
I have a POJO
#DynamoDBTable(tableName = "abc_xy_dev_MyProjectName_Employee")
public class Employee implements Cloneable {
}
On Prod I want its name to be abc_xy_prod_MyProjectName_Employee.
So, I wrote a TableNameResolver
public static class MyTableNameResolver implements TableNameResolver {
public static final MyTableNameResolver INSTANCE = new MyTableNameResolver();
#Override
public String getTableName(Class<?> clazz, DynamoDBMapperConfig config) {
final TableNameOverride override = config.getTableNameOverride();
String tableNameToReturn = null;
if (override != null) {
final String tableName = override.getTableName();
if (tableName != null) {
System.out.println("MyTableNameResolver ==================================");
return tableName;
}
}
String env = System.getenv("DEPLOYMENT_ENV");
for(Annotation annotation : clazz.getAnnotations()){
if(annotation instanceof DynamoDBTable){
DynamoDBTable myAnnotation = (DynamoDBTable) annotation;
if ("production".equals(env)){
tableNameToReturn = myAnnotation.tableName().replace("_dev_", "_prod_");
}
else {
tableNameToReturn = myAnnotation.tableName();
}
}
}
return tableNameToReturn;
}
}
This works by creating a table with the name abc_xy_prod_MyProjectName_Employee in production.
However, I have a repository with the following code
#EnableScan
public interface EmployeeRepository extends CrudRepository<Employee, String>
{
#Override
<S extends Employee> S save(S employee);
Optional<Employee> findById(String id);
#Override
List<Employee> findAll();
Optional<Employee> findByEmployeeNumber(String EmployeeNumber);
}
Thus when i try to call the method findAll via a endpoint /test case, i get the exception
There was an unexpected error (type=Internal Server Error,
status=500). User:
arn:aws:iam::87668976786:user/svc_nac_ps_MyProjectName_prod is not
authorized to perform: dynamodb:Scan on resource:
:table/abc_xy_dev_MyProjectName_Employee (Service: AmazonDynamoDBv2;
Status Code: 400; Error Code: AccessDeniedException; Request ID:
aksdnhLDFL)
i.e MyTableNameResolver doesn't get called internally when the respository methods are executed. It still points to table name with the name abc_xy_dev_MyProjectName_Employee given in the annotation #DynamoDBTable(tableName = "abc_xy_dev_MyProjectName_Employee")
You have used spring JPA as persistence dynamoDB Integration.
Below configuration can be used to set table name override as part of spring boot configuration.
Sample example is found in https://github.com/ganesara/SpringExamples/tree/master/spring-dynamo
Map Dynamo db repository with user defined mapper config reference
#EnableDynamoDBRepositories(basePackages = "home.poc.spring", dynamoDBMapperConfigRef="dynamoDBMapperConfig")
Mapper Config for table override is as below
#Bean
public DynamoDBMapperConfig dynamoDBMapperConfig() {
DynamoDBMapperConfig mapperConfig = new DynamoDBMapperConfig
.Builder()
.withTableNameOverride(DynamoDBMapperConfig.TableNameOverride.withTableNamePrefix("PROD_"))
.build();
return mapperConfig;
}
Full configuration for reference
#Configuration
#EnableDynamoDBRepositories(basePackages = "home.poc.spring", dynamoDBMapperConfigRef="dynamoDBMapperConfig")
public class DynamoDBConfig {
#Value("${amazon.dynamodb.endpoint}")
private String amazonDynamoDBEndpoint;
#Value("${amazon.aws.accesskey}")
private String amazonAWSAccessKey;
#Value("${amazon.aws.secretkey}")
private String amazonAWSSecretKey;
#Bean
public AmazonDynamoDB amazonDynamoDB() {
AmazonDynamoDB amazonDynamoDB
= new AmazonDynamoDBClient(amazonAWSCredentials());
if (!StringUtils.isEmpty(amazonDynamoDBEndpoint)) {
amazonDynamoDB.setEndpoint(amazonDynamoDBEndpoint);
}
return amazonDynamoDB;
}
#Bean
public AWSCredentials amazonAWSCredentials() {
return new BasicAWSCredentials(
amazonAWSAccessKey, amazonAWSSecretKey);
}
#Bean
public DynamoDBMapperConfig dynamoDBMapperConfig() {
DynamoDBMapperConfig mapperConfig = new DynamoDBMapperConfig
.Builder()
.withTableNameOverride(DynamoDBMapperConfig.TableNameOverride.withTableNamePrefix("PROD_"))
.build();
return mapperConfig;
}
#Bean
public DynamoDBMapper dynamoDBMapper() {
return new DynamoDBMapper(amazonDynamoDB(), dynamoDBMapperConfig());
}
}
You are using DynamoDBMapper (the Java SDK). Here is how I use it. Lets say I have a table called Users, with an associated User POJO. In DynamoDB I have DEV_Users and LIVE_Users.
I have an environment variable 'ApplicationEnvironmentName' which is either DEV or LIVE.
I create a custom DynamoDBMapper like this:
public class ApplicationDynamoMapper {
private static Map<String, DynamoDBMapper> mappers = new HashMap<>();
private static AmazonDynamoDB client = AmazonDynamoDBClientBuilder.standard()
.withRegion(System.getProperty("DynamoDbRegion")).build();
protected ApplicationDynamoMapper() {
// Exists only to defeat instantiation.
}
public static DynamoDBMapper getInstance(final String tableName) {
final ApplicationLogContext LOG = new ApplicationLogContext();
DynamoDBMapper mapper = mappers.get(tableName);
if (mapper == null) {
final String tableNameOverride = System.getProperty("ApplicationEnvironmentName") + "_" + tableName;
LOG.debug("Creating DynamoDBMapper with overridden tablename {}.", tableNameOverride);
final DynamoDBMapperConfig mapperConfig = new DynamoDBMapperConfig.Builder().withTableNameOverride(TableNameOverride.withTableNameReplacement(tableNameOverride)).build();
mapper = new DynamoDBMapper(client, mapperConfig);
mappers.put(tableName, mapper);
}
return mapper;
}
}
My Users POJO looks like this:
#DynamoDBTable(tableName = "Users")
public class User {
...
}
When I want to use the database I create an application mapper like this:
DynamoDBMapper userMapper = ApplicationDynamoMapper.getInstance(User.DB_TABLE_NAME);
If I wanted to a load a User, I would do it like this:
User user = userMapper.load(User.class, userId);
Hope that helps.
am working with JPA(EclipseLink) and Derby. In my object there is a boolean field. Before a merge operation, the field is set to true. but after the merge, the field still holds the false value.
#Entity
#Access(AccessType.PROPERTY)
public class SleepMeasure extends AbstractEntity {
private static final long serialVersionUID = 1361849156336265486L;
...
private boolean WeatherDone;
public boolean isWeatherDone() { // I have already tried with the "getWeatherDone()"
return WeatherDone;
}
public void setWeatherDone(boolean weatherDone) {
WeatherDone = weatherDone;
}
...
}
It doesn't seem to matter whether, I use "getWeatherDone()" or "isWeatherDone()".
using code:
public class WeatherDataCollectorImpl{
...
private void saveMeasures(WeatherResponse mResponse, SleepMeasure sleep) throws Exception {
AppUser owner = sleep.getOwner();
...
sleep.setWeatherDone(Boolean.TRUE);
reposService.updateEntity(sleep,SleepMeasure.class);
}
...
}
And here is my repository class
public class RepositoryImpl{
...
public <T extends AbstractEntity> T updateEntity(T entity, Class<T> type) throws RepositoryException {
openEM();
EntityTransaction tr = em.getTransaction();
try {
tr.begin();
{
// entity.weatherdone has value true
entity = em.merge(entity);
// entity.weatherdone has value false
}
tr.commit();
} catch (Exception e) {
tr.rollback();
}
return entity;
}
...
}
JPA console Info: There is no error, nor warning and not even any info that the boolean column shall be updated.
--Merge clone with references com.sleepmonitor.persistence.entities.sleep.SleepMeasure#b9025d
...
--Register the existing object // other objects
...
--Register the existing object com.sleepmonitor.persistence.entities.sleep.SleepMeasure#1ba90cc
So how do I solve this small problem.
Note:
Derby defined this field as "SMALLINT".
thanks.
Oh God! I found my problem. Actually I realised, it was not only the boolean field, but the whole object could not be updated.
While trying to complete a bideirection referencing, I stupidly did this in a setter property instead of an addMethod() .
public void setSleepProperties(SleepProperties sleepProperties) {
this.sleepProperties = sleepProperties;
if (!(sleepProperties == null)) {
this.sleepProperties.setSleepMeasure(this);
}
}
Instead of:
public void addSleepProperties(SleepProperties sleepProperties) {
this.sleepProperties = sleepProperties;
if (!(sleepProperties == null)) {
this.sleepProperties.setSleepMeasure(this);
}
}
So I ended up with the referenced entity (sleepProperties.sleepMeasure) over-writing the updates on the owning entity just before a merge. That was very defficult to find, and I think have learned a big lesson from it. Thanks to all who tried to help me out.
The "addMethod()" solved my problem.