ORM Konfiguration

Wie ordne ich eine Tabelle, die keinen Primärschlüssel hat?

Das SQLAlchemy ORM benötigt, um eine bestimmte Tabelle zuordnen zu können, mindestens eine Spalte, die als Primärschlüsselspalte gekennzeichnet ist; Mehrspaltige, d.h. zusammengesetzte Primärschlüssel sind natürlich ebenfalls möglich. Diese Spalten müssen dem Datenbank nicht tatsächlich als Primärschlüsselspalten bekannt sein, obwohl es ratsam ist. Es ist nur notwendig, dass die Spalten sich wie ein Primärschlüssel verhalten, z. B. als eindeutiger und nicht-null-fähiger Identifikator für eine Zeile.

Die meisten ORMs verlangen, dass Objekte einen Primärschlüssel definiert haben, da das Objekt im Speicher einer eindeutig identifizierbaren Zeile in der Datenbanktabelle entsprechen muss; zumindest ermöglicht dies, dass das Objekt für UPDATE- und DELETE-Anweisungen gezielt ausgewählt werden kann, die nur die Zeile dieses Objekts und keine andere beeinflussen. Die Bedeutung des Primärschlüssels geht jedoch weit darüber hinaus. In SQLAlchemy sind alle ORM-zugeordneten Objekte jederzeit innerhalb einer Session über ein Muster namens Identitäts-Map eindeutig mit ihrer spezifischen Datenbankzeile verknüpft. Dies ist zentral für das von SQLAlchemy verwendete Unit-of-Work-System und auch entscheidend für die gängigsten (und weniger gängigen) Muster der ORM-Nutzung.

Hinweis

Es ist wichtig zu beachten, dass wir hier nur vom SQLAlchemy ORM sprechen; eine Anwendung, die auf Core aufbaut und sich nur mit Table-Objekten, select()-Konstrukten und ähnlichem beschäftigt, **benötigt keinen** Primärschlüssel, der in irgendeiner Weise auf einer Tabelle vorhanden oder mit ihr verbunden ist (obwohl in SQL alle Tabellen wirklich eine Art Primärschlüssel haben sollten, damit Sie bestimmte Zeilen tatsächlich aktualisieren oder löschen können).

In fast allen Fällen hat eine Tabelle einen sogenannten Kandidatenschlüssel, d. h. eine Spalte oder eine Reihe von Spalten, die eine Zeile eindeutig identifizieren. Wenn eine Tabelle dies wirklich nicht hat und tatsächlich vollständig doppelte Zeilen aufweist, entspricht die Tabelle nicht der ersten normalen Form und kann nicht zugeordnet werden. Andernfalls können die Spalten, die den besten Kandidatenschlüssel bilden, direkt dem Mapper zugewiesen werden.

class SomeClass(Base):
    __table__ = some_table_with_no_pk
    __mapper_args__ = {
        "primary_key": [some_table_with_no_pk.c.uid, some_table_with_no_pk.c.bar]
    }

Noch besser ist es, wenn Sie vollständig deklarierte Tabellenmetadaten verwenden und das Flag primary_key=True für diese Spalten setzen.

class SomeClass(Base):
    __tablename__ = "some_table_with_no_pk"

    uid = Column(Integer, primary_key=True)
    bar = Column(String, primary_key=True)

Alle Tabellen in einer relationalen Datenbank sollten Primärschlüssel haben. Selbst eine Many-to-Many-Assoziationstabelle – der Primärschlüssel wäre die Kombination der beiden Assoziationsspalten.

CREATE TABLE my_association (
  user_id INTEGER REFERENCES user(id),
  account_id INTEGER REFERENCES account(id),
  PRIMARY KEY (user_id, account_id)
)

Wie konfiguriere ich eine Spalte, die ein Python-Reservatwort oder ähnlich ist?

Spaltenbasierte Attribute können beliebige Namen in der Zuordnung erhalten. Siehe Deklarative zugeordnete Spalten explizit benennen.

Wie erhalte ich eine Liste aller Spalten, Beziehungen, zugeordneten Attribute usw. einer zugeordneten Klasse?

Diese Informationen sind alle vom Mapper-Objekt verfügbar.

Um auf den Mapper für eine bestimmte zugeordnete Klasse zuzugreifen, rufen Sie die Funktion inspect() darauf auf.

from sqlalchemy import inspect

mapper = inspect(MyClass)

Von dort aus können alle Informationen über die Klasse über Eigenschaften wie

Ich erhalte eine Warnung oder Fehlermeldung bezüglich "Implizite Kombination von Spalte X unter Attribut Y"

Diese Bedingung bezieht sich auf den Fall, dass eine Zuordnung zwei Spalten enthält, die aufgrund ihres Namens unter demselben Attributnamen zugeordnet werden, es aber keinen Hinweis darauf gibt, dass dies beabsichtigt ist. Eine zugeordnete Klasse muss explizite Namen für jedes Attribut haben, das einen unabhängigen Wert speichern soll; wenn zwei Spalten denselben Namen haben und nicht disambiguiert werden, fallen sie unter dasselbe Attribut, und die Auswirkung ist, dass der Wert von einer Spalte in die andere **kopiert** wird, basierend darauf, welche Spalte zuerst dem Attribut zugewiesen wurde.

Dieses Verhalten ist oft wünschenswert und wird ohne Warnung zugelassen, wenn die beiden Spalten über eine Fremdschlüsselbeziehung innerhalb einer Vererbungszuordnung verbunden sind. Wenn die Warnung oder der Fehler auftritt, kann das Problem gelöst werden, indem entweder die Spalten anders benannten Attributen zugewiesen werden oder, wenn die Kombination gewünscht ist, column_property() verwendet wird, um dies explizit zu machen.

Angesichts des folgenden Beispiels

from sqlalchemy import Integer, Column, ForeignKey
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()


class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)


class B(A):
    __tablename__ = "b"

    id = Column(Integer, primary_key=True)
    a_id = Column(Integer, ForeignKey("a.id"))

Ab SQLAlchemy-Version 0.9.5 wird diese Bedingung erkannt und warnt, dass die id-Spalte von A und B unter dem gleichnamigen Attribut id kombiniert wird, was oben ein ernstes Problem darstellt, da es bedeutet, dass der Primärschlüssel eines B-Objekts immer das Spiegelbild seines A sein wird.

Eine Zuordnung, die dies löst, sieht wie folgt aus

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)


class B(A):
    __tablename__ = "b"

    b_id = Column("id", Integer, primary_key=True)
    a_id = Column(Integer, ForeignKey("a.id"))

Angenommen, wir wollten, dass A.id und B.id Spiegelbilder voneinander sind, obwohl B.a_id dort ist, wo A.id verknüpft ist. Wir könnten sie über column_property() kombinieren.

class A(Base):
    __tablename__ = "a"

    id = Column(Integer, primary_key=True)


class B(A):
    __tablename__ = "b"

    # probably not what you want, but this is a demonstration
    id = column_property(Column(Integer, primary_key=True), A.id)
    a_id = Column(Integer, ForeignKey("a.id"))

Ich verwende Declarative und setze primaryjoin/secondaryjoin mit einem and_() oder or_() und erhalte eine Fehlermeldung bezüglich Fremdschlüsseln.

Tun Sie dies?

class MyClass(Base):
    # ....

    foo = relationship(
        "Dest", primaryjoin=and_("MyClass.id==Dest.foo_id", "MyClass.foo==Dest.bar")
    )

Das ist ein and_() aus zwei String-Ausdrücken, für die SQLAlchemy keine Zuordnung anwenden kann. Declarative erlaubt es, dass relationship()-Argumente als Strings angegeben werden, die mit eval() in Ausdrucksobjekte umgewandelt werden. Dies geschieht jedoch nicht innerhalb eines and_()-Ausdrucks – es ist eine spezielle Operation, die Declarative nur auf die **Gesamtheit** dessen anwendet, was als String an primaryjoin oder andere Argumente übergeben wird.

class MyClass(Base):
    # ....

    foo = relationship(
        "Dest", primaryjoin="and_(MyClass.id==Dest.foo_id, MyClass.foo==Dest.bar)"
    )

Oder, wenn die benötigten Objekte bereits verfügbar sind, überspringen Sie die Strings.

class MyClass(Base):
    # ....

    foo = relationship(
        Dest, primaryjoin=and_(MyClass.id == Dest.foo_id, MyClass.foo == Dest.bar)
    )

Dasselbe gilt für alle anderen Argumente wie foreign_keys.

# wrong !
foo = relationship(Dest, foreign_keys=["Dest.foo_id", "Dest.bar_id"])

# correct !
foo = relationship(Dest, foreign_keys="[Dest.foo_id, Dest.bar_id]")

# also correct !
foo = relationship(Dest, foreign_keys=[Dest.foo_id, Dest.bar_id])


# if you're using columns from the class that you're inside of, just use the column objects !
class MyClass(Base):
    foo_id = Column(...)
    bar_id = Column(...)
    # ...

    foo = relationship(Dest, foreign_keys=[foo_id, bar_id])

Was sind default, default_factory und insert_default und was sollte ich verwenden?

Hier gibt es einen kleinen Konflikt in der SQLAlchemy-API aufgrund der Einführung von PEP-681 Dataclass-Transforms, das bei seinen Namenskonventionen streng ist. PEP-681 kommt ins Spiel, wenn Sie MappedAsDataclass verwenden, wie in Deklarative Dataclass-Zuordnung gezeigt. Wenn Sie MappedAsDataclass nicht verwenden, kommt es nicht zur Anwendung.

Teil Eins – Klassisches SQLAlchemy ohne Dataclasses

Wenn Sie MappedAsDataclass **nicht** verwenden, was in SQLAlchemy seit vielen Jahren der Fall ist, unterstützt die Konstruktion mapped_column() (und Column) einen Parameter mapped_column.default. Dies gibt einen Standardwert auf Python-Seite an (im Gegensatz zu einem serverseitigen Standardwert, der Teil der Schemadefinition Ihrer Datenbank wäre), der beim Ausgeben einer INSERT-Anweisung angewendet wird. Dieser Standardwert kann **jeder** statische Python-Wert sein, wie eine Zeichenkette, **oder** eine aufrufbare Python-Funktion, **oder** ein SQLAlchemy SQL-Konstrukt. Eine vollständige Dokumentation für mapped_column.default finden Sie unter Vom Client aufgerufene SQL-Ausdrücke.

Wenn Sie mapped_column.default mit einer ORM-Zuordnung verwenden, die MappedAsDataclass **nicht** verwendet, erscheint dieser Standardwert/Aufruf **nicht sofort auf Ihrem Objekt, wenn Sie es erstellen**. Er wird erst angewendet, wenn SQLAlchemy eine INSERT-Anweisung für Ihr Objekt erstellt.

Ein sehr wichtiger Punkt ist, dass bei der Verwendung von mapped_column() (und Column) der klassische Parameter mapped_column.default unter einem neuen Namen verfügbar ist, nämlich mapped_column.insert_default. Wenn Sie eine mapped_column() erstellen und **keine** MappedAsDataclass verwenden, sind die Parameter mapped_column.default und mapped_column.insert_default **synonym**.

Teil Zwei – Verwendung der Dataclass-Unterstützung mit MappedAsDataclass

Wenn Sie MappedAsDataclass verwenden, d. h. die spezifische Form der Zuordnung, die unter Deklarative Dataclass-Zuordnung verwendet wird, ändert sich die Bedeutung des Schlüsselworts mapped_column.default. Wir erkennen an, dass es nicht ideal ist, dass dieser Name sein Verhalten ändert, aber es gab keine Alternative, da PEP-681 verlangt, dass mapped_column.default diese Bedeutung annimmt.

Wenn Dataclasses verwendet werden, muss der Parameter mapped_column.default so verwendet werden, wie er unter Python Dataclasses beschrieben wird – er bezieht sich auf einen konstanten Wert wie eine Zeichenkette oder eine Zahl und wird **sofort auf Ihr Objekt angewendet, wenn es erstellt wird**. Er wird derzeit auch auf den Parameter mapped_column.default von Column angewendet, wo er in einer INSERT-Anweisung automatisch verwendet wird, auch wenn er auf dem Objekt nicht vorhanden ist. Wenn Sie stattdessen einen aufrufbaren Wert für Ihre Dataclass verwenden möchten, der auf das Objekt angewendet wird, wenn es erstellt wird, würden Sie mapped_column.default_factory verwenden.

Um auf das reine INSERT-Verhalten von mapped_column.default zuzugreifen, das im obigen Teil eins beschrieben wird, würden Sie stattdessen den Parameter mapped_column.insert_default verwenden. mapped_column.insert_default stellt bei der Verwendung von Dataclasses weiterhin einen direkten Weg zum Core-Level "Standard"-Prozess dar, bei dem der Parameter ein statischer Wert oder ein Aufruf sein kann.

Zusammenfassungstabelle

Konstrukt

Funktioniert mit Dataclasses?

Funktioniert ohne Dataclasses?

Akzeptiert Skalarwerte?

Akzeptiert Aufrufe?

Füllt Objekt sofort auf?

mapped_column.default

Nur wenn keine Dataclasses vorhanden sind

Nur wenn Dataclasses vorhanden sind

mapped_column.insert_default

mapped_column.default_factory

Nur wenn Dataclasses vorhanden sind