Exclude _id from $project when unspecified, instead of implicitly including it

XMLWordPrintableJSON

    • Type: Improvement
    • Resolution: Fixed
    • Priority: Major - P3
    • 1.0.0
    • Affects Version/s: None
    • Component/s: Query
    • None
    • 1
    • None
    • Needed
    • Hide

      The extension no longer relies on MongoDB implicitly retaining _id. When the terminal $project stage of a query does not specify _id, the JDBC adapter injects an explicit exclusion, so the query returns exactly the projected fields. This applies to native MQL queries as well as to the queries Hibernate generates.

      The native query documentation needs to state the following.

      • A native query whose terminal $project omits _id does not return _id. MongoDB would otherwise return it implicitly.
      • A native query that binds its result to an entity must project _id explicitly, because the entity identifier maps to it. Omitting it fails at read time with an "Unknown column label" error naming _id.
      • Projecting _id explicitly, either as an inclusion or as a computed value, leaves the query unchanged. The adapter only fills in an exclusion when _id is absent.

      The first pipeline below returns only the title; the second returns the title and the identifier and is the form required when binding to an entity.

      {aggregate: "books", pipeline: [{$project: {title: 1}}]}
      {aggregate: "books", pipeline: [{$project: {title: 1, _id: 1}}]}
      
      Show
      The extension no longer relies on MongoDB implicitly retaining _id . When the terminal $project stage of a query does not specify _id , the JDBC adapter injects an explicit exclusion, so the query returns exactly the projected fields. This applies to native MQL queries as well as to the queries Hibernate generates. The native query documentation needs to state the following. A native query whose terminal $project omits _id does not return _id . MongoDB would otherwise return it implicitly. A native query that binds its result to an entity must project _id explicitly, because the entity identifier maps to it. Omitting it fails at read time with an "Unknown column label" error naming _id . Projecting _id explicitly, either as an inclusion or as a computed value, leaves the query unchanged. The adapter only fills in an exclusion when _id is absent. The first pipeline below returns only the title; the second returns the title and the identifier and is the form required when binding to an entity. {aggregate: "books" , pipeline: [{$project: {title: 1}}]} {aggregate: "books" , pipeline: [{$project: {title: 1, _id: 1}}]}
    • None
    • None
    • None
    • None
    • None
    • None

      Summary

      Explicitly exclude _id from the terminal $project when a query does not specify it, instead of implicitly including it in the JDBC result column list.

      Current behavior

      MongoStatement.getFieldNamesFromProjectStage appends _id to the result-set field-name list whenever the terminal $project does not mention it, mirroring MongoDB's rule that $project retains _id unless it is explicitly excluded. This applies to both generated (HQL/Criteria) and native queries, which share the same executeQuery path.

      Problems with implicit inclusion

      • Advertises a column the query did not select. getColumnCount() is one higher than the projection, and findColumn("_id") succeeds for a query that did not project _id. This is inert today (nothing reads the extra column) but semantically incorrect.
      • Returns an unused _id on every result document.

      Proposed change

      In the shared JDBC adapter (MongoStatement.executeQuery), inject _id: 0 into the terminal $project when it does not already specify _id, and remove the append in getFieldNamesFromProjectStage. The result-set field names then equal the projection's declared keys, and the pipeline output contains exactly the projected fields, for both generated and native queries.

      Entity loads are unaffected: they always select the id column, so _id is already present and the injection is a no-op.

      Rationale for native queries

      Injecting _id: 0 modifies a user-authored native query, so it needs justification:

      • The adapter already rewrites native MQL; it is not a verbatim passthrough. Native queries flow through MongoConnection.prepareStatement, which rewrites Hibernate's ?/:name parameter markers and strips the SQL-style parentheses Hibernate wraps around multi-valued parameters, because neither is valid MQL. Normalizing an omitted _id to _id: 0 is the same class of adapter-level normalization.
      • Parity. Generated and native queries share executeQuery/getFieldNamesFromProjectStage. Handling _id in the shared path keeps them identical. Excluding _id only for generated queries (for example in the translator) would leave native queries carrying an implicit _id.
      • Consistency with the project's handling of MongoDB's implicit _id inclusion, which it otherwise avoids. A native projection {$project: {title: 1}} names title; without this change the result also contains _id. Returning exactly the projected fields is the more predictable contract.
      • The change only suppresses a field the user did not name; it never removes or alters a projected field. A native author who wants _id projects it (_id: 1 or a computed value), and the injection becomes a no-op. The behavior is overridable by the query author.

      This does change the effective result of a native query the author wrote. The native-query documentation must state that projecting to an entity requires explicitly projecting _id, and that an omitted _id is suppressed rather than implicitly returned.

      Origin and prior discussion

      The _id append was added in PR #30 (ResultSet implementation), where its design was worked out in a review thread on MongoStatement (starting here). Summarized below because the thread is unlikely to be revisited.

      The append was added to mirror MongoDB's implicit _id inclusion for native queries. Points from that discussion that bear on this ticket:

      • The original concern was that appending _id at the end of the field-name list would not line up with MongoDB returning _id first. That concern does not apply: MongoResultSet resolves every column by name (getKey -> currentDocument.get(key), and findColumn by label), never by physical BSON position, so the order of the field-name list relative to the returned document does not matter.
      • The thread also flagged the case this ticket resolves: a native query that omits _id from its $project. MongoDB still returns _id in each result document, but the field-name list is derived from the $project, which does not name _id. If the result binds to something that needs the id column — for example an entity whose @Id maps to _id — Hibernate looks that column up by label and fails at read time with Unknown column label [_id]. The append was added to avoid that failure by adding _id to the field-name list whenever the $project omits it; the cost is that it then advertises a phantom _id column (and an off-by-one getColumnCount()) for the many queries that omit _id and do not need it.

      Two corrections to the record, established while investigating this ticket:

      • The thread states that generated queries "invariably include a _id: 0" in the $project. The shipped translator does not emit _id: 0; it builds the $project from the selected columns only. Generated queries avoid the implicit-_id problem through name-based column resolution, not exclusion.
      • Consequently the append is inert for every generated query (entity selects project _id explicitly; scalar/partial selects never read the implicit _id) and affects only native queries that omit _id but map to something that needs it. Removing the append breaks zero integration tests.

      Note on how this ticket resolves the flagged case: it injects _id: 0 so the query returns exactly the projected fields, and a native author who needs _id (for example for an entity binding) must project it explicitly; otherwise they get the same clear Unknown column label [_id].

            Assignee:
            Jeffrey Yemin
            Reporter:
            Jeffrey Yemin
            Slav Babanin
            Votes:
            0 Vote for this issue
            Watchers:
            1 Start watching this issue

              Created:
              Updated:
              Resolved: