Reputation: 7059
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() {}
/* ... */
}
Upvotes: 1
Views: 3505
Reputation: 7059
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 aStatsAttributes.class
somehow in the mongoTemplate query. If anyone knows how to, please share 🙏
Upvotes: 2