Add multitenancy module: Hibernate 7 @TenantId vs schema-per-tenant (shared pool + search_path, pool per tenant), JWT tenant filter, isolation failure modes on PostgreSQL 17 (Hibernate 7.4.14.Final, Spring Boot 4.1.2)
Co-Authored-By: Claude Sonnet 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_017ic4b4VQVEJWetTT1dYzyJ
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
# @TenantId on PostgreSQL (PostgreSQL 17.11), Hibernate 7.4.14.Final
|
||||
|
||||
== DDL Hibernate generated (read back from the catalog)
|
||||
column amount numeric nullable=YES
|
||||
column id bigint nullable=NO
|
||||
column number character varying nullable=YES
|
||||
column tenant_id character varying nullable=NO
|
||||
unique UNIQUE (tenant_id, number)
|
||||
|
||||
== insert: nobody sets tenant_id in Java
|
||||
-- as acme: save(INV-1)
|
||||
sql> insert into invoice (amount,number,tenant_id) values (?,?,?)
|
||||
entity.tenantId after save = acme
|
||||
-- as globex: save(INV-1) - same invoice number, allowed by UNIQUE(tenant_id, number)
|
||||
sql> insert into invoice (amount,number,tenant_id) values (?,?,?)
|
||||
ids: acme INV-1=1, globex INV-1=2 (one IDENTITY column shared by all tenants)
|
||||
|
||||
== reads are filtered
|
||||
-- as acme: findAll()
|
||||
sql> select i1_0.id,i1_0.amount,i1_0.number,i1_0.tenant_id from invoice i1_0 where i1_0.tenant_id = ?
|
||||
rows = 2, numbers = [INV-1, INV-2]
|
||||
-- as globex: findById(acme's id 1)
|
||||
sql> select i1_0.id,i1_0.amount,i1_0.number,i1_0.tenant_id from invoice i1_0 where i1_0.id=? and i1_0.tenant_id = ?
|
||||
result = Optional.empty
|
||||
|
||||
== bulk HQL is scoped too
|
||||
-- as acme: update Invoice set amount = 0
|
||||
sql> update invoice i1_0 set amount=0 where i1_0.tenant_id = ?
|
||||
rows updated = 2 (globex's row untouched)
|
||||
|
||||
== what @TenantId does NOT cover: native SQL
|
||||
-- as acme: native query select count(*) from invoice
|
||||
result = 3 (acme owns 2 rows; the native query saw every tenant)
|
||||
|
||||
== no tenant set
|
||||
-- strict resolver (throws when TenantContext is empty): repo.findAll() with no tenant
|
||||
org.springframework.transaction.CannotCreateTransactionException: Could not open JPA EntityManager for transaction <- caused by com.ankurm.multitenancy.tenant.MissingTenantException: no tenant in TenantContext: refusing to open a tenant-scoped session
|
||||
-- lenient resolver (returns null): repo.findAll() with no tenant
|
||||
org.springframework.transaction.CannotCreateTransactionException: Could not open JPA EntityManager for transaction <- caused by org.hibernate.HibernateException: SessionFactory configured for multi-tenancy, but no tenant identifier specified
|
||||
|
||||
== writing a row for a different tenant than the session's
|
||||
-- as acme: new Invoice with tenantId manually set to globex, saveAndFlush
|
||||
org.springframework.dao.DataIntegrityViolationException: assigned tenant id differs from current tenant id [globex != acme] for entity com.ankurm.multitenancy.discriminator.Invoice.tenantId <- caused by org.hibernate.PropertyValueException: assigned tenant id differs from current tenant id [globex != acme] for entity com.ankurm.multitenancy.discriminator.Invoice.tenantId
|
||||
Reference in New Issue
Block a user