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

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

Related

Multiple grouping and sorting in Collection entity

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

Java Spring Deserializing Nested objects using RestTemplate

I am using Java Spring boot restTemplate and I am trying to deserialize the below JSON into their corresponding objects. However it is returning null.
Am I doing this the right way? Should I return a String response Entity and then convert?
{
"Events": [
{
"Id": 3584588,
"Url": "https://api.wildapricot.org/v2/accounts/257051/Events/3584588",
"EventType": "Regular",
"StartDate": "2019-10-07T07:00:00-05:00",
"EndDate": "2019-10-11T12:00:00-05:00",
"Location": "Renaissance Montgomery Hotel & Spa",
"RegistrationEnabled": false,
"RegistrationsLimit": null,
"PendingRegistrationsCount": 0,
"ConfirmedRegistrationsCount": 0,
"CheckedInAttendeesNumber": 0,
"InviteeStat": {
"NotResponded": 0,
"NotAttended": 0,
"Attended": 0,
"MaybeAttended": 0
},
"Tags": [
"event"
],
"AccessLevel": "AdminOnly",
"StartTimeSpecified": true,
"EndTimeSpecified": true,
"HasEnabledRegistrationTypes": false,
"Name": "2020 Montgomery IT Summit"
},
{
"Id": 3584591,
"Url": "https://api.wildapricot.org/v2/accounts/257051/Events/3584591",
"EventType": "Rsvp",
"StartDate": "2019-10-03T00:00:00-05:00",
"EndDate": "2019-10-31T00:00:00-05:00",
"Location": "Here",
"RegistrationEnabled": true,
"RegistrationsLimit": null,
"PendingRegistrationsCount": 0,
"ConfirmedRegistrationsCount": 0,
"CheckedInAttendeesNumber": 0,
"InviteeStat": {
"NotResponded": 0,
"NotAttended": 0,
"Attended": 0,
"MaybeAttended": 0
},
"Tags": [
"volunteer"
],
"AccessLevel": "Public",
"StartTimeSpecified": false,
"EndTimeSpecified": false,
"HasEnabledRegistrationTypes": true,
"Name": "Volunteer Event"
}
]
}
Here is my call:
ResponseEntity<WaEvents> response = restTemplate.exchange(uri,
HttpMethod.GET,
request,
WaEvents.class
);
return response.getBody().getEvents();
Here is my WaEvents Class:
#Data
public class WaEvents implements Serializable {
#JsonUnwrapped
#JsonProperty("Events")
private List<WaEvent> events;
}
Here is the WaEvent Class
#Data
#JsonIgnoreProperties(ignoreUnknown = true)
public class WaEvent {
#JsonProperty("Id")
public Integer id;
#JsonProperty("Name")
public String name;
#JsonProperty("Location")
public String location;
#JsonProperty("StartDate")
public LocalDate startDate;
#JsonProperty("EndDate")
public LocalDate endDate;
#JsonProperty("IsEnabled")
public Boolean isEnabled;
#JsonProperty("Description")
public String description;
#JsonProperty("RegistrationLimit")
public Integer RegistrationLimit;
}
As explained here with an example :
public class Parent {
public int age;
public Name name;
}
public class Name {
public String first, last;
}
Without #JsonUnwrapped, the JSON is :
{
"age" : 18,
"name" : {
"first" : "Joey",
"last" : "Sixpack"
}
}
With #JsonUnwrapped, the JSON is :
{
"age" : 18,
"first" : "Joey",
"last" : "Sixpack"
}
So #JsonUnwrapped will flatten the properties and events won't exist anymore :
{
"Id": 3584588,
"Name": "2020 Montgomery IT Summit",
"Location": "Renaissance Montgomery Hotel & Spa",
"StartDate": "2019-10-07T07:00:00-05:00",
"EndDate": "2019-10-11T12:00:00-05:00",
...
}
Try to remove #JsonUnwrapped

how to mapped java embbeded object with JSON

I build an google Calendar API, and i miss understand a point with my json files.
I succeed to create my java object with my json files but here the issue:
i have two classes :
public class User {
private String email;
private String firstname;
private String lastname;
Entity entity;
``
and my Entity
`` public class Entity {
private String name;
private String entityType;
private Entity rootEntity;``
here my json file :
for user
``[
{
"firstname": "Jean-Marc",
"lastname": "Chevereau",
"email": "xxxxxxx#xxxxx.com",
"entity": {
"name":"BFA",
"entityType":"secteur"
}
},
{
"firstname": "Florent",
"lastname": "Hamlin",
"email": "xxxxxxx#xxxxx.com",
"entity": {
"name":"IT",
"entityType":"secteur"
}
},
{
"firstname": "Benoit",
"lastname": "Micaud",
"email": "xxxxxxx#xxxxx.com",
"entity": {
"name":"EX",
"entityType":"offre",
"rootEntity":{
"name":"BFA"
}
}
}
]``
And a Entity json file
```[
{
"name": "BFA",
"entityType": "secteur",
"rootEntity": "",
},
{
"name": "EX",
"entityType": "Offre",
"rootEntity": "BFA",
}
}
]
But here the trouble. if in my User.json i write theEntity Name, i dont want to write entitytype and rootEntity, because if i write Entity Name is BFA, it will always be the same entitType and the rootEntity.
In others words, my json Entity will be always the same,and if i just put the name we know that refers to an entity object.
For instance, in this user.json file, I will just need to put
[
{
"firstname": "Jean-Marc",
"lastname": "Chevereau",
"email": "xxxxxxx#xxxxx.com",
"entity": {
"name":"BFA",
}
},
{
"firstname": "Florent",
"lastname": "Hamlin",
"email": "xxxxxxx#xxxxx.com",
"entity": {
"name":"IT",
}
},
{
"firstname": "Benoit",
"lastname": "Micaud",
"email": "xxxxxxx#xxxxx.com",
"entity": {
"name":"EX",
}
}
]
In Json-lib you have a JsonConfig to specify the allowed fields:
JsonConfig jsonConfig=new JsonConfig();
jsonConfig.registerPropertyExclusion(Entity.class,"rootEntity");
jsonConfig.registerPropertyExclusion(Entity.class,"entityType");
JSON json = JSONSerializer.toJSON(objectToWrite,jsonConfig);
I suppose com.fasterxml.jackson's #JsonIgnore annotation should help.
public class Entity {
private String name;
#JsonIgnore
private String entityType;
#JsonIgnore
private Entity rootEntity;
}

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
}

Mapping JSONArray in RestTemplate Spring

I am trying to map this JSONArray using Spring RestTemplate:
[{
"Command": "/usr/sbin/sshd -D",
"Created": 1454501297,
"Id": "e00ca61f134090da461a3f39d47fc0cbeda77fbbc0610439d3c16a932686b612",
"Image": "ubuntu:latest",
"Labels": {
},
"Names": [
"/nova-c1896fbd-1309-4da2-8d77-b4fe4c02fa8e"
],
"Ports": [
],
"Status": "Up 2 hours"
}, {
"Command": "/usr/sbin/sshd -D",
"Created": 1450106126,
"Id": "7ffc9dbdd200e2c23adec442abd656ed57306955332697cb7da979f36ebf3b22",
"Image": "ubuntu:latest",
"Labels": {
},
"Names": [
"/nova-93b9ae40-8135-48b7-ac17-12094603b28c"
],
"Ports": [
],
"Status": "Up 2 hours"
}]
Here is ContainersInfo class:
#JsonIgnoreProperties(ignoreUnknown = true)
public class ContainersInfo {
private String Id;
private List<String> Names;
public String getId() {
return Id;
}
public void setId(String id) {
Id = id;
}
public List<String> getNames() {
return Names;
}
public void setNames(List<String> names) {
Names = names;
}
}
However I get null when I want to get the data:
ContainersInfo[] containers = syncRestTemplate.getForObject("http://192.168.1.2:4243/containers/json?all=1", ContainersInfo[].class);
for (int i = 0; i < containers.length; i++)
System.out.println("id:" + containers[i].getId());
The resulting output is as follows:
id:null
id:null
Any idea, what I should do?
Your JSON field names are in pascal case as opposed to camel case (which is usually the case). Set Jackson naming strategy to PascalCaseStrategy, i.e by adding #JsonNaming(PascalCaseStrategy.class) annotation into ContainersInfo class.

Categories