MapStruct cannot find members from generic Set property - java

I started using MapStruct 1.4.0.CR1. I'm also using Gradle:
dependencies {
annotationProcessor("org.mapstruct:mapstruct-processor:${project.property("mapstruct.version")}")
implementation("org.mapstruct:mapstruct:${project.property("mapstruct.version")}")
}
I have some JPA entities I'm trying to map:
public class Exam implements Serializable {
// More class members here
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "exam", orphanRemoval = true)
private Set<Scan> scans;
public Exam() { } // ...no-argument constructor required by JPA
public Exam(final Builder builder) {
// ...set the rest also
scans = builder.scans;
}
// getters (no setters), hashCode, equals, and builder here
}
public class Scan implements Serializable {
// More class members here
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "scan", orphanRemoval = true)
private Set<Alarm> alarms;
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.LAZY, mappedBy = "scan", orphanRemoval = true)
private Set<Isotope> isotopes;
protected Scan() { } // ...no-argument constructor required by JPA
public Scan(final Builder builder) {
// ...set the rest also
alarms = builder.alarms;
isotopes = builder.isotopes;
}
// getters (no setters), hashCode, equals, and builder here
}
I have similar classes for mapping, but they don't have as many fields/members as the JPA entities, moreover, they are on a completely different sub-system (hence the mapping). The problem is that MapStruct is telling me there are no isotopes within Scans: java: No property named "scans.isotopes" exists in source parameter(s). Did you mean "scans.empty"?.
Basically, isotopes and alarms are not contained within a Set of scans in the (new) mapped Exam class. This is my ExamMapper:
#FunctionalInterface
#Mapper(componentModel = "spring", injectionStrategy = InjectionStrategy.CONSTRUCTOR)
public interface ExamMapper {
// #Mapping(source = "scans.alarms", target = "alarms")
#Mapping(source = "scans.isotopes", target = "isotopes")
Exam valueFrom(tld.domain.Exam entity);
}
Is there a way to accomplish this? I think this may be trivial, but I'm fairly new to MapStruct ;)

The source and target attributes of the #Mapping can only reference bean properties.
This means that when using scans.isotopes, it will look for a property isotopes in Set<Scan> and thus the compile error.
In order to solve this you'll need to provide some custom mappings. From what I can understand you will need to do flat mapping here as well. The reason for that is that you have multiple scans, and each scan has multiple isotopes. You need to gather all of that and map it into a single collection.
One way to achieve this is in the following way:
#Mapper(componentModel = "spring", injectionStrategy = InjectionStrategy.CONSTRUCTOR)
public interface ExamMapper {
#Mapping(source = "scans", target = "isotopes")
Exam valueFrom(tld.domain.Exam entity);
Isotope valueFrom(tld.domain.Isotope isotope);
default Set<Isotope> flatMapIsotopes(Set<Scan> scans) {
return scans.stream()
.flatMap(scan -> scan.getIsotopes().stream())
.map(this::valueFrom)
.collect(Collectors.toSet());
}
}

Related

Model Mapper - using custom methods

A springboot project where I need to construct a DTO for a dashboard view using nominated fields from the parent and nominated fields from the newest of each of the children.
The entities are Plane which has a OneToMany relationship with Transponder, Maint Check and Transmitter.
Plane
#Entity
#Data
public class Plane {
#Id
#GeneratedValue(strategy = IDENTITY)
private Long id;
private String registration;
#JsonIgnore
#OneToMany(cascade = CascadeType.ALL, mappedBy = "plane")
private List<Transponder> listTransponder = new ArrayList<>();
#JsonIgnore
#OneToMany(cascade = CascadeType.ALL, mappedBy = "plane")
private List<Transmitter> listTransmitter = new ArrayList<>();
#JsonIgnore
#OneToMany(cascade = CascadeType.ALL, mappedBy = "plane")
private List<MaintCheck> listMaintCheck = new ArrayList<>();
Transponder
#Entity
#Data
#NoArgsConstructor
#AllArgsConstructor
public class Transponder {
#Id
#GeneratedValue(strategy = IDENTITY)
private Long id;
private String code;
private LocalDate dateInserted;
#ManyToOne(fetch = FetchType.LAZY, optional = true)
private Plane plane;
}
Maint Check and Transmitter have similar entities with a LocalDate field.
PlaneDTO looks liike
#Data
public class PlaneDTO {
private String registration;
private LocalDate maintCheck; // date of most recent Maint Check
private String transponderCode; // string of most recent Transponder code
private Integer channel; // Intger of most recent Transmitter Freq
}
I have attempted to consruct this PlaneDTO in the service layer, but I am manually doing much of the sorting of the lists of Transponder, Transmitter and Maint Check to get the most recent record from these lists.
//DRAFT METHOD CONSTRUCT DTO
#Override
public PlaneSummaryDTO getPlaneSummaryDTOById(Long id) {
Plane Plane = this.get(id);
PlaneSummaryDTO PlaneSummaryDTO = new PlaneSummaryDTO();
ModelMapper mapper = new ModelMapper();
PlaneSummaryDTO = modelMapper.map(get(id), PlaneSummaryDTO.class);
PlaneSummaryDTO.setTRANSPONDERCode(getNewestTRANSPONDERCode(Plane));
PlaneSummaryDTO.setLastMaintCheck(getNewestMaintCheckDate(Plane));
PlaneSummaryDTO.setChannel(getTransmitterCode(Plane));
PlaneSummaryDTO.setChannelOffset(getTransmitterOffset(Plane));
return PlaneSummaryDTO;
}
// RETURN NEWEST DATE OF MAINT CHECK BY CATCH DATE
public LocalDate getNewestMaintCheckDate(Plane Plane) {
List<MaintCheck> listMaintCheck = new ArrayList<>(Plane.getListMaintCheck());
MaintCheck newest = listMaintCheck.stream().max(Comparator.comparing(MaintCheck::getCatchDate)).get();
return newest.getCatchDate();
}
// RETURN NEWEST TRANSPONDER CODE FROM Plane BY DATE INSERTED
public String getNewestTransponderCode(Plane Plane) {
List<Transponder> listTransponder = new ArrayList<>(Plane.getListTransponder());
Transponder newest = listTransponder.stream().max(Comparator.comparing(Transponder::getDateInserted)).get();
return newest.getCode();
}
// OTHER METHODS TO GET MOST RECENT RECORD
QUESTION Is there a better way to calculate the most recent record of the child, using model mapper more efficiently (custom method?)
I am open to changing to MapStruct if it better supports getting the most recent child.
I briefly used ModelMapper in the past. I would suggest using mapstruct since I personaly find it easier to use. I know your mapping can be done there ;). In Mapstruct your Mapper could look something like this:
#MapperConfig(
componentModel = "spring",
builder = #Builder(disableBuilder = true)
)
public interface PlaneMapper {
#Mapping(target = "lastMaintCheck", ignore = true)
PlaneDTO planeToPlaneDTO(Plane plane);
#AfterMapping
default void customCodePlaneMapping(Plane source, #MappingTarget PlaneDTO target) {
target.setLastMaintCheck(source.getListMaintCheck.stream().max(Comparator.comparing(Transponder::getDateInserted)).get())
}
Your mapper call would then only be one line:
#Service
#RequiuredArgsConstructor
public class someService{
private final PlaneMapper planeMapper;
public void someMethod(){
....
PlaneDTO yourMappedPlaneDTO = planeMapper.planeToPlaneDTO(plane);
....
}
I did not fill in all values. But i hope the concept is clear.
EDIT:
You would also have to add the dependency of "mapstruct-processor" so that the MapperImpl classes can be gererated.
<dependency>
<groupId>org.mapstruct</groupId>
<artifactId>mapstruct-processor</artifactId>
<version>${org.mapstruct.version}</version>
<scope>provided</scope>
<optional>true</optional>
</dependency>
So here's a different approach to the data model. I'll be using Transponder because the others are analog.
The target domain model could look like this:
#Data
class Plane {
Long id;
String registration;
Transponder activeTransponder;
}
#Data
class Transponder {
Long id;
Integer code; // this is a 4-digit octal number, why String? your call though
Long planeId;
Instant assignStart;
Instant assignEnd;
}
In the database, it would be sufficient to store the id and registration for the plane, because you can determine the current transponder with a proper query on the db entity, eg where transponder.planeId=id and transponder.assignEnd IS NULL. You can also store the transponderId of course, but then you'd need to take care to keep the data consistent between the tables.
If you want a history of all transponders - which to me seems like an entirely different use case to me, you can easily retrieve it in a separate service with a query getTransponderHistoryByPlane(long planeId) with a query like from transponders t where t.planeId=$planeId sorted by t.assignStart.
Again, this does depend on your use cases, and assumes that you usually only need one transponder for a given plane except in special cases, like from a different endpoint.
Anyway, this were my thoughts on the domain model, and you were aiming for your Dto; however, this is then easily mapped with mapstruct like (assuming you do the same for maintCheck and transmitter)
#Mapper
interface PlaneDtoMapper {
#Mapping(target = "transponderCode", source = "transponder.code")
#Mapping(target = "maintCheck", source = "maintCheck.date")
#Mapping(target = "channel", source = "transmitter.channel")
PlaneDTO fromPlane(Plane p);
}
You don't need the "registration" mapping because the fields in plane and dto are the same, and the #Mapping annotations tell mapstruct which subfields of which fields of plane to use.

Spring Data Projection size()

Is there a way to return the size of a collection via rest api projection?
Consider this example:
The data:
#Entity
#Table
public class MyData {
// id
// ...
#OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
#JoinColumn(name = "mydata")
private final Set<User> users = new HashSet<>();
// getters/setters ...
}
the repository:
#RepositoryRestResource
public interface MyDataRepository extends PagingAndSortingRepository<MyData, Long> {
}
the projection:
#Projection(name = "short", types = {MyData.class})
public interface MyDataProjection {
// neither of those work
// #Value("#{target.getUsers().size()}")
#Value("#{target.users.size()}")
Integer nrUsers();
}
I want to get the number of Users in a MyData-Object returned via REST api.
For example: my-domain/my-service/mydatas/123/?projection=short
should return:
{
"nrUsers": 4;
...
}
Is it possible anyway?
The naming convention ist to start with a "get" since the attributes of the projection are methods, not fields. So this works:
#Value("#{target.users.size()}")
Integer getNrUsers();
(instead of the previous "nrUsers()")

mappedBy reference an unknown target entity property with inheritance

I am trying to store a entity inside a Database with hibernate. I have got the following classes:
#Entity
public class UsableRemoteExperiment extends RemoteExperiment {
private List<ExperimentNodeGroup> nodeGroups = new ArrayList<>();
#OneToMany(mappedBy = "experiment", cascade = CascadeType.ALL, orphanRemoval = true)
public List<ExperimentNodeGroup> getNodeGroups() {
return nodeGroups;
}
public void setNodeGroups(final List<ExperimentNodeGroup> nodeGroups) {
this.nodeGroups = nodeGroups;
}
/* More getters and setters for other attributes */
The Experiment Node Group looks like this:
#Entity
public class ExperimentNodeGroup extends NodeGroup {
private List<Node> nodes = new ArrayList<>();
/* More getters and setters for other attributes */
And the NodeGroup Class looks like this:
#Entity
public abstract class NodeGroup extends GeneratedIdEntity {
protected Experiment experiment;
#ManyToOne(optional = false)
#JsonIgnore
public Experiment getExperiment() {
return experiment;
}
/* More getters and setters for other attributes */
Now when i try to compile the Code, I get this error:
Caused by: org.hibernate.AnnotationException: mappedBy reference an
unknown target entity property:
[...].ExperimentNodeGroup.experiment
in
[...].UsableRemoteExperiment.nodeGroups
It's one of the quirks of the hibernate where it does not work as expected with mappedBy and inheritance. Could you try specifying targetEntity as well? Here's the documentation and this is what it says:
The entity class that is the target of the association. Optional only
if the collection property is defined using Java generics. Must be
specified otherwise.
You can try specifying targetEntity = ExperimentNodeGroup.class or targetEntity = Transaction.class and see if that makes any difference.
I think the problem here is that you need to also put on a setter, hibernate assumes that if you have a getter to get from a database you will need a setter to read from it, you can either put a setter, or use
#Entity(access = AccessType.FIELD)
and put the annotations on your attributes.

How to use restful web service with java persistence entities

How to annotate and use a java persistent object in a onetomany bidirectional relationship so that the entity can be converted to its XML representation which when taken up by a restful client can be be converted back to an entity object again
Here is a REAL code from one of my projects which probably (if I got it right) does exactly what you need.
#Entity
#Table(name = "KIOSK")
#XmlRootElement
public class RealKiosk implements Kiosk {
private List<Device> kioskDevices = new ArrayList<Device>();
#OneToMany(fetch = FetchType.LAZY, targetEntity = DeviceImpl.class, mappedBy = "kiosk", cascade = CascadeType.ALL)
#XmlElement(type = DeviceImpl.class)
public List<Device> getKioskDevices() {
return kioskDevices;
}
public void setKioskDevices(List<Device> kioskDevices) {
this.kioskDevices = kioskDevices;
}
}
In rare cases you would use
#XmlAnyElement(lax = true)
instead of
#XmlElement(type = DeviceImpl.class)
But if you are not using interfaces and just using classes only
#XmlRootElement
should be sufficient.
But all of that is relevant if you are not using Jackson with Spring for example. If so that would be a little bit different story.

Hibernate create domain instance with foreign key

i have domain classes like this:
extremely Simplified:
class Person extends Domain {
Set<Contracts> contracts;
//...
#OneToMany(fetch = FetchType.LAZY, mappedBy = "person", cascade = CascadeType.ALL)
public Set<Contracts> getContracts() {
return contracts;
}
//...
}
class Contract extends Domain {
Person person;
//...
#JoinColumn(name = "PERSON__ID", nullable = false)
public Person getPerson() {
return this.person;
}
//...
}
I got a instance of a person and want to create a new contract.
All over reflection.
My first try was something like that:
Domain domain = ...;
Method getter = resolveGetter(domain); // Person.getContracts()
ParameterizedType returnType = (ParameterizedType) getter.getGenericReturnType();
Class<Domain> cls = (Class<Domain>)returnType.getActualTypeArguments()[0];
Domain instance = cls.newInstance();
OneToMany annotation = getter.getAnnotation(OneToMany.class);
String mappedBy = annotation.mappedBy();
and now i need the setter method from the cls wich the mappedBy descripe to set the person in the contract.
But how i get the setter correctly?
A possible solution is via method name of the class but i think its ugly. And i dont find a method of the hibernate framework to do this.
Has someone an idea?
Usage:
Java 6 hibernate-core-3.6.10
hibernate-commons-annotations-3.2.0
hibernate-jpa-2.0-api-1.0.1
-- Update:
I got the hibernate property via
JavaReflectionManager manager = new JavaReflectionManager();
XClass xClass = manager.toXClass(cls);
List<XProperty> declaredProperties = xClass.getDeclaredProperties(XClass.ACCESS_PROPERTY); // Our domains use only Property Access
for (XProperty xProperty : declaredProperties) {
if(xProperty.getName().equals(mappedBy)) {
// How to set the value in the property?
}
}
Now were the question how to set the value in the property?

Categories

Resources