SQLAlchemy: Joined Table Inheritance

Eigentlich lässt die Dokumentation des ORM SQLAlchemy für Python keine Wünsche offen. Auch das Klassenmapping mit Polymorphy ist gut dokumentiert. Was aber noch etwas zu kurz kommt ist die Datenbanksicht.

Ich möchte hier das Beispiel der Dokumentation aufgreifen.
Von einer Basisklasse „Employee“ werden zwei weitere Klassen „Manager“ und „Engineer“ abgeleitet.

Grundsätzlich gibt es verschiedene Strategien Klassenhirarchien abzubilden

  • Joined Table Inheritance:
    Bei dieser Variante gibt es für jede Klasse eine eigene Tabelle. Die beiden Subklassen erhalten eigene Tabellen mit Referenzen auf die Basisklasse.
  • Single Table Inheritance:
    In dieser Form wird die ganze Klassenhierarchie in einer einzelnen Tabelle abgebildet. Aus Blick des Python Codes ist alles sauber. In der Datenbank gibt es aber in der Tabelle Felder, die nur abhängig vom Typ befüllt werden dürfen. Echte Datenbank-Constraints sind damit fast unmöglich.
  • Concrete Table Inheritance:
    Hier gibt es auch wieder für jede Klasse eine Tabelle. Diesesmal haben die Subklassen aber keine Foreign Key Beziehung mit der Basisklasse. Jede Tabelle dupliziert auch die Spalten der Basisklasse. Aus Sicht der Datenbank gibt es nun keine Zentrale Tabelle mehr in der alle „Employees“ erfasst sind. Es lässt sich damit auch kein Foreign Key Constraint mehr setzen.

Um ein sauberes Datenbankmodell zu erhalten, ist somit nur die „Joined Table Inheritance“ zu empfehlen:

  • Auf Spaltenebene können ohne Problem Check-Constraints und Not-Null-Constraints definiert werden.
  • Auch ohne die Kenntnis des SQLALchemy Mappings ergibt sich für andere Anwendungen ganz automatisch welche Felder für welche Klasse gefüllt werden können.
  • Die Basisklasse kann durch andere Tabellen via Foreign Key Constraint referenziert werden.

Wie wird nun eine Joined Table Inheritance in Python gemappt?

class Employee(Base):
    __tablename__ = 'employee'
    Id = Column(Integer
                , primary_key=True)
    Name = Column(String(50))

class Manager(Employee):
    __tablename__ = 'manager'
    Id = Column(Integer
                , ForeignKey(Employee.Id)
                , primary_key=True)
    ManagerStatus = Column(String(50))

    __mapper_args__ = {
        'polymorphic_identity': 'manager',
    }

class Engineer(Employee):
    __tablename__ = 'engineer'
    Id = Column(Integer
                , ForeignKey(Employee.Id)
                , primary_key=True)

    EngineerDegree= Column(String(50))

    __mapper_args__ = {
        'polymorphic_identity': 'engineer',
    }

Daraus lassen sich diese Create-Statements generieren.

create_engine('sqlite:///', echo= True)
Base.metadata.create_all(bind=engine)

CREATE TABLE employee (
	"Id" INTEGER NOT NULL, 
	"Name" VARCHAR(50), --Felder der Basisklasse sind hier  ...
	PRIMARY KEY ("Id")
);
-- und werden in den abgeleiteten Tabelle nicht mehr wiederholt
CREATE TABLE engineer (
	"Id" INTEGER NOT NULL, 
	"EngineerDegree" VARCHAR(50), 
	PRIMARY KEY (id), 
	FOREIGN KEY(id) REFERENCES employee ("Id")
);

CREATE TABLE manager (
	"Id" INTEGER NOT NULL, 
	"ManagerStatus" VARCHAR(50), 
	PRIMARY KEY (id), 
	FOREIGN KEY(id) REFERENCES employee ("Id")
);

Wichtig ist dabei der Fremdschlüssel in den abgeleiteten Klassen, der auf die Basisklasse zeigt:

#Employee.Id bezieht sich dabei auf den Klassen/Attributnamen,
#nicht etwa auf Tabellen oder Spaltenbezeichnungen
Id = Column(Integer,ForeignKey(Employee.Id), primary_key=True)

Vergisst man diesen Fremdschlüssel erhält man folgende Meldung:

  Can’t find any foreign key relationships between ‚employee‘ and ‚manager‘.

 

Dieser Beitrag wurde unter Programmierung, Python abgelegt und mit , , , , verschlagwortet. Setze ein Lesezeichen auf den Permalink.