mapstruct - Update existing bean - ignoring 'id' field in all the child/nested beans (arraylists, sets etc..) - mapstruct

I have a parent class with many child entities.
There are 2 instances of this parent class.
Want to copy data of 1 instance to another one (ignoring the 'id' property in all the child entities)
---- hiding getters and setters for brevity
public class IdBean {
private Long id;
}
public class City extends IdBean {
private String name;
}
public class Country extends IdBean {
private String name;
private List<City> cities;
}
public class Student extends IdBean {
private String name;
}
public class School extends IdBean {
private String name;
private List<Student> students;
private List<Country> countries;
}
#MapperConfig(mappingInheritanceStrategy = MappingInheritanceStrategy.AUTO_INHERIT_FROM_CONFIG)
public interface SchoolCentralConfig {
#Mapping(ignore = true, target = "id")
IdBean updateBeanEntityFromDto(IdBean dto);
}
#Mapper(config = SchoolCentralConfig.class)
public interface SchoolMapper {
SchoolMapper INSTANCE = Mappers.getMapper( SchoolMapper.class );
#Mapping(target = "companies.id", ignore = true )
void updateSchoolFromDto(School schoolDTO, #MappingTarget School schoolEntity);
}
I want to ignore all the 'id' property from all the nested fields.

MappingInheritanceStrategy.AUTO_INHERIT_FROM_CONFIG does (unfortunately) not work for nested methods yet.
What you can do is write a signature for Country and Student as well. Then it will work.
So:
#Mapper(config = SchoolCentralConfig.class)
public interface SchoolMapper {
SchoolMapper INSTANCE = Mappers.getMapper( SchoolMapper.class );
void updateSchoolFromDto(School schoolDTO, #MappingTarget School schoolEntity);
void updateStudentFromDto(Student studentDTO, #MappingTarget Student studentEntity);
// etc
}

Related

Mapstruct: how to map multiple fields from DTO to an object in Entity?

i have this DTO:
#NoArgsConstructor
public class DataDTO implements DTO {
private static final long serialVersionUID = -5105904799152965475L;
private Long deviceId;
private OffsetDateTime generatedOn;
public Long getDeviceId() {
return deviceId;
}
public void setDeviceId(Long deviceId) {
this.deviceId = deviceId;
}
public OffsetDateTime getGeneratedOn() {
return generatedOn;
}
public void setGeneratedOn(OffsetDateTime generatedOn) {
this.generatedOn = generatedOn;
}
}
i have this MongoDB document:
#Document(collection = "data")
#EqualsAndHashCode
public class DataDocument {
private static final long serialVersionUID = 1772572723546311500L;
#Id
private IdByDeviceIdAndGeneratedOn id;
public DataDocument() {
}
public IdByDeviceIdAndGeneratedOn getId() {
return id;
}
public void setId(IdByDeviceIdAndGeneratedOn id) {
this.id = id;
}
}
and this is the #Id class for MongoDB Document:
#EqualsAndHashCode
#ToString
public class IdByDeviceIdAndGeneratedOn {
#Id
private final Long deviceId;
#Id
#Field("generated_on")
#DateTimeFormat(iso = DateTimeFormat.ISO.DATE_TIME)
private final OffsetDateTime generatedOn;
public IdByDeviceIdAndGeneratedOn(final Long deviceId, final OffsetDateTime generatedOn) {
this.deviceId = Objects.requireNonNull(deviceId);
this.generatedOn = Objects.requireNonNull(generatedOn);
}
public Long getDeviceId() {
return deviceId;
}
public OffsetDateTime getGeneratedOn() {
return generatedOn;
}
}
this is the mapper for this Key class:
#Mapper(unmappedTargetPolicy = ReportingPolicy.ERROR, componentModel = "spring")
public interface IdByDeviceIdAndGeneratedOnMapper {
default IdByDeviceIdAndGeneratedOn toId(final Long deviceId, final OffsetDateTime generatedOn) {
return new IdByDeviceIdAndGeneratedOn(deviceId, generatedOn);
}
default Long getDeviceId(final IdByDeviceIdAndGeneratedOn id) {
return id.getDeviceId();
}
default OffsetDateTime getGeneratedOn(final IdByDeviceIdAndGeneratedOn id) {
return id.getGeneratedOn();
}
and this is the #Mapper for DataDTO and DataDocument:
#Mapper( unmappedTargetPolicy = ReportingPolicy.ERROR,
uses = {IdByDeviceIdAndGeneratedOnMapper.class,
AccelerometerDocumentMapper.class,
GpsDocumentMapper.class,
GsmDocumentMapper.class
})
public interface DataDocumentMapper extends DocumentMapper<DataDTO, DataDocument> {
}
this is the generic mapper:
/**
* Contract for a generic dto to entity mapper.
*
* #param <DTO> - DTO source type parameter.
* #param <DOCUMENT> - MongoDB Document destination type parameter.
*/
public interface DocumentMapper<DTO, DOCUMENT> {
DOCUMENT toDocument(DTO dto);
DTO toDto(DOCUMENT document);
}
Currently i'm receiving this errors:
for MongoDB Data docment:
Unmapped target property: "id".
for DTO:
Unmapped target properties: "deviceId, generatedOn".
How to solve this errors without loosing immutability of Id class?
What you are trying to do is to use (using constructors to construct objects) is not yet supported. There is an open issue for it #73.
However, you can achieve what you are looking for by using Object factories, this is for the toDocument mapping, for the toDto mapping you can use nested source mappings.
Your mapper would look like:
#Mapper(uses = {AccelerometerDocumentMapper.class,
GpsDocumentMapper.class,
GsmDocumentMapper.class},
componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface DataDocumentMapper extends DocumentMapper<DataDTO, DataDocument> {
#Mapping(target = "id", source = "dto")
#Override
DataDocument toDocument(DataDTO dto);
#ObjectFactory
default IdByDeviceIdAndGeneratedOn createId(DataDTO dto) {
return dto == null ? null : new IdByDeviceIdAndGeneratedOn(dto.getDeviceId(), dto.getGeneratedOn());
}
#Mapping(target = "deviceId", source = "id.deviceId")
#Mapping(target = "generatedOn", source = "id.generatedOn")
#Override
DataDTO toDto(DataDocument document);
}
NB: You can also make DataDocumentMapper abstract class and make the createId method protected, in case you don't want to expose it in the interface
this is solved my problem, but this doesnt look elegant.
Maybe there is more elegant way?
#Mapper(uses = {AccelerometerDocumentMapper.class,
GpsDocumentMapper.class,
GsmDocumentMapper.class},
imports = {IdByDeviceIdAndGeneratedOn.class},
componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface DataDocumentMapper extends DocumentMapper<DataDTO, DataDocument> {
#Override
#Mapping(target = "id", expression = "java( new IdByDeviceIdAndGeneratedOn(dto.getDeviceId(), dto.getGeneratedOn()) )")
DataDocument toDocument(DataDTO dto);
#Override
#Mapping(target = "deviceId", expression = "java( document.getId().getDeviceId() )")
#Mapping(target = "generatedOn", expression = "java( document.getId().getGeneratedOn() )")
DataDTO toDto(DataDocument document);
}

Disable additional criteria only in some entity relations

I am making an application based on JPA/EclipseLink, I implemented a soft delete functionality using #AdditionalCriteria in the root class of the hierarchy of entities (all entities inherit from this).
My problem is that now, I need to create a special entity that contains multiple relationships with other entities; and I need recover all relationed entities, including soft deleted ones. It is possible disabled #AdditionalCriteria only in the relations of this special entity with EclipseLink? If not, what is the best option to do this? My code looks like the following:
////Example of my top entity class (all others inherit from this)
#AdditionalCriteria("this.deleted = false")
public abstract class TopHierarchyClass {
···
#Column(name = "deleted")
protected boolean deleted = false;
···
}
//Example of entity that needs recover all relationed entities including soft deleted
#Entity
#Table(name = "special_entities")
public class SpecialEntity extends EntityBase {
···
#JoinColumn(name = "iditem", referencedColumnName = "id")
#ManyToOne(fetch = FetchType.LAZY)
private Item item;
···
}
#Entity
#Table(name = "items")
public class Item extends EntityBase {
···
}
Thanks in advance
Create a new entity for the same table without the #AdditionalCriteria. This way you can retrieve all records from that table without applying the additional filter.
For example:
public interface Person {
Long getId();
void setId(Long id);
Date getDeletedAt();
void setDeletedAt(Date deletedAt);
}
#MappedSupperclass
public class PersonEntity implements Person {
#Id
private Long id;
private Date deletedAt;
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public Date getDeletedAt() { return deletedAt; }
public void setDeletedAt(Date deletedAt) { this.deletedAt = deletedAt; }
}
#Entity
#Table(name = "people")
#AdditionalCriteria("this.deletedAt is null")
public class ActivePersonEntity extends PersonEntity {
}
#Entity
#Table(name = "people")
public class RawPersonEntity extends PersonEntity {
}
public class PeopleRepository {
#PersistenceContext
private EntityManager em;
public List<Person> all() {
return all(false);
}
public List<Person> all(boolean includeSoftDeleted) {
if (!includeSoftDeleted) {
return em.createQuery("select p from ActivePersonEntity p", ActivePersonEntity.class).getResultList();
} else {
return em.createQuery("select p from RawPersonEntity p", RawPersonEntity.class).getResultList();
}
}
}
Also, if your #AdditionalCriteria is in a super class, you may override it by declaring a new empty #AdditionalCriteria in a sub class:
You can define additional criteria on entities or mapped superclass.
When specified at the mapped superclass level, the additional criteria
definition applies to all inheriting entities, unless those entities
define their own additional criteria, in which case those defined for
the mapped superclass are ignored.
#AdditionalCriteria doc

Using Pageable to query a collection

I have two entities. A NewsCategory and a NewsItem which have a one-to-many relationship.
NewsCategory
#Entity
public class NewsCategory extends AbstractEntity<Long> {
private String name;
#OneToMany(cascade = CascadeType.ALL)
private List<NewsItem> items = new ArrayList<>();
public String getName() {
return name;
}
public void setName(final String name) {
this.name = name;
}
public List<NewsItem> getItems() {
return items;
}
}
NewsItem
#Entity
public class NewsItem extends AbstractEntity<Long> {
private String title;
private LocalDate startDate;
private LocalDate endDate;
private String resource;
#Column(columnDefinition = "text")
private String content;
// getters and setters...
}
Repository interface
I would like to have the items collection to be pageable but I'm having some difficulties with defining the repository interface for it.
This interface does not work like expected.
public interface NewsCategoryRepository extends JpaRepository<NewsCategory, Long> {
#Query("SELECT e.items FROM #{#entityName} e WHERE e = ?1")
public List<NewsItem> findItems(NewsCategory category, Pageable pageable);
}
When executing findItems() the following exception is thrown.
Caused by: org.hibernate.QueryException: illegal attempt to dereference collection [newscatego0_.id.items] with element property reference [startDate] [SELECT e.items FROM NewsCategory e WHERE e = ?1 order by e.items.startDate asc]
How can I modify the above interface so it will return a portion of the items property using Spring Data and Pageable?

Why entityManager.contains returns different results?

This is in JPA2 (EclipseLink) and JSF2.
I have an entity class Student:
#Entity
public class Student implements Serializable {
private static final long serialVersionUID = 1L;
#Id
#GeneratedValue(strategy = GenerationType.AUTO)
private Long id;
private String firstname;
private String lastname;
private int age;
public Student(String firstname, String lastname, int age) {
this.firstname = firstname;
this.lastname = lastname;
this.age = age;
}
public Student() {
}
// accessors and mutators here
}
Session bean StudentFacade that inherits AbstractFacade:
public abstract class AbstractFacade<T> {
private Class<T> entityClass;
public AbstractFacade(Class<T> entityClass) {
this.entityClass = entityClass;
}
protected abstract EntityManager getEntityManager();
public void create(T entity) {
getEntityManager().persist(entity);
}
public T edit(T entity) {
return getEntityManager().merge(entity);
}
public void remove(T entity) {
getEntityManager().remove(getEntityManager().merge(entity));
}
public T find(Object id) {
return getEntityManager().find(entityClass, id);
}
// other methods: findAll, findRange, count
}
#Stateless
public class StudentFacade extends AbstractFacade<Student> {
#PersistenceContext(unitName = "jpa2testsPU")
private EntityManager em;
#Override
protected EntityManager getEntityManager() {
return em;
}
public StudentFacade() {
super(Student.class);
}
public boolean contains(Student s) {
return getEntityManager().contains(s);
}
public void testContains() {
Student s = find(1L);
boolean isContains = getEntityManager().contains(s);
}
}
This is my JSF Managed Bean:
#ManagedBean
#RequestScoped
public class IndexController {
#EJB
private StudentFacade studentFacade;
/**
* Creates a new instance of IndexController
*/
public IndexController() {
}
public String test() {
Student s = new Student("John", "Doe", 20);
studentFacade.create(s);
Student s1 = studentFacade.find(1L); // This works because table only has 1 record
boolean isContains = studentFacade.contains(s);
return null;
}
}
When I run test() from managed bean, isContains is false. But when testContains() in StudentFacade is called, isContains is true. Why is this?
StudentFacade is a Stateless Session Bean (SSB). The contents of its instance variables are not guaranteed to be preserved across method calls (reference). It's like having a different instance of EntityManager created for each method invocation.
When you run your test from the managed bean, you invoke two different methods on the SSB, therefore a different EntityManager instance is created for each call, and the second one does not contain the Student instance because it has not been loaded yet.
But when you run your test inside a method of the SSB itself, the same EntityManager is used for the scope of the entire method, therefore the call to contains() returns true.

JPA Mapping, parent to children and child to parent with two classes and abstract class

I have have two classes that inherit from an abstract class and have a parent-children relation.
So I use annotation OneToMany and ManyToOne but the parent entity in child class is always null.
Can Someone help me please, I have spend several hours to googling and test many conf without success.
These are code from my classes :
public #Table(name="flowentity") #Entity abstract class FlowEntity {
final static Logger log = LoggerFactory.getLogger(FlowEntity.class);
//Globals informations concerning the flow state
private #Id #GeneratedValue(strategy = GenerationType.IDENTITY) Integer flowId = 0;
private String flowName;
private #OneToMany(fetch=FetchType.EAGER, cascade=CascadeType.ALL)
Set<PeopleEntity> actorSet = new HashSet<>();
//Global parameters for most of flows
//Organizational parameters
private #OneToOne(fetch=FetchType.EAGER, cascade=CascadeType.ALL)
#JoinColumn(name="organisationalEntity_Id")
OrganisationalEntity organisationalEntity;
...
public #Table(name="ams_newCPEntity") #Entity class NewMultiCPEntity extends FlowEntity {
private #OneToMany(targetEntity=NewCPEntity.class, fetch=FetchType.EAGER, cascade=CascadeType.ALL,mappedBy="parent")
Set<NewCPEntity> cpList = new HashSet<NewCPEntity>();
//Constructor
public NewMultiCPEntity(){
setFlowName(EnumFlow.N_CP_M.getFlowAcronym());
}
...
public #Table(name="ams_newCPEntity") #Entity class NewCPEntity extends FlowEntity {
final static Logger log = LoggerFactory.getLogger(NewCPEntity.class);
private boolean formNCPValidated;
private #ManyToOne #JoinColumn(name="parent_Id", nullable=false)
NewMultiCPEntity parent;
public NewCPEntity(){
log.debug("Instanciation of a new CP");
setFlowName(EnumFlow.N_CP.getFlowAcronym());
}
public #Override OrganisationalEntity getOrganisationalEntity(){
return parent.getOrganisationalEntity();
}
...
If I don't add the #JoinColumn annotation, JPA create an association table but is not able to retrieve the parent whereas the association can be done directly by requesting in database.
Thankyou very much to help.
Regards,
Thank you Chris for your comment, you are right, I forget to change the name of the table. I don't think it was the problem because the inheritance mapping is in one table flowentity with a DTYPE discriminator column.
Finally I resolve my problem by setting parent attributs when adding a new child like this :
public #Table #Entity class NewMultiCPEntity extends FlowEntity {
private #OneToMany(targetEntity=NewCPEntity.class, fetch=FetchType.EAGER, cascade=CascadeType.ALL)
List<NewCPEntity> cpList = new ArrayList<>();
//Constructor
public NewMultiCPEntity(){
setOrganisationalEntity(new OrganisationalEntity());
setFlowName(EnumFlow.N_CP_M.getFlowAcronym());
}
public List<NewCPEntity> getNCPList(){
if(cpList == null){
cpList = new ArrayList<>();
}
if(cpList.isEmpty()){
addCPEntity(new NewCPEntity());
}
return Collections.unmodifiableList(cpList);}
public boolean removeCPEntity(NewCPEntity entity){
return cpList.remove(entity);
}
public boolean addCPEntity(NewCPEntity entity){
entity.setParent(this);
entity.setOrganisationalEntity(this.getOrganisationalEntity());
return cpList.add(entity);
}
And I remove the override of getOrganizationalEntity in the child :
public #Table #Entity class NewCPEntity extends FlowEntity {
final static Logger log = LoggerFactory.getLogger(NewCPEntity.class);
private #ManyToOne(targetEntity=NewMultiCPEntity.class,cascade=CascadeType.ALL)
NewMultiCPEntity parent;
public NewCPEntity(){
log.debug("Instanciation of a new CP");
setFlowName(EnumFlow.N_CP.getFlowAcronym());
}
public NewMultiCPEntity getParent() {
return parent;
}
public void setParent(NewMultiCPEntity parent){
this.parent = parent;
}
Regards,