2012-10-08 6 views
8

Ich habe zwei Tabellen, sagen A und B. Beide haben eine Primärschlüssel-ID. Sie haben eine Viele-zu-Viele-Beziehung, SEC.Sqlalchemy: sekundäre Beziehung Update

SEC = Table('sec', Base.metadata, 
    Column('a_id', Integer, ForeignKey('A.id'), primary_key=True, nullable=False), 
    Column('b_id', Integer, ForeignKey('B.id'), primary_key=True, nullable=False) 
) 

class A(): 
    ... 
    id = Column(Integer, primary_key=True) 
    ... 
    rels = relationship(B, secondary=SEC) 

class B(): 
    ... 
    id = Column(Integer, primary_key=True) 
    ... 

Betrachten wir dieses Stück Code.

a = A() 
b1 = B() 
b2 = B() 
a.rels = [b1, b2] 
... 
#some place later 
b3 = B() 
a.rels = [b1, b3] # errors sometimes 

Manchmal bekomme ich einen Fehler in der letzten Zeile

duplicate key value violates unique constraint a_b_pkey 

In meinem Verständnis zu sagen, ich glaube, es versucht (a.id, b.id) in ‚s‘ Tabelle wieder hinzufügen was zu einem eindeutigen Beschränkungsfehler führt. Ist es das, was es ist? Wenn ja, wie kann ich das vermeiden? Wenn nicht, warum habe ich diesen Fehler?

Antwort

3

Der Fehler, den Sie erwähnen, ist in der Tat von der Einfügung eines Konfliktwertes in die sec-Tabelle. Um sicher zu sein, dass es sich bei der Operation, die Sie denken, um eine vorherige Änderung handelt, aktivieren Sie die SQL-Protokollierung und prüfen Sie, welche Werte eingefügt werden sollen, bevor ein Fehler auftritt.

Beim Überschreiben eines Viele-zu-Viele-Sammlungswerts vergleicht SQLAlchemy den neuen Inhalt der Sammlung mit dem Status in der Datenbank und gibt entsprechend Lösch- und Einfügeanweisungen aus. Wenn Sie nicht in SQLAlchemy-Interna herumstöbern, sollten Sie auf zwei Arten auf diesen Fehler stoßen.

Zuerst ist die gleichzeitige Änderung: Prozess 1 ruft den Wert a.rels ab und merkt, dass er leer ist, während Prozess 2 auch a.rels holt, auf [b1, b2] setzt und die Spülung übernimmt (a, b1) , (a, b2) Tupel, Prozess 1 setzt a.rels auf [b1, b3] und bemerkt, dass der vorherige Inhalt leer war und wenn er versucht, das zweite Tupel (a, b1) zu löschen, erhält er einen doppelten Schlüsselfehler. Die richtige Aktion in solchen Fällen ist normalerweise, die Transaktion von oben zu wiederholen. Sie können serializable transaction isolation verwenden, um in diesem Fall stattdessen einen Serialisierungsfehler zu erhalten, der sich von einem Geschäftslogikfehler unterscheidet, der einen doppelten Schlüsselfehler verursacht. Der zweite Fall tritt auf, wenn Sie es geschafft haben, SQLAlchemy davon zu überzeugen, dass Sie den Datenbankstatus nicht kennen müssen, indem Sie die Ladestrategie des Rels-Attributs auf noload setzen. Dies kann beim Definieren der Beziehung durch Hinzufügen des Parameters lazy='noload' oder beim Abfragen und Aufrufen von .options(noload(A.rels)) für die Abfrage erfolgen. SQLAlchemy wird annehmen, dass die sec-Tabelle keine übereinstimmenden Zeilen für Objekte enthält, die mit dieser Strategie geladen sind.

+0

Ich bin mir nicht wirklich sicher warum.Ich muss es richtig testen und werde es euch wissen lassen. Danke für die Hilfe. – Sri

8

Das Problem besteht darin, dass Sie sicherstellen möchten, dass die von Ihnen erstellten Instanzen eindeutig sind. Wir können einen alternativen Konstruktor erstellen, der einen Cache von vorhandenen nicht-committeten Instanzen überprüft oder die Datenbank nach einer vorhandenen festgeschriebenen Instanz abfragt, bevor eine neue Instanz zurückgegeben wird. Hier

ist eine Demonstration eines solchen Verfahrens:

from sqlalchemy import Column, Integer, String, ForeignKey, Table 
from sqlalchemy.engine import create_engine 
from sqlalchemy.ext.declarative.api import declarative_base 
from sqlalchemy.orm import sessionmaker, relationship 

engine = create_engine('sqlite:///:memory:', echo=True) 
Session = sessionmaker(engine) 
Base = declarative_base(engine) 

session = Session() 


class Role(Base): 
    __tablename__ = 'role' 

    id = Column(Integer, primary_key=True) 
    name = Column(String, nullable=False, unique=True) 

    @classmethod 
    def get_unique(cls, name): 
     # get the session cache, creating it if necessary 
     cache = session._unique_cache = getattr(session, '_unique_cache', {}) 
     # create a key for memoizing 
     key = (cls, name) 
     # check the cache first 
     o = cache.get(key) 
     if o is None: 
      # check the database if it's not in the cache 
      o = session.query(cls).filter_by(name=name).first() 
      if o is None: 
       # create a new one if it's not in the database 
       o = cls(name=name) 
       session.add(o) 
      # update the cache 
      cache[key] = o 
     return o 


Base.metadata.create_all() 

# demonstrate cache check 
r1 = Role.get_unique('admin') # this is new 
r2 = Role.get_unique('admin') # from cache 
session.commit() # doesn't fail 

# demonstrate database check 
r1 = Role.get_unique('mod') # this is new 
session.commit() 
session._unique_cache.clear() # empty cache 
r2 = Role.get_unique('mod') # from database 
session.commit() # nop 

# show final state 
print session.query(Role).all() # two unique instances from four create calls 

Die create_unique Methode der example from the SQLAlchemy wiki inspiriert wurde. Diese Version ist viel weniger verschachtelt, Einfachheit gegenüber Flexibilität bevorzugen. Ich habe es in Produktionssystemen ohne Probleme verwendet.

Es gibt offensichtlich Verbesserungen, die hinzugefügt werden können; Das ist nur ein einfaches Beispiel. Die get_unique Methode könnte von einer UniqueMixin übernommen werden, um für eine beliebige Anzahl von Modellen verwendet zu werden. Eine flexiblere Formulierung von Argumenten könnte implementiert werden. Dies beseitigt auch das Problem, dass mehrere Threads widersprüchliche Daten einfügen, die von Ants Aasma erwähnt werden; Handhabung, die komplexer ist, sollte aber eine offensichtliche Erweiterung sein. Das überlasse ich dir.