SQLAlchemy - ORM - Arbeiten mit Zuordnungstabellen

Installation und Anwendung von Datenbankschnittstellen wie SQLite, PostgreSQL, MariaDB/MySQL, der DB-API 2.0 und sonstigen Datenbanksystemen.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@Sophus:
Hab mir Deinen Code nicht angeschaut, allerdings eine Anmerkung zu dieser Frage:
Sophus hat geschrieben:Nun frage ich mich: soll ich durch die Brust ins Auge alle Abfragen auf ihren Erfolg hin überprüfen, und bei einer erfolgreichen Abfrage nur das entsprechende Objekt an die entsprechende Beziehung übergeben? Klingt sehr aufwendig. Ich habe nämlich gehofft, dass es sowas wie eine ignore-Einstellung gibt, wenn ein Objekt nicht existiert.
Ja. Eine DB ist eine externe Daten-Resource, welche im Ideafall "heilige Daten" enthält. Damit ist gemeint, dass die Daten in der DB geprüft und valide sind zu jeden Zeitpunkt einer möglichen Abfrage. Um das sicherzustellen, bringt eine DB eigene Mechanismen mit, welche zumindest die Konsistenz auf DB-Modell-Ebene sicherstellen kann. Deshalb ist es ratsam, die eigenen Datenhaltungsbelange so gut wie möglich in dieses "Datenbanksprech" zu übersetzen. Damit kann man fast alle Konsistenzansprüche umsetzen.
Es gibt allerdings Anforderungen, wo das auf reiner DB-Ebene sehr umständlich wird. ORMs sind dann oft nicht in der Lage, sowas abbilden zu können (oder nur mit kruden Verrenkungen). Meist ist dann das DB-Modell nicht gut gewählt und das Problem lässt sich mit dessen Überarbeitung beheben. Wenn es dann immer noch klemmt, obliegt es Dir als Entwickler zu entscheiden, wie und wo die Konsistenz geprüft wird. Diese Prüfung ist aber wichtig, damit die Daten nicht korrumpiert werden. Also niemals weglassen.

Das mit der ignore-Sache versteh ich nicht.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@jerch: Danke für deine Antwort. Allerdings habe ich das Gefühl, dass die Antwort an meinem Anliegen etwas vorbei geht.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@Sophus:
Du solltest das Modell überdenken. Immer dann, wenn man zu viele Seitenanforderungen an ein DB-Modell stellen muss, läufts nicht rund. Und für die Frage, ob Du die Rückgabe prüfen muss - ja, nur dann kannst Du sinnvoll damit arbeiten. `None` ist die einzig semantisch sinnvolle Art, auf etwas nicht Existierendes zu antworten. Wie hättest Du das gelöst in einem Objektkontext?
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@jerch: Inwiefern überdenken? Was ist an einer 1:n-Beziehung verkehrt? Eine Person kann viele Eigenschaften haben. Diese Eigenschaften werden durch die QComboBox() hinzugesetzt. In meinem obigen Beispiel sind es 8 Eigenschaften. Das heißt dann für mich, dass ich zunächst nur für eine Person 8 Abfragen prüfen muss. Das ist noch recht harmlos. Von den 8 Eigenschaften können, sagen wir mal, 6 Eigenschaften vorhanden sein, weil zum Beispiel der Benutzer auch nur 6 Eigenschaften ausgewählt hat, und die anderen 2 Eigenschaften eben nicht. Und dann müsste ich noch gucken, mit welchen von den 6 Abfragen ich arbeiten soll. Sind es die Haarfarbe, Augenfarbe, Geschlecht etc...? Oder sind es andere? Und demnach müsste ich die PERSON()-Klasse mit den entsprechenden Schlüsselargumenten zeugen. Das heißt, wenn Benutzer die Harfarbe der Person nicht angibt, dann fällt das Schlüsselargument hair_color bei der Erzeugung von PERSON() weg. Du merkst schon, es wird ein laaaaaaanger Abfrage-Kampf, und das nur für die PERSO()-Tabelle.

Daher habe ich gehofft, dass ihr einen besseren Weg kennt? Mit Try-except komme ich nicht weit.

EDIT:
Meine andere Idee, als Notlösung, wäre, dass ich eben nicht mehr mit ORM-Objekten arbeite, sondern mit IDs. Und da bei mir die Fremdschlüssel-Spalten NULL sein dürfen (nullable=True), und ein None in Python für eine Datenbank NULL bedeutet, wäre das durchaus weniger schmerzhaft. Jedoch darf ich dann die IDs nicht an die Beziehungen übergeben, sondern muss händisch an die Fremdschlüssel-Spalten übergeben. Wenn also eine Abfrage nicht erfolgreich ist, bekomme ich eben ein None zurück, und dieses None kann ich dann in die Spalte der entsprechenden Fremdschlüssel-Spalte setzen.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@Sophus:
Ich verstehe Dein Problem leider nicht, und auch nicht inwiefern IDs da zur Lösung beitragen sollen.

Ich geb mal ein Bsp. für Dein skizziertes Problem:

Person darf 1:n Eigenschaften haben --> foreignkey in `Eigenschaften` auf `Person`
Damit lassen sich beliebige Eigenschaften abbilden, heisst wenn eine leere Liste zurückkommt, hat die Person keine Eigenschaften. Ansonsten kommt eine Liste mit dem Eigenschaftsobjekten zurück. Damit kannst Du direkt weiterarbeiten.

Wenn Du die Eigenschaften auswählbar machen möchtest, brauchst Du eine separate Eigenschaften-Tabelle und eine weitere für Realisierungen (meint Person XY realisiert Eigenschaft Z). Das ist klassisches many-to-many, was alle guten ORMs dieser Welt können.

Da Du von Combobox sprichst, gehe ich von Letzterem aus, heisst es gibt irgendwo her bereits existierende Eigenschaften und du möchtest diese mit der `Person` verknüpfen. Wo ist jetzt das Problem? Du liest zunächst alle existierenden Eigenschaften aus, um die Combobox mit Möglichkeiten vorbelegen zu können und setzt den Haken dort, wo das Many2Many-Feld der `Person` die Eigenschaft enthält. Damit hast die "Realisierungen" abgebildet. Da ist nix mit Einzelabfragen, wenn das DB-Modell stimmt.

Edit: Wie gesagt - habe Deinen Code nicht angeschaut, lange Codeschnippel lösen da bei mir so einen nicht-lesen-Reflex aus. Falls Du für jede Eigenschaft separat eine Eingabe rendern willst - ist egal, wenn obiges Modell stimmt. Dann fummelst Du das eben nicht in ein Multiselect, sondern iterierst für Deine Eingabe-Widgets darüber...
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@jerch: Ich habe mal eben auf der Schnelle einen halbwegs brauchbaren Quelltext zusammengesetzt. Leider kriege ich den Quelltext nicht auf drei Zeilen runter gebrochen.

Aber gehen wir mal Stück für Stück vor. Zunächst hier mein Modell. Du siehst, dass Person() mehrere 1:n-Beziehungen pflegt. Hier habe ich nur mal 2x 1:n-Beziehungen genommen.

Code: Alles auswählen

import sqlalchemy
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy import Column, Integer, String, ForeignKey
from sqlalchemy.orm import sessionmaker, relationship, backref

sqlite_uri = 'sqlite:///test.sqlite'
 
engine = sqlalchemy.create_engine(sqlite_uri, echo=True)
 
Base = declarative_base()

class PERSON(Base):

    __tablename__ = "person"

    id = Column(Integer, primary_key=True, unique=True, autoincrement=True)
    nickname = Column(String(255))
    alias_name  = Column(String (255))
    name_normally_used = Column(String(50), nullable=False)
    first_middle_name = Column(String(255))
    last_name = Column(String(100))

    gender_id = Column(Integer, ForeignKey('person_gender.id'))
    gender = relationship("PERSON_GENDER")

    hair_color_id = Column(Integer, ForeignKey('person_hair_color.id'))
    hair_color = relationship("PERSON_HAIR_COLOR")

class PERSON_GENDER(Base):

    __tablename__ = "person_gender"

    id = Column(Integer, primary_key=True, unique=True, autoincrement=True)

    gender = Column(String(50), nullable=False, unique=True)

class PERSON_HAIR_COLOR(Base):

    __tablename__ = "person_hair_color"

    id = Column(Integer, primary_key=True, unique=True, autoincrement=True)
    hair_color = Column(String(50), nullable=False, unique=True)

Base.metadata.create_all(engine)
Session = sessionmaker(bind=engine)
session = Session()
Jetzt starte ich diese beiden Abfragen. Die gender-Abfrage demonstriert, dass diese nicht klappen solle. Denn niemand ist vom Geschlecht her 'nix'. Für 'nix' können wir eben auch eine Situation vorstellen, dass der Benutzer beim Anlegen kein Geschlecht ausgesucht hat. Das heißt, result_gender wird None sein.

Code: Alles auswählen

result_gender = session.query(PERSON_GENDER).filter(PERSON_GENDER.salutation=='nix').first()
result_hair_color = session.query(PERSON_HAIR_COLOR).filter(PERSON_HAIR_COLOR.salutation=='blond').first()
Jetzt tun wir mal so, als wollen wir den Datensatz anlegen: In dieser Variante werden ORM-Objekte an folgende Beziehung des PERSON(): gender und hair_color. Das klappt aber nur solange, wie es ORM-Objekte wirklich gibt.

Code: Alles auswählen

person_one = PERSON(name_normally_used='Kevin',
                    alias_name = 'Pseudo_nick',
                    gender = result_gender,
                    hair_color = result_hair_color)
session.add(person_one)
session.commit()
Wir wissen aber, dass PERSON() zwei Fremdschlüsselspalten hat: gender_id und hair_color_id. Und diese Saplten erlauben auch NULL - in Python wäre es None.

Code: Alles auswählen


result_gender = session.query(PERSON_GENDER).filter(PERSON_GENDER.salutation=='nix').first()
result_hair_color = session.query(PERSON_HAIR_COLOR).filter(PERSON_HAIR_COLOR.salutation=='blond').first()

# ternary conditional operator, wenn der Rückgabe Wert None ist, bekommt
# bekommen die Variablen None, ansonsten bekommen die ID
get_gender= None if result_person_gender is None else result_person_gender.id
get_hair_color = None if result_hair_color is None else result_hair_color .id

# Hier arbeiten wir nicht mit den ORM-Objekten,
# sondern mit IDs oder eben NOne und übergeben diese
# Werte direkt der Fremdschlüssel-Spalte und nicht der Beziehung
person_one = PERSON(name_normally_used='Kevin',
                    alias_name = 'Pseudo-Nick',
                    gender_id = get_gender,
                    hair_color_id = get_hair_color )
session.add(person_one)
session.commit()
BlackJack

@Sophus: Und was ist jetzt das Problem damit?

Ich habe jetzt einfach mal das hier laufen lassen:

Code: Alles auswählen

person_one = PERSON(name_normally_used='Kevin',
                    alias_name = 'Pseudo_nick',
                    gender = None,
                    hair_color = None)
session.add(person_one)
session.commit()
Und das läuft. Trägt wie erwartet NULL für `gender_id` und `hair_color_id` ein.
[codebox=text file=Unbenannt.txt]sqlite> select * from person;
1||Pseudo_nick|Kevin||||[/code]
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@BlackJack: Das Problem ist, dass du und Sirius3, das ihr mir eingetrichtert habt, dass man mit Objekten arbeiten will, nicht mit IDs. Und in diesem Fall missachte ich euren Ratschlag. Würde ich mit Objekten arbeiten, und die Query findet aber nichts, existiert kein Objekt, und diese können dann auch nicht als Fremdschlüssel eingetragen werden. Damit das Problem klappt, musste ich euren Ratschlag missachten. Und ich habe gehofft, weiterhin mit Objekten, anstatt mit IDs arbeiten zu können. Nur was ist, wenn kein Objekt existiert? Ich wollte mir die unzählige If-Abfragen ersparen.
BlackJack

@Sophus: Ich verstehe Dein Problem nicht. Der Wert für „kein Objekt“ ist `None`. Zeig mal konkreten Code und die konkrete Ausnahme dazu. Bei mir funktioniert das nämlich.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@Sophus:
Wenn beim Abspeichern ein Geschlecht ausgewählt ist, was es nicht gibt - was soll denn Deiner Meinung nach da passieren?

Übrigens ist das der Versuch der DB, die Daten konsistent zu halten ;)
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Ich bastle euch gleich zwei Varianten. Gebt mir etwas Zeit :)
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Ok, ich ziehe meine Aussage zurück. Als ich meine Beispiele noch einmal minimiert habe, fiel mir der Fehler meinerseits auf. Schande über mein Haupt. 8)
Antworten