-
Type:
Improvement
-
Resolution: Unresolved
-
Priority:
Major - P3
-
None
-
Affects Version/s: None
-
Component/s: Query
-
None
-
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].