mongo aggregation:- push all fields using Spring - spring

if I have a collection of books :-
{author: "tolstoy", title:"war & peace", price:100, pages:800}
{author: "tolstoy", title:"Ivan Ilyich", price:50, pages:100}
and if i want a result like this after grouping them by author :-
{ author: "tolstoy",
books: [
{author: "tolstoy", title:"war & peace", price:100, pages:800}
{author: "tolstoy", title:"Ivan Ilyich", price:50, pages:100}
]
}
using raw mongo queries I can do something like this:-
{$group: {
_id: "$author",
books:{$push: {author:"$author", title:"$title", price:"$price", pages:"$pages"}},
}}
But how do I do this using spring , I tried something like this:-
private GroupOperation getGroupOperation() {
return group("author").push("title").as("title").push("price").as("price").push("pages").as("pages");
}
but this does not seem to work. Any help would be appreciated.
UPDATE:-
I used the solution as in the link suggested by #Veeram and it works great but I ran into another issue when I project it. I have my projection class which looks like:-
public class BookSummary{
private String author;
private List<Book> bookList;
//all getters and setters below
}
The group method looks like this:-
private GroupOperation getGroupOperation() {
return group("author").push(new BasicDBObject("id","$_id").append("title","$title").append("pages","$pages").append("price","$price")).as("bookList");
}
the projection method looks like this:-
private ProjectionOperation getProjectOperation() {
return project("author").and("bookList").as("bookList");
}
and the final aggregation operation:-
mongoTemplate.aggregate(Aggregation.newAggregation(groupOperation,projectionOperation), Book.class, BookSummary.class).getMappedResults();
However this gives the result:-
[
{
"author": null,
"bookList": [
{
"id": null,
"title": "title1",
"pages": "100",
"price":"some price"
},
{
"id": null,
"title": "title2",
"pages": "200",
"price":"some price"
}
]
}
]
Why is the author and id null here? Any help would be appreciated

You should be projecting using _id instead in the project phase.
private ProjectionOperation getProjectOperation() {
return project("_id").and("bookList").as("bookList");
}

Related

Spring Data Mongo: Compare Two Dates in the Same Document

A brief overview of the document I am working with:
#NoArgsConstructor
#AllArgsConstructor
#EqualsAndHashCode
#Data
#SuperBuilder(toBuilder = true)
public class BreachBrand {
#Id
private String id;
#CreatedDate
#Field("created_date")
#DiffIgnore
private Instant createdDate;
#LastModifiedDate
#Field("last_modified_date")
#DiffIgnore
private Instant lastModifiedDate;
}
What I am trying to do is compare the lastModifiedDate to the createdDate. So I created a criteria object like so:
criteria.add(Criteria.where("last_modified_date").gt("ISODate('created_date')"));
I've also tried:
criteria.add(Criteria.where("last_modified_date").gt("created_date"));
which is then used in the match operation of an Aggregation object. Using the first criteria code snippet, the aggregation looks like this:
{ "aggregate" : "__collection__", "pipeline" : [{ "$lookup" : { "from" : "brands", "localField" : "brand_dfp_id", "foreignField" : "dfp_id", "as" : "brand"}}, { "$match" : { "$and" : [{ "last_modified_date" : { "$gt" : "ISODate('created_date')"}}]}}, { "$sort" : { "date" : -1}}, { "$skip" : 0}, { "$limit" : 25}], "allowDiskUse" : true, "collation" : { "locale" : "en", "strength" : 1}}
The mongoTemplate object executes the aggregate method w/o error but no records are returned.
I'm suspecting that the gt(Object o) method is expecting an object that is an actual value to use to compare against. All is good when I use an actual date:
criteria.add(Criteria.where("last_modified_date").gt(Instant.parse("2019-05-18T17:07:25.333+00:00")));
As an interesting aside the following works in mongoshell:
db.breaches.find({$where: "this.last_modified_date>this.created_date"}).pretty();
And the following works in Compass (but the export to language button will not display the output):
/**
* $match operation
*/
{
last_modified_date: {$gt: ISODate('created_date')}
}
EDIT:
It appears I need to use a projection to determine if last_modified_date is greater than created date. I got this to work in compass:
[{
$project: {
greater: {
$gt: [
'$last_modified_date',
'$created_date'
]
},
doc: '$$ROOT'
}
}, {
$match: {
greater: true
}
}]
I'm having issues moving that into a projection though:
ProjectionOperation projectionOperation = project("last_modified_date", "created_date").andExpression("$gt", "$last_modified_date", "$created_date").as("greater");
I've also tried this:
ProjectionOperation projectionOperation = project("last_modified_date", "created_date").andExpression("$gt", Arrays.asList("$last_modified_date", "$created_date")).as("greater");
Results in an exception when creating the aggregation:
Aggregation aggregation = newAggregation(
lookup("brands", "brand_dfp_id", "dfp_id", "brand"),
projectionOperation,
matchOperation, //Criteria.where("greater").is(true)
sortOperation,
skipOperation,
limitOperation
)
.withOptions(AggregationOptions.builder()
.allowDiskUse(true)
.collation(Collation.of("en").strength(Collation.ComparisonLevel.primary())).build());
exception:
java.lang.IllegalArgumentException: Invalid reference 'date'!
at org.springframework.data.mongodb.core.aggregation.ExposedFieldsAggregationOperationContext.getReference(ExposedFieldsAggregationOperationContext.java:114)
at org.springframework.data.mongodb.core.aggregation.ExposedFieldsAggregationOperationContext.getReference(ExposedFieldsAggregationOperationContext.java:86)
at org.springframework.data.mongodb.core.aggregation.SortOperation.toDocument(SortOperation.java:74)
at org.springframework.data.mongodb.core.aggregation.AggregationOperation.toPipelineStages(AggregationOperation.java:55)
at org.springframework.data.mongodb.core.aggregation.AggregationOperationRenderer.toDocument(AggregationOperationRenderer.java:56)
at org.springframework.data.mongodb.core.aggregation.AggregationPipeline.toDocuments(AggregationPipeline.java:77)
at org.springframework.data.mongodb.core.aggregation.Aggregation.toPipeline(Aggregation.java:705)
at org.springframework.data.mongodb.core.AggregationUtil.createPipeline(AggregationUtil.java:95)
at org.springframework.data.mongodb.core.MongoTemplate.doAggregate(MongoTemplate.java:2118)
at org.springframework.data.mongodb.core.MongoTemplate.aggregate(MongoTemplate.java:2093)
at org.springframework.data.mongodb.core.MongoTemplate.aggregate(MongoTemplate.java:1992)

How can I make jackson to use different views on nested entities of the same type?

Before I start questioning let me give you a simplified example of my case:
Imagine you have Views:
public final class Views {
public interface Id { }
public interface IdText extends Id { }
public interface FullProfile extends IdText { }
}
You also have a class User which has subscribers that are of the same type User.
The properties id and username are serialized in the Views.IdText.class view. And the property subscribers is serialized in the Views.FullProfile.class view.
#Entity
public class User implements UserDetails {
#Id
#JsonView(Views.IdText.class)
private Long id;
#JsonView(Views.IdText.class)
private String username;
#JsonIdentityReference
#JsonIdentityInfo(
property = "id",
generator = ObjectIdGenerators.PropertyGenerator.class
)
#JsonView(Views.FullProfile.class)
private Set<User> subscribers = new HashSet<>();
}
And a controller (ProfileController) that has a method called get that returns a user's profile.
#RestController
public class ProfileController {
#GetMapping("{id}")
#JsonView(Views.FullProfile.class)
public User get(#PathVariable("id") User user) {
// ... some service methods that has the necessary logic.
return user;
}
}
As you can see, the method serializes the user's profile in the Views.FullProfile.class view, so the output is:
{
"id": 39,
"username": "ryan",
"subscribers": [
{
"id": 42,
"username": "elliott",
"subscribers": [
{
"id": 432,
"username": "oliver",
"subscribers": [
{
"id": 2525,
"username": "james",
"subscribers": [
39,
432
]
},
{
// ... a lot of useless data goes on.
}
]
},
{
"id": 94923,
"username": "lucas",
"subscribers": [
345, 42
]
}
]
},
{
"id": 345,
"username": "jocko",
"subscribers": [
94923
]
}
]
}
When a user's profile is being serialized, I don't need the user's subscribers to be serialized in the Views.FullProfile.class view
but in the Views.IdText.class view so that the output would be:
{
"id": 39,
"username": "ryan",
"subscriptions": [
{
"id": 42,
"username": "elliott"
},
{
"id": 345,
"username": "jocko"
}
]
}
How can I make jackson to use different views on nested entities of the same type?
Or what else do I have to do to make that happen?
After some time of continuous searching I found someone issued the same problem on Github: #JsonIgnoreProperties should support nested properties #2940
As stated in that issue:
No plans to ever implement this (due to delegating design, will not be possible with current Jackson architecture), closing.

Spring Data Mongo: How to filtering documents by optional attributes?

A few Documents that already stored in Mongo db:
{
"companyName": "Google",
"departmentName": "Sales"
},
{
"companyName": "Google",
"departmentName": "HR"
},
{
"companyName": "Amazon",
"departmentName": "Marketing"
}
I need to implement a method that will receive 2 attributes: companyName, departmentName (one of them can be optional) and will return a list of found documents, for example:
when departmentName is null and companyName is Google method returns 2 documents:
{
"companyName": "Google",
"departmentName": "Sales"
},
{
"companyName": "Google",
"departmentName": "HR"
}
when companyName is null and departmentName is Marketing only one Document returned:
{
"companyName": "Amazon",
"departmentName": "Marketing"
}
I tried to implement it in various ways, but no one fits my needs:
public interface CompanyRepository extends ReactiveMongoRepository<Company, String> {
Flux<Company> findByCompanyNameAndDepartmentName(String companyName, String departmentName);
}
returns 0 results, when companyName or departmentName is null.
Option 2:
Company company = Company.builder()
.companyName(null)
.departmentName("Marketing")
.build();
repository.findAll(Example.of(company))
.subscribe(System.out::println);
is also prints 0 results, but I expect to see one document.
so, please advice for to implement proper search?
Could you try with the below repository interface:
public interface CompanyRepository extends ReactiveMongoRepository<Company, String> {
Flux<Company> findByCompanyNameOrDepartmentName(String companyName, String departmentName);
}
Change is JPA method name change from findByCompanyNameAndDepartmentName to findByCompanyNameOrDepartmentName. Since you need the result if any one of the field matches your input.
Reference: https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa.query-methods.query-creation

Problem to structure property of an object using [apollo / graphql]

Problem
Hello friends,
I am working on an api using Apollo Server.
I am having the problem of how to display the nextEpisodeDate property only once. My solution shows nextEpisodeDate in all sub-array in the episodes property and it shouldn't be like that.
I hope someone can help me !
JSON Example
"episodes": [
{
"nextEpisodeDate": "2020-01-17"
},
{
"episode": 3,
"id": "53789/dorohedoro-3",
"imagePreview": "https://cdn.animeflv.net/screenshots/3274/3/th_3.jpg"
},
{
"episode": 2,
"id": "53755/dorohedoro-2",
"imagePreview": "https://cdn.animeflv.net/screenshots/3274/2/th_3.jpg"
},
{
"episode": 1,
"id": "53705/dorohedoro-1",
"imagePreview": "https://cdn.animeflv.net/screenshots/3274/1/th_3.jpg"
}
]
typeDefs
const resolvers = require('./resolvers');
const {gql} = require('apollo-server');
const typeDefs = gql `
extend type Query{
latest_anime: [Animes]
}
type Animes{
title: String
poster: String
synopsis: String
debut: String
type: String
rating: String
genres: [String]
episodes: [Episodes]
}
type Episodes{
nextEpisodeDate: String
episode: String
id: String
imagePreview: String
}
`
module.exports = {
typeDefs,
resolvers
};
Apollo Playground
query{
latest_anime{
title
poster
synopsis
debut
type
rating
genres
episodes{
nextEpisodeDate
episode
id
imagePreview
}
}
}
Apollo Playground Output
{
"data": {
"latest_anime": [
{
"title": "Tsugumomo OVA",
"poster": "https://animeflv.net/uploads/animes/covers/3275.jpg",
"synopsis": "OVA 4.6Kazuya Kagami nunca va a ningún lado sin su preciada “Sakura Obi” que su madre le regaló. Un día, una hermosa chica vestida con un kimono llamada Kiriha aparece ante él. Naturalmente, ella comienza a vivir en su habitación. ¿Naturalmente? ¡Esto solo es el inicio de la embarazosa y confusa...",
"debut": null,
"type": "OVA",
"rating": "4.6",
"genres": [
"accion",
"comedia",
"ecchi",
"escolares",
"seinen",
"sobrenatural"
],
"episodes": [
{
"nextEpisodeDate": null,
"episode": null,
"id": null,
"imagePreview": null
},
{
"nextEpisodeDate": null,
"episode": "1",
"id": "53753/tsugumomo-ova-1",
"imagePreview": "https://cdn.animeflv.net/screenshots/3275/1/th_3.jpg"
}
]
},
]
}
}
The only way you can get the desired response structure is to have two separate types. A field must have exactly one type, but you can use an abstract type like a union or interface in order to have each individual item in the list resolve to one of multiple types at runtime.
type AiredEpisode implements Episode {
id: String
episode: String
imagePreview: String
}
type UpcomingEpisode implements Episode {
id: String
nextEpisodeDate: String
}
interface Episode {
id: String
}
type Anime {
episodes: [Episode]
# other fields
}
You would then query the episodes like this:
query {
latest_anime {
episodes {
# fields on the interface itself like id are common to all
# implementing types so they don't need to be inside a fragment
id
# fields specific to one of the types need to be inside a fragment
... on UpcomingEpisode {
nextEpisodeDate
}
... on AiredEpisode {
id
episode
imagePreview
}
}
}
}
Side note: if your API doesn't return an id for the upcoming episodes, you should still provide one (you could use the show's id, for example, you just want to make sure it's unique). This will ensure that you don't run into caching issues if you use a client like Apollo on the front end.

Spring Data MongoDB Aggregation using project and $map

I want to know the SpringData equivalent code for the following native mongoDB query.
{
$project: {
_id: "$_id",
login: "$login",
firstName: "$firstName",
lastName: "$lastName",
email: "$email",
deactivateFlag: "$deactivateFlag",
createdOn: "$createdOn",
createdBy: {
"$map": {
"input": "$created_by",
"as": "u",
"in": {
"name": {
"$concat": ["$$u.firstName", " ", "$$u.lastName"]
},
}
}
}
}
}
This is an old question but if anyone else finds it on a Google search this is how to solve it. In fact you can do this for any complex mongodb query that Spring JPA can't handle out of the box.
You need to write a custom Document to inject into the projection. The example is in Kotlin but you'll get the idea for Java. I've not tested it properly but should get you what you want.
val mapExpression = BasicDBObject()
mapExpression["input"] = "\$created_by"
mapExpression["as"] = "u"
val concat = BasicDBObject("\$concat", listOf("\$\$u.firstName", " ", "\$\$u.lastName"))
mapExpression["in"] = BasicDBObject("name", concat)
val createdByFilter = Document("\$map", mapExpression)
project("_id", "login", "firstName", "lastName", "email", "deactivateFlag", "createdOn").and(createdByFilter).`as`("createdBy")

Resources