Ho progettato un database a istanza singola per un'applicazione multi-tenant che utilizza le chiavi composite per imporre la separazione dei tenant al livello del database (evitando così relazioni errate di terzo grado).
Quindi lo schema assomiglia a questo:
TABLE Tenants (
TenantId int NOT NULL IDENTITY(1,1)
Name nvarchar(100) NOT NULL
IsAdmin bit NOT NULL
PRIMARY KEY( TenantId )
)
TABLE Users (
TenantId int NOT NULL
UserId int NOT NULL IDENTITY(1,1)
UserName nvarchar(100) NOT NULL
PRIMARY KEY( TenantId, UserId )
CONSTRAINT FK_Tenants_Users FOREIGN KEY ( TenantId ) REFERENCES Tenants ( TenantId )
)
TABLE Documents (
TenantId int NOT NULL
DocumentId int NOT NULL IDENTITY(1,1)
CreatedByUserId int NOT NULL
ModifiedByUserId int NOT NULL
PRIMARY KEY ( TenantId, DocumentId )
CONSTRAINT FK_Tenants_Documents FOREIGN KEY ( TenantId ) REFERENCES Tenants ( TenantId )
CONSTRAINT FK_Documents_Creators FOREIGN KEY ( TenantId, CreatedBy ) REFERENCES Users ( TenantId, CreatedBy )
CONSTRAINT FK_Documents_Modifiers FOREIGN KEY ( TenantId, ModifiedBy ) REFERENCES Users ( TenantId, ModifiedBy )
)
Ci sono molte altre tabelle nel sistema, ma tutte condividono lo stesso concetto in cui se un'entità "appartiene" a un tenant, la chiave primaria di quell'entità è composta e include TenantId
.
... che aiuta a prevenire situazioni in cui Document
di ModifiedByUserId
potrebbe fare riferimento a UserId
in un altro titolare. Dato che gli inquilini devono essere completamente separati, questa applicazione è l'ideale.
Tranne ... come dovrebbero accadere le azioni amministrative? Nota che in questo sistema un Tenant può essere contrassegnato come IsAdmin
che ha lo scopo di dare loro la possibilità non solo di accedere alle risorse di ciascun Tenant, ma anche di modificare e creare risorse - il che è un problema perché una% di amministrazione diTenantId
sarebbe diversa - quindi un utente amministratore non può creare una risorsa in un altro tenancy contrassegnato come proveniente da tale utente.
... almeno per lo scenario in cui un amministratore modifica una risorsa esistente, il valore ModifiedByUserId
potrebbe rimanere invariato.
Quindi per abilitare questo scenario (creando nuove risorse in un altro tenant) ho alcune opzioni:
- Dai a ogni inquilino una voce "Utente fantasma" che rappresenta le azioni amministrative. Il rovescio della medaglia è che confonde il set-set
Users
avendo un'entità che in realtà non rappresenta un utente umano (quindi cosa impostare per cose come nome, email, ecc.). Un lieve vantaggio di questo approccio rispetto all'opzione 2 (sotto) è che mantiene la segregazione completa degli inquilini, anche con i titolari amministrativi, in modo che una locazione possa essere ridotta a un'altra istanza DB senza conflitti di identità o riferimenti interrotti. - Rimuovi
TenantId
dalle chiavi primarie composte diUser
- lo svantaggio è che indebolisce la segregazione dei tenant. - Ruba l'ID utente di un utente esistente nel tenant, forse l'account utente associato al titolare della proprietà, ma ciò comporterebbe una cattiva pista di controllo in quanto non sarebbe "loro" che ha apportato la modifica.
Ci sono altre opzioni per gestire questa situazione?
Mi sto appoggiando all'opzione 1 (utenti Ghost) ma non mi sembra giusto - e aggiunge anche complessità in quanto la logica del caso speciale dovrebbe impostare il valore della chiave esterna non per l'utente corrente, ma per il fantasma -utente, se l'utente corrente appartiene a un titolare dell'amministratore:
resource.CreatedByUserId = currentUser.TenantId == resource.TenantId ? currentUser.UserId ? getGhostUserId( resource.TenantId )