Hat SQLAlchemy ein Äquivalent zu get_or_create von Django?


160

Ich möchte ein Objekt aus der Datenbank abrufen, wenn es bereits vorhanden ist (basierend auf den angegebenen Parametern), oder es erstellen, wenn dies nicht der Fall ist.

Djangos get_or_create(oder Quelle ) tut dies. Gibt es eine entsprechende Abkürzung in SQLAlchemy?

Ich schreibe es gerade explizit so aus:

def get_or_create_instrument(session, serial_number):
    instrument = session.query(Instrument).filter_by(serial_number=serial_number).first()
    if instrument:
        return instrument
    else:
        instrument = Instrument(serial_number)
        session.add(instrument)
        return instrument

4
Für diejenigen, die nur ein Objekt hinzufügen möchten, wenn es noch nicht existiert, siehe session.merge: stackoverflow.com/questions/12297156/…
Anton Tarasenko

Antworten:


96

Das ist im Grunde der Weg, es gibt keine Abkürzung AFAIK.

Sie könnten es natürlich verallgemeinern:

def get_or_create(session, model, defaults=None, **kwargs):
    instance = session.query(model).filter_by(**kwargs).first()
    if instance:
        return instance, False
    else:
        params = dict((k, v) for k, v in kwargs.iteritems() if not isinstance(v, ClauseElement))
        params.update(defaults or {})
        instance = model(**params)
        session.add(instance)
        return instance, True

2
Ich denke, wenn Sie "session.Query (model.filter_by (** kwargs) .first ()" lesen, sollten Sie "session.Query (model.filter_by (** kwargs)). First ()".
pkoch lesen

3
Sollte es eine Sperre geben, damit ein anderer Thread keine Instanz erstellt, bevor dieser Thread die Chance dazu hat?
EoghanM

2
@EoghanM: Normalerweise ist Ihre Sitzung threadlokal, daher spielt dies keine Rolle. Die SQLAlchemy-Sitzung soll nicht threadsicher sein.
Wolph

5
@WolpH Es kann ein anderer Prozess sein, der versucht, denselben Datensatz gleichzeitig zu erstellen. Schauen Sie sich Djangos Implementierung von get_or_create an. Es prüft auf Integritätsfehler und setzt die ordnungsgemäße Verwendung eindeutiger Einschränkungen voraus.
Ivan Virabyan

1
@IvanVirabyan: Ich nahm an, dass @EoghanM über die Sitzungsinstanz sprach. In diesem Fall sollte sich try...except IntegrityError: instance = session.Query(...)um den session.addBlock ein befinden.
Wolph

109

Nach der Lösung von @WoLpH ist dies der Code, der für mich funktioniert hat (einfache Version):

def get_or_create(session, model, **kwargs):
    instance = session.query(model).filter_by(**kwargs).first()
    if instance:
        return instance
    else:
        instance = model(**kwargs)
        session.add(instance)
        session.commit()
        return instance

Damit kann ich jedes Objekt meines Modells abrufen oder erstellen.

Angenommen, mein Modellobjekt ist:

class Country(Base):
    __tablename__ = 'countries'
    id = Column(Integer, primary_key=True)
    name = Column(String, unique=True)

Um mein Objekt zu erhalten oder zu erstellen, schreibe ich:

myCountry = get_or_create(session, Country, name=countryName)

3
Für diejenigen unter Ihnen, die wie ich suchen, ist dies die richtige Lösung, um eine Zeile zu erstellen, falls diese noch nicht vorhanden ist.
Spencer Rathbun

3
Müssen Sie die neue Instanz nicht zur Sitzung hinzufügen? Andernfalls passiert nichts, wenn Sie im aufrufenden Code eine session.commit () ausgeben, da die neue Instanz nicht zur Sitzung hinzugefügt wird.
CadentOrange

1
Danke dafür. Ich fand das so nützlich, dass ich einen Kern davon für die zukünftige Verwendung erstellt habe. gist.github.com/jangeador/e7221fc3b5ebeeac9a08
jangeador

Wo muss ich den Code einfügen? Erhalte ich einen Ausführungskontextfehler?
Victor Alvarado

7
Wenn Sie die Sitzung als Argument übergeben, ist es möglicherweise besser, das zu vermeiden commit(oder zumindest nur ein flushstattdessen zu verwenden). Dies überlässt die Sitzungskontrolle dem Aufrufer dieser Methode und riskiert nicht, ein vorzeitiges Commit auszugeben. Auch die Verwendung von one_or_none()anstelle von ist first()möglicherweise etwas sicherer.
Exhuma

52

Ich habe mit diesem Problem gespielt und eine ziemlich robuste Lösung gefunden:

def get_one_or_create(session,
                      model,
                      create_method='',
                      create_method_kwargs=None,
                      **kwargs):
    try:
        return session.query(model).filter_by(**kwargs).one(), False
    except NoResultFound:
        kwargs.update(create_method_kwargs or {})
        created = getattr(model, create_method, model)(**kwargs)
        try:
            session.add(created)
            session.flush()
            return created, True
        except IntegrityError:
            session.rollback()
            return session.query(model).filter_by(**kwargs).one(), False

Ich habe gerade einen ziemlich umfangreichen Blog-Beitrag über alle Details geschrieben, aber ein paar gute Ideen, warum ich das verwendet habe.

  1. Es wird in ein Tupel entpackt, das Ihnen sagt, ob das Objekt vorhanden ist oder nicht. Dies kann in Ihrem Workflow häufig hilfreich sein.

  2. Die Funktion bietet die Möglichkeit, mit @classmethoddekorierten Erstellerfunktionen (und spezifischen Attributen) zu arbeiten.

  3. Die Lösung schützt vor Race-Bedingungen, wenn mehr als ein Prozess mit dem Datenspeicher verbunden ist.

EDIT: Ich habe zu geändert session.commit(), session.flush()wie in diesem Blog-Beitrag erklärt . Beachten Sie, dass diese Entscheidungen spezifisch für den verwendeten Datenspeicher sind (in diesem Fall Postgres).

BEARBEITEN 2: Ich habe mit einem {} als Standardwert in der Funktion aktualisiert, da dies ein typisches Python-Gotcha ist. Danke für den Kommentar , Nigel! Wenn Sie neugierig auf dieses Problem sind, lesen Sie diese StackOverflow-Frage und diesen Blog-Beitrag .


1
Im Vergleich zu dem, was spencer sagt , ist diese Lösung das gut , da es Race Conditions verhindert (durch Begehen / Spülen der Sitzung, passen) und imitiert perfekt , was Django tut.
Kiddouk

@kiddouk Nein, es ahmt nicht "perfekt" nach. Django get_or_createist nicht threadsicher. Es ist nicht atomar. Außerdem gibt Django get_or_createein True-Flag zurück, wenn die Instanz erstellt wurde, oder ein False-Flag, wenn dies nicht der Fall ist.
Kar

@ Kate, wenn Sie sich Djangos ansehen, macht get_or_createes fast genau das Gleiche. Diese Lösung gibt auch das True/FalseFlag zurück, um zu signalisieren, ob das Objekt erstellt oder abgerufen wurde, und ist auch nicht atomar. Thread-Sicherheit und atomare Aktualisierungen sind jedoch ein Problem für die Datenbank, nicht für Django, Flask oder SQLAlchemy, und werden sowohl in dieser als auch in Djangos Lösung durch Transaktionen in der Datenbank gelöst.
Erik

1
Angenommen, für einen neuen Datensatz wurde ein Feld ungleich Null angegeben, wodurch IntegrityError ausgelöst wird. Das Ganze wird durcheinander gebracht, jetzt wissen wir nicht, was tatsächlich passiert ist und wir bekommen einen weiteren Fehler, dass kein Datensatz gefunden wird.
Rajat

2
Sollte der IntegrityErrorFall nicht zurückkehren, Falseda dieser Client das Objekt nicht erstellt hat?
Kevmitch

11

Eine modifizierte Version von eriks ausgezeichneter Antwort

def get_one_or_create(session,
                      model,
                      create_method='',
                      create_method_kwargs=None,
                      **kwargs):
    try:
        return session.query(model).filter_by(**kwargs).one(), True
    except NoResultFound:
        kwargs.update(create_method_kwargs or {})
        try:
            with session.begin_nested():
                created = getattr(model, create_method, model)(**kwargs)
                session.add(created)
            return created, False
        except IntegrityError:
            return session.query(model).filter_by(**kwargs).one(), True
  • Verwenden Sie eine verschachtelte Transaktion, um nur das Hinzufügen des neuen Elements zurückzusetzen, anstatt alles zurückzusetzen (siehe diese Antwort , um verschachtelte Transaktionen mit SQLite zu verwenden).
  • Bewegen Sie sich create_method. Wenn das erstellte Objekt Beziehungen hat und über diese Beziehungen Mitglieder zugewiesen werden, wird es automatisch zur Sitzung hinzugefügt. Erstellen Sie z. B. eine book, die eine entsprechende Beziehung hat, user_idund fügen Sie sie userdann book.user=<user object>innerhalb der Sitzung hinzu. Dies bedeutet, dass es sich im Inneren befinden muss , um von einem eventuellen Rollback zu profitieren. Beachten Sie, dass automatisch ein Flush ausgelöst wird.create_methodbookcreate_methodwithbegin_nested

Beachten Sie, dass MySQL , wenn verwenden, muss die Transaktionsisolationsstufe eingestellt werden , READ COMMITTEDanstatt REPEATABLE READdafür zu arbeiten. Djangos get_or_create (und hier ) verwendet dieselbe Strategie, siehe auch die Django- Dokumentation .


Ich mag es, dass dadurch vermieden wird, dass nicht verwandte Änderungen rückgängig gemacht werden. Die IntegrityErrorerneute Abfrage schlägt jedoch möglicherweise immer noch NoResultFoundmit der MySQL-Standardisolationsstufe fehl, REPEATABLE READwenn die Sitzung das Modell zuvor in derselben Transaktion abgefragt hat. Die beste Lösung, die ich finden könnte, besteht darin, session.commit()vor dieser Abfrage anzurufen , was ebenfalls nicht ideal ist, da der Benutzer dies möglicherweise nicht erwartet. Die Antwort, auf die verwiesen wird, hat dieses Problem nicht, da session.rollback () den gleichen Effekt hat wie das Starten einer neuen Transaktion.
Kevmitch

Huh, bis. Würde das Einfügen der Abfrage in eine verschachtelte Transaktion funktionieren? Sie haben Recht, dass commitdas Funktionieren dieser Funktion möglicherweise schlechter ist als das Ausführen einer Funktion rollback, obwohl dies für bestimmte Anwendungsfälle akzeptabel sein kann.
Adversus

Ja, wenn Sie die erste Abfrage in eine verschachtelte Transaktion einfügen, kann zumindest die zweite Abfrage funktionieren. Es schlägt jedoch weiterhin fehl, wenn der Benutzer das Modell zuvor in derselben Transaktion explizit abgefragt hat. Ich habe entschieden, dass dies akzeptabel ist, und der Benutzer sollte nur gewarnt werden, dies nicht zu tun oder auf andere Weise die Ausnahme abzufangen und selbst zu entscheiden, ob er dies tut commit(). Wenn ich den Code richtig verstehe, ist es das, was Django tut.
Kevmitch

In der Django- Dokumentation heißt es, "READ COMMITTED , so it does not look like they try to handle this. Looking at the [source](https://github.com/django/django/blob/master/django/db/models/query.py#L491) confirms this. I'm not sure I understand your reply, you mean the user should put his/her query in a nested transaction? It's not clear to me how a SAVEPOINT" zu verwenden REPEATABLE READ. Wenn kein Effekt vorliegt, scheint die Situation nicht mehr zu retten. Wenn der Effekt vorliegt, kann die allerletzte Abfrage verschachtelt werden.
Adversus

Das ist interessant READ COMMITED, vielleicht sollte ich meine Entscheidung überdenken, die Datenbankstandards nicht zu berühren. Ich habe getestet, dass das Wiederherstellen eines SAVEPOINTvon vor einer Abfrage den Eindruck erweckt, dass diese Abfrage nie stattgefunden hat REPEATABLE READ. Daher fand ich es notwendig, die Abfrage in der try-Klausel in eine verschachtelte Transaktion einzuschließen, damit die Abfrage in der IntegrityErrorExcept-Klausel überhaupt funktionieren kann.
Kevmitch

6

Dieses SQLALchemy-Rezept macht den Job schön und elegant.

Als Erstes müssen Sie eine Funktion definieren, die eine Sitzung erhält, mit der Sie arbeiten können, und der Sitzung () ein Wörterbuch zuordnen, das die aktuellen eindeutigen Schlüssel verfolgt.

def _unique(session, cls, hashfunc, queryfunc, constructor, arg, kw):
    cache = getattr(session, '_unique_cache', None)
    if cache is None:
        session._unique_cache = cache = {}

    key = (cls, hashfunc(*arg, **kw))
    if key in cache:
        return cache[key]
    else:
        with session.no_autoflush:
            q = session.query(cls)
            q = queryfunc(q, *arg, **kw)
            obj = q.first()
            if not obj:
                obj = constructor(*arg, **kw)
                session.add(obj)
        cache[key] = obj
        return obj

Ein Beispiel für die Verwendung dieser Funktion wäre ein Mixin:

class UniqueMixin(object):
    @classmethod
    def unique_hash(cls, *arg, **kw):
        raise NotImplementedError()

    @classmethod
    def unique_filter(cls, query, *arg, **kw):
        raise NotImplementedError()

    @classmethod
    def as_unique(cls, session, *arg, **kw):
        return _unique(
                    session,
                    cls,
                    cls.unique_hash,
                    cls.unique_filter,
                    cls,
                    arg, kw
            )

Und schließlich das einzigartige Modell get_or_create erstellen:

from sqlalchemy import Column, Integer, String, create_engine
from sqlalchemy.orm import sessionmaker
from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

engine = create_engine('sqlite://', echo=True)

Session = sessionmaker(bind=engine)

class Widget(UniqueMixin, Base):
    __tablename__ = 'widget'

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

    @classmethod
    def unique_hash(cls, name):
        return name

    @classmethod
    def unique_filter(cls, query, name):
        return query.filter(Widget.name == name)

Base.metadata.create_all(engine)

session = Session()

w1, w2, w3 = Widget.as_unique(session, name='w1'), \
                Widget.as_unique(session, name='w2'), \
                Widget.as_unique(session, name='w3')
w1b = Widget.as_unique(session, name='w1')

assert w1 is w1b
assert w2 is not w3
assert w2 is not w1

session.commit()

Das Rezept geht tiefer in die Idee ein und bietet verschiedene Ansätze, aber ich habe diesen mit großem Erfolg verwendet.


1
Ich mag dieses Rezept, wenn nur ein einziges SQLAlchemy Session-Objekt die Datenbank ändern kann. Ich kann mich irren, aber wenn andere Sitzungen (SQLAlchemy oder nicht) die Datenbank gleichzeitig ändern, sehe ich nicht, wie dies vor Objekten schützt, die möglicherweise von anderen Sitzungen erstellt wurden, während die Transaktion ausgeführt wird. In diesen Fällen denke ich, dass Lösungen, die auf dem Leeren nach session.add () und der Ausnahmebehandlung wie stackoverflow.com/a/21146492/3690333 beruhen, zuverlässiger sind.
TrilceAC

3

Das semantisch am nächsten ist wahrscheinlich:

def get_or_create(model, **kwargs):
    """SqlAlchemy implementation of Django's get_or_create.
    """
    session = Session()
    instance = session.query(model).filter_by(**kwargs).first()
    if instance:
        return instance, False
    else:
        instance = model(**kwargs)
        session.add(instance)
        session.commit()
        return instance, True

SessionIch bin mir nicht sicher, wie koscher es ist, sich auf eine global in sqlalchemy definierte Version zu verlassen , aber die Django-Version nimmt keine Verbindung auf, also ...

Das zurückgegebene Tupel enthält die Instanz und einen Booleschen Wert, der angibt, ob die Instanz erstellt wurde (dh es ist falsch, wenn wir die Instanz aus der Datenbank lesen).

Django get_or_createwird oft verwendet, um sicherzustellen, dass globale Daten verfügbar sind, sodass ich mich zum frühestmöglichen Zeitpunkt verpflichte.


Dies sollte funktionieren, solange die Sitzung erstellt und nachverfolgt wird. scoped_sessionDies sollte eine thread-sichere Sitzungsverwaltung implementieren (gab es diese im Jahr 2014?).
Cowbert

2

Ich habe @Kevin leicht vereinfacht. Lösung, um zu vermeiden, dass die gesamte Funktion in eine if/ else-Anweisung eingeschlossen wird. Auf diese Weise gibt es nur eine return, die ich sauberer finde:

def get_or_create(session, model, **kwargs):
    instance = session.query(model).filter_by(**kwargs).first()

    if not instance:
        instance = model(**kwargs)
        session.add(instance)

    return instance

1

Abhängig von der von Ihnen gewählten Isolationsstufe würde keine der oben genannten Lösungen funktionieren. Die beste Lösung, die ich gefunden habe, ist ein RAW-SQL in der folgenden Form:

INSERT INTO table(f1, f2, unique_f3) 
SELECT 'v1', 'v2', 'v3' 
WHERE NOT EXISTS (SELECT 1 FROM table WHERE f3 = 'v3')

Dies ist unabhängig von der Isolationsstufe und dem Grad der Parallelität transaktionssicher.

Achtung: Um die Effizienz zu steigern, ist es ratsam, einen INDEX für die eindeutige Spalte zu haben.

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.