I am new to spring boot technology.
I have a problem, how to map many to many relationships using spring boot when passed the objectId of another collection and retrieve the details of that object too.
Ex:
There are two models named Vehicles & Categories. When creating a new vehicle type we can pass the Category Id like below.
When retrieving the Vehicle should display like this.
This example developed using node.js and express.js.
Like that way how I can do that for spring boot. I tried with #JoinColumns, #DBRef and none of them doesn't work for me.
Here are my models.
Vehicle model
#Document(collection = "Vehicle")
public class Vehicle {
#Id
private String id;
private String name;
private String type;
private List<Category> categoryList;
//getters and setters go here
Category model
#Document(collection = "Category")
public class Category {
#Id
private String id;
private String name;
private Double amount;
//getters and setters go here
It's better if you can tell me the way of implementing the Controller and Service or Repository according to this.
Thanks in advance.
can't help you with spring-boot stuff but you need to do a $lookup operation to join the foreign documents using the mongodb driver you're using.
the following is how it's done using mongo shell:
db.Vehicle.aggregate(
[
{ $match: { _id: "vehicle-id" } },
{
$lookup: {
from: "Category",
localField: "categories._id",
foreignField: "_id",
as: "categories"
}
}
])
see it in action here: https://mongoplayground.net/p/OgXrAxE2uXu
ගුඩ් ලක්!
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]}}}]);
A classic DDD rule is that aggregates should reference each other via their ID, and that the ID of an aggregate should be a value object. So something like this (in Java):
class MyAggregate {
private MyAggregateId id;
private String someOtherField;
...
}
class MyAggregateId {
private UUID id;
}
How can i make MongoDB use MyAggregateId instead of the autogenerated ObjectID? Is that possible?
Bonus points if the answer involves doing this with spring-boot, where i hoped i could just declare public interface MyAggregateRepository extends MongoRepository<MyAggregate, MyAggregateId> {} but that does not seem to work.
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 need help to get the data from another document I have the following class.
#Data
#Document(collection = "tmVersion")
public class TmVersion {
#Id
private String id;
private String cVrVersionId;
#DBRef
private TaApplicationVersion taApplicationVersion;
}
and
#Data
#Document(collection = "taApplicationVersion")
public class TaApplicationVersion {
#Id
private String id;
private String dVrAppName;
private String dVrAppCode;
}
This is my repository in which I map what I want to be shown but in taApplicationVersion I need to show all this object also how is it done?
#Query(value="{}", fields="{'cVrVersionId': 1, 'taApplicationVersion.dVrAppName': 2,
'dVrVersionNumber': 3}")
Page<TmVersion> getAllVersionWithOutFile(Pageable pageable)
Couple of things to mention here.
If you want this kind of join between tables, then you need to rethink your choice of Mongodb as database. No Sql Databases thrive on the fact that there is very less coupling between tables(collections). So if you are using #DBRef, it negates that. Mongodb themselves do not recommend using #DBRef.
This cannot be achieved with the method like you have in the repository. You need to use Projections. Here is the documentation for that.
Create a Porjection interface like this. Here you can control which fields you need to include in the Main class(TmVersion)
#ProjectedPayload
public interface TmVersionProjection {
#Value("#{#taApplicationVersionRepository.findById(target.taApplicationVersion.id)}")
public TaApplicationVersion getTaApplicationVersion();
public String getId();
public String getcVrVersionId();
}
Change the TmVersionRepository like this
public interface TmVersionRepository extends MongoRepository<TmVersion, String> {
#Query(value="{}")
Page<TmVersionProjection> getAllVersionWithOutFile(Pageable pageable);
}
Create a new Repository for TaApplicationVersion. You can add #Query on top of this method and control which fields from subclass needs to be returned.
public interface TaApplicationVersionRepository extends MongoRepository<TaApplicationVersion, String> {
TaApplicationVersion findById(String id);
}
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>. ;)