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?
Related
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());
}
}
I have the following problem: I have three connected classes. I have annotated them but I am getting wrong results (described below):
#Entityd
#Table(name = "ClassA")
public class ClassA{
#Id
#GeneratedValue
private Long id = 0L;
...
#OneToMany(fetch = FetchType.EAGER,cascade=CascadeType.ALL)
#Fetch(FetchMode.SELECT)
#Column(name = "ClassBList")
private List<ClassB> listB;
...
}
#Entity
#Table(name="ClassB")
public class ClassB {
#Id
#GeneratedValue
private Long id = 0L;
...
#OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
#Fetch(FetchMode.SELECT)
#Column(name = "ClassCList")
private List<ClassC> listC;
...
}
#Entity
#Table(name="ClassC")
public class ClassC {
#Id
#GeneratedValue()
private Long id = 0L;
...
#ElementCollection
private List<String> listD;
...
}
When I work with this structure for the first ClassA I create,save and load everything is ok. For a new instance of ClassA which I save to repo and load again, I suddenly have the strings of the first ClassA in listD.
The result I need is that every class is "independently" saved. So the collections of each class should hold unique (each one with its own id and sublists) objects.
What would be the best way (annotations) to model this classes in Java 8 with Spring Boot 2.2.0.M5 and javax.persistence-api 2.2 ?
EDIT:
I have now removed class B and rewrote classA to:
#Entity
#Table(name = "ClassA")
public class ClassA{
#Id
#GeneratedValue
private Long id = 0L;
...
#OneToMany(fetch = FetchType.LAZY, orphanRemoval = true, cascade = CascadeType.ALL)
#MapKey(name = "type")
private Map<String,Set<ClassC>> classCmap;
...
}
This is giving me an error like:
org.hibernate.AnnotationException: Use of #OneToMany or #ManyToMany targeting an unmapped class
How can I model/solve/annotate this?
If you don't need to query data based on listD, I would suggest to keep the list as text in the database and use a converter:
#Converter
public class ListDConverter implements AttributeConverter<List<String>, String> {
private ObjectMapper objectMapper = new ObjectMapper();
#Override
public String convertToDatabaseColumn(List<String> listD) {
try {
return objectMapper.writeValueAsString(listD);
} catch(IOException e){
return null;
}
}
#Override
public List<String> convertToEntityAttribute(String stringListD) {
if(stringListD == null){
return Collections.emptyList();
}
try {
return objectMapper.readValue(stringListD, new TypeReference<List<String>>() {});
}catch(IOException e){
return Collections.emptyList();
}
}
}
and in your ClassC entity class :
#Convert(converter = ListDConverter.class)
private List<String> listD;
Why do I like this approach :
No extra table and joins => better performance
Easier to read listD in the database
#ElementCollection describes a table. So your code is probably creating a "listD" table with one column of type string, with no primary key.
Also, do you really want to use the SELECT fetch mode? That's going to generate 1 + b + b*c queries when you could just implement your data as sets (since you have unique identifiers) and use JOIN, which would result in one and only one query.
See this site for an explanation on how to use #ElementCollection.
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()")
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.
There is a foreign key in my entity :
#Entity
#Table(name = "role")
public class Role {
#Id
#Column(name = "role_code")
private String code;
#Column(name = "role_lib")
private String lib;
#ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
#JoinTable(name = "role_menu" , joinColumns = {#JoinColumn(name = "role_code")} , inverseJoinColumns = {#JoinColumn(name = "menu_id")} )
#JsonManagedReference
private Set<Menu> menus = new HashSet<Menu>();
// getters and setters
}
It is said in Hibernate documentation that relationship attributes should be of type Interface. Now my problem is when dealing with an instance of this class and using the getMenus() method :
#Override
#Transactional
public Set<Menu> getListMenu(String role_code) {
return ((Role)get(role_code)).getMenus();
}
I want to cast it to a HashSet , but I got castException of persistent object at runtime. So how to make it to be HashSet ?
You cannot cast if the implementations of Set is not HashSet. But you can create a object:
new HashSet(obj.getMenus);
But it's always better to use interfaces, not the implementation.
Here is a note from Hibernate doc:
Hibernate will actually replace the HashSet with an instance of
Hibernate's own implementation of Set. Be aware of the following
errors:
......
(HashSet) cat.getKittens(); // Error!
And here is why you don't actually need to cast nor to create a new object:
The persistent collections injected by Hibernate behave like HashMap,
HashSet, TreeMap, TreeSet or ArrayList, depending on the interface
type.