Dědičnost v SQL databázi

5
Srp/09
2

Moderní relační databáze přejímají i některé myšlenky z objektového paradigmatu. Díky tomu můžeme využívat dědičnost, kterou známe z objektového programování i v databázi. Dnes se podíváme na to, jak dědičnost funguje v PostgreSQL.

Co je dědičnost?

Dědění je v OOP vztah mezi „předkem“ a „potomkem“. Anglicky se tento vztah označuje jako „IS-A“ tzn. „je“ – potomek je předkem a zároveň má nějaké dodatečné vlastnosti.

Mějme např. domácí sbírku, která obsahuje položky. Položka je předek a její potomci jsou knihy, filmy, nebo hudebniny. Každá položka má autora a rok vydání, ale knihy mají navíc počet stránek, kdežto vlastností hudebního alba jsou skladby, které obsahuje.

Potomci mají vlastnosti předka (rok vydání, autora) plus nějaké svoje specifické vlastnosti, které jiní potomci nebo předek nemají (počet stran, skladby).

Když ale chceme evidovat, co jsme komu půjčili, pracujeme obecně s položkou sbírky – abstrahujeme od toho, zda se jedná o knížku nebo film, prostě si poznamenáme, co a kdy jsme komu půjčili.

Objektové programování vs. databáze

V objektovém programování pracujeme s třídami a instancemi (objekty). Třídy jsou vzorem objektu (např. třída Položka, nebo třída Kniha). Zatímco instance je konkrétním výskytem objektu (např. konkrétní kniha). V OOP funguje dědičnost na úrovni tříd.

Relační databáze stojí na jiném paradigmatu než objektové programování, ale přesto tu můžeme nalézt volnou analogii. Na databázovou tabulku se můžeme dívat jako na třídu a na jednotlivé záznamy jako na instance této třídy.

V databázích, které podporují dědičnost si tedy můžeme vytvořit tabulky, které od sebe budou dědit určité atributy (sloupečky). Vytvoříme si tedy tabulku „položka“ se sloupečky „rok vydání“ a „autor“ a tabulku „kniha“, která bude jejím potomkem a bude navíc obsahovat atribut „počet stran“.

Zajímavou vlastností je, že se dědičnost netýká jen struktur (sloupečků), ale i vlastních dat. Díky tomu v tabulce „položka“ vidíme nejen záznamy, které jsme v této tabulce vytvořili, ale i všechny záznamy z tabulek, které jsou potomkem položky (knihy, filmy). V tabulce „položka“ samozřejmě vidíme jen ty sloupečky, které jsou společné všem, zatímco v tabulkách potomků vidíme i jejich specifické atributy.

Oproti např. Javě se databázová dědičnost v PostgreSQL liší ještě jednou věcí: tabulka může být potomkem více přímých předků, a dědí tak všechny jejich atributy.

Využití dědičnosti v PostgreSQL

Po přečtení oficiální dokumentace budeme možná trochu zklamaní. Narážíme totiž na problém s primárními a cizími klíči.

  1. Vytvoříme-li totiž v tabulce potomka záznam s primárním klíčem 1 a v tabulce jiného potomka záznam se stejným primárním klíčem, máme v tabulce předků záznamy s duplicitními „primárními klíči“. 
  2. Budeme-li se chtít pomocí cizího klíče odkázat na tabulku předka (např. výpůjčka obecné položky), můžeme se odkázat pouze na záznamy, které jsou přímo v tabulce „položka“ nikoli na záznamy v tabulkách potomků.

Tím se ale nenecháme odradit a ukážeme si řešení, jak využívat výhod dědičnosti i referenční integrity zároveň.

Řešení spočívá ve vytvoření jedné pomocné tabulky a použití pravidel (rule). Pomocná tabulka obsahuje jediný sloupeček (číselné ID), nad kterým uděláme primární klíč. Tabulky předka i potomků na její primární klíč navážeme pomocí cizích klíčů.

Ukázka

Vytvoříme si databázové tabulky pro naši domácí sbírku, která obsahuje knížky, filmy a hudbu. Tyto tři tabulky budou potomky tabulky „položka“. Dědičnost nám umožní např. snadno přidávat další společné atributy (sloupečky) do všech tabulek (přidáme je jen do tabulky předka) a můžeme se pomocí cizích klíčů z jiných tabulek odkazovat jak na jednotlivé konkrétní typy (knížka, film…), tak i na položky sbírky obecně.

Tabulka „polozka“ – předek ostatních tabulek:

CREATE TABLE sbirka.polozka
(
  id bigint NOT NULL DEFAULT nextval('sbirka.polozka_seq'::regclass),
  nazev character varying(255) NOT NULL,
  datum timestamp with time zone DEFAULT now(),
  rok_vydani integer,
  autor bigint,
  CONSTRAINT polozka_pk PRIMARY KEY (id),
  CONSTRAINT polozka_autor_fk FOREIGN KEY (autor)
      REFERENCES sbirka.autor (id) MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE NO ACTION,
  CONSTRAINT polozka_fk FOREIGN KEY (id)
      REFERENCES sbirka.polozka_id (id) MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED
)
WITH (OIDS=FALSE);
ALTER TABLE sbirka.polozka OWNER TO sqlvyukavlastnik;
GRANT ALL ON TABLE sbirka.polozka TO sqlvyukavlastnik;
GRANT SELECT ON TABLE sbirka.polozka TO sqlvyukapiskoviste;

-- Index: sbirka.fki_polozka_autor_fk

-- DROP INDEX sbirka.fki_polozka_autor_fk;

CREATE INDEX fki_polozka_autor_fk
  ON sbirka.polozka
  USING btree
  (autor);


-- Rule: "polozka_delete ON sbirka.polozka"

-- DROP RULE polozka_delete ON sbirka.polozka;

CREATE OR REPLACE RULE polozka_delete AS
    ON DELETE TO sbirka.polozka DO  DELETE FROM sbirka.polozka_id
  WHERE polozka_id.id = old.id;

-- Rule: "polozka_insert ON sbirka.polozka"

-- DROP RULE polozka_insert ON sbirka.polozka;

CREATE OR REPLACE RULE polozka_insert AS
    ON INSERT TO sbirka.polozka DO  INSERT INTO sbirka.polozka_id (id) 
  VALUES (new.id);

-- Rule: "polozka_update ON sbirka.polozka"

-- DROP RULE polozka_update ON sbirka.polozka;

CREATE OR REPLACE RULE polozka_update AS
    ON UPDATE TO sbirka.polozka DO  UPDATE sbirka.polozka_id SET id = new.id
  WHERE polozka_id.id = old.id;

Pomocná tabulka „polozka_id“:

CREATE TABLE sbirka.polozka_id
(
  id bigint NOT NULL,
  CONSTRAINT polozka_id_pk PRIMARY KEY (id)
)
WITH (OIDS=FALSE);
ALTER TABLE sbirka.polozka_id OWNER TO sqlvyukavlastnik;
COMMENT ON TABLE sbirka.polozka_id IS 'Pomocná tabulka – slouží k řešení dědičnosti, PK a FK.';

Sekvence pro automatické číslování položek:

CREATE SEQUENCE sbirka.polozka_seq
  INCREMENT 1
  MINVALUE 1
  MAXVALUE 9223372036854775807
  START 16
  CACHE 1;
ALTER TABLE sbirka.polozka_seq OWNER TO sqlvyukavlastnik;

Tabulka „kniha“ – potomek položky (ostatní tabulky potomků jsou analogické):

CREATE TABLE sbirka.kniha
(
-- Inherited:   id bigint NOT NULL DEFAULT nextval('sbirka.polozka_seq'::regclass),
-- Inherited:   nazev character varying(255) NOT NULL,
  isbn sbirka.isbn,
  pocet_stran integer,
-- Inherited:   datum timestamp with time zone DEFAULT now(),
-- Inherited:   rok_vydani integer,
-- Inherited:   autor bigint,
  CONSTRAINT kniha_pk PRIMARY KEY (id),
  CONSTRAINT kniha_polozka_fk FOREIGN KEY (id)
      REFERENCES sbirka.polozka_id (id) MATCH SIMPLE
      ON UPDATE NO ACTION ON DELETE NO ACTION DEFERRABLE INITIALLY DEFERRED
)
INHERITS (sbirka.polozka)
WITH (OIDS=FALSE);
ALTER TABLE sbirka.kniha OWNER TO sqlvyukavlastnik;
GRANT ALL ON TABLE sbirka.kniha TO sqlvyukavlastnik;
GRANT SELECT ON TABLE sbirka.kniha TO sqlvyukapiskoviste;

-- Rule: "kniha_delete ON sbirka.kniha"

-- DROP RULE kniha_delete ON sbirka.kniha;

CREATE OR REPLACE RULE kniha_delete AS
    ON DELETE TO sbirka.kniha DO  DELETE FROM sbirka.polozka_id
  WHERE polozka_id.id = old.id;

-- Rule: "kniha_insert ON sbirka.kniha"

-- DROP RULE kniha_insert ON sbirka.kniha;

CREATE OR REPLACE RULE kniha_insert AS
    ON INSERT TO sbirka.kniha DO  INSERT INTO sbirka.polozka_id (id) 
  VALUES (new.id);

-- Rule: "kniha_update ON sbirka.kniha"

-- DROP RULE kniha_update ON sbirka.kniha;

CREATE OR REPLACE RULE kniha_update AS
    ON UPDATE TO sbirka.kniha DO  UPDATE sbirka.polozka_id SET id = new.id
  WHERE polozka_id.id = old.id;

Všimněte si pravidel (rule), která slouží k zajištění integrity a jedinečnosti primárních klíčů potomků.

  • Při INSERTu vkládáme zároveň záznam do pomocné tabulky „polozka_id“ – pokud bychom se pokoušeli vytvořit potomka s primárním klíčem stejným, jako má jiný potomek, neprošlo by to kvůli primárnímu klíči tabulky „polozka_id“
  • Při UPDATu změníme i záznam v tabulce „polozka_id“ – díky tomu nemůžeme přiřadit potomkovi stejné ID jako má některá jiná položka.
  • Při DELETu odstraníme i záznam z pomocné tabulky, abychom neblokovali příslušné ID pro případné další využití.

Cizí klíče u potomků a předka bylo potřeba definovat jako DEFERRABLE INITIALLY DEFERRED. Bez toho by totiž selhávaly příkazy, které jsme definovali v pravidlech.

Snadné zjištění „třídy“ objektu

Z tabulky „polozka“ můžeme získat seznam všech položek v našem systému (knihy, filmy, hudba), ale nevíme, z které tabulky který záznam pochází.

Vyrobíme si proto pohled (view), díky kterému uvidíme ID, název a „třídu“ všech objektů:

CREATE OR REPLACE VIEW sbirka.polozka_trida AS 
 SELECT p.id, p.nazev, pg_class.relname AS trida
   FROM sbirka.polozka p
   JOIN pg_class ON p.tableoid = pg_class.oid;

ALTER TABLE sbirka.polozka_trida OWNER TO sqlvyukavlastnik;
GRANT ALL ON TABLE sbirka.polozka_trida TO sqlvyukavlastnik;
GRANT SELECT ON TABLE sbirka.polozka_trida TO sqlvyukapiskoviste;
COMMENT ON VIEW sbirka.polozka_trida IS 'Pomocný pohled, který nám umožní snadno dohledat třídu dané položky podle jejího ID.';

V příštím článku si ukážeme, jak používat dědičnost i v databázových systémech, které ji samy od sebe nepodporují…

Podpora dedicnosti v PostgreSQL je spis pozustatkem experimentalni akademicke ery nez cilene udrzenou a rozvijenou funkci. Muzete narazit na nektere problemy - napr. s implementaci referencni integrity. Osobne bych se dedicnosti vyhl, vyjma partitioningu, kde neni jine reseni.

Už se těším na další článek – tohle sice vypadá hezky, ale ne každý má to štěstí, používat databázi s funkcí dědičnosti.