Multiple grouping and sorting in Collection entity - java

I have entities as shown below -
#Entity
public class Certification {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String role; // can have values like "Architect", "Developer", "Business Practioner"
private int score;
private Date expiryDate
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "solution_id")
private Solution solution;
#ManyToOne(fetch = FetchType.LAZY)
#JoinColumn(name = "region_id")
private Region region;
}
#Entity
public class Solution {
private String name; //can have values like "Analytics", "Campaign", "Marketo"
}
#Entity
public class Region{
private String name; //can have values like "EMEA", "APAC", "JAPAN" & "AMERICAS"
}
#Entity
public class Employee {
#OnetoMany(fetch = FetchType.LAZY)
private List<Certification> certifications
}
After pulling the employee certifications, I need to do multiple grouping and sorting and then pull the count of certification under a specific role. First I group by Solution name, then by Region, and then by Role. Each of these groups needs to be sorted alphabetically. For example, under solution group, "Analytics" should come first then "Campaign". Under region "APAC" should come first then "EMEA". And under the roles group, "Architecture" should come first then "Business Prationer".
{
"expertise": [
{
"solutionName": "Analytics",
"regions": [
{
"name": "APAC",
"roles": [
{
"name": "Architect",
"certifiedEmployees": 12
},
{
"name": "Business Practitioner",
"certifiedEmployees": 9
}
]
},
{
"name": "EMEA",
"roles": [
{
"name": "Architect",
"certifiedEmployees": 12
}
]
}
]
},
{
"solutionName": "Campaign",
"regions": [
{
"name": "APAC",
"roles": [
{
"name": "Architect",
"certifiedEmployees": 12
},
{
"name": "Business Practitioner",
"certifiedEmployees": 9
}
]
},
{
"name": "EMEA",
"roles": [
{
"name": "Architect",
"certifiedEmployees": 12
},
{
"name": "Back-end Developer",
"certifiedEmployees": 9
}
]
}
]
}
]
}
I tried using Collectors.groupingBy and Comparator.comparing but unable to achieve the desired results.

Found a better way to group a list using java streams and groupingBy. It's short and helps me avoid a lot of coding
credentials.stream().collect(groupingBy(c -> c.getSolution().getName(), groupingBy(c -> c.getRegion().getName())));

Related

#ManyToOne relations missing from JSON result

I have just started using Spring boot and I am using the default repository api to retrieve db data as json.
I have added a #ManyToOne relation to my Song and Artist Entity.
But now i am not getting the Artist object in my json response from the server and its not really clear to me, how I can include it without missing out on the pagination functions from the PagingAndSorting repository.
I am using the spring-data-rest-jpa.
My response now looks like:
"_embedded": {
"songs": [
{
"id": 1,
"title": "SongTitle",
"genre": "Rap",
"length": 500,
"_links": {
"self": {
"href": "http://localhost:8080/api/songs/1"
},
"song": {
"href": "http://localhost:8080/api/songs/1"
},
"artist": {
"href": "http://localhost:8080/api/songs/1/artist"
}
}
}
]
},
"_links": {
"first": {
"href": "http://localhost:8080/api/songs?page=0&size=1"
},
"self": {
"href": "http://localhost:8080/api/songs?size=1"
},
"next": {
"href": "http://localhost:8080/api/songs?page=1&size=1"
},
"last": {
"href": "http://localhost:8080/api/songs?page=19&size=1"
},
"profile": {
"href": "http://localhost:8080/api/profile/songs"
}
},
"page": {
"size": 1,
"totalElements": 20,
"totalPages": 20,
"number": 0
}
}
But I want it rather to be like this:
"_embedded": {
"songs": [
{
"id": 1,
"title": "SongTitle",
"genre": "Rap",
"length": 500,
"artist": {
"id": 1,
"name": "Artistname"
}
"_links": {
"self": {
"href": "http://localhost:8080/api/songs/1"
},
"song": {
"href": "http://localhost:8080/api/songs/1"
},
"artist": {
"href": "http://localhost:8080/api/songs/1/artist"
}
}
}
]
},
"_links": {
"first": {
"href": "http://localhost:8080/api/songs?page=0&size=1"
},
"self": {
"href": "http://localhost:8080/api/songs?size=1"
},
"next": {
"href": "http://localhost:8080/api/songs?page=1&size=1"
},
"last": {
"href": "http://localhost:8080/api/songs?page=19&size=1"
},
"profile": {
"href": "http://localhost:8080/api/profile/songs"
}
},
"page": {
"size": 1,
"totalElements": 20,
"totalPages": 20,
"number": 0
}
}
Song.java
#Getter
#Setter
#Entity
#Table(name = "song")
public class Song {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", nullable = false, unique = true)
private Long id;
#NotNull
#NotBlank(message = "The song has to have a title")
private String title;
#NotNull
#NotBlank(message = "The song has to have a genre")
private String genre;
#NotNull
#Min(value = 1, message = "The song has to have a song length in seconds")
private int length;
#ManyToOne(cascade = CascadeType.ALL)
#JoinColumn(name = "artist_id", referencedColumnName = "id")
private Artist artist;
/* #Version
private long version;*/
public Song() {
}
public Song(String title, Artist artist, String genre, int length) {
this.title = title;
this.artist = artist;
this.genre = genre;
this.length = length;
}
public void setArtist(Artist artist) {
this.artist = artist;
}
public Artist getArtist() {
return artist;
}
}
Artist.java
#Getter
#Setter
#Entity
#Table(name = "artist")
public class Artist {
#Id
#GeneratedValue(strategy = GenerationType.IDENTITY)
#Column(name = "id", nullable = false)
private Long id;
#NotNull
#NotBlank(message = "The artist has to have a name")
private String name;
#JsonIgnore
#OneToMany(mappedBy = "artist")
private List<Song> songs;
public Artist() {
}
public Artist(String name) {
this.name = name;
}
For the sake of testing I wrote a method in my SongController:
#GetMapping
List<Song> getSongs() {
return songRepository.findAll();
}
The result includes the Artist object, but won't have any pagination to it. How could I include it?
Json Result:
[
{
"id": 1,
"title": "SongTitle",
"genre": "Rap",
"length": 500,
"artist": {
"id": 1,
"name": "ArtistName"
}
}
]
After all your useful suggestions I have found an answer:
I have changed the return type of my Method in my controller to Page and made use of the PageRequest Class which looks like this:
#GetMapping
public Page<Song> getSongs(#RequestParam(defaultValue = "0") int page, #RequestParam(defaultValue = "5") int size) {
PageRequest pr = PageRequest.of(page, size);
return songRepository.findAll(pr);
}
Also used some defaultvalues to avoid some exceptions ;)
Use either #JsonIdentityInfo or #JsonIgnore and remove #JsonBackReference. Here is the example with #JsonIgnore
public class Artist {
public Long id;
public String name;
public List<Song> songs;
}
public class Song {
public Long id;
public String title;
public String genre;
public int length;
#JsonIgnore
public Artist artist;
}
#JsonManagedReference or #JsonBackReference won't help in this case (read more on https://www.baeldung.com/jackson-bidirectional-relationships-and-infinite-recursion):
(...) we can use the #JsonIgnore annotation to simply ignore one of the sides of the relationship, thus breaking the chain. [my addition: the chain of recurrent calls]
Here is the example with #JsonIdentityInfo:
#JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
public class Artist { ... }
#JsonIdentityInfo(
generator = ObjectIdGenerators.PropertyGenerator.class,
property = "id")
public class Song { ... }

How i can select a partial UDT value in Cassandra db with Spring?

I'm triyng to create a customized query in cassandra to retrieve less values from a table but cassandra is unable to retrieve an specific value inside UDT. For example:
I have these classes
#Data
#Builder
#Table
public class User {
#PrimaryKeyColumn
private UUID id;
private String name;
private String surname;
private Address address;
}
#Data
#Builder
#UserDefinedType
public class Address {
private String street;
private String city;
private String country;
}
and the repository
#Repository
public interface UserRepository extends ReactiveCassandraRepository<User, UUID> {
Flux<User> findAllById(UUID id);
}
Here everything is ok. When I call to "findAllById" i get
[
{
"id": "8cb5d25d-472a-4555-ac4b-d5bad1fb4318",
"name": "Lucke",
"surname": "Perry",
"address": {
"street": "tapiporlas",
"city": "croqueta",
"country": "spain"
}
},
{
"id": "07f9891a-4cbc-45fb-9aa8-8dd48765b73f",
"name": "Scotty",
"surname": "Pippen",
"address": {
"street": "amargura",
"city": "palma",
"country": "spain"
}
}
]
But I don't want all values. I want user name and street. I run a select "select name, address.street from user where id =" inside cassandra db and it works. So I created a query for that inside the repository
#Query("select name, address.street from user where id =?0")
Flux<User> findAllByIdReduced(UUID id);
and the resulted json is
[
{
"name": "Lucke"
},
{
"name": "Scotty"
}
]
The expected will be
[
{
"name": "Lucke",
"address": {
"street": "tapiporlas"
}
},
{
"name": "Scotty",
"address": {
"street": "amargura"
}
}
]
I tried to initialize the Address object but created it empty.
I tried to put the query "#Query("select name, address from user where id =?0")" but it fill the full object.
Any idea about select a partial UDT?

Spring Data Mongodb Aggregation - Group by nested objects and build DTO

I have the following Employee data in MongoDB
{
"_id": {
"$oid": "625f09bb1a96bf42ff4c4006"
},
"employeeId": 1234,
"email": "jason#acme.com",
"firstName": "Jason",
"lastName": "Stuart",
"currentCTC": 1201117.61,
"department": {
"$ref": "department",
"$id": {
"$oid": "625f09bb1a96bf42ff4c4005"
}
}
}
{
"_id": {
"$oid": "625f09bb1a96bf42ff4c4006"
},
"employeeId": 1235,
"email": "jasons#acme.com",
"firstName": "Jasons",
"lastName": "Stuarts",
"currentCTC": 1201117.61,
"department": {
"$ref": "department",
"$id": {
"$oid": "625f09bb1a96bf42ff4c4005"
}
}
}
My Spring #Document looks like this:
// Employee.java
#Data
#Document
public class Employee {
#Id
private String id;
private Long employeeId;
private String email;
private String firstName;
private String middleName;
private String lastName;
private Gender gender;
private double currentCTC;
#DBRef
private Department department;
}
// Department.java
#Document
#Data
public class Department {
#Id
private String id;
private String name;
}
Now, my requirement is to find the sum of salaries Department-wise.. I need the data to be in the following way:
[
{
"department": {
"id": "625f09bb1a96bf42ff4c4006",
"name": "Engineering"
},
"cost": 31894773.01
},
{
"department": {
"id": "625f09bb1a96bf42ff4c4006",
"name": "Marketing"
},
"cost": 4552325.25
}
]
I created an aggregate function like this in Spring Data:
public List<DepartmentCost> getDepartmentCosting() {
GroupOperation groupByDepartment = group("department").sum("currentCTC").as("cost").first("$$ROOT").as("department");
Aggregation aggregation = Aggregation.newAggregation(groupByDepartment);
AggregationResults<DepartmentCost> results = mongoTemplate.aggregate(aggregation, "employee", DepartmentCost.class);
return results.getMappedResults();
}
And my expected DepartmentCost.java
#Data
#Document
public class DepartmentCost {
#DBRef
private Department department;
private double cost;
}
Now when I try this API out, I get the data correctly, but I do not get department name. It comes as null. I get a response like
[
{
"department": {
"id": "625f09bb1a96bf42ff4c4006",
"name": null,
},
"cost": 2241117.6100000003
},
{
"department": {
"id": "625f09bb1a96bf42ff4c400a",
"name": null,
},
"cost": 14774021.43
},
{
"department": {
"id": "625f09bc1a96bf42ff4c4013",
"name": null,
},
"cost": 14879633.97
}
]
How can I get the department details expanded in my model? Please help..
After a couple of attempts, I figured it out. All I had to do was this:
GroupOperation groupByDepartment = group("department").sum("currentCTC").as("cost").first("$department").as("department");
as opposed to:
GroupOperation groupByDepartment = group("department").sum("currentCTC").as("cost").first("$$ROOT").as("department");

Error parsing JSON (MismatchedInputException)

I'm having problems parsing JSON, this is the error:
out of START_OBJECT token; nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of `java.util.ArrayList<packagename....>` out of START_OBJECT token
And I know why it is happening I just don't know how to fix it. This JSON works:
{
"status_code": "SUCCESS",
"time": {
"date": "Mar 23, 2021 1:14:39 AM"
},
"info": [
{
"person": "2.2",
"role": "TEACHER"
},
{
"person": "2.3",
"role": "TEACHER"
}
]
}
This one does not:
{
"status_code": "SUCCESS",
"time": {
"date": "Mar 23, 2021 3:49:27 AM"
},
"info": {
"id": "1",
"person": [
{
"identifier": "John",
"role": "TEACHER"
},
{
"identifier": "Homer",
"role": "TEACHER"
},
{
"identifier": "Michael",
"role": "TEACHER"
},
{
"identifier": "Sarah",
"role": "TEACHER"
}
]
}
}
The problem seems to be the { character in front of the info field because with [ works. So this is the method I'm using to parse the JSON:
public Mono<PersonResponse> searchById(String id) {
return webClient.get().uri(id).retrieve().bodyToMono(PersonResponse.class);
}
Also tried:
public Mono<PersonResponse[]> searchById(String id) {
return webClient.get().uri(id).retrieve().bodyToMono(PersonResponse[].class);
}
Error right on line: 1, column: 1. Any suggestions of how to implement the method?
EDIT: Added classes.
PersonResponse:
public class PersonResponse implements Serializable{
private static final long serialVersionUID = 7506229887182440471L;
public String status_code;
public Timestamp time;
public List<PersonDetails> info;
public PersonResponse() {}
...getters / setters / toSting
PersonDetails:
public class PersonDetails implements Serializable{
private static final long serialVersionUID = 1294417456651475410L;
private int id;
private List<Person> person;
public PersonDetails(int version) {
super();
this.version = version;
}
...getters / setters / toSting
Person
public class Person implements Serializable{
private static final long serialVersionUID = 3290753964441709903L;
private String identifier;
private String role;
public Person(String identifier, String role) {
super();
this.identifier = identifier;
this.role = role;
}
...getters / setters / toSting
The problem isn't the JSON necessarily, it's that the JSON structure doesn't match your PersonResponse class. There's an info variable in PersonResponse that requires an array of what I assume to be persons, in the second example you're trying to push an object in there, which you can't. You have to either change your JSON, which you don't seem to want in this case, or the class you're trying to parse it to.
You need to restructure the info variable in PersonResponse to match the object you're trying to parse to it.

How can I return multi-level json using Hibernate/JPA in Spring Boot

I have a Postgres Database that has 4 tables Parents, Children, Groups and Group_Membership.
Groups can have multiple parents and Parents can have multiple groups. Parents can have multiple children but children can only have one parent.
This is the dumbed down version of the schema.
I am using Spring Boot with Hibernate JPA.
Parent.java
#Entity
#Table(name = "parents")
public class Parent {
#Id
#GeneratedValue
#Column(name="parent_id")
private Long parentId;
#Column(name= "first_name")
private String firstName;
#Column(name= "last_name")
private String lastName;
#OneToMany(mappedBy="parent")
private Set<Child> children;
#ManyToMany(cascade = { CascadeType.ALL })
#JoinTable(
name= "Group_Membership",
joinColumns = { #JoinColumn(name = "parent_id") },
inverseJoinColumns = { #JoinColumn(name = "group_id") }
)
private Set<Group> groups = new HashSet<>();
//Constructor
//Getters and Setters
}
Child.java
#Entity
#Table(name = "children")
public class Child {
#Id
#GeneratedValue
#Column(name= "child_id")
private Long childId;
#Column(name= "first_name")
private String firstName;
#Column(name= "last_name")
private String lastName;
#ManyToOne
#JoinColumn(name="parent_id", nullable=false)
private Parent parent;
//Constructor
//Getters and Setters
}
Group.java
#Entity
#Table(name = "groups")
public class Group {
#Id
#GeneratedValue
#Column(name= "group_id")
private Long groupId;
private String name;
#ManyToMany(mappedBy = "groups")
private Set<Parent> parents = new HashSet<>();
//Constructor
//Getters and Setters
}
I have repositories for all of them set up like this:
public interface GroupRepository extends PagingAndSortingRepository<Group, Long> {
#RestResource(rel = "name-contains", path = "containsName")
Page<Group> findByNameContains(#Param("name") String name, Pageable page);
}
Group Membership Table
CREATE TABLE GROUP_MEMBERSHIP (
PARENT_ID INT NOT NULL,
GROUP_ID INT NOT NULL,
PRIMARY KEY (PARENT_ID, GROUP_ID),
CONSTRAINT GROUP_MEMBERSHIP_IBFK_1
FOREIGN KEY (PARENT_ID) REFERENCES PARENTS (PARENT_ID),
CONSTRAINT GROUP_MEMBERSHIP_IBFK_2
FOREIGN KEY (GROUP_ID) REFERENCES GROUPS (GROUP_ID)
);
When I go to http://localhost:8080/groups
I get this response:
{
"_embedded": {
"groups": [
{
"name": "Hyde Park",
"_links": {
"self": {
"href": "http://localhost:8080/groups/1"
},
"group": {
"href": "http://localhost:8080/groups/1"
},
"parents": {
"href": "http://localhost:8080/groups/1/parents"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/groups"
},
"profile": {
"href": "http://localhost:8080/profile/groups"
},
"search": {
"href": "http://localhost:8080/groups/search"
}
},
"page": {
"size": 20,
"totalElements": 1,
"totalPages": 1,
"number": 0
}
}
Then when I want to look at the parents in the group I go to http://localhost:8080/groups/1/parents
Response
{
"_embedded": {
"parents": [
{
"firstName": "Cherice",
"lastName": "Giannoni",
"_links": {
"self": {
"href": "http://localhost:8080/parents/1"
},
"parent": {
"href": "http://localhost:8080/parents/1"
},
"groups": {
"href": "http://localhost:8080/parents/1/groups"
},
"children": {
"href": "http://localhost:8080/parents/1/children"
}
}
},
{
"firstName": "Aylmer",
"lastName": "Feckey"
"_links": {
"self": {
"href": "http://localhost:8080/parents/2"
},
"parent": {
"href": "http://localhost:8080/parents/2"
},
"groups": {
"href": "http://localhost:8080/parents/2/groups"
},
"children": {
"href": "http://localhost:8080/parents/2/children"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/groups/1/parents"
}
}
}
Finally when I want to see the children of the first parent in the group I go to http://localhost:8080/parents/1/children
Response
{
"_embedded": {
"children": [
{
"firstName": "Richard",
"lastName": "Giannoni"
"_links": {
"self": {
"href": "http://localhost:8080/children/2"
},
"child": {
"href": "http://localhost:8080/children/2"
},
"parent": {
"href": "http://localhost:8080/children/2/parent"
}
}
},
{
"firstName": "Deeanne",
"lastName": "Giannoni"
"_links": {
"self": {
"href": "http://localhost:8080/children/1"
},
"child": {
"href": "http://localhost:8080/children/1"
},
"parent": {
"href": "http://localhost:8080/children/1/parent"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/parents/1/children"
}
}
}
I would like to be able to call one endpoint like http://localhost:8080/groups/search/findAllGroupMembers?group_id=1
and have it return multi level json with the group, all parents in the group, and all of the children of each parent.
I know how to write a query with sub-queries to return this information, but I was just curious if there is a more "JPA/Hibernate" way to do this?
Thanks!
EDIT: Fixed using Alan Hay's answer
GroupFullProjection.java
#Projection(name = "groupFullProjection", types = {Group.class})
public interface GroupFullProjection {
Long getGroupId();
String getName();
Set<ParentFullProjection> getParents();
}
ParentFullProjection.java
#Projection(name = "parentFullProjection", types = {Parent.class})
public interface ParentFullProjection {
Long getParentId();
String getFirstName();
String getLastName();
Set<Child> getChildren();
}
json Response with all required information
Endpoint: http://localhost:8080/groups/1?projection=groupFullProjection
{
"name": "Hyde Park",
"groupId": 1,
"parents": [
{
"children": [
{
"firstName": "Richard",
"lastName": "Giannoni",
},
{
"firstName": "Deeanne",
"lastName": "Giannoni",
}
],
"parentId": 1,
"firstName": "Cherice",
"lastName": "Giannoni",
"_links": {
"self": {
"href": "http://localhost:8080/parents/1{?projection}",
"templated": true
},
"groups": {
"href": "http://localhost:8080/parents/1/groups"
},
"children": {
"href": "http://localhost:8080/parents/1/children"
}
}
},
{
"children": [
{
"firstName": "Hanson",
"lastName": "Feckey",
}
],
"parentId": 2,
"firstName": "Aylmer",
"lastName": "Feckey",
"_links": {
"self": {
"href": "http://localhost:8080/parents/2{?projection}",
"templated": true
},
"groups": {
"href": "http://localhost:8080/parents/2/groups"
},
"children": {
"href": "http://localhost:8080/parents/2/children"
}
}
}
],
"_links": {
"self": {
"href": "http://localhost:8080/groups/1"
},
"group": {
"href": "http://localhost:8080/groups/1{?projection}",
"templated": true
},
"parents": {
"href": "http://localhost:8080/groups/1/parents"
}
}
}
Lazy/Eager loading has absolutely nothing to do with it. The REST services in your application are provided by Spring Data Rest and having an understanding of why it looks like it does and how you can change it is probably worth learning.
https://docs.spring.io/spring-data/rest/docs/current/reference/html/#repository-resources.fundamentals
Essentially you get the links to the associations because these entities have their own repositories which are exposed as REST respources. If children/parent were not exposed as REST reources then the data would be inlined (as there would be no other way to access them).
You can however use Projections to fetch alternative views of the data. So you can then define a projection that would inline the assocations. Clients can request this specific view of the data.
e.g. http://localhost:8080/groups/1?projection=groupFullProjection
This involves creating a simple interface that defines the properties to be exposed in that view of the data.
See: https://docs.spring.io/spring-data/rest/docs/current/reference/html/#projections-excerpts
This might look something like:
#Projection(name = "parentFullProjecton", types = { Parent.class })
interface ParentFullProjection{
// inline the child collection
Set<Child> getChildren();
// other fields
}
#Projection(name = "groupFullProjection", types = { Group.class })
interface GroupFullProjection{
//inline the parents collection and use the view which inlines Children
Set<ParentFullProjecton> getParent();
// other fields
}

Categories