How to query nested objects from MongoDB using Spring Boot (REST) and TextQuery? - spring

I am writing RESTful API to search MongoDB collection named 'global' by criteria using TextQuery. My problem is that I cannot access nested object fields when doing query.
For example this works:
GET localhost:8080/search?criteria=name:'name'
And this does not:
GET localhost:8080/search?criteria=other.othername:'Other Name'
I have MongoDB json structure (imported from JSON into 'global' collection as whole nested objects)
[{
"name": "Name",
"desc": "Desc",
"other" {
"othername": "Other Name",
}
},
{
"name": "Name",
"desc": "Desc",
"other" {
"othername": "Other Name",
}
}
]
And classes (with getters & setters & etc):
#Document(collection="global")
public class Global{
#TextIndexed
String name;
#TextIndexed
String desc;
Other other;
...
}
public class Other{
String othername;
...
}
My controller has method
#GetMapping("/search")
public Iterable<Global> getByCriteria(#RequestParam("criteria") String criteria) {
...
}
And I am trying to write text search with
public Iterable<Global> findByCriteria(String criteria) {
TextCriteria criteria = TextCriteria.forDefaultLanguage().matching(criteria);
TextQuery query = TextQuery.queryText(criteria);
return mongoTemplate.find(query, Global.class);
}

You need to add #TextIndexed to your other field.
public class Global{
#TextIndexed
String name;
#TextIndexed
String desc;
#TextIndexed
Other other;
...
}
Note: All nested object fields are searchable
or you may add #TextIndexed for each nested object field:
public class Other {
#TextIndexed
String othername;
...
}

Related

How to use Jackson for parse object follow json type?

I have two Json objects like :
Object 1
{
"value": {
"data": [
"John",
"Justin",
"Tom"
],
"isGraduated": false
}
}
Object 2
{
"value": {
"data": {
"info": {
"background": {
"primarySchool" : "A school",
"univeristy": "X univeristy"
},
"name": "John",
"gender": "male",
"dayOfBirth": "1995-04-24"
}
},
"isGraduated": false
}
}
How can I deserialize the data field to list of strings or class(I've already declared) by using Jackson?
Edit
Add class Info declaration.
public class Info {
#JsonProperty("background")
private BackGround backGround;
#JsonProperty("name")
private String name;
#JsonProperty("gender")
private String gender;
#JsonProperty("dayOfBirth")
private String dayOfBirth;
public static class BackGround {
#JsonProperty("primarySchool")
private String primarySchool;
#JsonProperty("univeristy")
private String univeristy;
}
}
Looking at your JSON objects, there is no way you can figure out what will be there in data parameter. So you can use JsonNode as type for data parameter.
Note: This is the object hierarchy I have created to represent JSON objects
#ToString
class Wrapper {
private Value value;
// getter & setter
}
#ToString
class Value {
private JsonNode data;
private Boolean isGraduated;
// getter & setter
}
#ToString
class Data {
private Info info;
// getter & setter
}
#ToString
class Info {
private Background background;
private String name;
private String gender;
private String dayOfBirth;
// getter & setter
#ToString
static class Background {
private String primarySchool;
private String univeristy;
// getter & setter
}
}
Then you can check the node type before deserialize between List<String> and Info.calss like this,
JsonNodeType type = value.getValue().getData().getNodeType();
You will see type = JsonNodeType.ARRAY if the json object is type 1 and type = JsonNodeType.OBJECT if the json object is type 2.
Check this exaple,
public class Main {
public static void main(String[] args) throws IOException {
// String s = "{\"value\":{\"data\":[\"John\",\"Justin\",\"Tom\"],\"isGraduated\":false}}";
String s = "{\"value\":{\"data\":{\"info\":{\"background\":{\"primarySchool\":\"A school\",\"univeristy\":\"X univeristy\"},\"name\":\"John\",\"gender\":\"male\",\"dayOfBirth\":\"1995-04-24\"}},\"isGraduated\":false}}";
ObjectMapper om = new ObjectMapper();
Wrapper wrapper = om.readValue(s, Wrapper.class);
JsonNodeType type = wrapper.getValue().getData().getNodeType();
if (type == JsonNodeType.ARRAY) {
List<String> data = om.convertValue(wrapper.getValue().getData(), new TypeReference<List<String>>() {});
System.out.println(data);
} else if (type == JsonNodeType.OBJECT) {
Data data = om.convertValue(wrapper.getValue().getData(), Data.class);
System.out.println(data);
}
}
}
Not the general approach but approach for your specific case
ObjectMapper mapper = new ObjectMapper();
ObjectNode root = (ObjectNode) mapper.readTree(jsonContent);
JsonNode data = root.get("value").get("data");
if (data.has("info")) {
Info result = mapper.convertValue(data.get("info"), Info.class);
// handle result as Info instance
} else {
List<String> result = mapper.convertValue(data, new TypeReference<List<String>>() {});
// handle result as list of strings
}

Order results based on count using spring boot specification API

Consider the entity below.
PS: The model has more fields but for the question to be short I have posted only the relevant fields
Class Employee {
private String name;
private String country;
private String region;
private String department
#OneToMany
private Set<Skill> skills;
}
Class Skill {
private name;
}
I am using spring boot Specification API to filter employees on different fields like region, country, and so on.
public class EmployeeSpec implements Specification<Employee> {
#Override
public Predicate toPredicate(Root<Employee> root, CriteriaQuery<?> cq, CriteriaBuilder cb) {
String fielName = //some field name
String fieldValue = //some field value
switch (fielName ) {
case "country":
return cb.equal(root.get("country"), fieldValue);
case "department":
return cb.equal(root.get("department"), fieldValue);
case "region":
return cb.equal(root.get("region"), fieldValue);
}
}
I want to order the results such that employee with maximum skills comes first. I am not sure how to implement this using Specification.
You can use CriteriaBuilder.size(..). For your case, the code will look like:
cq.orderBy(cb.desc(cb.size(root.get("skills"))));

Find all embedded documents from manual reference in MongoDB

I use MongoDB and Spring Boot in a project. I used manual reference to point out a collection, My structure is as follows:
Reel collection
{
_id : "reel_id_1",
name: "reel 1",
category :[
{
_id : "category_id_1",
name: "category 1",
videos: ["video_id_1","video_id_2"]
}
]
}
Video collection
{
_id: "video_id_1", // first document
name: "mongo"
}
{
_id: "video_id_2", // seconddocument
name: "java"
}
Java classes are
#Document
#Data
public class Reel {
#Id
private ObjectId _id;
private String name;
List<Category> category;
}
#Data
public class Category {
#Id
private ObjectId _id=new ObjectId();
private String name;
Video videos;
}
#Document
#Data
public class Video {
#Id
private ObjectId _id = new ObjectId();
private String name;
}
I tried to join both document via mongoTemplate
public List<Reel> findById(ObjectId _id) {
LookupOperation lookupOperation = LookupOperation.newLookup()
.from("video")
.localField("category.videos")
.foreignField("_id")
.as("category.videos");
UnwindOperation unwindOperation = Aggregation.unwind("category");
Aggregation agg = newAggregation(unwindOperation,match(Criteria.where("_id").is(_id)),lookupOperation);
Aggregation aggregation = newAggregation(lookupOperation);
List<Reel> results = mongoTemplate.aggregate(aggregation, "reel", Reel.class).getMappedResults();
return results;
}
But it throws an error.
Failed to instantiate java.util.List using constructor NO_CONSTRUCTOR with arguments
But since I use "unwind", I created a new Entity UnwindReel and add Category category instead of List<Category> category. And used
List<UnwindReel> results = mongoTemplate.aggregate(aggregation, "reel", UnwindReel.class).getMappedResults();
It combines only first video (video_id_1) object. How can I get all objects inside videos array? Is there any method to fetch?
Your JSON stored in database has wrong structure. Your Reel class expects list of Category, but in database you have stored as nested object.
You need to add this stage just after $lookup
{
"$addFields": {
"category": {
"$map": {
"input": "$category.videos",
"in": {
"videos": "$$this"
}
}
}
}
}
Java code
public List<Reel> findById(String _id) {
Aggregation aggregation = Aggregation.newAggregation(
Aggregation.match(Criteria.where("_id").is(_id)),
Aggregation.lookup(mongoTemplate.getCollectionName(Video.class), "category.videos", "_id", "category.videos"),
new AggregationOperation() {
#Override
public Document toDocument(AggregationOperationContext context) {
return new Document("$addFields",
new Document("category", new Document("$map", new Document("input", "$category.videos")
.append("in", new Document("videos", "$$this")))));
}
})
.withOptions(AggregationOptions.builder().allowDiskUse(Boolean.TRUE).build());
LOG.debug(
aggregation.toString().replaceAll("__collection__", mongoTemplate.getCollectionName(Reel.class)));
return mongoTemplate.aggregate(aggregation, mongoTemplate.getCollectionName(Reel.class), Reel.class)
.getMappedResults();
}
Recomendations
Do not hard-code collection name, use better mongoTemplate.getCollectionName method
Always log aggregation pipeline before performing, it helps debugging.
If your collection will grow in the future, use {allowDiskUse: true} MongoDb aggregation option.

how to serialize multi-level json using jackson in spring?

This is the response body that is expected out the endpoint /foo/bar.
As you can see, this is nested json and hence for each level do I require a Java object. what is the efficient way to construct this?. I am using Spring 4.1 which in turn uses Jackson to serialize POJOs.
{
"user": {
"status": {
"state": "active",
"message": [
"registered on 01.10.2015"
]
}
},
"features": {
"xbox": {
"state": "enabled",
"message": [
"foo",
"bar"
]
},
"playstation": {
"state": "disabled",
"message": [
"hello ",
"world"
]
}
}
}
Here is something I have thought
#RequestMapping("foo/bar")
#ResponseBody
public Result getData(long id){
Result result = getUserData(id);
return result;
}
public class Result {
private User user;
private List<Feature> features;
//getters and setters
}
public class Status {
private State state;
private Message messages;
//getters and setteers
}
public class State {
private String state;
//getters and setters
}
public class Message {
private List<String> messages;
//getters and setters
}
Similarly for "features" node, I will construct Java POJO for each level of Json object. I am aware of using objectMapper and JsonNode but wondering if it is possible to effectively construct nested json using java objects.
Classes like State could be an enum, if you know for sure there will only be a limited domain of values. And Message shouldn't be a separate class- just embed List<String> message directly in Status.
If you want a more compact representation, you can always provide your own serializer/deserializer classes to customise the translation.

Error during use $within operator with $elemMatch

I'm using spring-data-mongodb (version 1.0.2.RELEASE) and mongodb (version 2.2).
I have an object A that contain a list of object Location. The classes are the following:
public class A {
#Id
private ObjectId id;
private List<Location> places;
//GETTER AND SETTER
}
public class Place {
private String name;
private String description;
#GeoSpatialIndexed
private double[] location;
//GETTER AND SETTER
}
I need to find all objects A with a specific location.
I tried to use together the operators $within and $elemMatch as following:
#Query(value = "{'places' : { $elemMatch: { location: {'$within' : {'$center' : [?0, ?1]} } }}}")
public List<A> findByLocation(Point location, double radius);
When i run this query, i receive the following exception:
org.springframework.data.mongodb.UncategorizedMongoDbException: can't find special index: 2d for: { places: { $elemMatch: { location: { $within: { $center: [ { x: 41.904159, y: 12.549132 }, 0.07000000000000001 ] } } } } }; nested exception is com.mongodb.MongoException: can't find special index: 2d for: { places: { $elemMatch: { location: { $within: { $center: [ { x: 41.904159, y: 12.549132 }, 0.07000000000000001 ] } } } } }
Any suggestions?
Regards
Apparently there's no 2d index on the location attribute, and MongoDB requires such an index to be present. I don't know if Spring should be creating that index by itself, but if it doesn't, you can create the index via
db.collection.ensureIndex({"location":"2d"})
where collection is replaced by the name of your collection.
I solved my problem changing A class. I replaced the list of Place with a list of org.springframework.data.mongodb.core.geo.Point as follow:
public class A {
#Id
private ObjectId id;
#GeoSpatialIndexed
private List<Point> places;
//GETTER AND SETTER
}
And i replaced old query with a new one as follow:
#Query(value = "{'places' : {'$within' : {'$center' : [?0, ?1]} } }")
public List<A> findByLocation(Point location, double radius);
And now it's work!
Thanks to all!

Resources