I have two objects and two projection objects like below,
Main Objects
public class MainItem {
private String name;
private List<subItem> subItems;
}
public class subItem {
private String id;
private String groupId;
private String displayName;
private Status status;
}
Projection objects
public class MainItemLight {
private String name;
private List<subItemLight> subItemList;
}
public class subItemLight {
private String id;
private String name;
}
I'm trying to map Main objects to projection objects and return a list of MainItemLight objects. Below is my code,
mongoTemplate.aggregate(
newAggregation(project("name")
.and("subItems").as("subItemList")
.and("subItems.displayName").as("subItemList.name")
),
"MyCollection", MainItemLight.class).getMappedResults();
When I'm trying to map subItems.displayName to subItemList.name I get bellow exception,
Command failed with error 40176 (Location40176): 'Invalid $project
:: caused by :: specification contains two conflicting paths.
Cannot specify both 'subItemList.name' and 'subItemList'
Any idea how to fix this?
You need to do it like this:
db.collection.aggregate([
{
$project: {
name: 1,
subItemList: {
$map: {
input: "$subItems",
as: "item",
in: {
id: "$$item.id",
name: "$$item.displayName"
}
}
}
}
}
])
MongoPlayground
MongoTemplate
Aggregation agg = newAggregation(project("name").and(
VariableOperators.Map.itemsOf("subItems").as("item").andApply(
doc -> new Document()
.append("id", "$$item.id")
.append("name", "$$this.displayName")
)
).as("subItemList"));
Note: The $map implementation is not friendly in Spring Mongo, so wee need to implement it manually.
Source: SpringData mongoDB API for Aggregation $map
Related
I am trying to get count of likes and if user is liked this post in Mongo.
I managed to get this via native query with facets, but problems is how can i map this two fields on my custom java class (LikeStatus.class)?
thanks in advance!
please code below:
POJO:
public class LikeStatus {
String entityId;
LikedEntityType entityType;
long likesCount;
boolean isLikedByUser;
}
Document class:
public class Like {
#Id
private String id;
#EqualsAndHashCode.Include
private String entityId;
#EqualsAndHashCode.Include
private String profileId;
#EqualsAndHashCode.Include
private LikedEntityType entityType;
private LocalDateTime createdAt = LocalDateTime.now();
}
Query i used in Mongo:
> db.likes.aggregate({$facet:
{count:[
{$match:{entityId:"entityId"},
$match:{entityType:"OFFER"}}, {$count:"count"}],
isliked:[{$match:{profileId:"profileId4"}}, {$count:"isliked"}]}}).pretty();
and gives me result:
{
"count" : [
{
"count" : 3
}
],
"isliked" : [
{
"isliked" : 1
}
]
}
I managed to find solution which is suited my needs, hope it will be useful who faced with the same kind of queries in Mongodb and it will give some idea how it can be solved)
Java solution: i used Facet object to collect two aggregation request in one query like this:
In repository layer i created query:
default Aggregation getLikeStatus(String entityId, String entityType, String profileId){
FacetOperation facet = Aggregation.facet(match(where(ENTITY_ID_FIELD).is(entityId).and(ENTITY_TYPE_FIELD).is(entityType)),
Aggregation.count().as(LIKES_COUNT_FIELD)).as(LIKES_COUNT_FIELD)
.and(match(where(ENTITY_ID_FIELD).is(entityId)
.and(ENTITY_TYPE_FIELD).is(entityType)
.and(PROFILE_ID_FIELD).is(profileId)),
Aggregation.count().as(IS_LIKED_BY_USER_FIELD)).as(IS_LIKED_BY_USER_FIELD);
ProjectionOperation project = project()
.and(ConditionalOperators.ifNull(ArrayOperators.ArrayElemAt.arrayOf(LIKES_COUNT_FIELD).elementAt(0)).then(0)).as(LIKES_COUNT_FIELD)
.and(ConditionalOperators.ifNull(ArrayOperators.ArrayElemAt.arrayOf(IS_LIKED_BY_USER_FIELD).elementAt(0)).then(0)).as(IS_LIKED_BY_USER_FIELD)
.andExclude("_id");
return newAggregation(facet, project);
}
then in service layer it returns Document object which is mapped on my custom class LikeStatus fields:
Document status = template.aggregate(likeRepo.getLikeStatus(entityId, entityType, profileId), Like.class, Document.class).getUniqueMappedResult();
my custom POJO:
#Data
#NoArgsConstructor
#AllArgsConstructor
public class LikeStatus {
String entityId;
LikedEntityType entityType;
long likesCount;
boolean isLikedByUser;
}
Also i post native query solution in Mongo for reference:
db.likes.aggregate([
{$facet:
{"likesCountGroup":[
{$match:{entityId:"entityId", entityType:"TYPE"}},{$count:"likesCount"}
],
"isUserLikedGroup":[
{$match:{entityId:"entityId", entityType:"TYPE", profileId:"604cd12c-1633-4661-a773-792a6ec22187"}},
{$count:"isUserLiked"}
]}},
{$addFields:{}},
{$project:{"likes":{"$ifNull":[
{$arrayElemAt:["$likesCountGroup.likesCount", 0]},0]},
"isUser":{"$ifNull:[{$arrayElemAt["$isUserLikedGroup.isUserLiked",0]},0]}}}]);
I'm new to Spring-boot and MongoDB. In MongoDB a manual reference between two collections works fine. The mapping in Spring-boot seems not to work. I really don't know what else to check.Below all the relevant details, sorry for the long question.
The reason not to use DBref is because I might need the projections.
The "players" collection has this schema(any other not allowed)
{"_id":{"$oid":"5f56021d61738cc35de79438"},
"name":"Romeo",
"entryDate":{"$date":"2020-08-23T22:00:00.000Z"}}`
The "games" collection has the following schema
{
"_id":{"$oid":"5f5614a361738cc35de7943b"},
"dices":{
"value1":1,
"value2":6
},
"gameScore":1,
"player_id":{"$oid":"5f56021d61738cc35de79438"}
}
The aggregation in MongoDB Compass
[{
$match: {
_id: ObjectId('5f56021d61738cc35de79438')
}
}, {
$lookup: {
from: 'games',
localField: '_id',
foreignField: 'player_id',
as: 'games'
}
}]
yields
In Spring-boot the POJOs are:
#Document(collection = "players")
public class Player {
#Id
private String id;
private String name;
private LocalDate entryDate= LocalDate.now();
private List<Game> game;
public Player(){};
public Player(String name) {
this.name = name;
}
//getters and setters for all properties, including game
}
#Document(collection = "games")
public class Game {
#Id
private String id;
private Dices dices;
private Integer gameScore;
#Field(value = "player_id")
private String playerId;
public Game(){};
public Game(Dices dices) {
this.dices = dices;
}
//getters and setters for all properties
}
public class Dices {
private int value1;
private int value2;
public Dices(){}
public Dices(int value1, int value2) {
this.value1 = value1;
this.value2 = value2;
}
//getters and setters for both properties
In Postman
GET findAll players shows:
[{"id":"5f56021d61738cc35de79438","name":"Romeo","entryDate":[2020,8,24],"game":null},{"id":"5f5602e361738cc35de79439","name":"Julieta","entryDate":[2020,8,24],"game":null},
....]
game is shown because I added also getters and setters for this property, just trying to find the way to properly mapping the games as manual references to players
GET findAll games:
[{"id":"5f5614a361738cc35de7943b","dices":{"value1":1,"value2":6},"gameScore":1,"playerId":"5f56021d61738cc35de79438"},
{"id":"5f5619f561738cc35de7943c","dices":{"value1":2,"value2":5},"gameScore":1,"playerId":"5f5602e361738cc35de79439"},
{"id":"5f561a5461738cc35de7943d","dices":{"value1":3,"value2":3},"gameScore":0,"playerId":"5f56021d61738cc35de79438"},
...]
GET lh:8080/players/5f56021d61738cc35de79438/games
yields an empty array, this is why I assume that the mapping between the collections in Spring-boot fails.
The GamesRepository
#Repository
public interface GameRepository extends MongoRepository<Game, String> {
List<Game> findAll();
List<Game> findGamesByPlayerId(String playerId);
}
The method in the service
#Override
public List<Game> findAllGamesByPlayerId(String playerId) {
Optional<Player> playerDB= playerRepository.findById(playerId);
if(playerDB.isPresent()) {
return gameRepository.findGamesByPlayerId(playerId);
}
else throw new ResourceNotFoundException("Player with id: "+playerId+" does not exist");
}
and the GameController
#GetMapping("/{ID}/games")
public ResponseEntity<List<Game>> getAllGamesByPlayerId (#PathVariable("ID") String playerId){
return ResponseEntity.ok()
.body(gameService.findAllGamesByPlayerId(playerId));
}
Tips are welcome!
Aggregations don't work with MongoRepostory unless you use #DBRef. But using #DBRef is not recommended. What you did in aggregation can be converted into Aggregation pipeline of Spring data.
For that you need to autowired the MongoTemplate
#Autowired
MongoTemplate mongoTemplate;
Then you can convert the aggregation you have written. I haven't tested it, since your aggregation is working, this should work.
public List<Object> test(ObjectId id){
Aggregation aggregation = Aggregation.newAggregation(
match(Criteria.where("_id").is(id)),
lookup("games","_id","player_id","games")
).withOptions(AggregationOptions.builder().allowDiskUse(Boolean.TRUE).build());
return mongoTemplate.aggregate(aggregation, mongoTemplate.getCollectionName(Players.class), Object.class).getMappedResults();
}
I am integrating the Spring-Data-mongo with query-dsl, so i need to Generate Q-files for query-dsl queries
Here is my Order POJO:
public class Order {
private List<Map<String,Object>> items;
}
My Q file of Order.java
public class QOrder extends EntityPathBase<Order> {
private static final long serialVersionUID = -682690651L;
public static final QOrder order = new QOrder("order");
public final ListPath<java.util.Map<String, Object>, SimplePath<java.util.Map<String, Object>>> items = this.<java.util.Map<String, Object>, SimplePath<java.util.Map<String, Object>>>createList("items", java.util.Map.class, SimplePath.class, PathInits.DIRECT2);
public QOrder(String variable) {
super(Order.class, forVariable(variable));
}
public QOrder(Path<? extends Order> path) {
super(path.getType(), path.getMetadata());
}
public QOrder(PathMetadata metadata) {
super(Order.class, metadata);
}
}
and sample json's of order is
{
"items": [{
"itemName": "phone",
"quantity": <Integer-Number>
}
]
}
Now i want to retrieve all the order's from mongo for which any Item exist with qunatity 1.
Now i am generating my predicate like below.
"QSensorData.sensorData.data.any().eq(Some-QueryDSL-Expression)".
I am unable to identify what need to pass in eq method for filtering nested map values.
Change Order class to include List attribute where Item contains itemName and quantity fields. Something like
public class Order {
private List<Item> items;
}
public class Item {
private String itemName;
private Integer quantity;
}
Generate Q classes.
Use below query to return all items where there is atleast one item with quantity with 1.
BooleanExpression expression = QOrder.order.items.any().quantity.eq(1);
List<Order> results = repository.findAll(expression);
As noted in the comment to return all filtered items with value 1 you have to use aggregation query.
Something like
Static Imports
import static org.springframework.data.mongodb.core.aggregation.Aggregation.*;
import static org.springframework.data.mongodb.core.aggregation.ArrayOperators.Filter.filter;
import static org.springframework.data.mongodb.core.aggregation.ComparisonOperators.Eq.valueOf;
Aggregation Query
Aggregation aggregation = newAggregation(
project().and(filter("items")
.as("item")
.by(valueOf(
"item.quantity")
.equalToValue(
1)))
.as("items");
);
List<Order> results = mongoTemplate.aggregate(aggregation, Order.class, Order.class)
I've been reading a lot that the use of DBRef for Collection Mapping in Spring Data/MongoDB is discouraged. So, how can I implement a mapping that stores an array of ObjectId taken from those objects inside the students collection?
Assuming that I have the following POJO model:
#Document (collection = "courses")
public class Course {
#Id
private String id;
private String name;
private List<Student> students = new LinkedList<Student>();
//.. constructors, getters and setters ..
}
public interface CourseRepository extends MongoRepository<Course, String> { }
The result should be something like this:
courses
{
_id : ObjectId("foo"),
_class: "model.Course",
name: "MongoDB for Dummies",
students: [ ObjectId("foo2"), ObjectId("foo3"), ... ]
}
Instead of this:
courses
{
_id : ObjectId("foo"),
_class: "model.Course",
name: "MongoDB for Dummies",
students: [
DBRef("student", ObjectId("foo2")),
DBRef("student", ObjectId("foo3"))
]
}
Thanks!
You might wanna try the obvious and change students to a List<ObjectId>. ;)
I have the following Play Framework entity (using Morphia for persistence) as part of a generic blogging app:
#Entity
public class Comment extends Model {
...
#Reference
#Indexed
public SiteUser commenter;
public static List<Comment> getLastCommentsByUsers(final List<SiteUser> users) {
final Query<Comment> query ds().createQuery(Comment.class);
query.field(commenter).hasAnyOf(users);
return query.asList();
}
}
SiteUser:
#Entity(noClassnameStored=true)
public class SiteUser extends AbstractUser {
public String realName;
}
AbstractUser:
public class AbstractUser extends Model {
#Indexed(value= IndexDirection.DESC, unique = true)
public String emailAddress;
#Required
public String password;
}
The method getLastCommentsByUsers() is supposed to return all comments by the users in the users parameter, but I always get an empty List back. The reason that Commment is a separate collection is to be able to retrieve last X Comments by certain users across their associated Posts, which isn't possible if the Comment is embedded in the Post collection.
Is there something wrong with my query (should I be using something other than hasAnyOf), or is it a problem with the relationship mapping - should I be using ObjectId instead?
I use the in() method with a list or set and its working perfectly. Here's a snippet:
List<String> keywordList;
List<Product> products = Product.find().field("keywords").in(keywordList).asList();
This should work for collection of embedded or references too.
You should use List<Key<SiteUser>> to query:
public static List<Comment> getLastCommentsByUsers(final List<SiteUser> users) {
final Query<Comment> query ds().createQuery(Comment.class);
query.field(commenter).hasAnyOf(toKeys(users)); // convert to keys
return query.asList();
}
public static List<Key<SiteUser>> toKeys(List<SiteUser> users) {
List<Key<SiteUser>> keys = new ArrayList<Key<SiteUser>>();
for(SiteUser user: users) {
keys.add(ds().getMapper().getKey(user));
}
return keys;
}
Or you can just get the keys by:
List<Key<SiteUser>> keys = ds().createQuery(SiteUser.class).query().filter(...).asKeyList();