Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
258 views
in Technique[技术] by (71.8m points)

java - 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() {}

  /* ... */
}
See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Answer

0 votes
by (71.8m points)

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 ??


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome to OStack Knowledge Sharing Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...