Spring Data Mongo Custom Repository Query with ObjectID - java

I have a mongo query I want to implement as a Spring Mongo Repository
db.collection.find({ 'items':
{ $elemMatch: {
'refund.$id' : ObjectId('5638cab2e4b07ff212618d7e')
}
}
})
My Repository interface is
#Query("{ 'items': { $elemMatch: { 'refund.$id' : ObjectId(?0) } } }")
RMA findRMAByItemRefund(String refundId);
It throws JSONParseException
Caused by: com.mongodb.util.JSONParseException:
{ 'items': { $elemMatch: { 'refund.$id' : ObjectId("_param_0") } } }
^
at com.mongodb.util.JSONParser.parse(JSON.java:216)

As the accepted solution did not yield any results in my case, I had to find another solution, that was:
Instead of using the autogenerated query functionality defined via #Query(...) I opted for manually building the DBObject used to query mongo, and defining the functionality as a default implementation in the interface, thus keeping it "clean"
(" - because had to introduce a _query method)
Note: This solution only works in Java 1.8+
public interface SomeRepository extends MongoRepository<SomeEntity, String> {
#Query("{'nestedEntity._id': ?0}")
SomeEntity findByNestedEntityId_DoesntWork(String nestedEntityId);
#Query("?0")
SomeEntity _query(DBObject query);
default SomeEntity findByNestedEntityId(String nestedEntityId) {
DBObject queryObject = BasicDBObjectBuilder
.start("nestedEntity._id", new ObjectId(nestedEntityId))
.get();
return this._query(queryObject);
}
}

Try this
#Query("{ 'items': { $elemMatch: { 'refund.id' : ?0 } } }")
RMA findRMAByItemRefund(String refundId);
From my experience, ?0 should standalone and not used as a mongo function parameter.
Also, is $id your own assumption or the field is actually stored as $id. If not, I will go with refund.id

I guess my problem was a little different, but since I couldn't find answer anywhere, I believe it's worth mentioning.
Basically I wanted to do the search by ObjectId AND userId, so here's what I've done:
#Query("{ '_id': ?0, 'userId': ?1 }")
T findByObjectIdAndUserId(final ObjectId objectId, final Long userId);

This is the example object on mongo document:
{
"_id" : ObjectId("5c052a43f14008522c1c90c8"),
"_class" : "com.gdn.payment.report.entity.ReportDocument",
"status" : "DONE",
"report" : {
"id" : ObjectId("5c0508bcf14008460015eb07"),
"startDate" : NumberLong(1542795843188),
"endDate" : NumberLong(1543568857157),
"fields" : [
"customerInfo.billingAddress.streetAddress",
"customerInfo.name",
"items[*].name",
"orderId",
"type"
]
}
}
You can make the repository method for searching report.id only by using ObjectId data type, I've tried using just string and it cannot work, here are my repository method:
Page<ReportDocument> findByReport_id(ObjectId reportId, Pageable pageable)
for creating ObjectId you can do it using:
new ObjectId({input-reportId-string})

I'd suggest using Spring Data Derived Queries. You can also use QueryDsl on top.
Then you'd be able to write much more complex queries without having to deal with secondary issues such as query syntax and parsing.

Related

Spring data Elasticsearch | Full text field search by repository

I am using Elastic search repository as per https://docs.spring.io/spring-data/elasticsearch/docs/current/reference/html/#elasticsearch.repositories only to read an existing indexed data.
I have an analysed field let's say fullName, for which I am creating s search method in repository as follows:
Person.Java
class Person{
#Field("ID")
#Id
long _id;
#Field(value = "FULL_NAME", type = FieldType.Text)
String fullName;
}
Repository is as:
#Repository
public interface PersonDataRepository extends ElasticsearchRepository<Person, Long> {
//this does't work
List<Person> findAllByFullNameIn(List<String> fullNames);
//this works
List<Person> findAllByFullName(String fullName);
}
Since the field is analysed, PersonDataRepository.findAllByFullNameIn(Stream.of("ABC").collect(Colelctors.toList())) doesn't produce any results, while PersonDataRepository.findAllByFullName("ABC") works well.
I found out that this is due to the analysed String field and If I switch to keyword, it should work.
Anybody knows a way around this using Spring data elasticsearch?
Versions:
Springboot - 2.3.1.RELEASE
Spring Data Elasticsearch: 4.0.1.RELEASE
ElasticSearch - 7.6.2
This was a bug and was recently fixed. It will be contained in versions 4.0.4 and 4.1.RC1
Edit: Both of these versions are released now
Solved it by writing a manual query using #Query annotation.
Passed searchquery using keyword instead of complete name as:
{
"query":{
"bool":{
"must":[
{
"bool":{
"must":[
{
"terms":{
"FULL_NAME.keyword":[
"ABC"
]
}
}
]
}
}
]
}
},
"version":true
}

Mongo repository findByDateBefore doesn't return results

I want to query a mongo collection to fetch documents created before a given date. I use the MongoRepository query findByDateBefore(). But turns out, it doesn't return any records. Here's my code implementation (pseudo):
MyDocument model
#Document(collection = "mydocument")
public class MyDocument {
#Id
String name;
Date creationDate;
}
The repository:
#Repository
public interface MyDocumentRepository extends MongoRepository<MyDocument, String> {
List<MyDocument> findByCreationDateBefore(Date date);
// #Query("{'creationDate': {"$lt": ?0}}")
// List<MyDocument> findbyCreationDateBefore(Date date)
}
Service code:
java.util.Date today = new Date();
List<MyDocument> documents = myDocumentRepository.findByCreationDateBefore(today);
The query logged after the service call:
MongoTemplate: find using query: { "creationDate" : { "$lt" : { "$date" : "2020-03-12T13:17:23.784Z"}}} fields: null for class: class com.myrepo.model.MyDocument in collection: mydocument
This return no results. I have tried using custom query (commented code in repository) which also doesn't return any records. Also I tried running the logged query in the mongo console and it does not return any results:
{ "creationDate" : { "$lt" : { "$date" : "2020-03-12T13:17:23.784Z"}}}
But when I use new Date, in mongo console, it works fine. e.g.:
{ "creationDate" : { "$lt" : new Date("2020-03-12T13:17:23.784Z")}}.
Solutions mentioned here doesn't work for me.

How to aggregate in spring data mongo db a nested object and avoid a PropertyReferenceException?

I am creating a new endpoint in springboot that will return simple stats on users generated from an aggregate query in a mongo database. However I get a PropertyReferenceException. I have read multiple stackoverflow questions about it, but didn't find one that solved this problem.
We have a mongo data scheme like this:
{
"_id" : ObjectId("5d795993288c3831c8dffe60"),
"user" : "000001",
"name" : "test",
"attributes" : {
"brand" : "Chrome",
"language" : "English" }
}
The database is filled with multiple users and we want using Springboot aggregate the stats of users per brand. There could be any number of attributes in the attributes object.
Here is the aggregation we are doing
Aggregation agg = newAggregation(
group("attributes.brand").count().as("number"),
project("number").and("type").previousOperation()
);
AggregationResults<Stats> groupResults
= mongoTemplate.aggregate(agg, Profile.class, Stats.class);
return groupResults.getMappedResults();
Which produces this mongo query which works:
> db.collection.aggregate([
{ "$group" : { "_id" : "$attributes.brand" , "number" : { "$sum" : 1}}} ,
{ "$project" : { "number" : 1 , "_id" : 0 , "type" : "$_id"}} ])
{ "number" : 4, "type" : "Chrome" }
{ "number" : 2, "type" : "Firefox" }
However when running a simple integration test we get this error:
org.springframework.data.mapping.PropertyReferenceException: No property brand found for type String! Traversed path: Profile.attributes.
From what I understand, it seems that since attributes is a Map<String, String> there might be a schematic problem. And in the mean time I can't modify the Profile object.
Is there something I am missing in the aggregation, or anything I could change in my Stats object?
For reference, here are the data models we're using, to work with JSON and jackson.
The Stats data model:
#Document
public class Stats {
#JsonProperty
private String type;
#JsonProperty
private int number;
public Stats() {}
/* ... */
}
The Profile data model:
#Document
public class Profiles {
#NotNull
#JsonProperty
private String user;
#NotNull
#JsonProperty
private String name;
#JsonProperty
private Map<String, String> attributes = new HashMap<>();
public Stats() {}
/* ... */
}
I found a solution, which was a combination of two problems:
The PropertyReferenceException was indeed caused because attributes is a Map<String, String> which means there is no schemes for Mongo.
The error message No property brand found for type String! Traversed path: Profile.attributes. means that the Map object doesn't have a brand property in it.
In order to fix that without touching my orginal Profile class, I had to create a new custom class which would map the attributes to an attributes object having the properties I want to aggreate on like:
public class StatsAttributes {
#JsonProperty
private String brand;
#JsonProperty
private String language;
public StatsAttributes() {}
/* ... */
}
Then I created a custom StatsProfile which would leverage my StatsAttributes and would be similar to the the original Profile object without modifying it.
#Document
public class StatsProfile {
#JsonProperty
private String user;
#JsonProperty
private StatsAttributes attributes;
public StatsProfile() {}
/* ... */
}
With that I made disapear my problem with the PropertyReferenceException using my new class StatsAggregation in the aggregation:
AggregationResults<Stats> groupResults
= mongoTemplate.aggregate(agg, StatsProfile.class, Stats.class);
However I would not get any results. It seems the query would not find any document in the database. That's where I realied that production mongo objects had the field "_class: com.company.dao.model.Profile" which was tied to the Profile object.
After some research, for the new StatsProfile to work it would need to be a #TypeAlias("Profile"). After looking around, I found that I also needed to precise a collection name which would lead to:
#Document(collection = "profile")
#TypeAlias("Profile")
public class StatsProfile {
/* ... */
}
And with all that, finally it worked!
I suppose that's not the prettiest solution, I wish I would not need to create a new Profile object and just consider the attributes as a StatsAttributes.class somehow in the mongoTemplate query. If anyone knows how to, please share 🙏

spring data mongo geospatial query

I have been trying to use a geospatial query to fetch data into a pojo with no success.
Here is an example data in my monogdb collection
{
"_id" : ObjectId("597b8c9a21871eeabd5a1cf5"),
"amb_id" : 10,
"amb_number" : "KL25 8945",
"driver_name" : "Amal Shaji",
"driver_licence_id" : "12/4562/2017",
"ownership" : "Amrita Institute of Medical Science",
"ownership_address" : "Peeliyadu Road, Ponekkara, Edappally, Ernakulam",
"location" : {
"type" : "Point",
"coordinates" : [
76.293485,
10.032871
]
}
}
The below mongo query works perfectly fine in the mongoshell
db.trial.find(
{ location :
{ $near :{
$geometry :{
type : "Point" ,
coordinates : [ 76.2 , 9.9 ] } ,
$maxDistance : 20000 }
}
}
)
.pretty();
Here is the pojo that I have been trying to fetch the data into
#Document(collection = "trial")
public class Ambulance {
#Id
String id;
#Field("amb_id")
String ambulanceId;
#Field("amb_number")
String licensePlateNumber;
#Field("driver_name")
String driverName;
#Field("driver_licence_id")
String driverLicenseNumber;
#Field("ownership")
String ownerShip;
#Field("ownership_address")
String ownerShipAddress;
#GeoSpatialIndexed(name="Location")
Double[] location;
//setters and getters
}
Here is the repository I have been using
#ComponentScan
#Repository
public interface AmbulanceRepo extends MongoRepository<Ambulance, String> {
GeoResults<Ambulance> findByLocationNear(Point p, Distance d);
}
and the controller
#RestController
#CrossOrigin(origins = "http://localhost:4200")
#RequestMapping("/")
public class NearbyAmbulanceController {
private AmbulanceRepo ambulanceRepo;
#Autowired
public NearbyAmbulanceController(AmbulanceRepo repo){
this.ambulanceRepo = repo;
}
#RequestMapping(value="/nearbyAmbulance",method = RequestMethod.POST)
#ResponseBody
public GeoResults<Ambulance> getAmbulanceDetails(
#RequestBody LocationRequest locationRequest){
System.out.println("lati "+locationRequest.getLatitude()+ " long "+locationRequest.getLongitude()+" d "+locationRequest.getDistance());
// List<Ambulance> ambulanceList=this.ambulanceRepo.findByLocationNear(new Point(Double.valueOf(locationRequest.getLongitude()),Double.valueOf(locationRequest.getLatitude())),new Distance(locationRequest.getDistance(), Metrics.KILOMETERS));
Point point = new Point(locationRequest.getLatitude(), locationRequest.getLongitude());
Distance distance = new Distance(locationRequest.getDistance(), Metrics.KILOMETERS);
GeoResults<Ambulance> ambulanceList=this.ambulanceRepo.findByLocationNear(point,distance);
System.out.println(ambulanceList);
return ambulanceList;
}
}
Every time I try it I get no results. I am sure that I have data in the given point and nearby locations and I was even able to use the mongoshell to fetch those. I have feeling that the problem is with the way that I have annotated the location field in the entity. Is there anything that I am missing? Any help is appreiciated.
Spring MongoDb Geo spatial query: It is tested well.
public List<MachineDetails> searchbylocation(String lat, String longt, String maxDistance) {
BasicQuery query1 = new BasicQuery("{geoLocation:{ $near: { $geometry: { type: 'Point',coordinates: ["+ lat+","+ longt+" ] }, $minDistance: 10, $maxDistance: "+maxDistance+"}}}");
List<MachineDetails> venues1 = mtemplate.find(query1, MachineDetails.class);
return venues1;
}
Seems like there were more than one database in mongodb that had collections by the name 'trial'. I tried removing all those and now it is working fine. Spring data wasn't able to figure out which collection to be queried on as there were multiple collection with the same name in different databases. Hope this helps someone, cause I have just wasted a couple of days on this.

Spring repository auto casts entities with different class types

I'm using MongoRepository interface to extend my custom repositories for different entities. Now I faced with problem, let's assume an example:
I have 2 entities:
#Document(collection = "person")
public class Employee {
private String position;
}
and
#Document(collection = "person")
public class Manager {
private String major;
}
repositories for both:
#Repository
public interface ManagerRepository extends MongoRepository<Manager, String> {}
and
#Repository
public interface EmployeeRepository extends MongoRepository<Employee, String> {}
Everything goes well when I saving 2 models:
{
"_id" : ObjectId("5541f988d4c603ebac18a147"),
"_class" : "com.igmtechnology.gravity.core.init.test.Manager",
"major" : "majority"
}
{
"_id" : ObjectId("5541f988d4c603ebac18a148"),
"_class" : "com.igmtechnology.gravity.core.init.test.Employee",
"position" : "developer"
}
But when I'm doing findAll() from one of repositories I'm getting 2 objects and one of them spring is automatically casting to another one.
How can avoid this auto casting? Or how can specify which class I need to get?
For both of the repositories, you can use the #Query annotation to specify a MongoDB JSON query string that will be used instead of query derived from the method's name (you must know that there's a convention for parsing the repository's method names and for building MongoDB queries).
So, by using #Query, you can do:
#Repository
public interface ManagerRepository extends MongoRepository<Employee, String>
#Query(value="{ '_class' : 'com.igmtechnology.gravity.core.init.test.Manager' }")
List<Person> findAllManagers();
}
Behind the scenes, this will generate a query, similar to this one:
db.person.findAll({'_class' ; 'com.igmtechnology.gravity.core.init.test.Manager'});
However, there's a minor problem with this code. If you change the fully-qualified class name of Manager, then the query would not throw a RuntimeException, but would return nothing. In this case you can use a wildcard within the #Query.
#Query(value="{ '_class' : ?0 }")
List<Person> findAllManagers(String className);
Then, when you invoke the method, you can just do:
managerRepository.findAllManagers(Manager.class.getName());
The provided Manager.class.getName() will replace the ?0 wildcard and your query will built properly.
Same goes for the Employee repository with the difference that you have to provide the fully-qualified class name of Employee in the #Query's value attribute.
More info:
Spring-data MongoDB repositories

Categories

Resources