SQLAlchemy - ORM - Arbeiten mit Zuordnungstabellen

Installation und Anwendung von Datenbankschnittstellen wie SQLite, PostgreSQL, MariaDB/MySQL, der DB-API 2.0 und sonstigen Datenbanksystemen.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Hallo Leute,

im nachfolgenden ausführbaren Quelltext werden insgesamt drei Tabellen angelegt. Die Konstruktion zielt auf die n:m-Beziehung ab. Nun, einige Datensätze werden nach der erfolgreichen Ausführung in die Datenbank gespeichert. Jetzt möchte ich so simulieren, dass ich später, nachträglich, die Datensätze miteinander in Beziehung setzen möchte. Das Problem bei den meisten Beispielen, die ich im Internet finde, ist, dass diese so aufgeführt werden, dass erst einmal die einzelnen Tabellen mit Daten gefüllt werden, und anschließend in einem Rutsch die Zuordnungstabelle entsprechend gefüttert wird. Als Beispiel hier: Creating and populating a database using Python and SQLalchemy. Part 2: Classes and queries. Dieses Beispiel ist super und funktioniert.

ABER: Die Sache ist einfach die: Ich möchte nicht alles in einem Rutsch abfertigen. Ich möchte quasi später, zu einem anderen Zeitpunkt, mit den bereits abgespeicherten Datensätzen arbeiten, und diese in der Zuordnungstabelle unterbringen. Allerdings habe ich keine Idee, wie ich das bewerkstelligen soll.

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
from sqlalchemy.ext.associationproxy import association_proxy

#memory_url = 'sqlite:///:memory:')

#sqlite_url = 'sqlite:///test.db'

mysql_url = '{dbms}+{dbdriver}://{dbuser}:{dbuser_pwd}@{db_server_host}:{dbport}/{db_name}'.format(
           dbms='mysql', dbdriver='pymysql', dbuser='root', dbuser_pwd='', db_server_host='localhost', dbport=3306, db_name='test')

engine = sqlalchemy.create_engine(mysql_url, echo=True)

Base = declarative_base()


class Order_Product(Base):
  __tablename__ = 'order_product'
  order_id = Column(Integer, ForeignKey('orders.id'), primary_key=True)
  product_id = Column(Integer, ForeignKey('products.id'), primary_key=True)
  quantity = Column(String(30))

  order = relationship("Order", backref=backref("order_product", lazy='dynamic', cascade="all, delete-orphan" ))
  product = relationship("Product", backref=backref("order_product", lazy='dynamic', cascade="all, delete-orphan" ))

  def __init__(self, order, product, quantity):
    self.order = order
    self.product =  product
    self.quantity = quantity

class Product(Base):
  __tablename__ = 'products'
  id = Column(Integer,  primary_key=True, unique=True)
  name = Column(String(80))

  orders = relationship("Order", secondary="order_product")

  def __init__(self, name):
    self.name = name

class Order(Base):
  __tablename__ = 'orders'
  id = Column(Integer,  primary_key=True, unique=True)
  name = Column(String(80))

  orders = relationship("Product", secondary="order_product")

  def __init__(self, name):
    self.name = name


Base.metadata.create_all(engine)

Session = sessionmaker(bind=engine)
session = Session()

prod1 = Product(name="Oreo")
prod2 = Product(name="Hide and Seek")
prod3 = Product(name="Marie")
prod4 = Product(name="Good Day")


session.add_all([prod1, prod2, prod3, prod4])
session.commit()

order1 = Order( name = "First Order")
order2 = Order( name = "Second Order")
order3 = Order( name = "Third Order")
order4 = Order( name = "Fourth Order")

session.add_all([order1, order2, order3, order4])
session.commit()
# Done! User makes a long break
# One hour later
# User wants to fill the allocation table named Order_Product
# [...]
__deets__
User
Beiträge: 14494
Registriert: Mittwoch 14. Oktober 2015, 14:29

Verstehe ich nicht. Du kannst doch Order_Product Objekte anlegen wann du willst. Was geht denn da nicht?
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@__deets__: Darüber habe ich auch nachgedacht und habe folgendes kreiert (ab Zeile 80 bis 95). Leider klappt es nicht so, wie ich es mir erhofft habe. Ich stecke gedanklich irgendwo fest.

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
from sqlalchemy.ext.associationproxy import association_proxy
 
#memory_url = 'sqlite:///:memory:')
 
#sqlite_url = 'sqlite:///test.db'
 
mysql_url = '{dbms}+{dbdriver}://{dbuser}:{dbuser_pwd}@{db_server_host}:{dbport}/{db_name}'.format(
           dbms='mysql', dbdriver='pymysql', dbuser='root', dbuser_pwd='danny5658', db_server_host='localhost', dbport=3306, db_name='xarphus')
 
engine = sqlalchemy.create_engine(mysql_url, echo=True)
 
Base = declarative_base()
 
 
class Order_Product(Base):
  __tablename__ = 'order_product'
  order_id = Column(Integer, ForeignKey('orders.id'), primary_key=True)
  product_id = Column(Integer, ForeignKey('products.id'), primary_key=True)
  quantity = Column(String(30))
 
  order = relationship("Order", backref=backref("order_product", lazy='dynamic', cascade="all, delete-orphan" ))
  product = relationship("Product", backref=backref("order_product", lazy='dynamic', cascade="all, delete-orphan" ))
 
  def __init__(self, order, product, quantity):
    self.order = order
    self.product =  product
    self.quantity = quantity
 
class Product(Base):
  __tablename__ = 'products'
  id = Column(Integer,  primary_key=True, unique=True)
  name = Column(String(80))
 
  orders = relationship("Order", secondary="order_product")
 
  def __init__(self, name):
    self.name = name
 
class Order(Base):
  __tablename__ = 'orders'
  id = Column(Integer,  primary_key=True, unique=True)
  name = Column(String(80))
 
  orders = relationship("Product", secondary="order_product")
 
  def __init__(self, name):
    self.name = name
 
 
Base.metadata.create_all(engine)
 
Session = sessionmaker(bind=engine)
session = Session()
 
prod1 = Product(name="Oreo")
prod2 = Product(name="Hide and Seek")
prod3 = Product(name="Marie")
prod4 = Product(name="Good Day")
 
 
session.add_all([prod1, prod2, prod3, prod4])
session.commit()
 
order1 = Order( name = "First Order")
order2 = Order( name = "Second Order")
order3 = Order( name = "Third Order")
order4 = Order( name = "Fourth Order")
 
session.add_all([order1, order2, order3, order4])
session.commit()
 #Done! User makes a long break
 #One hour later
 #User wants to fill the allocation table named Order_Product
 #[...]

# First, we need the IDs from both - special order and special product
ord_id = None
prod_id = None

ord = session.query(Order)
prod = session.query(Product)

for order_elem in  ord.filter(Order.name == 'First Order'):
    ord_id = order_elem.id
    
for product_elem in  prod.filter(Product.name == 'Good Day'):
    prod_id = product_elem.id

allocation = Order_Product(order_id =ord_id, product_id =prod_id, quantity='Bad quality')
session.add(allocation)
session.commit()
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@Sophus: hilfreich wäre es, wenn Du auch die Fehlermeldung postest. Eingerückt wird immer mit vier Leerzeichen pro Zeile, nicht zwei. Und Du willst nicht mit IDs arbeiten, sondern mit den Objekten selbst:

Code: Alles auswählen

order = session.query(Order).filter(Order.name=="Second Order").one()
product = session.query(Product).filter(Product.name=="Marie").one()
allocation = Order_Product(order=order, product=product, quantity=100)
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@Sirius3: Es ist seltsam, dass du mich des Öfteren auf die Einrückungen ansprichst. Ich arbeite bei kleineren Tests und Beispielen mit der hauseigenen Python IDLE. Dort habe ich nichts verstellt und ich bekomme keinerlei Meldungen, dass meine Einrückungen nicht stimmen. Deswegen verwirren mich jedesmal deine Anmerkungen.

Zu deiner Aussage, ich wolle nicht mit IDs arbeiten: Das verwirrt mich, Wieso nicht? In Zuordnungstabellen kommen ja nur IDs rein. Wieso will ich nicht mit IDs arbeiten? Bestimmt verstehe ich dich falsch.

UPDATE
Wenn ich deine Variante nehme, Sirius3, dann bekomme ich folgende Meldung:
Traceback (most recent call last):
File "C:\Users\Sophus\Desktop\py_forum.py", line 95, in <module>
session.commit()
File "C:\Python27\lib\site-packages\sqlalchemy\orm\session.py", line 874, in commit
self.transaction.commit()
File "C:\Python27\lib\site-packages\sqlalchemy\orm\session.py", line 461, in commit
self._prepare_impl()
File "C:\Python27\lib\site-packages\sqlalchemy\orm\session.py", line 441, in _prepare_impl
self.session.flush()
File "C:\Python27\lib\site-packages\sqlalchemy\orm\session.py", line 2139, in flush
self._flush(objects)
File "C:\Python27\lib\site-packages\sqlalchemy\orm\session.py", line 2259, in _flush
transaction.rollback(_capture_exception=True)
File "C:\Python27\lib\site-packages\sqlalchemy\util\langhelpers.py", line 60, in __exit__
compat.reraise(exc_type, exc_value, exc_tb)
File "C:\Python27\lib\site-packages\sqlalchemy\orm\session.py", line 2223, in _flush
flush_context.execute()
File "C:\Python27\lib\site-packages\sqlalchemy\orm\unitofwork.py", line 389, in execute
rec.execute(self)
File "C:\Python27\lib\site-packages\sqlalchemy\orm\unitofwork.py", line 548, in execute
uow
File "C:\Python27\lib\site-packages\sqlalchemy\orm\persistence.py", line 181, in save_obj
mapper, table, insert)
File "C:\Python27\lib\site-packages\sqlalchemy\orm\persistence.py", line 799, in _emit_insert_statements
execute(statement, multiparams)
File "C:\Python27\lib\site-packages\sqlalchemy\engine\base.py", line 945, in execute
return meth(self, multiparams, params)
File "C:\Python27\lib\site-packages\sqlalchemy\sql\elements.py", line 263, in _execute_on_connection
return connection._execute_clauseelement(self, multiparams, params)
File "C:\Python27\lib\site-packages\sqlalchemy\engine\base.py", line 1053, in _execute_clauseelement
compiled_sql, distilled_params
File "C:\Python27\lib\site-packages\sqlalchemy\engine\base.py", line 1189, in _execute_context
context)
File "C:\Python27\lib\site-packages\sqlalchemy\engine\base.py", line 1396, in _handle_dbapi_exception
util.reraise(*exc_info)
File "C:\Python27\lib\site-packages\sqlalchemy\engine\base.py", line 1182, in _execute_context
context)
File "C:\Python27\lib\site-packages\sqlalchemy\engine\default.py", line 470, in do_execute
cursor.execute(statement, parameters)
File "C:\Python27\lib\site-packages\pymysql\cursors.py", line 164, in execute
query = self.mogrify(query, args)
File "C:\Python27\lib\site-packages\pymysql\cursors.py", line 143, in mogrify
query = query % self._escape_args(args, conn)
File "C:\Python27\lib\site-packages\pymysql\cursors.py", line 123, in _escape_args
return dict((key, conn.literal(val)) for (key, val) in args.items())
File "C:\Python27\lib\site-packages\pymysql\cursors.py", line 123, in <genexpr>
return dict((key, conn.literal(val)) for (key, val) in args.items())
File "C:\Python27\lib\site-packages\pymysql\connections.py", line 817, in literal
return self.escape(obj, self.encoders)
File "C:\Python27\lib\site-packages\pymysql\connections.py", line 810, in escape
return escape_item(obj, self.charset, mapping=mapping)
File "C:\Python27\lib\site-packages\pymysql\converters.py", line 27, in escape_item
val = encoder(val, mapping)
File "C:\Python27\lib\site-packages\pymysql\converters.py", line 110, in escape_unicode
return u"'%s'" % _escape_unicode(value)
File "C:\Python27\lib\site-packages\pymysql\converters.py", line 73, in _escape_unicode
return value.translate(_escape_table)
AttributeError: 'Order' object has no attribute 'translate'
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

Der Code wie du ihn gezeigt hast müsste schon in Zeile 93 eine Exception werfen weil Order_Product (schlechter Name) kein order_id Argument hat.

Das du cascade für die relationship definierst aber nicht für den ForeignKey ist irgendwie seltsam. Was willst du da erreichen? Grundsätzlich solltest du deine Verwendung von relationship überdenken, die so zu verwenden wie du es tust kann zu schwierig nachvollziehbaren Fehlern führen.

Ein primary key ist implizit unique, es gibt keinen Grund dies explizit zu definieren.

SQLAlchemy hat ein URL Objekt, es gibt keinen Grund zu String Formattierung zu greifen.

Zu guter letzt: MySQL ist eine ziemlich schlechte relationelle Datenbank, es ist eine katastrophal schlechte Datenbank in der Hand von Leuten die nicht wissen was sie tun. Tu dir einen Gefallen und nimm Postgres.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

DasIch hat geschrieben: Das du cascade für die relationship definierst aber nicht für den ForeignKey ist irgendwie seltsam. Was willst du da erreichen? Grundsätzlich solltest du deine Verwendung von relationship überdenken, die so zu verwenden wie du es tust kann zu schwierig nachvollziehbaren Fehlern führen.
Inwiefern sollte ich meine Verwendung von relationship überdenken? Und weshalb sollte mn die cascade für ForeignKey definieren? Recht die Definition in der Relaationship nicht aus? Vielleicht bringst du mir etwas Licht in meine Unwissenheit?

UPDATE:
Bezüglich der Cascade habe ich diese Antwort hier gelesen: SQLAlchemy: cascade delete. Demnach wird die Cascade auf backref definiert. Ich habe die Cascade-Definition in die backref geholt, und lazy ausserhalb von backref gesetzt.

Code: Alles auswählen

  order = relationship("Order", backref=backref("order_product", cascade="all, delete-orphan" ), lazy='dynamic')
  product = relationship("Product", backref=backref("order_product", cascade="all, delete-orphan" ), lazy='dynamic')
Oder

Meinst du das etwas so?

Code: Alles auswählen

class Order_Product(Base):
  __tablename__ = 'order_product'
  order_id = Column(Integer, ForeignKey('orders.id', ondelete='CASCADE'), primary_key=True)
  product_id = Column(Integer, ForeignKey('products.id', ondelete='CASCADE'), primary_key=True)
  quantity = Column(String(30))
 
  order = relationship("Order", backref=backref("order_product", cascade="all, delete-orphan" ), lazy='dynamic')
  product = relationship("Product", backref=backref("order_product", cascade="all, delete-orphan" ), lazy='dynamic')
BlackJack

@Sophus: Du implementierst die `__init__()`, rufst aber die von `Base` nicht auf. Das würde ich nicht riskieren, da könnten wichtige Dinge passieren. Du machst in den eigenen `__init__()`-Methoden aber auch nichts wirklich interessantes ausser zu ermöglichen die Namen der Argumente beim erstellen nicht angeben zu müssen. Was aber trotzdem gemacht wird.

Man will nicht mit IDs arbeiten weil man stattdessen direkt mit den Objekten arbeiten kann für die diese IDs stehen. Ausserdem sind IDs fehleranfälliger, denn bei den Objekten kann geprüft werden ob die auch tatsächlich dem entsprechen was erwartet wird und das Objekt gibt es auch ganz sicher, sonst könnte man es nicht übergeben oder zuweisen. Die IDs sind einfach nur Zahlen ohne weitere Informationen. Man kann aus versehen falsche Zahlen übergeben und zu so einer Zahl muss auch kein Objekt existieren wenn man etwas falsch gemacht hat. Und manchmal hat man auch Objekte wo es noch gar keine ID gibt, weil die gerade erst erzeugt worden sind. Dann kann man nur das Objekt verwenden.

Es ist aus Python-Sicht kein Fehler nicht vier Leerzeichen pro Einrückebene zu verwenden. Aber es ist eben *die* Konvention. Und die meisten IDEs und Editoren mit Plugins zur Python-Programmierung prüfen den Quelltext auf PEP8 und kennzeichnen solche Zeilen mit einer Warnung. IDLE sollte eigentlich in der Standardeinstellung vier Leerzeichen verwenden. Du benutzt schon die Tabulatortaste und tippst nicht tatsächlich Leerzeichen einzeln ein zum einrücken‽

Zur Zeichenkettenformatierung bei der URL für die Datenbank: Warum machst Du das überhaupt? Das schöne an diesen URLs ist ja, dass man die Daten für eine Verbindung als *eine* URL angeben, und zum Beispiel auch so in einer Konfigurationsdatei speichern kann.

Bei der CASCADE-Geschichte wäre erst einmal zu fragen was denn da überhaupt gelöscht werden kann/soll und was dann mit verschwinden soll. Erste Frage ist was denn gelöscht werden können soll. Normalerweise ist die erste Antwort von Datenbänklern auf diese Frage: Gar nichts. Womit man dann auch keine Probleme mit Kaskaden hat, weil die überflüssig sind wenn man nichts löscht. So einfach kann das manchmal sein. :-)
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@Blackjack: Ich hane in meiner IDLE nichts verstellt. Wieso sollte ich sowas tun? Och arbeite nur mit TAB-Tasten. In einigen Situationen, in denen bestimmte Konstrukte definiert werden, rückt IDLE von alleine ein.

Zur ID. Ich habe die Anmerkung Sirius3s berücksichtigt. Die Fehlermeldung habe ich weiter oben schon gepostet. Irgendwie scheint es mir nicht möglich zu sein mit dem Objekt zu arbeiten. Die Idee hatte ich ja auch schon, aber gleich verworfen, als ich Fehlermeldungen bekam. Daher arbeite ich in diesem Beispiel mit IDs.

Zu den __init__(): Diese habe ich in den Objekten deswegen genommen, damot ich an die Namen der Entitäten der Tabellen komme. Mehr muss erst einmal nicht gemacht werden, oder? Es geht ja um die Sichtbarkeit.

Zu den Cascaden. Was ich möchte ist, wenn ein Datensatz aus der Elterntabelle gelöscht wird, sollen die dazugehörigen Daten aus der Kindtabelle verschwinden. Man kann ja auch SET NULL nehmen oder einfach gar nichts machen. So ist es ja in der Standardeinstellung von MySQL. Ich möchte sie dann einfach gelöscht haben. Was soll man mit Datenleichen anfangen? :)
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@Sophus: da Du den Code, der zur Fehlermeldung führt, nicht zeigst, kann man auch nicht sagen, was Du da denn falsch gemacht hast.

Zu BlackJacks Anmerkung zu Cascade: wenn möglich, sollten Datenbanken so designed werden, dass ein Löschen gar nicht nötig oder gar möglich ist. Gerade bei einem Bestellvorgang mit Produkten will nicht, dass man einmal abgeschlossene Bestellungen noch ändern kann.
BlackJack

@Sophus: Also bei mir war und ist in IDLE immmer 4 Leerzeichen für die Einrückung eingestellt und ich habe da nie etwas dran geändert. Es wäre schon sehr komisch wenn das bei Dir anders ist.

Wenn Du mit den Objekten nicht arbeiten kannst, dann kannst Du mit dem ORM ja letztendlich nicht arbeiten, also solltest Du den Fehler beheben statt da irgendwie drumherum zu programmieren und zu hoffen das Du nicht wieder auf den Fehler stösst. Was nützt einem ein ORM wenn man das O nicht verwendet?

Die Begründung zur `__init__()` habe ich nicht verstanden. Die machen so wie wie sie da stehen keinen Sinn weil die letztlich nichts machen was die `Base.__init__()` nicht auch machen, ausser das die `Base.__init__()` vielleicht noch mehr macht, was jetzt nicht passiert, aber vielleicht wichtig wäre. Eventuell ist das ja sogar die Quelle für das Problem was Du mit den IDs umgehen willst. Wenn man eine `__init__()` überschreibt wo man von der Basisklasse nicht *genau* weiss das die nichts macht, muss man die der Basisklasse aufrufen. Selbst wenn die nichts macht, sollte man das tun, sonst fällt einem das auf die Füsse wenn irgendwann später dort doch noch Code dazu kommt der etwas tut. Aber wie gesagt, man kann die auch einfach löschen wenn sie sowieso nur die Attribute setzen.

Was sind denn jetzt Eltern und was sind Kinder? Es gibt hier drei Tabellen. Wo soll was gelöscht werden können, und welche Folgen soll das dann haben? Es macht ja beispielsweise keinen Sinn Produkte zu löschen wenn man eine Bestellung löscht, denn die Produkte möchte man ja für spätere Bestellungen noch zur Verfügung haben. Produkte die Bestandteil von einer Bestellung sind, kann man nicht löschen, denn dann geht die Bestellung ”kaputt”. Eigentlich kann man nur Produkte löschen die nich nicht Bestandteil einer Bestellung sind → dann gibt's nichts was kaskadierend passieren müsste. Oder man löscht eine noch nicht ausgeführte Bestellung und müsste dann alle Einträge dazu in der Verbindungstabelle löschen.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Hallo,

um die Verwirrung aus dem Weg zu räumen, habe ich meinen Beispiel-Quelltext überarbeitet. Als Beispiel habe ich die Situation Film und Genre genommen. Damit wäre auch die CASCADE-Thematik vom Tisch. Was hat sich geändert? Zunächst habe ich BlackJakcs Anmerkung berücksichtigt, dass die __init__()-Methoden aus den Tabellen entfernt werden soll. Dann habe ich Sirius3s Anmerkung berücksichtigt, dass man nicht mit IDs, sondern mit Objekten arbeiten soll, und ich habe __deets__ Anmerkung berücksichtigt, dass man nicht nur die CASCADE auf die m:n Beziehung definiert, sondern auch auf die Fremdschlüssel in der Zuordnungstabelle.

ABER: Wenn ich den nachfolgenden ausführbaren Quelltext ausführe, erhalte ich dennoch eine Fehlermeldung. Am Ende ist sie zu sehen.

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
from sqlalchemy.ext.associationproxy import association_proxy
 
#memory_url = 'sqlite:///:memory:')
 
#sqlite_url = 'sqlite:///test.db'
 
mysql_url = '{dbms}+{dbdriver}://{dbuser}:{dbuser_pwd}@{db_server_host}:{dbport}/{db_name}'.format(
           dbms='mysql', dbdriver='pymysql', dbuser='root', dbuser_pwd='', db_server_host='localhost', dbport=3306, db_name='test')
 
engine = sqlalchemy.create_engine(mysql_url, echo=True)
 
Base = declarative_base()
 
 
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" ))
  
class Film(Base):
  __tablename__ = 'film'
  id = Column(Integer,  primary_key=True, unique=True)
  name = Column(String(80))
 
  orders = relationship("Genre", secondary="allocation_film_genre")
  
class Genre(Base):
  __tablename__ = 'genre'
  id = Column(Integer,  primary_key=True, unique=True)
  name = Column(String(80))
 
  orders = relationship("Film", secondary="allocation_film_genre")
 
Base.metadata.create_all(engine)
 
Session = sessionmaker(bind=engine)
session = Session()
 
film1 = Film(name="Saw")
film2 = Film(name="Amageddon")
film3 = Film(name="Little Stuart")
film4 = Film(name="Doom Day")
 
 
session.add_all([film1, film2, film3, film4])
session.commit()
 
genre1 = Genre( name = "Horror")
genre2 = Genre( name = "Komoedie")
genre3 = Genre( name = "Psycho")
genre4 = Genre( name = "Thriller")
 
session.add_all([genre1, genre2, genre3, genre4])
session.commit()

film_obj = session.query(Film).filter(Film.name=="Saw").one()
genre_obj = session.query(Genre).filter(Genre.name=="Horror").one()

allocation = Allocation_Film_Genre(film_id=film_obj, genre_id=genre_obj)
session.add(allocation)
session.commit()
Fehlermeldung:
Traceback (most recent call last):
File "C:/Users/Sophus/Desktop/old_allocation.py", line 68, in <module>
session.commit()
File "C:\Python27\lib\site-packages\sqlalchemy\orm\session.py", line 874, in commit
self.transaction.commit()
File "C:\Python27\lib\site-packages\sqlalchemy\orm\session.py", line 461, in commit
self._prepare_impl()
File "C:\Python27\lib\site-packages\sqlalchemy\orm\session.py", line 441, in _prepare_impl
self.session.flush()
File "C:\Python27\lib\site-packages\sqlalchemy\orm\session.py", line 2139, in flush
self._flush(objects)
File "C:\Python27\lib\site-packages\sqlalchemy\orm\session.py", line 2259, in _flush
transaction.rollback(_capture_exception=True)
File "C:\Python27\lib\site-packages\sqlalchemy\util\langhelpers.py", line 60, in __exit__
compat.reraise(exc_type, exc_value, exc_tb)
File "C:\Python27\lib\site-packages\sqlalchemy\orm\session.py", line 2223, in _flush
flush_context.execute()
File "C:\Python27\lib\site-packages\sqlalchemy\orm\unitofwork.py", line 389, in execute
rec.execute(self)
File "C:\Python27\lib\site-packages\sqlalchemy\orm\unitofwork.py", line 548, in execute
uow
File "C:\Python27\lib\site-packages\sqlalchemy\orm\persistence.py", line 181, in save_obj
mapper, table, insert)
File "C:\Python27\lib\site-packages\sqlalchemy\orm\persistence.py", line 799, in _emit_insert_statements
execute(statement, multiparams)
File "C:\Python27\lib\site-packages\sqlalchemy\engine\base.py", line 945, in execute
return meth(self, multiparams, params)
File "C:\Python27\lib\site-packages\sqlalchemy\sql\elements.py", line 263, in _execute_on_connection
return connection._execute_clauseelement(self, multiparams, params)
File "C:\Python27\lib\site-packages\sqlalchemy\engine\base.py", line 1053, in _execute_clauseelement
compiled_sql, distilled_params
File "C:\Python27\lib\site-packages\sqlalchemy\engine\base.py", line 1189, in _execute_context
context)
File "C:\Python27\lib\site-packages\sqlalchemy\engine\base.py", line 1396, in _handle_dbapi_exception
util.reraise(*exc_info)
File "C:\Python27\lib\site-packages\sqlalchemy\engine\base.py", line 1182, in _execute_context
context)
File "C:\Python27\lib\site-packages\sqlalchemy\engine\default.py", line 470, in do_execute
cursor.execute(statement, parameters)
File "C:\Python27\lib\site-packages\pymysql\cursors.py", line 164, in execute
query = self.mogrify(query, args)
File "C:\Python27\lib\site-packages\pymysql\cursors.py", line 143, in mogrify
query = query % self._escape_args(args, conn)
File "C:\Python27\lib\site-packages\pymysql\cursors.py", line 123, in _escape_args
return dict((key, conn.literal(val)) for (key, val) in args.items())
File "C:\Python27\lib\site-packages\pymysql\cursors.py", line 123, in <genexpr>
return dict((key, conn.literal(val)) for (key, val) in args.items())
File "C:\Python27\lib\site-packages\pymysql\connections.py", line 817, in literal
return self.escape(obj, self.encoders)
File "C:\Python27\lib\site-packages\pymysql\connections.py", line 810, in escape
return escape_item(obj, self.charset, mapping=mapping)
File "C:\Python27\lib\site-packages\pymysql\converters.py", line 27, in escape_item
val = encoder(val, mapping)
File "C:\Python27\lib\site-packages\pymysql\converters.py", line 110, in escape_unicode
return u"'%s'" % _escape_unicode(value)
File "C:\Python27\lib\site-packages\pymysql\converters.py", line 73, in _escape_unicode
return value.translate(_escape_table)
AttributeError: 'Film' object has no attribute 'translate'
Sirius3
User
Beiträge: 17711
Registriert: Sonntag 21. Oktober 2012, 17:20

@Sophus: Du weist den IDs die Objekte zu, was keinen Sinn ergibt.

Code: Alles auswählen

allocation = Allocation_Film_Genre(film=film_obj, genre=genre_obj)
Und das Postfix _obj ist witzlos, da in Python alles ein Objekt ist.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Ok, ich glaube, ich habe den Fehler selbst gefunden. Ich habe die Anmerkung Sirius3s berücksichtigt, aber auf die Objekte keine iD abgerufen.

Hier meine Verbesserung:
Aus:

Code: Alles auswählen

film_obj = session.query(Film).filter(Film.name=="Saw").one()
genre_obj = session.query(Genre).filter(Genre.name=="horror").one()

allocation = Allocation_Film_Genre(film_id=film_obj, genre_id=genre_obj)
session.add(allocation)
session.commit()
Wird:

Code: Alles auswählen

film_obj = session.query(Film).filter(Film.name=="Saw").one()
genre_obj = session.query(Genre).filter(Genre.name=="horror").one()

allocation = Allocation_Film_Genre(film_id=film_obj.id, genre_id=genre_obj.id)
session.add(allocation)
session.commit()
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Sirius3 hat geschrieben:@Sophus: Du weist den IDs die Objekte zu, was keinen Sinn ergibt.

Code: Alles auswählen

allocation = Allocation_Film_Genre(film=film_obj, genre=genre_obj)
Und das Postfix _obj ist witzlos, da in Python alles ein Objekt ist.
Korrekt. Ich habe mich von deinem Beispiel leicht beirren lassen. Du hast in deinem Beispiel auch nur Objekte übergeben :)

Und allgemein: Im Grunde ist es egal, welche Namen ich wähle. Ich habt grundsätzlich etwas auszusetzen. Daher nehme ich fortan die Bemerkungen, bezüglich der Qualität der Namensgebungen, nicht sonderlich ernst. Egal was ich für Namen nehme, es wird immer ein schlechter Name für euch sein. Damit finde ich mich einfach ab :)
BlackJack

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

@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

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

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

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.
Antworten