Spring Data Projection size() - java

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()")

Related

MapStruct cannot find members from generic Set property

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());
}
}

How to ignore LAZY types dynamically on SpringBoot app?

I have a class:
#Entity
#Table(name = "restaurants")
public class Restaurant extends AbstractNamedEntity implements Serializable {
#OneToMany(fetch = FetchType.LAZY, mappedBy = "restaurant")
private Set<Meal> meals = Collections.emptySet();
//other fields, getters, setters, constructors
}
I'm getting my data with Spring Data:
#Repository
public interface RestaurantRepository extends CrudRepository<Restaurant, Integer> {
}
I have REST-controller, which produces JSON data:
#RestController
#RequestMapping(value = RestaurantController.REST_URL, produces = MediaType.APPLICATION_JSON_VALUE + ";charset=UTF-8")
public class RestaurantController {
static final String REST_URL = "/restaurants";
#Autowired
private RestaurantRepository repository;
#GetMapping("{id}")
public List<Restaurant> getOne(#PathVariable Integer id) {
return repository.findById(id);
}
}
How to avoid including that LAZY data (set of Meals) to get them to a SQL request?
As I know I need to write a custom JacksonObjectMapper, but I don't know how to do it
You can use #JsonIgnore annotation in order to ignore the mapping of a field. Then you should do this:
#JsonIgnore
#OneToMany(fetch = FetchType.LAZY, mappedBy = "restaurant")
private Set<Meal> meals = Collections.emptySet();
UPDATED
Based what you want to do "Ignore field dynamically when getting one or not getting alls" you can use #NamedEntityGraphs annotation to specific what fields you want to join, then by using #NamedEntityGraph you specify the path and boundaries for a find operation or query and you should use in your custom Repository the #EntityGraph annotation who allows to customize the fetch-graph based what you want to do.
So you should add the following code:
#Entity
#Table(name = "restaurants")
#NamedEntityGraphs({
#NamedEntityGraph(name="Restaurant.allJoins", includeAllAttributes = true),
#NamedEntityGraph(name="Restaurant.noJoins")
})
public class Restaurant extends AbstractNamedEntity implements Serializable {
}
#Repository
public interface RestaurantRepository extends CrudRepository<Restaurant, Integer> {
#EntityGraph(value = "Restaurant.allJoins", type = EntityGraphType.FETCH)
#Override
List<Restaurant> findAll();
#EntityGraph(value = "Restaurant.noJoins", type = EntityGraphType.FETCH)
#Override
Optional<Restaurant> findById(Integer id);
}

Java JPA Mapping Problem with nested Collections

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.

Criteria API in Spring Boot

I have the following (simplified) entity structure:
public class Animal {
private long id;
private int regionId;
private int categoryId;
#OneToMany(fetch = FetchType.LAZY, cascade = CascadeType.ALL, mappedBy = "animal")
private List<Attributes> attributes;
}
public class Attributes {
private long id;
#ManyToOne(fetch = FetchType.LAZY)
private Animal animal;
private String name;
private String value;
#OneToOne(fetch = FetchType.LAZY, cascade = CascadeType.ALL)
private Detail detail;
}
public class Detail {
private long id;
private String size;
private String type;
}
This is part of a Spring Boot application.
What I would like to achieve is to query Animal by both their own attributes and by attributes in Details.
My query needs to look like this:
GET: /animal/{regionId}/{categoryId}?size=medium,large&type=carnivorous,herbivore
This would mean that I need to request all animals that have a certain regionId and categoryId and that also have Details with size and type within the value list provided. Also - and I think this is the tricky part - the size and type parameters are optional, so the query needs to take that into account.
Currently I have a PlanReposiory that extends CrudRepository and provides basic query methods for the Plan entity.
I was trying to wrap my head around the Criteria API to figure out a way to use it to achieve this goal, but I don't understand how I can put all that in my repository. Any ideas?
You should have a look at Spring Data JPA Specifications:
https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#specifications
You have to extend your Repository from JpaSpecificationExecutor
public interface CustomerRepository
extends CrudRepository<Customer, Long>, JpaSpecificationExecutor {
…
}
And then you the a findAll method that takes Specification paramter:
List<T> findAll(Specification<T> spec);
Then you can create your specification based on the parameters passed in the URL:
public static Specification<Animal> bySizeAndType(String size, String type) {
return new Specification<Animal>() {
public Predicate toPredicate(Root<Animal> root, CriteriaQuery<?> query,
CriteriaBuilder builder) {
// JOIN Attributes
// JOIN Detail
if (size != null) {
// add condition
}
if (type != null) {
// add condition
}
return builder.where(...);
}
};
}
I hope this helps.

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