Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

@MongoId not working for nested object queries using a projection class #4557

Open
nniesen opened this issue Nov 14, 2023 · 2 comments
Open
Assignees
Labels
status: feedback-provided Feedback has been provided

Comments

@nniesen
Copy link
Contributor

nniesen commented Nov 14, 2023

The @MongoId annotation is not being taken into account for nested object queries using a projection class that doesn't include the nested object. This causes collection scans and results in query not finding the documents.

Actual: { "someRef.id" : "a"}
Expected: { "someRef._id" : "a"}

Using:

+--- org.springframework.boot:spring-boot-dependencies:3.1.5
|    +--- org.springframework.boot:spring-boot-starter-data-mongodb:3.1.5 (c)
|    +--- org.springframework.boot:spring-boot-starter:3.1.5 (c)
|    +--- org.mongodb:mongodb-driver-sync:4.6.1 (c)
|    +--- org.springframework.data:spring-data-mongodb:3.1.5 (c)

Example code:

@SpringBootApplication
@EnableMongoRepositories
public class ProjectionMongoIdApplication implements CommandLineRunner {
    @Autowired
    MongoTemplate mongoTemplate;

    @SuppressWarnings("resource")
    public static void main(String[] args) {
        SpringApplication.run(IndexIdMappingApplication.class, args);
    }

    @Override
    public void run(String... args) throws Exception {
        mongoTemplate.dropCollection(Widget.class);
        IndexOperations indexOps = mongoTemplate.indexOps(Widget.class);
        indexOps.ensureIndex(new Index("someRef._id", Direction.ASC));
        
        mongoTemplate.save(new Widget("123", new SomeRef("a", "Apple")));
        mongoTemplate.save(new Widget("456", new SomeRef("b", "Banana")));

        // queries { "someRef._id" : "a"} and uses index for "someRef._id"
        mongoTemplate.find(new Query(Criteria.where("someRef.id").is("a")), Widget.class).forEach(w -> {
            System.out.printf("query by someRef.id found %s:%s\n", w.id, w.name);
        });

        // queries { "someRef._id" : "a"} and uses index for "someRef._id"
        mongoTemplate.find(new Query(Criteria.where("someRef._id").is("a")), Widget.class).forEach(w -> {
            System.out.printf("query by someRef._id found found %s:%s\n", w.id, w.name);
        });

        // queries { "someRef._id" : "a"} and uses index for "someRef._id"
        Query query = new Query(Criteria.where("someRef.id").is("a"));
        query.fields().include("name");
        mongoTemplate.find(query, Widget.class).forEach(w -> {
            System.out.printf("query by someRef.id w include fields found %s:%s\n", w.id, w.name);
        });

        // BUG: queries { "someRef.id" : "a"}, finds no doc, and results in a collection scan because it can't find index for "someRef.id"
        mongoTemplate.find(new Query(Criteria.where("someRef.id").is("a")), WidgetProjection.class).forEach(w -> {
            System.out.printf("query by someRef.id w projection class found %s:%s\n", w.id, w.name);
        });

        // queries { "someRef._id" : "a"} and uses index for "someRef._id"
        mongoTemplate.find(new Query(Criteria.where("someRef._id").is("a")), WidgetProjection.class).forEach(w -> {
            System.out.printf("query by someRef._id w projection class found %s:%s\n", w.id, w.name);
        });
    }
}

@Document
class Widget {
    @Id
    String id;
    String name;
    SomeRef someRef;
    
    public Widget(String id, SomeRef someRef) {
        this.id = id;
        this.name = "name-" + id;
        this.someRef = someRef;
    }
}

@Document(collection = "widget")
class WidgetProjection {
    String id;
    String name;
//    SomeRef someRef; // Adding this makes it work but don't want it on the projection.
}

class SomeRef {
    @MongoId(FieldType.IMPLICIT)
    String id;
    String name;
    
    public SomeRef(String id, String name) {
        this.id = id;
        this.name = name;
    }
}

Logging (logging.level.org.springframework.data.mongodb.core: DEBUG)

... find using query: { "someRef._id" : "a"} fields: Document{{}} for class: class Widget ...
query by someRef.id found 123:name-123
... find using query: { "someRef._id" : "a"} fields: Document{{}} for class: class Widget ...
query by someRef._id found found 123:name-123
... find using query: { "someRef._id" : "a"} fields: Document{{name=1}} for class: class Widget ...
query by someRef.id w include fields found 123:name-123
... find using query: { "someRef.id" : "a"} fields: Document{{}} for class: class WidgetProjection ...
                                                    ^- BUG: using wrong id and no documents found and collection scan
... find using query: { "someRef._id" : "a"} fields: Document{{}} for class: class WidgetProjection ...
query by someRef._id w projection class found 123:name-123

Profiling:

db.setProfilingLevel(2)
db.getCollection("system.profile").find({"command.find": "widget"}, {"command.filter": 1, planSummary:1})

{
    "command" : {
        "filter" : {
            "someRef._id" : "a"
        }
    },
    "planSummary" : "IXSCAN { someRef._id: 1 }"
}
{
    "command" : {
        "filter" : {
            "someRef._id" : "a"
        }
    },
    "planSummary" : "IXSCAN { someRef._id: 1 }"
}
{
    "command" : {
        "filter" : {
            "someRef._id" : "a"
        }
    },
    "planSummary" : "IXSCAN { someRef._id: 1 }"
}
{
    "command" : {
        "filter" : {
            "someRef.id" : "a"
        }
    },
    "planSummary" : "COLLSCAN"
}
{
    "command" : {
        "filter" : {
            "someRef._id" : "a"
        }
    },
    "planSummary" : "IXSCAN { someRef._id: 1 }"
}
@spring-projects-issues spring-projects-issues added the status: waiting-for-triage An issue we've not yet triaged label Nov 14, 2023
@christophstrobl
Copy link
Member

christophstrobl commented Nov 15, 2023

Thank you for reaching out.
The snippet above works as designed. The query mapping is only able to use fields that are actually contained in the given domain type. WidgetProjection however does not contain the path to the property used in the criteria. Therefore it will treat the given input as is.

The fluent template API offers dedicated methods that allow to map the query against a given domain type and project results to a different one.

List<WidgetProjection> result = template.query(Widget.class)
	.as(WidgetProjection.class)
	.matching(where("someRef.id").is("a"))
	.all();

@christophstrobl christophstrobl self-assigned this Nov 15, 2023
@christophstrobl christophstrobl added status: waiting-for-feedback We need additional information before we can continue and removed status: waiting-for-triage An issue we've not yet triaged labels Nov 15, 2023
@nniesen
Copy link
Contributor Author

nniesen commented Nov 15, 2023

Thank you for reaching out. The snippet above works as designed. The query mapping is only able to use fields that are actually contained in the given domain type. WidgetProjection however does not contain the path to the property used in the criteria. Therefore it will treat the given input as is.

That's unfortunate since it's not intuitive and seems to behave differently than the rest of the API. It seems like the mapping should work off of the collections domain type not the projections domain type. I'm assuming the problem for you is that there is no difference between those two; i.e., you only have the class/type passed to the find.

It might be more obvious/discoverable if there was an additional signature of:

/** equivalent to query(Class<T> domainType).as(Class<R> resultType).matching(CriteriaDefinition criteria) */
public <R> List<R> find(Query query, Class<?> entityClass, Class<R> resultType)

@spring-projects-issues spring-projects-issues added status: feedback-provided Feedback has been provided and removed status: waiting-for-feedback We need additional information before we can continue labels Nov 15, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
status: feedback-provided Feedback has been provided
Projects
None yet
Development

No branches or pull requests

3 participants