SQLAlchemy Verständnis Problem

Installation und Anwendung von Datenbankschnittstellen wie SQLite, PostgreSQL, MySQL, der DB-API 2.0 und sonstigen Datenbanksystemen.
Antworten
stfnx
User
Beiträge: 7
Registriert: Samstag 22. Oktober 2022, 11:15

Hi, ich arbeite gerade an einem Flask Projekt mit Flask-SQLAlchemy und SQLite.
Die Code-Ausschnitte sind vereinfacht um es möglichst klar darzustellen.

Folgende Models hab ich definiert:

Code: Alles auswählen

class BaseMixin:
    id           = db.Column(db.Integer, primary_key=True)
    time_created = db.Column(db.DateTime(timezone=True), server_default=func.current_timestamp())
    time_updated = db.Column(db.DateTime(timezone=True), onupdate=func.current_timestamp())


class Part(BaseMixin, db.Model):
    description  = db.Column(db.String(100), nullable=False)
    category_id  = db.Column(db.Integer, db.ForeignKey("category.id"), nullable=False)
    case_id      = db.Column(db.Integer, db.ForeignKey("case.id"), nullable=False)
    def __repr__(self):
        return f"Part({self.id}, '{self.description}')"
        

class Category(BaseMixin, db.Model):
    name       = db.Column(db.String(100), unique=True, nullable=False)
    parts      = db.relationship("Part", backref="category")
    def __repr__(self):
        return f"Category({self.id}, '{self.name}')"


class Case(BaseMixin, db.Model):
    name    = db.Column(db.String(100), unique=True, nullable=False)
    parts   = db.relationship("Part", backref="case")
    def __repr__(self):
        return f"Case({self.id}, '{self.name}')"


sqlalchemy.orm.configure_mappers()
Ich möchte jetzt z.B. alle Parts anzeigen, die als category xyz haben:

Code: Alles auswählen

statement = db.select(Part).where(Part.category== "xyz")
results = db.session.execute(statement)
Das funktioniert leider nicht. Ich bekomme folgende Fehlermeldung:

Code: Alles auswählen

sqlalchemy.exc.ArgumentError:
    Mapped instance expected for relationship comparison to object.
    Classes, queries and other SQL elements are not accepted in this context;
    for comparison with a subquery, use Part.category.has(**criteria).
Wenn ich das mit ner Spalte mach die keine Relation ist, dann funktioniert es:

Code: Alles auswählen

statement = db.select(Part).where(Part.description== "Hello World")
results = db.session.execute(statement)

for i in results:
    print(i)
    
# (Part(1, 'Hello World'),)
Wenn ich es über die ID mache, geht es ebenfalls:

Code: Alles auswählen

statement = db.select(Part).where(Part.category_id == 1)
results = db.session.execute(statement)

for i in results:
    print(i)
    
# (Part(1, 'Hello World'),)
# (Part(2, 'Test'),)
Das Problem scheint also beim Backref zu liegen. Ich bekomme es aber einfach nicht raus. Ich habe die SQLAlchemy Doku zu diesem Thema schon mehrmals durch und bin der Meinung dass ich eigentlich alles richtig gemacht habe. Auch Google und StackOverflow habe ich schon durch... ich checks einfach nicht. Wenn mir da jemand auf die Sprünge helfen könnte, wäre ich mega dankbar!

Dann gibt es noch eine weitere Sache die ich überhaupt nicht verstehe:

Code: Alles auswählen

print(results)
# [(Part(1, 'Hello World'),), (Part(2, 'Test'),)]

for i in results:
    print(i)
    
# (Part(1, 'Hello World'),)
# (Part(2, 'Test'),)
Warum zum Geier ist das in einem Tuple? Ich muss quasi folgendes machen:

Code: Alles auswählen

for i in results:
    print(i[0])

# Part(1, 'HelloWorld')
# Part(2, 'Test')

for i in results:
    print(i[0].description)
    
# Hello World
# Test
__deets__
User
Beiträge: 12829
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ein Category Objekt schon probiert? Denn danach sieh die Fehlermeldung doch aus.
stfnx
User
Beiträge: 7
Registriert: Samstag 22. Oktober 2022, 11:15

__deets__ hat geschrieben: Samstag 22. Oktober 2022, 11:57 Ein Category Objekt schon probiert? Denn danach sieh die Fehlermeldung doch aus.
Was meinst du damit? Bitte ein bisschen genauer.
Wo soll ich was mit einem Category Objekt machen?
Benutzeravatar
sparrow
User
Beiträge: 3563
Registriert: Freitag 17. April 2009, 10:28

Part.category ist doch keine Zeichenkette. Wie soll es denn dann funktionieren, dort eine Zeichenkette als Filterargument zu übergeben?
Ich habe seit Ewigkeiten nichts mehr mit SQLAlchemy gemacht, aber entweder musst du dort zum Eingrenzen ein Objekt vom Typ Category benutzen - oder das ansprechen, nach dem du filtern willst. Also zum Beispiel den Namen. Oder die ID.

Und natürlich ist das Resultat in einer Liste. Oder ein anderes iterierbares Objekt. Welche Datenstruktur würdest du denn vorschlagen für eine (unbekannte) Anzahl gleichgeformter Datensätze?
stfnx
User
Beiträge: 7
Registriert: Samstag 22. Oktober 2022, 11:15

Vielleicht habe ich dich falsch verstanden, aber ich würde deine Aussage folgendermaßen interpretieren:

Code: Alles auswählen

statement = db.select(Part).where(Part.Category == "Hallo Welt")
results = db.session.execute(statement).all()
# AttributeError: type object 'Part' has no attribute 'Category'

statement = db.select(Part).where(Part.category.name == "Hallo Welt")
results = db.session.execute(statement).all()
# AttributeError: Neither 'InstrumentedAttribute' object nor 'Comparator' object associated with Part.category has an attribute 'name'
Dass mehrere Zeilen (results) in einer Liste sind, ist ja vollkommen klar und logisch. Von mir aus auch in einem Tuple. Aber Aber warum ist eine einzelne Zeile (i in results) in einem Tuple? Das ist ein einzeles Objekt (Part). Ein einzelnes Objekt in eine Liste/Tuple zu stecken macht keinen Sinn.
__deets__
User
Beiträge: 12829
Registriert: Mittwoch 14. Oktober 2015, 14:29

@sparrow: es geht um eine einzelne Ergebniszeile. Die ist immer ein Tupel.

@stfnx: sparrow hat das mit dem Objekt ja schon ausgeführt. Und das du ein Tupel bekommst, ist eine bewusste Entwurfsentscheidung. Denn ein solches Query könnte ja mehrere Spalten liefern. Dann *muss* es ein Tupel sein. Das dann künstlich nicht zu machen, nur weil man genau eine Spalte bekommt, ist inkongruent und macht es schwerer, code drumrum zu schreiben.
__deets__
User
Beiträge: 12829
Registriert: Mittwoch 14. Oktober 2015, 14:29

Wir haben uns überschnitten. Offensichtlich hat die Erklärung nicht ausgereicht. Was sparrow und ich meinen ist sowas hier:

Code: Alles auswählen

 statement = db.select(Part).where(Part.category == Category.find_by_name("Hallo Welt"))
Das find_by_name ist ausgedacht, es geht darum, dass die rechte Seite deines Vergleichs ein Objekt sein muss. Nicht ein Name, von dem SA keine Ahnung hat, wie daraus ein Category Objekt entstehen soll.
stfnx
User
Beiträge: 7
Registriert: Samstag 22. Oktober 2022, 11:15

Das hier ist das Ergebnis einer Anfrage.
Es sind mehrere Zeilen in einer Liste. Soweit so klar.

Code: Alles auswählen

[(Part(1, 'Hello World'),), (Part(2, 'Test'),)]
Das hier wiederum ist eine Zeile. Hier verstehe ich nicht warum das Tuple? Eine Zeile ist eine Zeile, in welchem Szenario wäre die Länge des Tuples denn >1?

Code: Alles auswählen

(Part(1, 'Hello World'),)
Die einzelnen Spalten dieser Zeile landen ja nicht in dem Tuple, sondern in dem Objekt.

Code: Alles auswählen

i[0].description  # description = Spalte
@__deets__ In der Doku ist es aber auch ein String: https://docs.sqlalchemy.org/en/14/orm/q ... statements
Zuletzt geändert von stfnx am Samstag 22. Oktober 2022, 12:38, insgesamt 1-mal geändert.
__deets__
User
Beiträge: 12829
Registriert: Mittwoch 14. Oktober 2015, 14:29

Du kannst Queries auch mit nur Werten bestimmter Spalten und auch aggregaten (min/max/sum) machen. Dann gibt’s Tupel mit mehreren Werten.

SA ist in Bezug auf seine ORM-Funktion etwas rudimentärer. Andere ORMs würden hier vielleicht nur Objekte liefern. Aber dafür eben auch nicht so flexibel mit SQL umgehen.
Benutzeravatar
sparrow
User
Beiträge: 3563
Registriert: Freitag 17. April 2009, 10:28

@stfnx: Zu deinem Edit: Klar ist das in der Doku ein String. Wenn man nach einer Spalte filtern möchte, die ein String ist. Willst du ja aber nicht. Deine Spalte ist für das ORM eine Spalte die "Category"-Objekte enthält. Und auf Datenbankebene steht da ein Fremschlüssel drin. Einfach beliebig etwas aus der Dokumentation suchen funktioniert nicht. Du musst schon nach deinem Problem gucken.

Du selektierst hier zufällig nur eine Spalte, aber es kann natürlich eine beliebige Anzahl sein. Selbst durch joins sollte das passieren.
Es würde mich aber wundern, wenn man nich tim Result auf das Objekt zugreifen kann. In der Dokumentation wird das ausführlich gemacht.
stfnx
User
Beiträge: 7
Registriert: Samstag 22. Oktober 2022, 11:15

Nagut, also wenn ihr euch so sicher seid, dass das mit dem Tuple passt, dann finde ich mich einfach damit ab.

https://docs.sqlalchemy.org/en/14/orm/q ... attributes
Jetzt hab ich das verstanden. :)

Wenn ich statt .all() .scalars() nehme, dann ist das Tuple weg und die Daten kommen so wie ich erwartet habe.

Code: Alles auswählen

results = db.session.execute(statement).all()
results = db.session.execute(statement).scalars()

for i in results:
    print(i.description)
Wenn das mit dem Select für euch so klar ist, wärt ihr dann so nett und würdet mir das in einem Code Schnipsel präsentieren? Oder gerne auch ein Link zu Doku. So verstehe ich es einfach nicht und finde in der Doku auch nichts passendes.
Benutzeravatar
sparrow
User
Beiträge: 3563
Registriert: Freitag 17. April 2009, 10:28

Wie gesagt: Entweder du musst ein Objekt vom Typ Category für den Filter verwenden und keine Zeichenkette. Das würde zum Beispiel funktionieren, indem du das selbst vorher selektierst oder eben an der Stelle tust. So wie du ja hier auch ein Objekt vom Typ "Part" selektierst.

Sinnvoller ist es das ohne zusätzlichen Datenbankhit zu tun. Da musst du in der Dokumentation schauen, wie man das über Relationen hinweg macht. Danach musst du in der Dokumentation schauen. Ich bin da vom Django ORM die sanfte Gangart gewohnt. Möglicherweise musst du explizit einen Join verwenden.
Benutzeravatar
sparrow
User
Beiträge: 3563
Registriert: Freitag 17. April 2009, 10:28

Und hier wird ausgiebig beschrieben, wie man mit "Related Objects" arbeitet.
Ich glaube, das hätte man finden können.
Benutzeravatar
__blackjack__
User
Beiträge: 11265
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Ich hab's nur überflogen, aber das mit den Tupeln kommt ja auch daher, dass hier die low-level API verwendet wird und nicht die ORM-API. Also SA ist nicht ”rudimentär” sondern es wird absichtlich (?) die API verwendet die unter dem ORM angesiedelt ist.
“It should be noted that no ethically-trained software engineer would ever consent to write a `DestroyBaghdad` procedure. Basic professional ethics would instead require him to write a `DestroyCity` procedure, to which `Baghdad` could be given as a parameter.” — Nathaniel Borenstein
stfnx
User
Beiträge: 7
Registriert: Samstag 22. Oktober 2022, 11:15

sparrow hat geschrieben: Samstag 22. Oktober 2022, 13:17Ich glaube, das hätte man finden können.
Das kannst du dir gerne sparen. Ist nicht so dass ich nach zwei Minuten probieren aufgegeben hätte und direkt hier gefragt habe. Ich sitze jetzt fast zwei Tage an dem Problem und es hat immer noch nicht Klick gemacht. :x
__blackjack__ hat geschrieben: Samstag 22. Oktober 2022, 14:13es wird absichtlich (?) die API verwendet die unter dem ORM angesiedelt ist.
Die Tutorials die ich mir vorab angeschaut habe, haben alle Part.query() verwendet.
Ist es das, worauf du anspielst?
Falls ja, das scheint laut Doku Legacy zu sein und es wird empfohlen stattdessen .select() und .execute() zu werwenden.
Falls nein, was meinst du dann?

Also... ich merke dass mir hier noch einiges an Grundverstädniss für 1. SQLALchemy und 2. Datenbanken im allgemeinen fehlt. Ich werde das Projekt an dieser Stelle erst mal pausieren und mir eine ordentliche Verständnis-Grundlage für dieses Thema aneignen. Ich denke das ist die vernünftigste Herangehensweise.

Danke an alle die versucht haben mir weiterzuhelfen.
Benutzeravatar
sparrow
User
Beiträge: 3563
Registriert: Freitag 17. April 2009, 10:28

@stfnx: Das grundlegende Einlesen was Relationale Datenbanken eigentlich sind, ist sinnvoll. In dem Zuge ganz wichtig einmal verstehen, was "Normalisierung" in dem Zusammenhang ist. Der Wikipedia-Artikel ist ganz gut.
SQL zu kennen, damit man versteht, was da unter der Haube passiert, ist ebenfalls gut.

Ich sehe auch nicht, wo sich dein Aussage findet, dass .query legacy ist. Hier ist das Tutorial zum ORM-Mapping mit SQLAlchemy. Das ist ziemlich wahrscheinlich das, was du dir zum Einstieg anschauen solltest.
stfnx
User
Beiträge: 7
Registriert: Samstag 22. Oktober 2022, 11:15

So. Ich konnte es nicht auf mir sitzen lassen, habe weiter probiert und endlich eine funktionierende Lösung gefunden. :twisted:

Code: Alles auswählen

statement = db.session.query(Part).join(Part.category).filter(Category.name == "xyz")
results = db.session.execute(statement).scalars().all()

print(results)
# [Part(1, 'Hello World'), Part(2, 'Test')]

for part in results:
    print(part.category.name)
# xyz
Super! Jetzt kann ich ruhig schlafen.
Trotzdem werde ich mich in das Thema erst tiefer einarbeiten, bevor ich hier weiter mache.
bb1898
User
Beiträge: 189
Registriert: Mittwoch 12. Juli 2006, 14:28

Zu "select" versus "query", aus dem oben verlinkten Tutorial, Abschnitt "Working with Data":
Selecting Rows with Core or ORM

For both Core and ORM, the select() function generates a Select construct which is used for all SELECT queries. Passed to methods like Connection.execute() in Core and Session.execute() in ORM, a SELECT statement is emitted in the current transaction and the result rows available via the returned Result object.
Habe ich immer so verstanden, dass "select" die aktuell benutzte und empfohlene Methode ist. Wenn ich nicht irrre, wird sie in diesem Tutorial auch durchgehend benutzt. Eine zweite Stelle in der Dokumentation finde ich im Augenblick gerade nicht wieder: da wird darauf hingewiesen, dass in SQLAlchemy 1.4 ff "query" unter der Haube "select" benutzt.
Aber sicherlich sind noch reichlich Tutorials unterwegs, die die etwas älteren Versionen zugrunde legen.
Antworten