sql >> Databasteknik >  >> RDS >> Database

Referensdatamönstret:utbyggbart och flexibelt

Att ha referenstabeller i din databas är väl ingen stor sak? Du behöver bara knyta en kod eller ID med en beskrivning för varje referenstyp. Men vad händer om du bokstavligen har dussintals och dussintals referenstabeller? Finns det ett alternativ till ett-bord-per-typ-metoden? Läs vidare för att upptäcka en generisk och utbyggbar databasdesign för att hantera alla dina referensdata.

Detta ovanligt utseende diagram är ett fågelperspektiv av en logisk datamodell (LDM) som innehåller alla referenstyper för ett företagssystem. Det är från en utbildningsinstitution, men det kan gälla datamodellen för vilken typ av organisation som helst. Ju större modell, desto fler referenstyper kommer du sannolikt att upptäcka.

Med referenstyper menar jag referensdata, eller uppslagsvärden, eller – om du vill vara flash – taxonomier . Vanligtvis används värdena som definieras här i rullgardinslistor i din applikations användargränssnitt. De kan också visas som rubriker på en rapport.

Denna specifika datamodell hade cirka 100 referenstyper. Låt oss zooma in och titta på bara två av dem.

Från detta klassdiagram ser vi att alla referenstyper utökar Root_Reference_Type . I praktiken betyder detta bara att alla våra referenstyper har samma attribut från Alt_Sequence genom till Type_Key inklusive, som visas nedan.

Attribut Beskrivning
Alt_Sequence Används för att definiera en alternativ sekvens när en icke-alfabetisk ordning krävs.
Description Beskrivningen av typen.
Effective_Period Definierar effektivt om referensposten är aktiverad eller inte. När en referens väl har använts kan den inte tas bort på grund av referensrestriktioner; det kan bara inaktiveras.
Pretty_Name Det vackra namnet på typen. Det här är vad användaren ser på skärmen.
Type_Key Den unika interna KEY för typen. Detta är dolt för användaren men applikationsutvecklare kan använda detta i stor utsträckning i sin SQL.

Typen av parti här är antingen en organisation eller en person. Typerna av kön är manliga och kvinnliga. Så det här är verkligen enkla fall.

Den traditionella referenstabelllösningen

Så hur ska vi implementera den logiska modellen i den fysiska världen av en faktisk databas?

Vi skulle kunna anta att varje referenstyp kommer att mappas till sin egen tabell. Du kan hänvisa till detta som det mer traditionella ett-bord-per-klass lösning. Det är enkelt nog och skulle se ut ungefär så här:




Nackdelen med detta är att det kan finnas dussintals och dussintals av dessa tabeller, alla har samma kolumner, alla gör i stort sett samma sak.

Dessutom kan vi skapa mycket mer utvecklingsarbete . Om ett användargränssnitt för varje typ krävs för att administratörer ska kunna behålla värdena, multipliceras mängden arbete snabbt. Det finns inga hårda och snabba regler för detta – det beror verkligen på din utvecklingsmiljö – så du måste prata med dina utvecklare för att förstå vilken effekt detta har.

Men med tanke på att alla våra referenstyper har samma attribut, eller kolumner, finns det ett mer generiskt sätt att implementera vår logiska datamodell? Ja, det finns! Och det kräver bara två tabeller .

Tvåbordslösningen

Den första diskussionen jag någonsin haft om detta ämne var i mitten av 90-talet, när jag arbetade för ett försäkringsbolag på London Market. Då gick vi direkt till fysisk design och använde mest naturliga/affärsnycklar, inte ID. Där referensdata fanns beslutade vi att behålla en tabell per typ som var sammansatt av en unik kod (VARCHAR PK) och en beskrivning. Faktum är att det fanns mycket färre referenstabeller då. Oftare än inte skulle en begränsad uppsättning affärskoder användas i en kolumn, möjligen med en definierad databaskontrollbegränsning; det skulle inte finnas någon referenstabell alls.

Men spelet har gått vidare sedan dess. Det här är en tvåbordslösning kan se ut så här:




Som du kan se är denna fysiska datamodell väldigt enkel. Men det är helt annorlunda än den logiska modellen, och inte för att något har blivit päronformat. Det beror på att ett antal saker gjordes som en del av fysisk design .

reference_type Tabellen representerar varje enskild referensklass från LDM. Så om du har 20 referenstyper i din LDM har du 20 rader med metadata i tabellen. reference_value Tabellen innehåller de tillåtna värdena för alla referenstyperna.

Vid tidpunkten för detta projekt var det några ganska livliga diskussioner mellan utvecklare. Vissa föredrog tvåbordslösningen och andra föredrog ett-bord-per-typ metod.

Det finns för- och nackdelar för varje lösning. Som du kanske gissar var utvecklarna mestadels oroade över hur mycket arbete användargränssnittet skulle ta. Vissa trodde att det skulle gå ganska snabbt att sätta ihop ett administratörsgränssnitt för varje tabell. Andra trodde att det skulle vara mer komplicerat att bygga ett enda administratörsgränssnitt men att det i slutändan skulle löna sig.

I just detta projekt gynnades tvåbordslösningen. Låt oss titta på det mer i detalj.

Det utvidgbara och flexibla referensdatamönstret

Eftersom din datamodell utvecklas över tiden och nya referenstyper krävs, behöver du inte fortsätta att göra ändringar i din databas för varje ny referenstyp. Du behöver bara definiera nya konfigurationsdata. För att göra detta lägger du till en ny rad i reference_type tabellen och lägg till dess kontrollerade lista över tillåtna värden till reference_value bord.

Ett viktigt koncept i denna lösning är att definiera effektiva tidsperioder för vissa värden. Till exempel kan din organisation behöva fånga ett nytt reference_value av "Proof of ID" som kommer att accepteras vid något framtida datum. Det är en enkel fråga att lägga till det nya reference_value med effective_period_from korrekt inställt datum. Detta kan göras i förväg. Fram till det datumet kommer den nya posten inte att visas i rullgardinsmenyn med värden som användarna av din applikation ser. Detta beror på att din applikation endast visar värden som är aktuella eller aktiverade.

Å andra sidan kan du behöva stoppa användare från att använda ett visst reference_value . I så fall uppdaterar du bara den med effective_period_to korrekt inställt datum. När den dagen har passerat kommer värdet inte längre att visas i rullgardinsmenyn. Den blir inaktiverad från den tidpunkten. Men eftersom den fortfarande fysiskt existerar som en rad i tabellen, upprätthålls referensintegriteten för de tabeller där det redan har refererats.

Nu när vi arbetade med tvåtabellslösningen blev det uppenbart att några ytterligare kolumner skulle vara användbara på reference_type tabell. Dessa fokuserade mest på gränssnittsproblem.

Till exempel, pretty_namereference_type tabellen lades till för användning i användargränssnittet. Det är användbart för stora taxonomier att använda ett fönster med en sökfunktion. Sedan pretty_name kan användas för fönstrets titel.

Å andra sidan, om en rullgardinslista med värden räcker, pretty_name kan användas för LOV-prompten. På liknande sätt kan beskrivning användas i användargränssnittet för att fylla i roll-over-hjälp.

Att ta en titt på vilken typ av konfiguration eller metadata som ingår i dessa tabeller hjälper till att förtydliga saker och ting lite.

Hur man hanterar allt det där

Även om exemplet som används här är väldigt enkelt, kan referensvärdena för ett stort projekt snabbt bli ganska komplexa. Så det kan vara tillrådligt att behålla allt detta i ett kalkylblad. Om så är fallet kan du använda själva kalkylarket för att generera SQL med hjälp av strängsammansättning. Detta klistras in i skript, som exekveras mot måldatabaserna som stödjer utvecklingens livscykel och produktionsdatabasen (live). Detta såddar databasen med alla nödvändiga referensdata.

Här är konfigurationsdata för de två LDM-typerna, Gender_Type och Party_Type :

PROMPT Gender_Type
INSERT INTO reference_type (id, pretty_name, ref_type_key, description, id_range_from, id_range_to)
	VALUES (rety_seq.nextval, 'Gender Type', 'GENDER_TYPE', ' Identifies the gender of a person.', 13000000, 13999999);
INSERT INTO reference_value (id, pretty_name, description, effective_period_from, alt_sequence, reference_type_id)
	VALUES (13000010,'Female', 'Female', TRUNC(SYSDATE), 10, rety_seq.currval);
INSERT INTO reference_value (id, pretty_name, description, effective_period_from, alt_sequence, reference_type_id)
	VALUES (13000020,'Male', 'Male', TRUNC(SYSDATE), 20, rety_seq.currval);

PROMPT Party_Type
INSERT INTO reference_type (id, pretty_name, ref_type_key, description, id_range_from, id_range_to)
	VALUES (rety_seq.nextval, 'Party Type', 'PARTY_TYPE', A controlled list of reference values that identifies the type of party.', 23000000, 23999999);
INSERT INTO reference_value (id, pretty_name, description, effective_period_from, alt_sequence, reference_type_id)
	VALUES (23000010,'Organisation', 'Organisation', TRUNC(SYSDATE), 10, rety_seq.currval);
INSERT INTO reference_value (id, pretty_name, description, effective_period_from, alt_sequence, reference_type_id)
	VALUES (23000020,'Person', 'Person', TRUNC(SYSDATE), 20, rety_seq.currval);

Det finns en rad i reference_type för varje LDM-undertyp av Root_Reference_Type . Beskrivningen i reference_type är hämtat från LDM-klassbeskrivningen. För Gender_Type , skulle detta lyda "Identifierar en persons kön". DML-kodavsnitten visar skillnaderna i beskrivningar mellan typ och värde, som kan användas i användargränssnittet eller i rapporter.

Du kommer att se den där reference_type kallas Gender_Type har tilldelats ett intervall på 13000000 till 13999999 för dess associerade reference_value.ids . I den här modellen är varje reference_type tilldelas ett unikt, icke-överlappande antal ID:n. Detta är inte strikt nödvändigt, men det tillåter oss att gruppera relaterade värde-ID:n. Det härmar ungefär vad du skulle få om du hade separata bord. Det är trevligt att ha, men om du inte tror att det finns någon fördel med detta kan du avstå från det.

En annan kolumn som lades till i PDM är admin_role . Här är varför.

Vilka är administratörer

Vissa taxonomier kan lägga till eller ta bort värden med liten eller ingen effekt. Detta inträffar när inga program använder sig av värdena i sin logik, eller när typen inte är ansluten till andra system. I sådana fall är det säkert för användaradministratörer att hålla dessa uppdaterade.

Men i andra fall måste mycket mer försiktighet utövas. Ett nytt referensvärde kan orsaka oavsiktliga konsekvenser för programlogik eller för nedströms system.

Anta till exempel att vi lägger till följande i taxonomin för könstyp:

INSERT INTO reference_value (id, pretty_name, description, effective_period_from, alt_sequence, reference_type_id)
	VALUES (13000040,'Not Known', 'Gender has not been recorded. Covers gender of unborn child, when someone has refused to answer the question or when the question has not been asked.', TRUNC(SYSDATE), 30, (SELECT id FROM reference_type WHERE ref_type_key = 'GENDER_TYPE'));

Detta blir snabbt ett problem om vi har följande logik inbyggd någonstans:

IF ref_key = 'MALE' THEN
	RETURN 'M';
ELSE
	RETURN 'F';
END IF;

Det är klart att "om du inte är man måste du vara kvinna"-logiken inte längre gäller i den utökade taxonomin.

Det är här admin_role kolumn spelar in. Det föddes från diskussioner med utvecklarna om den fysiska designen, och det fungerade tillsammans med deras UI-lösning. Men om en-tabell-per-klass-lösningen hade valts, då reference_type inte skulle ha funnits. Metadata som den innehöll skulle ha hårdkodats i applikationen Gender_Type table – , som varken är flexibel eller utdragbar.

Endast användare med rätt behörighet kan administrera taxonomin. Detta kommer sannolikt att baseras på sakkunskap (SME ). Å andra sidan kan vissa taxonomier behöva administreras av IT för att möjliggöra konsekvensanalys, grundlig testning och för att eventuella kodändringar ska släppas harmoniskt i tid för den nya konfigurationen. (Om detta görs genom ändringsförfrågningar eller på annat sätt är upp till din organisation.)

Du kanske har noterat att granskningskolumnerna created_by , created_date , updated_by och updated_date refereras inte alls i skriptet ovan. Återigen, om du inte är intresserad av dessa behöver du inte använda dem. Denna speciella organisation hade en standard som föreskrev att ha granskningskolumner på varje tabell.

Triggers:Att hålla saker konsekventa

Utlösare säkerställer att dessa granskningskolumner uppdateras konsekvent, oavsett källan till SQL (skript, din applikation, schemalagda batchuppdateringar, ad-hoc-uppdateringar, etc.).

--------------------------------------------------------------------------------
PROMPT >>> create REFERENCE_TYPE triggers
--------------------------------------------------------------------------------

CREATE OR REPLACE TRIGGER rety_bri
  BEFORE INSERT ON reference_type
  FOR EACH ROW
DECLARE
BEGIN
	IF (:new.id IS NULL) THEN
		:new.id := rety_seq.nextval;
	END IF;
	:new.created_by := function_to_get_user();
	:new.created_date := SYSDATE;
	:new.updated_by := :new.created_by;
	:new.updated_date := :new.created_date;
END rety_bri;
/

CREATE OR REPLACE TRIGGER rety_bru
  BEFORE UPDATE ON reference_type
  FOR EACH ROW
DECLARE
BEGIN
	:new.updated_by := function_to_get_user();
	:new.updated_date := SYSDATE;
END rety_bru;
/

--------------------------------------------------------------------------------
PROMPT >>> create REFERENCE_VALUE triggers
--------------------------------------------------------------------------------

CREATE OR REPLACE TRIGGER reva_bri
  BEFORE INSERT ON reference_value
  FOR EACH ROW
DECLARE
BEGIN
	IF (:new.type_key IS NULL) THEN
		-- create the type_key from pretty_name:
		:new.type_key := function_to_create_key(new.pretty_name);
	END IF;
	:new.created_by := function_to_get_user();
	:new.created_date := SYSDATE;
	:new.updated_by := :new.created_by;
	:new.updated_date := :new.created_date;
END reva_bri;
/

CREATE OR REPLACE TRIGGER reva_bru
  BEFORE UPDATE ON reference_value
  FOR EACH ROW
DECLARE
BEGIN
	-- once the type_key is set it cannot be overwritten:
	:new.type_key := :old.type_key;
	:new.updated_by := function_to_get_user();
	:new.updated_date := SYSDATE;
END reva_bru;
/

Min bakgrund är mestadels Oracle och tyvärr begränsar Oracle identifierare till 30 byte. För att undvika att överskrida detta får varje tabell ett kort alias på tre till fem tecken och andra tabellrelaterade artefakter använder det aliaset i sina namn. Så, reference_value alias är reva – de två första tecknen från varje ord. Före radinfogning och före raduppdatering förkortas till bri och bru respektive. Sekvensnamnet reva_seq , och så vidare.

Handkodning av triggers som denna, tabell efter tabell, kräver mycket demoraliserande pannplåtsarbete för utvecklare. Lyckligtvis kan dessa utlösare skapas via kodgenerering , men det är ämnet för en annan artikel!

Vikten av nycklar

ref_type_key och type_key kolumner är båda begränsade till 30 byte. Detta gör att de kan användas i SQL-frågor av PIVOT-typ (i Oracle. Andra databaser kanske inte har samma längdbegränsning för identifierare).

Eftersom nyckelunikitet säkerställs av databasen och utlösaren säkerställer att dess värde förblir detsamma hela tiden, kan – och bör – dessa nycklar användas i frågor och kod för att göra dem mer läsbara . Vad menar jag med detta? Tja, istället för:

SELECT … 
FROM …
INNER JOIN …
WHERE reference_value.id = 13000020

Du skriver:

SELECT … 
FROM …
INNER JOIN …
WHERE reference_value.type_key = 'MALE'

I grund och botten anger nyckeln tydligt vad frågan gör .

Från LDM till PDM, med utrymme att växa

Resan från LDM till PDM är inte nödvändigtvis en rak väg. Det är inte heller en direkt förvandling från det ena till det andra. Det är en separat process som introducerar sina egna överväganden och sina egna bekymmer.

Hur modellerar du referensdata i din databas?


  1. MySQL - hur många rader kan jag infoga i en enda INSERT-sats?

  2. Fråga med yttre kopplingar beter sig annorlunda i Oracle 12c

  3. Android-program för att konvertera SQLite-databasen till Excel

  4. Hur man ansluter till en databas med Workbench MySQL-klienten