matrei opened a new pull request, #16704:
URL: https://github.com/apache/grails-core/pull/16704

   ## Description
   
   This is the 8.0.x counterpart of #16703, adapted to the per-datastore tenant 
binding of 8.0.x, with GORM for Hibernate 7 as well.
   
   With `DISCRIMINATOR` multi-tenancy, only `Tenants.withoutId` and 
`@WithoutTenant` now run without a tenant. Running without a tenant is a state 
that `withoutId` binds, instead of something GORM derives from the value of the 
tenant id. A tenant id of `default`, the name of the default connection source, 
is therefore a tenant id like any other, whether a `TenantResolver` returns it 
or it is passed to `Tenants.withId`.
   
   GORM now also handles these cases the same way as a query on a multi-tenant 
entity:
   
   - A `TenantResolver` that returns `null` causes a `TenantNotFoundException`, 
as a resolver that finds no tenant does.
   - With a numeric tenant id, only `Tenants.withoutId` uses the tenant id `0`. 
A resolved `default` fails, as it already does with GORM for Hibernate.
   - GORM for Hibernate 5 and Hibernate 7 restrict a collection of a 
multi-tenant entity to the current tenant whether it is loaded lazily or 
fetched by join. That includes many-to-many collections and collections that a 
query fetches by join through another association. `load` and `proxy` of a 
multi-tenant entity run a query, as `get` does.
   
   ### Running without a tenant
   
   - `CurrentTenantHolder.withoutTenant(datastore, ...)` binds a private marker 
that equals `ConnectionSource.DEFAULT` for that datastore. The marker never 
leaves `CurrentTenantHolder`. Inside `withoutId`, `Tenants.currentId()` returns 
`default`, so code that uses the current tenant id as the name of a connection 
source keeps working.
   - The new `Tenants.isWithoutId(Datastore)` and `Tenants.isWithoutId()` tell 
whether the current code runs without a tenant id, for a datastore and for the 
default datastore. The multi-tenant event listeners of 
`grails-datamapping-core`, Hibernate 5 and Hibernate 7, and the Hibernate 
tenant filters, use them instead of comparing the tenant id with `default`.
   - The new `Tenants.withCurrent(MultiTenantCapableDatastore, Closure)` is 
used by both `withCurrent` overloads and by `DefaultTenantService.withCurrent`. 
Inside `withoutId`, `withCurrent` and `@CurrentTenant` therefore keep running 
without a tenant, and they pass `default` to a closure that takes the tenant id.
   - `GormStaticApi.withoutId` runs without a tenant for a multi-tenant 
datastore, instead of running with the tenant `default`. A closure with 
parameters gets the tenant id, or the tenant id and the session, as with 
`withId`.
   - `Tenants.currentId` throws `TenantNotFoundException` when the resolver 
returns `null`.
   - The core listener uses `0L` for a numeric tenant id only inside 
`withoutId`.
   
   The `DEFAULT` comparisons in `GormRegistry` and `DefaultDatastoreSelector` 
are unchanged. They choose a datastore by connection name, which matters only 
in `DATABASE` and `SCHEMA` mode.
   
   ### GORM for Hibernate 5 and Hibernate 7
   
   - `enableMultiTenancyFilter()` leaves the tenant filter off only inside 
`withoutId`. Otherwise it filters on the current tenant id, including `default`.
   - A new `MultiTenantCollectionEventListener` for each sets the filter before 
Hibernate initializes a collection of a multi-tenant entity. Lazy collections 
are therefore restricted in a fresh session and after `withoutId` as well.
   - A many-to-many collection of a multi-tenant entity gets the tenant filter. 
A filtered collection mapped `fetch: 'join'` is loaded with a select of its 
own, because Hibernate doesn't apply the filter to a collection that it fetches 
by join together with an instance loaded by id. On Hibernate 5 this is 
`GrailsDomainBinder`, on Hibernate 7 `ToManyEntityMultiTenantFilterBinder`.
   - The new `enableMultiTenancyFilter(PersistentEntity)` handles queries on an 
entity that isn't multi-tenant but has a collection of a multi-tenant entity, 
directly or through its associations. Before such a query, it sets the filter 
when there is a current tenant. Both listeners call it on every 
`PreQueryEvent`, which covers `join fetch` in HQL. The Hibernate 5 criteria 
builder calls it when it is created, for `fetchMode` in criteria. Hibernate 7 
runs a criteria query through its `HibernateQuery`, which fires the 
`PreQueryEvent`, so its builder is unchanged.
   - While GORM resolves the tenant for the filter, a query on such an entity 
doesn't call the resolver again. A resolver can therefore look the tenant up 
with GORM, for example with `Account.findByHost(host)`.
   - `load` and `proxy` of a multi-tenant entity run `get`.
   
   ### Documentation
   
   - Upgrade note 87 describes the changes and what to do about them. It also 
says that `Tenants.isWithoutId()` looks up the default datastore, as 
`Tenants.currentId()` does. Unlike in 7.0.x, it therefore throws an 
`IllegalStateException` when GORM isn't initialized or more than one GORM 
implementation is configured.
   - The discriminator multi-tenancy sections of the GORM for Hibernate 5 and 
Hibernate 7 guides cover:
     - working with every tenant through `withoutId` and `@WithoutTenant`;
     - collections, `load` and `proxy`;
     - the resolver call before a query on an entity that has a collection of a 
multi-tenant entity.
   - The partitioned multi-tenancy sections of the GORM for MongoDB and GORM 
for Neo4j guides explain how GORM sets the tenant id on save.
   
   ### Tests
   
   New specs:
   
   - `PartitionedMultiTenancyWithoutIdSpec`, in `grails-datamapping-core-test` 
(in-memory datastore) and in the Hibernate 5 and Hibernate 7 modules (H2), 
covers the same cases as on 7.0.x:
     - `isWithoutId`;
     - `default` from a resolver and from `withId` as an ordinary tenant;
     - nesting with `withId` and `withCurrent`;
     - `@CurrentTenant` and `@WithoutTenant` services;
     - saving inside `withoutId`.
   
     The in-memory spec adds an instance saved inside `withoutId` without a 
tenant id.
   - `PartitionedMultiTenancyNullTenantSpec` (in-memory) checks that a resolver 
that returns `null` fails `currentId`, queries and saves, and that `withoutId` 
still works.
   - `PartitionedMultiTenancyNumericTenantSpec` (in-memory) checks that a 
resolved `default` with a numeric tenant id fails, and that `withoutId` keeps 
using `0`.
   - `PartitionedMultiTenancyFilterSpec`, in the Hibernate 5 and Hibernate 7 
modules, covers:
     - lazy, join-fetched and many-to-many collections;
     - `join fetch` in HQL and `fetchMode` in criteria, also through another 
association;
     - `load` and `proxy`;
     - a resolver that returns `null`.
   - `PartitionedMultiTenancyResolverQuerySpec`, in the Hibernate 5 and 
Hibernate 7 modules, covers a resolver that queries with GORM, and an exception 
from the resolver.
   
   Updated specs:
   
   - `CurrentTenantHolderSpec` covers `isWithoutTenant` per datastore. It also 
checks that the id handed out inside `withoutTenant` is `default` and binds as 
an ordinary tenant.
   - `GormStaticApiSpec` checks that `withoutId` runs without a tenant for a 
multi-tenant datastore, and passes a closure the tenant id, or the tenant id 
and the session.
   - `ToManyEntityMultiTenantFilterBinderSpec` covers the tenant filter on a 
many-to-many collection, and `fetch: 'join'` becoming a select only for a 
multi-tenant collection.
   - `MultiTenantEventListenerSpec` (core) checks that a resolver that returns 
`null` fails and that `0L` is used only inside `withoutTenant`.
   - `MultiTenantEventListenerSpec` (Hibernate 7) has the same `null` change. 
The check for a collection of a multi-tenant entity moved into 
`enableMultiTenancyFilter(PersistentEntity)`, so its two features moved to the 
filter and resolver specs.
   
   These pass with the change:
   
   - all tests of `grails-datamapping-core`, `grails-datamapping-core-test`, 
`grails-data-hibernate5-core`, `grails-data-hibernate7-core` and 
`grails-datastore-web`;
   - the multi-tenancy specs of `grails-data-mongodb-core` and 
`grails-data-neo4j-core`, including the TCK's multi-tenant specs;
   - `codeStyle` for `grails-datamapping-core`, `grails-data-hibernate5-core` 
and `grails-data-hibernate7-core`.
   
   ### Merging up from 7.x
   
   Please merge this PR before #16703 is merged up from 7.2.x into 8.0.x. That 
merge then conflicts in these eight files:
   
   - `Tenants.groovy`
   - `DefaultTenantService.groovy`
   - the core `MultiTenantEventListener.java`
   - the Hibernate 5 `AbstractHibernateCriteriaBuilder.java`
   - the in-memory `PartitionedMultiTenancyWithoutIdSpec.groovy`
   - the Hibernate 5 and MongoDB multi-tenancy guides
   - `upgrading60x.adoc`
   
   In all of them, keep the 8.0.x side. The result is then 8.0.x with this PR. 
The other changes that #16703 makes to the Hibernate 5 module are the same as 
here, so they merge without conflicts. Upgrade note 19 doesn't come over: 
8.0.x's `upgrading60x.adoc` keeps its own version, and note 87 covers the 
change.
   
   If the merge-up comes first, the Hibernate 5 files and guides take the 7.0.x 
versions, and this PR has to be rebased.
   
   #16515 adds a `Tenants.isWithoutId()` with the same signature. Like on 
7.0.x, it can rebase onto this PR and drop its own.
   


-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to