Come modellare in modo efficace l'ereditarietà in un database?


131

Quali sono le migliori pratiche per modellare l'ereditarietà nei database?

Quali sono i compromessi (ad esempio queriability)?

(Sono più interessato a SQL Server e .NET, ma voglio anche capire come altre piattaforme affrontano questo problema.)


14
Se sei interessato alle "migliori pratiche", la maggior parte delle risposte sono semplicemente errate. Le migliori pratiche impongono che RDb e l'app siano indipendenti; hanno criteri di progettazione completamente diversi. Pertanto, la "modellazione dell'ereditarietà" in un database (o la modellazione di RDb per adattarsi a una singola app o linguaggio di app) è una pessima pratica, non informata, che infrange le regole di progettazione RDb di base e la paralizza.
PerformanceDBA


6
@PerformanceDBA Allora, qual è il tuo suggerimento per evitare l'ereditarietà nel modello DB? Diciamo che abbiamo 50 diversi tipi di insegnanti e che vogliamo collegare quel particolare insegnante con la classe. Come lo faresti senza avere eredità?
svlada,

1
@svlada. Questo è semplice da implementare in un RDb, quindi è richiesta "eredità". Poni una domanda, includi i defn della tabella e un esempio e risponderò in dettaglio. Se lo fai in termini di OO, sarà un casino reale.
PerformanceDBA

Risposte:


162

Esistono diversi modi per modellare l'ereditarietà in un database. Quale scegli dipende dalle tue esigenze. Ecco alcune opzioni:

Tabella per tipo (TPT)

Ogni classe ha il suo tavolo. La classe base contiene tutti gli elementi della classe base e ogni classe che ne deriva ha una propria tabella, con una chiave primaria che è anche una chiave esterna della tabella della classe base; la classe della tabella derivata contiene solo i diversi elementi.

Quindi per esempio:

class Person {
    public int ID;
    public string FirstName;
    public string LastName;
}

class Employee : Person {
    public DateTime StartDate;
}

Si tradurrebbe in tabelle come:

table Person
------------
int id (PK)
string firstname
string lastname

table Employee
--------------
int id (PK, FK)
datetime startdate

Tabella per gerarchia (TPH)

C'è una singola tabella che rappresenta tutta la gerarchia dell'ereditarietà, il che significa che molte delle colonne saranno probabilmente sparse. Viene aggiunta una colonna discriminatore che indica al sistema che tipo di riga è.

Date le classi sopra, si finisce con questa tabella:

table Person
------------
int id (PK)
int rowtype (0 = "Person", 1 = "Employee")
string firstname
string lastname
datetime startdate

Per tutte le righe di tipo 0 (Persona), la data di inizio sarà sempre nulla.

Tavolo per calcestruzzo (TPC)

Ogni classe ha una propria tabella completamente formata senza riferimenti ad altre tabelle.

Date le classi sopra, si finisce con queste tabelle:

table Person
------------
int id (PK)
string firstname
string lastname

table Employee
--------------
int id (PK)
string firstname
string lastname
datetime startdate

23
"Quale scegli dipende dalle tue esigenze", ti preghiamo di elaborare, poiché ritengo che le ragioni delle scelte costituiscano il nocciolo della domanda.
Alex

12
Vedi il mio commento sulla domanda. L'uso di nuovi nomi divertenti per termini tecnici Rdb che sono esistiti porta alla confusione. "TPT" è sottotipo di supertipo. "TPH" è non normalizzato, un errore grave. "TPH" è ancora meno normalizzato, un altro errore grave.
PerformanceDBA

45
Solo un DBA potrebbe presumere che la denormalizzazione sia sempre un errore. :)
Brad Wilson,

7
Mentre ammetterò che la denormalizzazione comporta in alcuni casi un aumento delle prestazioni, ciò è interamente dovuto a una separazione incompleta (o inesistente) tra la struttura logica e fisica dei dati nel DBMS. Sfortunatamente la maggior parte dei DBMS commerciali soffre di questo problema. @PerformanceDBA è corretto. La sotto-normalizzazione è un errore di giudizio, sacrificando la coerenza dei dati per la velocità. Purtroppo, è una scelta che un DBA o uno sviluppatore non avrebbero mai bisogno di fare se il DBMS fosse progettato correttamente. Per la cronaca, non sono un DBA.
Kenneth Cochran,

6
@ Brad Wilson. Solo uno sviluppatore denormalizzerebbe, "per prestazioni", o altrimenti. Spesso, non è de-normalizzazione, la verità è che è non normalizzata. Che de-normalizzazione o non normalizzata sia un errore, è un dato di fatto, supportato dalla teoria e sperimentato da milioni di persone, non è una "presunzione".
PerformanceDBA

133

La corretta progettazione del database non è come la corretta progettazione degli oggetti.

Se stai pianificando di utilizzare il database per qualcosa di diverso dalla semplice serializzazione dei tuoi oggetti (come report, query, uso multi-applicazione, business intelligence, ecc.) Allora non consiglio alcun tipo di semplice mappatura dagli oggetti alle tabelle.

Molte persone pensano a una riga in una tabella di database come un'entità (ho passato molti anni a pensare in quei termini), ma una riga non è un'entità. È una proposta Una relazione di database (ovvero tabella) rappresenta una dichiarazione di fatto sul mondo. La presenza della riga indica che il fatto è vero (e viceversa, la sua assenza indica che il fatto è falso).

Con questa comprensione, puoi vedere che un singolo tipo in un programma orientato agli oggetti può essere memorizzato in una dozzina di relazioni diverse. E una varietà di tipi (uniti per eredità, associazione, aggregazione o completamente non affiliati) possono essere parzialmente archiviati in una singola relazione.

È meglio chiedersi, quali fatti vuoi archiviare, a quali domande vuoi rispondere, quali rapporti vuoi generare.

Una volta creato il progetto DB corretto, è semplice creare query / viste che consentano di serializzare i propri oggetti su tali relazioni.

Esempio:

In un sistema di prenotazione alberghiera, potrebbe essere necessario memorizzare il fatto che Jane Doe ha una prenotazione per una camera al Seaview Inn per il 10-12 aprile. È un attributo dell'entità cliente? È un attributo dell'entità hotel? È un'entità di prenotazione con proprietà che includono clienti e hotel? Potrebbe essere una o tutte queste cose in un sistema orientato agli oggetti. In un database, non è nessuna di queste cose. È semplicemente un dato di fatto.

Per vedere la differenza, considera le seguenti due query. (1) Quante prenotazioni alberghiere ha Jane Doe per il prossimo anno? (2) Quante camere sono prenotate per il 10 aprile al Seaview Inn?

In un sistema orientato agli oggetti, query (1) è un attributo dell'entità cliente e query (2) è un attributo dell'entità hotel. Quelli sono gli oggetti che espongono quelle proprietà nelle loro API. (Anche se, ovviamente, i meccanismi interni con cui vengono ottenuti quei valori possono comportare riferimenti ad altri oggetti.)

In un sistema di database relazionale, entrambe le query esaminerebbero la relazione di prenotazione per ottenere i loro numeri e concettualmente non è necessario preoccuparsi di qualsiasi altra "entità".

Pertanto, è tentando di archiviare fatti sul mondo, piuttosto che tentare di archiviare entità con attributi, che viene costruito un database relazionale adeguato. E una volta che è stato progettato in modo appropriato, è possibile costruire facilmente query utili che non sono state ancora sognate durante la fase di progettazione, poiché tutti i fatti necessari per soddisfare tali domande si trovano al loro posto.


12
+1 Infine, un'isola di autentica conoscenza in un mare di ignoranza (e rifiuto di imparare qualsiasi cosa al di fuori del loro ambito). D'accordo, non è magico: se RDb è progettato usando principi RDb, è facile "mappare" o "proiettare" qualsiasi "classe". Costringere RDb a requisiti di classe è semplicemente errato.
PerformanceDBA

2
Risposta interessante. Come suggeriresti di modellare l'esempio Persona-Dipendente nella risposta accettata?
sevenforce,

2
@ sevenforce-Il design del DB dipende in realtà dai requisiti del sistema, che non sono indicati. Non ci sono abbastanza informazioni fornite per decidere. In molti casi può essere appropriato qualcosa di simile al design "table-per-type", se non seguito in modo slavish. Ad esempio, la data di inizio è probabilmente una buona proprietà per un oggetto Employee, ma nel database dovrebbe davvero essere un campo nella tabella di impiego, poiché una persona potrebbe essere assunta più volte con più date di inizio. Questo non ha importanza per gli oggetti (che utilizzerebbero il più recente), ma è importante nel database.
Jeffrey L Whitledge,

2
Certo, la mia domanda riguardava principalmente il modo di modellare l'eredità. Ci scusiamo per non essere stato abbastanza chiaro. Grazie. Come hai detto, molto probabilmente dovrebbe esserci una Employmenttabella, che raccoglie tutti gli impieghi con le loro date di inizio. Quindi, se conoscere l'attuale data di inizio del lavoro di un Employerè importante, questo potrebbe essere un caso d'uso corretto per un View, che include quella proprietà eseguendo una query? (nota: sembra a causa del '-' subito dopo il mio nick non ho ricevuto alcuna notifica sul tuo commento)
sevenforce

5
Questa è una vera gemma di risposta. Avrà bisogno di un po 'di tempo per affondare davvero e richiedere un po' di esercizio per ottenere il giusto risultato, ma ha già influenzato il mio processo di pensiero sulla progettazione di database relazionali.
MarioDS

9

Risposta breve: non lo fai.

Se hai bisogno di serializzare i tuoi oggetti, usa un ORM, o ancora meglio qualcosa come activerecord o prevaylence.

Se è necessario archiviare i dati, archiviarli in modo relazionale (facendo attenzione a ciò che si sta archiviando e prestando attenzione a ciò che Jeffrey L Whitledge ha appena detto), non uno influenzato dalla progettazione degli oggetti.


3
+1 Il tentativo di modellare l'ereditarietà in un database è uno spreco di buone risorse relazionali.
Daniel Spiewak,

7

I modelli TPT, TPH e TPC sono i modi in cui vai, come menzionato da Brad Wilson. Ma un paio di note:

  • le classi figlio che ereditano da una classe base possono essere viste come entità deboli nella definizione della classe base nel database, nel senso che sono dipendenti dalla loro classe base e non possono esistere senza di essa. Ho visto molte volte che ID univoci sono archiviati per ogni tabella figlio mantenendo l'FK nella tabella padre. Un FK è appena sufficiente ed è ancora meglio avere l'abilitazione in cascata on-delete per la relazione FK tra le tabelle figlio e base.

  • In TPT, vedendo solo i record della tabella di base, non è possibile trovare quale classe figlio rappresenta il record. Questo a volte è necessario, quando si desidera caricare un elenco di tutti i record (senza farlo select su ogni tabella figlio). Un modo per gestirlo è quello di avere una colonna che rappresenta il tipo della classe figlio (simile al campo rowType nel TPH), mescolando così il TPT e il TPH in qualche modo.

Supponiamo di voler progettare un database che contenga il seguente diagramma della classe di forma:

public class Shape {
int id;
Color color;
Thickness thickness;
//other fields
}

public class Rectangle : Shape {
Point topLeft;
Point bottomRight;
}

public class Circle : Shape {
Point center;
int radius;
}

La progettazione del database per le classi precedenti può essere così:

table Shape
-----------
int id; (PK)
int color;
int thichkness;
int rowType; (0 = Rectangle, 1 = Circle, 2 = ...)

table Rectangle
----------
int ShapeID; (FK on delete cascade)
int topLeftX;
int topLeftY;
int bottomRightX;
int bottomRightY;

table Circle
----------
int ShapeID; (FK on delete cascade)  
int centerX;
int center;
int radius;

4

Esistono due tipi principali di ereditarietà che è possibile impostare in un DB, tabella per entità e tabella per Gerarchia.

Tabella per entità è dove si dispone di una tabella di entità di base che ha proprietà condivise di tutte le classi figlio. Quindi, per classe figlio si dispone di un'altra tabella ciascuna con solo le proprietà applicabili a quella classe. Sono collegati 1: 1 dai loro PK

testo alternativo

Tabella per gerarchia è dove tutte le classi hanno condiviso una tabella e le proprietà opzionali sono nullable. Il loro è anche un campo discriminatore che è un numero che indica il tipo attualmente detenuto dal record

testo alternativo SessionTypeID è discriminatore

Il target per gerarchia è più veloce da interrogare poiché non è necessario unire (solo il valore del discriminatore), mentre il target per entità è necessario eseguire join complessi per rilevare di che tipo è qualcosa e recuperare tutti i suoi dati.

Modifica: le immagini che mostro qui sono schermate di un progetto a cui sto lavorando. L'immagine dell'asset non è completa, quindi la sua svuotamento, ma era principalmente per mostrare come la sua configurazione, non cosa mettere nei tuoi tavoli. Questo dipende da te ;). La tabella delle sessioni contiene informazioni sulla sessione di collaborazione virtuale e può essere di diversi tipi di sessioni a seconda del tipo di collaborazione coinvolto.


Considererei anche la classe Target per Concrete non proprio modellare bene l'ereditarietà e quindi non ho mostrato.
Mattlant,

Potresti aggiungere un riferimento da dove proviene l'illustrazione?
chryss,

Dove sono le immagini di cui stai parlando alla fine della tua risposta?
Musa Haidari,

1

Dovresti normalizzare il tuo database e questo rispecchierebbe effettivamente la tua eredità. Potrebbe avere un peggioramento delle prestazioni, ma è così con la normalizzazione. Probabilmente dovrai usare il buon senso per trovare l'equilibrio.


2
perché le persone credono che la normalizzazione di un database degrada le prestazioni? la gente pensa anche che il principio DRY degrada le prestazioni del codice? da dove viene questa errata percezione?
Steven A. Lowe,

1
Forse perché la denormalizzazione può migliorare le prestazioni, quindi la normalizzazione la degrada, relativamente parlando. Non posso dire che sono d'accordo, ma è probabilmente così.
Matthew Scharley,

2
All'inizio, la normalizzazione potrebbe avere un piccolo effetto sulle prestazioni, ma nel tempo, con l'aumentare del numero di righe, i JOIN efficienti inizieranno a sovraperformare le tabelle più voluminose. Naturalmente, la normalizzazione ha altri, maggiori vantaggi: coerenza e mancanza di ridondanza, ecc.
Rob,

1

ripetizione di una risposta di thread simile

nel mapping OR, l'ereditarietà viene mappata su una tabella principale in cui le tabelle padre e figlio utilizzano lo stesso identificatore

per esempio

create table Object (
    Id int NOT NULL --primary key, auto-increment
    Name varchar(32)
)
create table SubObject (
    Id int NOT NULL  --primary key and also foreign key to Object
    Description varchar(32)
)

SubObject ha una relazione di chiave esterna con Object. quando si crea una riga Oggetto secondario, è necessario innanzitutto creare una riga Oggetto e utilizzare l'ID in entrambe le righe

EDIT: se stai cercando anche di modellare il comportamento, avresti bisogno di una tabella Type che elenca le relazioni di ereditarietà tra le tabelle e specifichi l'assembly e il nome della classe che ha implementato il comportamento di ogni tabella

sembra eccessivo, ma tutto dipende da cosa vuoi usarlo!


Quella discussione finì per riguardare l'aggiunta di un paio di colonne in ogni tabella, non sulla modellazione dell'ereditarietà. Penso che il titolo di quella discussione debba essere cambiato per riflettere meglio la natura della domanda e della discussione.
Anche Mien,

1

Utilizzando SQL ALchemy (Python ORM), è possibile eseguire due tipi di ereditarietà.

Quello che ho avuto esperienza è usare una tabella di singoli e avere una colonna discriminante. Ad esempio, un database di pecore (niente scherzo!) Memorizzava tutte le pecore in una tabella e Rams ed Ewes venivano gestiti usando una colonna di genere in quella tabella.

Pertanto, è possibile eseguire una query per tutte le pecore e ottenere tutte le pecore. Oppure puoi eseguire una query solo per Ram e otterrà solo Rams. Puoi anche fare cose come avere una relazione che può essere solo un ariete (cioè il padre di una pecora) e così via.


1

Si noti che alcuni motori di database forniscono già meccanismi di ereditarietà nativamente come Postgres . Guarda la documentazione .

Ad esempio, si dovrebbe interrogare il sistema Persona / Dipendente descritto in una risposta sopra come questo:

  / * Questo mostra il nome di tutte le persone o dipendenti * /
  SELEZIONA nome DA persona; 

  / * Mostra la data di inizio di tutti i dipendenti * /
  SELEZIONA data di inizio DA Dipendente;

In questo è la scelta del tuo database, non devi essere particolarmente intelligente!

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.