Skip to content

Conversation

@NathanQingyangXu
Copy link
Contributor

@NathanQingyangXu NathanQingyangXu commented Aug 11, 2025

A little bit simplified than the original POC but given MongoDB's lookup and unwind stages are not identifical to SQL joining, there are still quite some subtle tech challenges.

Just realized this PR only covers basic @ManyToOne (maybe also @OneToOne, but definitively not @OneToMany(mappedBy=...), which requires tons of code refactoring on our MQL translator; I created a broken simple integration testing class so my colleagues who love challenge could try. It is hard, I have to prewarn here). Table joining is the heart and soul of SQL and Hibernate ORM, so we have to build our features little by little.

Take the following enitties with natural hiararchical relationshiop as example:

@Entity
@Table(name = "countries")
class Country { ... }

@Entity
@Table(name = "provinces")
class Province {
   ...
    @ManyToOne
    Country country
}

when we load a province, internally MQL translator is supposed to translate the table joining from provinces to countries; MQL's lookup and unwind will help as below:

  • during lookup stage, we need to create an array field containing the joining result; a natural way is to use the table alias generated by Hibernate (c1_0 for countries and p1_0 for provinces), so after the joining, provinces doc will contain a new array field named c1_0; given this is a toOne association, the new array field contains only one entry
  • during the next unwind stage, the array field will be transformed to a non-array field, ending up with SQL's cartisan product analog

but there is big difference between the above transformation and SQL's table joinining. SQL's table joining has no embedding relatinoshiop and each table involved in the join will have its own global namespace (or its table alias created automatically by Hibernate).

Let us consider the project stage which requires irrevocable changes to our existing code for sure. In the SQL AST model, the projection list will be as below:

SELECT p1_0._id, c1_0._id, c1_0.name, p1_0.name
from province as p1_0 left join countries as c1_0 
on p1_0.country__id = c1_0._id

So in our MQL doc, c1 is embedded as a new field of provinces collection, this PR introudces some minor code loigc to translate the above SQL projection list as below:

{
  "aggregate": "provinces",
  "pipeline": [
    {
      "$lookup": {
        "from": "countries",
        "localField": "country_code",
        "foreignField": "_id",
        "as": "c1_0"
      }
    },
    {
      "$unwind": {
        "path": "$c1_0"
      }
    },
    {
      "$match": {
        ... ...
      }
    },
    {
      "$project": {
        "f0": "$_id",
        "f1": "$c1_0._id",
        "f2": "$c1_0.name",
        "f3": "$name",
        "_id": false
      }
    }
  ]
}

Now new project fields need to be accommodated other than the entity fields per se, so the above projection relies on the projection set feature; the new assigned field names doesn't matter for Hibernate v6 only requires the order is aligned correctly (ResultSet JDBC methods accepting field names are not used by Hibernate ORM at all!).

Another complexity is the lookup could be recursive (e.g. cities -> provinces -> countries), but the final project SQL AST uses flat global table namespaces. We need to map the deeply nested lookup new field (e.g. p1_0.c1_0 is the new country nested doc in cities collection), so some transformation logic is required. That is why a new columnQualifierFullPaths map field was introduced in AbstractMqlTranslator, so it could translate c1_0.name SQL projection entry to p1_0.c1_0.name in the cities collection doc.

After the table joining emulation is sorted out, the following typical association relationships integration testing cases all passed:

  1. @OneToOne
  2. @ManyToOne
  3. @OneToMany
  4. @ManyToMany

@NathanQingyangXu NathanQingyangXu changed the base branch from main to HIBERNATE-48 August 11, 2025 19:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants