SQLAlchemy - ORM - Arbeiten mit Zuordnungstabellen

Installation und Anwendung von Datenbankschnittstellen wie SQLite, PostgreSQL, MySQL, der DB-API 2.0 und sonstigen Datenbanksystemen.
BlackJack

Donnerstag 4. Mai 2017, 20:30

@Sophus: Das man Objekte statt IDs verwenden sollte und vier statt zwei Leerzeichen pro Ebene einrücken ist Dir auch egal, mal sehen wie lange noch jemand Lust hat zu antworten… ;-)
Benutzeravatar
Sophus
User
Beiträge: 1074
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Donnerstag 4. Mai 2017, 20:35

@BlackJack: Man darf nicht alles so bitterernst nehmen :) Und zu den Objekten statt IDs. Ich kapier es einfach nicht. Ich arbeite doch mit dem Objekt, auf die ich eine ID abrufe. Was genau meinst du? Und zu den Leerzeichen. Ich werde mir gleich einmal meine IDLE vornehmen.

Update:
Ok, jetzt habe ich es kapiert.

In der Zuordnungstabelle gibt es zwei definierte Beziehungen (genre und film):

Code: Alles auswählen

class Allocation_Film_Genre(Base):
    
    __tablename__ = 'allocation_film_genre'
    genre_id = Column(Integer, ForeignKey('genre.id'), primary_key=True)
    film_id = Column(Integer, ForeignKey('film.id'), primary_key=True)

    genre = relationship("Genre", backref=backref("allocation_film_genre", lazy='dynamic', cascade="all, delete-orphan" ))
    film = relationship("Film", backref=backref("allocation_film_genre", lazy='dynamic', cascade="all, delete-orphan" ))
Und genau den beiden Beziehungen werden die Objekte zugewiesen. Mein Denkfehler war, dass ich doch die hübsche ID brauche, und diese dann der Zuordnungstabelle übergebe. Und ich fragte mich die ganze Zeit "Wo zum Geier sind die IDs" bzw. "Wo zum Geier wird die ID hergeholt".
Zuletzt geändert von Sophus am Donnerstag 4. Mai 2017, 20:49, insgesamt 1-mal geändert.
BlackJack

Donnerstag 4. Mai 2017, 20:45

@Sophus: Also arbeitest Du doch mit der ID. Sirius hat ja schon gezeigt wie man das machen würde.
Benutzeravatar
Sophus
User
Beiträge: 1074
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Donnerstag 4. Mai 2017, 20:50

@BlackJack: Ich habe meine Antwort weiter oben aktualisiert.
Benutzeravatar
Sophus
User
Beiträge: 1074
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Freitag 25. August 2017, 19:12

Ich möchte diesen Beitrag wiederbeleben, weil ich denke, dass ich hier sehr gut anknüpfen kann. Ich stoße gerade, scheinbar, auf eine Grenze. Sirius3 hat ja bereits darauf hingewiesen, dass man mit ORM-Objekten arbeiten möchte, anstatt mit IDs. Soweit alles fein. Die Vorteile wurde von BlackJack schon erwähnt. Aber diese Arbeitsweise besagt ja auch, dass ein Objekt erfolgreich kreiiert sein muss, um dieses Objekt zum Beispiele an einer Beziehung zu übergeben. In meinem alten Beispiel war es die Verknüpfungstabelle mit einer n:m-Beziehung.

Nun folgende Situation: Ich arbeite gerade an einer 1:n-Beziehung. Auf der Benutzeroberfläche kann der Benutzer einige freie Texte in die QLineEdit() eingeben. Zum Beispiel Namen, einer Person. Daneben gibt es auch ein paar QComboBox(), die mit Daten einer anderen Tabelle gefüllt werden. Nehmen wir mal an, in einer QComboBox() werden die Geschlechter geladen. Nun möchte der Benutzer eine Person anlegen, jedoch wählt die Person kein Geschlecht aus - ist auch keine Angabepflicht. Da nichts in der QComboBox() ausgewählt wurde, schlägt die Abfrage fehl, und ein ORM-Objekt wird nicht erzeugt. Demzufolge übergebe ich einer Beziehung einen Wert, jedoch kein ORM-Objekt. Man könnte sich mit vielen Hilfsmitteln behelfen. Zum Beispiel kann man sagen, wenn die Abfrage des Geschlecht nicht erfolgreich war, da der Benutzer kein Geschlecht ausgewählt hat, dann soll das Geschlecht nicht an die Beziehung übergeben werden. Aber ich habe leider mehr als nur 2 QComboBox() auf der Oberfläche. Nach meiner Vorgehensweise müsste ich erst einmal nach und nach alles Abfragen überprüfen, ob diese oder jene Abfrage erfolgreich war und wenn etwas erfolgreich war, kann das erzeugte ORM-Objekt an die entsprechende Beziehung übergeben werden. Klingt sehr aufwendig und fehleranfällig.

Hier ein Pseudo-Beispiel:

Code: Alles auswählen

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)

    gender_id = Column(Integer, ForeignKey('person_gender.id', onupdate="cascade", ondelete='SET NULL'), nullable=True, unique=True)
    gender = relationship("PERSON_GENDER", single_parent=True, cascade="all, delete-orphan")#passive_deletes=True)

    hair_color_id = Column(Integer, ForeignKey('person_hair_color.id', onupdate="cascade", ondelete='SET NULL'), nullable=True, unique=True)
    hair_color = relationship("PERSON_HAIR_COLOR", single_parent=True, cascade="all, delete-orphan")#passive_deletes=True)

    eye_color_id = Column(Integer, ForeignKey('person_eye_color.id', onupdate="cascade", ondelete='SET NULL'), nullable=True, unique=True)
    eye_color = relationship("PERSON_EYE_COLOR", single_parent=True, cascade="all, delete-orphan")#passive_deletes=True)

    title_id = Column(Integer, ForeignKey('person_title.id', onupdate="cascade", ondelete='SET NULL'), nullable=True, unique=True)
    title = relationship("PERSON_TITLE", single_parent=True, cascade="all, delete-orphan")#passive_deletes=True)

    salutation_id = Column(Integer, ForeignKey('person_salutation.id', onupdate="cascade", ondelete='SET NULL'), nullable=True, unique=True)
    salutation = relationship("PERSON_SALUTATION", single_parent=True, cascade="all, delete-orphan")#passive_deletes=True)

    place_id = Column(Integer, ForeignKey('general_place.id', onupdate="cascade", ondelete='SET NULL'), nullable=True, unique=True)
    place = relationship("GENERAL_PLACE", single_parent=True, cascade="all, delete-orphan")#passive_deletes=True)

    religion_id = Column(Integer, ForeignKey('person_religion.id', onupdate="cascade", ondelete='SET NULL'), nullable=True, unique=True)
    religion = relationship("PERSON_RELIGION", single_parent=True, cascade="all, delete-orphan")#passive_deletes=True)

    relationship_status_id = Column(Integer, ForeignKey('person_relationship_status.id', onupdate="cascade", ondelete='SET NULL'), nullable=True, unique=True)
    relationship_status = relationship("PERSON_RELATIONSHIP_STATUS", single_parent=True, cascade="all, delete-orphan")#passive_deletes=True)    

result_gender = self._session.query(PERSON_GENDER).filter(PERSON_GENDER.gender=='komisch').first()
result_hair_color = self._session.query(PERSON_HAIR_COLOR).filter(PERSON_HAIR_COLOR.gender=='blond').first()
result_eye_color = self._session.query(PERSON_EYE_COLOR).filter(PERSON_EYE_COLOR.gender=='blau').first()
result_title = self._session.query(PERSON_TITLE).filter(PERSON_TITLE.gender=='Prof.').first()
result_salutation = self._session.query(PERSON_SALUTATION).filter(PERSON_SALUTATION.gender=='Herr').first()
result_place = self._session.query(GENERAL_PLACE).filter(GENERAL_PLACE.gender=='Mond').first()
result_religion = self._session.query(PERSON_RELIGION).filter(PERSON_RELIGION.gender=='Spagetti').first()
result_relationship_status = self._session.query(PERSON_RELATIONSHIP_STATUS).filter(PERSON_RELATIONSHIP_STATUS.gender=='ledig').first()

p1 = PERSON(name_normally_used ='Kevin', gender =result_gender, hair_color=result_hair_color,
eye_color =result_eye_color , title=result_title , salutation =result_salutation , place =result_place , 
religion =result_religion , relationship_status=result_relationship_status )

self._session.add(p1)
self._session.commit()
Zur Veranschaulichung soll diejenige Abfrage, die fehlschlägt, mittels der first()-Funktion absichtlich ein None zurückgeben, denn wir gehen mal davon aus, dass das Geschlecht "komisch", der Ort (place) "Mond" und die Religion "Spagetti" nicht geben werden. Diese Abfragen sind somit nicht erfolgreich, und im nächsten Schritt wird trotzdem all die None-Werte an die entsprechenden Beziehungen übergeben.
Die Fehlermeldung könnte dann wie folgt aussehen:
AttributeError: 'None' object has no attribute '_sa_instance_state'
Ist ja auch logisch, denn es handelt sich hierbei keineswegs um ein ORM-Objekt. Und genau hier setze ich mit meinen Gedanken an. Wie gesagt, es gibt mehrere QComboBox() auf meiner Oberfläche. 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.
jerch
User
Beiträge: 1630
Registriert: Mittwoch 4. März 2009, 14:19

Freitag 25. August 2017, 20:17

@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: 1074
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Freitag 25. August 2017, 20:22

@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: 1630
Registriert: Mittwoch 4. März 2009, 14:19

Freitag 25. August 2017, 20:28

@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: 1074
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Freitag 25. August 2017, 20:39

@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: 1630
Registriert: Mittwoch 4. März 2009, 14:19

Freitag 25. August 2017, 21:05

@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: 1074
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Freitag 25. August 2017, 21:43

@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

Freitag 25. August 2017, 21:59

@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: 1074
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Freitag 25. August 2017, 22:07

@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

Freitag 25. August 2017, 22:20

@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: 1630
Registriert: Mittwoch 4. März 2009, 14:19

Freitag 25. August 2017, 22:27

@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 ;)
Antworten