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:
2026-10-29 05:52:26 +00:00
co-authored by Claude Sonnet 5.5
parent ef2d2aa8ea
commit 98f4ed7411
45 changed files with 1820 additions and 0 deletions
+43
View File
@@ -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