Affrontare il fatto che le chiavi primarie non fanno parte del dominio aziendale


25

In quasi tutte le circostanze, le chiavi primarie non fanno parte del dominio aziendale. Certo, potresti avere alcuni importanti oggetti rivolti all'utente con indici univoci ( UserNameper utenti o OrderNumberper ordini) ma nella maggior parte dei casi, non è necessario che le aziende identifichino apertamente gli oggetti di dominio con un singolo valore o insieme di valori, a chiunque ma forse un utente amministrativo. Anche in questi casi eccezionali, specialmente se si utilizzano identificatori univoci globali (GUID) , si desidera o si desidera utilizzare una chiave alternativa anziché esporre la chiave primaria stessa.

Quindi, se la mia comprensione del design guidato dal dominio è accurata, le chiavi primarie non devono e quindi non devono essere esposte e una buona liberazione. Sono brutti e restringono il mio stile. Ma se scegliamo di non includere le chiavi primarie nel modello di dominio, ci sono conseguenze:

  1. Ingenuamente, gli oggetti di trasferimento dati (DTO) derivati ​​esclusivamente da combinazioni di modelli di dominio non avranno chiavi primarie
  2. I DTO in arrivo non avranno una chiave primaria

Quindi, è sicuro di dire che se vuoi davvero rimanere puro ed eliminare le chiavi primarie nel tuo modello di dominio, dovresti essere pronto a gestire ogni richiesta in termini di indici univoci su quella chiave primaria?

Detto in altro modo, quale delle seguenti soluzioni è l'approccio corretto per gestire l'identificazione di oggetti particolari dopo aver rimosso PK nei modelli di dominio?

  1. Essere in grado di identificare gli oggetti che devi affrontare con altri attributi
  2. Ripristino della chiave primaria nel DTO; vale a dire, eliminare il PK durante il mapping dalla persistenza al dominio, quindi ricombinare il PK durante il mapping dal dominio al DTO?

EDIT: Facciamo questo concreto.

Dire il mio modello di dominio è VoIPProviderche comprende campi come Name, Description, URL, così come riferimenti, come ProviderType, PhysicalAddresse Transactions.

Ora diciamo che voglio costruire un servizio web che consentirà agli utenti privilegiati di gestire VoIPProvideri messaggi di posta elettronica .

Forse un ID intuitivo è inutile in questo caso; dopo tutto, i provider VoIP sono aziende i cui nomi tendono ad essere distinti in senso informatico e persino abbastanza distinti in senso umano per motivi di lavoro. Quindi potrebbe essere sufficiente dire che un unico VoIPProviderè completamente determinato da (Name, URL). Quindi ora diciamo che ho bisogno di un metodo in PUT api/providers/voipmodo che gli utenti privilegiati possano aggiornare i VoIPprovider. Inviano a VoIPProviderDTO, che include molti ma non tutti i campi da VoIPProvider, incluso alcuni potenzialmente appiattimento. Tuttavia, non riesco a leggere le loro menti e devono ancora dirmi di quale provider stiamo parlando.

Sembra che io abbia 2 (forse 3) opzioni:

  1. Includere una chiave primaria o alternativa nel mio modello di dominio e inviarlo al DTO e viceversa
  2. Identifica il fornitore a cui teniamo tramite l'indice univoco, ad esempio (Name, Url)
  3. Introduci una sorta di oggetto intermedio che può sempre mappare tra livello di persistenza, dominio e DTO in un modo che non esponga i dettagli di implementazione sul livello di persistenza, ad esempio introducendo un identificatore temporaneo in memoria quando passi da dominio a DTO e viceversa,

1
Punta a riflettere: spesso le comunicazioni con gli esperti del dominio si impoveriscono quando si utilizza la PK surrogata quando esiste una buona chiave aziendale. Sembra che finiamo per lavorare per il framework ORM, invece che viceversa.
Tulains Córdova,

@ user61852 bene indipendentemente da ORM, anche se sei veramente di basso livello, hai ancora bisogno di una chiave primaria nell'implementazione del livello di database. Pertanto, sono d'accordo sul fatto che il surrogato PK ti offra vantaggi rispetto all'effettivo PK utilizzato da uno specifico meccanismo di persistenza, ma se tale PK rappresenta davvero un oggetto di business che è significativo, è necessariamente unico e quindi ha almeno una proprietà di business unica che definisce no?
tacos_tacos_tacos,

1
Tutti i vantaggi dei surrogati sono legati al computer e nessuno all'uomo.
Tulains Córdova,

2
@ user61852: sono d'accordo al 100% (ho scritto qualcosa di diverso?). Per i mezzi di comunicazione, utilizzare una "chiave aziendale". Aggiungi contorni univoci anche per qualsiasi chiave aziendale. Ma evita l'uso delle chiavi aziendali per implementare effettivamente i riferimenti al tuo database.
Doc Brown,

2
le chiavi aziendali sono eternamente uniche - fino a quando non lo sono. Se si utilizzano le chiavi aziendali come primarie quando ciò accade, la modifica delle regole aziendali interrompe più elementi.
psr

Risposte:


31

Questo è il modo in cui lo risolviamo (da più di 15 anni, quando non è stato inventato nemmeno il termine "domain driven design"):

  • quando si mappa il modello di dominio su un'implementazione del database o un modello di classe in un linguaggio di programmazione specifico, si ha una regola semplice e coerente come "per ogni oggetto di dominio mappato a una tabella relazionale, la chiave primaria è" TablenameID ".
  • questa chiave primaria è completamente artificiale, ha sempre lo stesso tipo e nessun significato commerciale - solo una chiave surrogata
  • la "versione grafica" del tuo modello di dominio (quella che usi per parlare con i tuoi esperti di dominio) non contiene chiavi primarie. Non li esponi direttamente agli esperti (ma li esponi a chiunque stia effettivamente implementando il codice per il sistema).

Quindi ogni volta che hai bisogno di una chiave primaria per scopi tecnici (come mappare le relazioni con un database), ne hai una disponibile, ma finché non vuoi "vederla", cambia il tuo livello di astrazione nel "modello di esperti di dominio" ". E non devi mantenere "due modelli" (uno con PK e uno senza); invece, mantieni solo un modello senza PK e usa un generatore di codice per creare il DDL per il tuo DB, che aggiunge automaticamente il PK secondo le regole di mappatura.

Si noti che ciò non proibisce l'aggiunta di "chiavi aziendali" come un "OrderNumber" aggiuntivo, oltre al surrogato OrderID. Tecnicamente queste chiavi aziendali diventano chiavi alternative durante il mapping al database. Evita semplicemente di usarli per creare riferimenti ad altre tabelle, preferisci sempre usare le chiavi surrogate se possibile, questo renderà le cose molto più facili.

Per il tuo commento: l'utilizzo di una chiave surrogata per identificare i record non è un'operazione correlata al business, è un'operazione puramente tecnica. Per chiarire questo, guarda il tuo esempio: finché non definisci ulteriori contrapposizioni univoche, sarebbe possibile avere due oggetti VoIPProvider con la stessa combinazione di (nome, url), ma VoIPProviderID diversi.


Che dire del passaggio di prendere l'oggetto di dominio dall'oggetto di persistenza restituito (entità, modello di riga di tabella o altro), passare a DTO, ripristinarlo e tornare alla persistenza? Questo viene fatto semplicemente tramite una chiave surrogata (ovvero una definizione di unicità orientata al business) che richiede una risoluzione su ciascuna operazione di persistenza?
tacos_tacos_tacos,

1
@tacos_tacos_tacos: atteniamoci al tuo esempio VoIPProvider. Vorrei effettivamente aggiungere un "VoIPProviderID" al tuo DTO, almeno dal "lato dell'implementazione" (se hai anche una versione grafica per i tuoi esperti di dominio, probabilmente non lo mostrerei lì). Ai fini dell'aggiornamento, il modo standard di identificare un VoIPProvider specifico dovrebbe essere tramite il "VoIPProviderID" recuperato quando si estraggono i dati dal database. Se gli utenti della tua API preferiscono l'identificazione per (nome, URL), forniscilo in aggiunta. ...
Doc Brown,

... e se le prestazioni sembrano diventare un problema reale e misurabile, puoi anche considerare di memorizzare nella cache il mapping (nome, url) su VoIPProviderID da qualche parte. Ma non consiglierei di implementare una tale ottimizzazione in anticipo, prematuramente.
Doc Brown,

1
Le chiavi naturali a volte sembrano davvero avvincenti, ma è troppo facile essere bruciate (ad es. "Whoops, ora ho più inquilini e non è più unico").
Casey,

1
@corsiKa: nel contesto di ciò che l'OP ha chiesto, raccomanderei caldamente di avere una chiave puramente auto-generata "OrderID" (che non è stampata su alcuna ricevuta, ma usata solo per cose interne come riferimenti di database) e una chiave aziendale separata "OrderNumber" (che può, ad esempio, contenere qualcosa come l'anno in corso, che può essere utilizzato per l'ordinamento e il filtro, che può essere modificato / corretto in seguito e che può essere stampato sulle ricevute). OP ha chiesto "Domain Driven Design", "OrderNumber" fa parte del modello di dominio, mentre "OrderID" è solo un dettaglio di implementazione.
Doc Brown,

4

Devi essere in grado di identificare molti oggetti con un indice univoco, e non è quello che è presente una chiave primaria (o almeno implica che ne sia presente una).

Sono disponibili indici univoci in modo da poter limitare ulteriormente lo schema del DB, non come sostituto all'ingrosso dei PK. Se non stai esponendo i PK perché sono brutti, ma stai invece esponendo una chiave unica ... in realtà non stai facendo nulla di diverso. (Suppongo che non stai mescolando un PK e una colonna identità qui?)


Quando desidero eseguire un'operazione che coinvolge un'istanza specifica di un oggetto di dominio persistente, devo essere in grado di includere in modo esplicito nel DTO (tramite una sorta di chiave, ID primario o alternativo user-friendly univoco a quella tabella e potrebbe anche essere un indice sulla tabella) o implicitamente (tramite una combinazione di campi i cui valori identificano in modo univoco un record specifico) ... e inoltre, in senso pratico, avrei bisogno di inviare nella DTO qualche forma di tracciamento delle modifiche se dovessi farlo diversamente (OriginalVal vs NewVal per tutti i campi che identificano il record), no?
tacos_tacos_tacos,

la domanda esplicita v implicita non è la stessa differenza? Puoi avere un PK che si estende su più colonne, proprio come un indice univoco. Non vedo alcuna differenza tra loro per i tuoi scopi.
gbjbaanb,

Certo, potremmo avere un PK su più colonne per esempio. Ma per me sta perdendo qualcosa sulla banca dati (la memoria) che non dovrebbe avere NIENTE a che fare con il cuore e l'anima, l'entità aziendale. Se una tupla di campi di entità aziendale capita di estendere il PK per il DB, allora grande. Ma non dovrebbe essere necessariamente il contrario, vero?
tacos_tacos_tacos,

Lo stai pensando troppo. Un indice univoco è tanto un artefatto dello schema DB quanto un PK. Pensala in questo modo: un PK è solo il primo (o primario) indice univoco. è "speciale" perché in genere è necessario solo 1 di tali indici.
gbjbaanb,

Vero, ma qualsiasi oggetto di dominio significativo dovrebbe essere identificabile rigorosamente da almeno uno dei suoi campi relativi all'attività, no? Il fatto che questo definisca un indice nel DB è più per motivi di prestazioni che da usare per interrogare facilmente il DB ... Preferirei un PK a una colonna rispetto a un indice univoco a 6 colonne, e servono davvero a scopi diversi - un PK (o indice con un numero limitato di campi) è disponibile anche per comodità del DBA / DBD, giusto?
tacos_tacos_tacos,

4

Senza chiavi primarie nel frontend non esiste un modo semplice per il backend di sapere cosa si sta inviando. Per risolvere il problema, avrai bisogno di un sacco di lavoro extra per analizzare i dati, il che danneggerebbe le prestazioni e probabilmente richiederebbe più tempo ed essere più brutto che collegare una chiave a ogni articolo.

Ad esempio, diciamo che voglio modificare un messaggio in un'app; come farebbe l'app a sapere quale messaggio voglio modificare senza una chiave primaria allegata? La modifica degli oggetti avviene sempre e farlo senza chiavi è pressoché impossibile. Ma se hai oggetti che non dovrebbero essere modificati, salta la chiave se pensi che sia fonte di distrazione, ma avere chiavi primarie può migliorare le prestazioni qui.


Ebbene, il messaggio è un esempio estremo, ma anche allora sapremmo MessageSender, MessageRecipient, TimeSent- che dovrebbe essere unico.
tacos_tacos_tacos,

1
@tacos_tacos_tacos, come si creano gli FK nelle altre tabelle? Dovrebbe essere MessageSenderId, che probabilmente si associa a una tabella Users su UserId. Non vorrai usare UserName come chiave tra le tabelle, poiché ciò potrebbe cambiare e diventare un incubo per la manutenzione. Questo è il motivo per cui generalmente si uniscono le tabelle solo usando le chiavi primarie e non un'altra colonna (ci sono certamente delle eccezioni). Questa struttura di db deve ancora essere applicata. Ora puoi sempre andare a un modello CQRS per la tua app ... nel qual caso le regole cambiano. Soprattutto se si utilizza anche il sourcing di eventi.
CaffGeek,

4

Il motivo per cui utilizziamo un PK non commerciale è quello di garantire che il nostro sistema abbia un metodo semplice e coerente per determinare ciò che l'utente desidera.

Vedo che hai risposto con un commento: MessageSender, MessageRecipient, TimeSent (per un messaggio). Puoi ANCORA avere ambiguità in questo modo (ad esempio, con i messaggi generati dal sistema che si attivano su qualcosa che accade spesso). E come hai intenzione di convalidare MessageSender e MessageRecipient qui? Supponiamo che tu li convalidi usando FirstName, Lastname, DateOfBirth, alla fine ti imbatterai in una situazione in cui hai 2 persone nate nello stesso giorno con lo stesso identico nome. Per non parlare del fatto che ti imbatterai in una situazione in cui hai un messaggio chiamato tacostacostacos-America-1980-Doc Brown-France-1965-23/5/2014-11:43:54.003UTC+200. È un mostro di un nome e non hai ancora alcuna garanzia che ne avrai solo 1.

il motivo per cui utilizziamo una chiave primaria è perché SAPPIAMO che sarà unica per la durata del software, indipendentemente dai dati immessi, e SAPPIAMO che sarà un formato prevedibile (cosa succede se la tua chiave sopra ha un trattino in un nome utente? l'intero sistema va a merda).

Non è necessario mostrare il proprio ID all'utente. Puoi nasconderlo (in bella vista se necessario, attraverso l'URL).

Un altro motivo per cui un PK è così utile è qualcosa che puoi dedurre da quanto sopra: un PK lo rende in modo da non dover forzare il computer a interpretare il codice generato dall'utente. Se un utente cinese usa il tuo codice e inserisce un gruppo di caratteri cinesi, il tuo codice improvvisamente non deve essere in grado di lavorare con questi internamente, ma può semplicemente usare la Guida generata dal sistema. Se hai un utente arabo che inserisce la scrittura araba, il tuo sistema non deve farcela internamente, ma fondamentalmente può ignorare che sono lì.

Come altri hanno già detto, un Guid è qualcosa che può essere archiviato internamente in una dimensione fissa. Sai con cosa lavori ed è qualcosa che può essere universalmente usato. Non è necessario creare regole di progettazione su come creare un determinato identificatore e salvarlo. Se il tuo sistema accetta solo le prime 10 lettere di un nome, non vedrà alcuna differenza tra Michael Guggenheimer e Michael Gugstein e confonderà questi 2. Se lo tagli a qualsiasi lunghezza arbitraria, puoi confonderti. Se si limita l'input dell'utente, è possibile riscontrare problemi con le limitazioni dell'utente.

Quando guardo i sistemi esistenti come Dynamics CRM, usano anche la chiave interna (PK) per consentire all'utente di richiamare un singolo record. Se un utente ha una query che non coinvolge l'ID, restituisce un array di possibili risposte e lascia che l'utente scelga da esso. Se c'è qualche possibilità di ambiguità, daranno la scelta all'utente.

Infine, è anche un po 'di sicurezza attraverso l'oscurità. Se non conosci l'ID record, l'unica opzione è indovinarlo. se l'ID è facile da indovinare (poiché le informazioni di cui è fatto sono pubblicamente disponibili), chiunque può modificarlo. Puoi persino spingere l'utente a cambiarlo attraverso i classici metodi CSRF o XSS. Ora, ovviamente, la tua sicurezza dovrebbe già aver reso conto e mitigare quelli prima di pubblicare la versione live, ma dovresti comunque rendere più difficile il verificarsi di potenziali abusi.


1

Quando si emette un identificatore per un sistema esterno, è necessario fornire solo URI o, in alternativa, una chiave o un set di chiavi con le stesse proprietà di un URI, anziché esporre direttamente la chiave primaria del database (da qui in poi farò riferimento a sia URI che una chiave o un insieme di chiavi che hanno le stesse proprietà dell'URI del solo URI, in altre parole un URI di seguito non significa necessariamente URI RFC 3986).

Un URI può contenere o meno la chiave primaria dell'oggetto e può o meno essere effettivamente composto da chiavi alternative. Non importa davvero. Ciò che conta è che solo il sistema che genera l'URI è autorizzato a dividere o combinare l'URI per comprendere quale sia l'oggetto riferito. I sistemi esterni devono sempre utilizzare l'URI come identificatore opaco. Non importa se un utente umano è in grado di identificare che una parte dell'URI è in realtà una chiave surrogata del database o che è costituita da più chiavi aziendali raggruppate insieme o che in realtà è una base64 di tali valori. Questi sono irrilevanti. Ciò che conta è che non è necessario richiedere al sistema esterno di comprendere cosa significa l'identificatore per utilizzare l'identificatore. Ai sistemi esterni non dovrebbe mai essere richiesto di analizzare i componenti all'interno dell'identificatore o di combinare un identificatore con altri identificatori per fare riferimento a qualcosa nel sistema.

L'uso di GUID soddisfa alcuni di questi criteri, tuttavia identificatore come GUID può essere difficile da riproporre nell'oggetto anche all'interno del sistema, quindi le chiavi che sono opache anche nel tuo sistema come un GUID dovrebbero essere impiegate solo se il client analizza effettivamente l'URI / identificatore rappresenta un rischio per la sicurezza.

Tornando al tuo esempio VoIP, supponi che un provider VoIP possa essere determinato in modo univoco da (VoIPProviderID) o da (Nome, URL) o da (GUID). Quando un sistema esterno deve aggiornare il provider VoIP, può semplicemente passare un PUT / provider / by-id / 1234 o PUT /provider/foo-voip/bar-domain.comoppure PUT /3F2504E0-4F89-41D3-9A0C-0305E82C3301il sistema capirà che il sistema esterno desidera aggiornare il Provider VoIP. Questi URI sono generati dal tuo sistema e solo il tuo sistema deve capire che tutti significano la stessa cosa. Il sistema esterno dovrebbe semplicemente considerare qualunque cosa sia nell'URI come sostanzialmente un PUT <whatever>.

Supponiamo di avere i dati per diversi provider VoIP memorizzati in diverse tabelle con schemi diversi (quindi un set di chiavi completamente diverso identifica ciascun provider VoIP in base alla tabella in cui sono memorizzati). Quando si dispone di un URI, è possibile accedervi in ​​modo uniforme dal sistema esterno, indipendentemente da come il sistema identifica il particolare provider VoIP. Per il sistema esterno è tutto solo un puntatore opaco.

Quando il tuo sistema utilizza un URI per fare riferimento agli oggetti in questo modo, non stai perdendo nulla riguardo al modo in cui implementi il ​​tuo sistema. Si genera l'URI e il client semplicemente glielo restituisce.


0

Dovrò prendere di mira questa affermazione selvaggiamente inaccurata e ingenua:

Forse un ID intuitivo è inutile in questo caso; dopo tutto, i provider VoIP sono aziende i cui nomi tendono ad essere distinti in senso informatico e persino abbastanza distinti in senso umano per motivi di lavoro.

I nomi sono terribili come chiavi, poiché cambiano spesso. Una società può avere molti nomi durante la sua vita, la società può unire, dividere, unire di nuovo, creare una filiale distinta a fini fiscali specifici che ha 0 dipendenti ma tutti i clienti che assumono dipendenti da una filiale completamente diversa.

Quindi entriamo nel fatto che i nomi delle aziende non sono nemmeno lontanamente univoci, come mostrato dal punto di riferimento Apple vs Apple .

Un buon mappatore o framework relazionale di oggetti dovrebbe sottrarre le chiavi primarie e renderle invisibili, ma sono lì e di solito saranno l' unico modo per identificare in modo univoco un oggetto nel database.

Per riferimento, preferisco come django gestisce questo:

class VoipProvider(models.Model):
    name=fields.TextField()
    address=fields.TextField()

class Customer(models.Model):
    name=fields.TextField()
    address=fields.TextField()
    voipProvider=fields.ForeignKeyField(VoipProvider)

In questo modo, i dettagli di un fornitore di un cliente possono accedere al codice utilizzando:

myCustomer.voipProvider.name #returns the name of the customers VOIP Provider.

Mentre le chiavi primarie / esterne non sono visibili, sono presenti e possono essere utilizzate per accedere agli elementi, ma vengono sottratte.


Hai assolutamente ragione, ma suppongo che intendessi dire che "in alcuni domini, forse c'è una naturale tupla di valori che sono sempre unici" Se cambiano, ma sono ancora unici, identificano comunque un singolo record.
tacos_tacos_tacos,

0

Penso che spesso guardiamo ancora in modo errato questo problema dal punto di vista del DB: oh, non esiste una chiave naturale, quindi dobbiamo creare una chiave surrogata. Oh no, non possiamo esporre la chiave surrogata negli oggetti del dominio, che perde ecc.

Tuttavia, a volte un atteggiamento migliore è questo: se un oggetto business (dominio) non ha una chiave naturale, allora forse dovrebbe esserne dato uno. Questo è un duplice problema del dominio aziendale: in primo luogo, le cose hanno bisogno di un'identità, anche in assenza di database. In secondo luogo, sebbene cerchiamo di fingere che la persistenza sia un'idea astratta invisibile al dominio, la realtà è che la persistenza è ancora un concetto di business. Ovviamente, ci sono problemi in cui la chiave naturale scelta non è supportata dal DB come chiave primaria (ad es. GUID su alcuni sistemi) - in questo caso sarà necessario aggiungere una chiave surrogata.

Quindi, finisci in un posto molto simile, ad esempio il tuo cliente ha un ID intero, ma invece di sentirti male perché è trapelato dal DB al dominio, ti senti felice perché l'azienda ha concordato che a tutti i clienti verrà assegnato un ID e lo stai persistendo nel DB, come è necessario. Sei ancora libero di presentare un surrogato, ad esempio per supportare la ridenominazione dell'ID cliente.

Questo approccio significa anche che se un oggetto di dominio colpisce il livello di persistenza e non ha un ID, è probabilmente una sorta di oggetto valore, quindi non è necessario un ID.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.