Czy SQLAlchemy ma odpowiednik get_or_create w Django?

160

Chcę pobrać obiekt z bazy danych, jeśli już istnieje (na podstawie podanych parametrów) lub utworzyć go, jeśli nie.

Django get_or_create(lub źródło ) robi to. Czy istnieje równoważny skrót w SQLAlchemy?

Obecnie piszę to wyraźnie w ten sposób:

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
FogleBird
źródło
4
Dla tych, którzy chcą tylko dodać obiekt, jeśli jeszcze nie istnieje, zobacz session.merge: stackoverflow.com/questions/12297156/…
Anton Tarasenko

Odpowiedzi:

96

W zasadzie tak to zrobić, nie ma łatwo dostępnego skrótu AFAIK.

Możesz to oczywiście uogólnić:

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
Wolph
źródło
2
Myślę, że gdzie czytasz "session.Query (model.filter_by (** kwargs) .first ()", powinieneś przeczytać "session.Query (model.filter_by (** kwargs)). First ()".
pkoch
3
Czy powinna istnieć blokada wokół tego, aby inny wątek nie utworzył instancji, zanim ten wątek będzie miał szansę?
EoghanM
2
@EoghanM: Normalnie twoja sesja byłaby wątkowa, więc to nie ma znaczenia. Sesja SQLAlchemy nie jest przeznaczona do obsługi wątków.
Wolph
5
@WolpH może to być inny proces, który próbuje jednocześnie utworzyć ten sam rekord. Spójrz na implementację get_or_create w Django. Sprawdza, czy nie występują błędy integralności, i opiera się na odpowiednim wykorzystaniu unikalnych ograniczeń.
Ivan Virabyan
1
@IvanVirabyan: Zakładałem, że @EoghanM mówił o instancji sesji. W takim przypadku try...except IntegrityError: instance = session.Query(...)wokół session.addbloku powinien znajdować się znak .
Wolph
109

Po rozwiązaniu @WoLpH, oto kod, który działał dla mnie (wersja prosta):

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

Dzięki temu mogę get_or_create dowolny obiekt mojego modelu.

Załóżmy, że mój obiekt modelu to:

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

Aby pobrać lub stworzyć mój obiekt, piszę:

myCountry = get_or_create(session, Country, name=countryName)
Kevin.
źródło
3
Dla tych z Was, którzy szukają jak ja, jest to właściwe rozwiązanie, aby utworzyć wiersz, jeśli jeszcze nie istnieje.
Spencer Rathbun
3
Nie musisz dodawać nowej instancji do sesji? W przeciwnym razie, jeśli wprowadzisz session.commit () w kodzie wywołującym, nic się nie stanie, ponieważ nowa instancja nie zostanie dodana do sesji.
CadentOrange
1
Dziękuję Ci za to. Uznałem to za tak przydatne, że stworzyłem jego streszczenie do wykorzystania w przyszłości. gist.github.com/jangeador/e7221fc3b5ebeeac9a08
jangeador
gdzie muszę umieścić kod ?, dostaję wypracowanie błędu kontekstu wykonania?
Victor Alvarado
7
Biorąc pod uwagę, że przekazujesz sesję jako argument, lepiej byłoby unikać commit(lub przynajmniej używać tylko flusha). Pozostawia to kontrolę sesji wywołującemu tę metodę i nie ryzykuje przedwczesnego zatwierdzenia. Ponadto użycie one_or_none()zamiast first()może być nieco bezpieczniejsze.
exhuma
52

Bawiłem się tym problemem i otrzymałem dość solidne rozwiązanie:

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

Właśnie napisałem dość obszerny wpis na blogu ze wszystkimi szczegółami, ale kilka pomysłów, dlaczego go użyłem.

  1. Rozpakowuje się do krotki, która mówi, czy obiekt istniał, czy nie. Często może to być przydatne w Twoim przepływie pracy.

  2. Funkcja daje możliwość pracy z @classmethoddekorowanymi funkcjami kreatora (i specyficznymi dla nich atrybutami).

  3. Rozwiązanie chroni przed warunkami wyścigu, gdy do magazynu danych jest podłączony więcej niż jeden proces.

EDYCJA: Zmieniłem session.commit()na, session.flush()jak wyjaśniono w tym poście na blogu . Należy zauważyć, że te decyzje są specyficzne dla używanego magazynu danych (w tym przypadku Postgres).

EDYCJA 2: Zaktualizowałem za pomocą {} jako wartości domyślnej w funkcji, ponieważ jest to typowa gotcha Pythona. Dzięki za komentarz , Nigel! Jeśli ciekawi Cię ten problem, sprawdź to pytanie StackOverflow i ten post na blogu .

erik
źródło
1
W porównaniu z tym, co mówi Spencer , to rozwiązanie jest dobre, ponieważ zapobiega warunkom wyścigu (poprzez zatwierdzanie / opróżnianie sesji, uwaga) i doskonale naśladuje to, co robi Django.
kiddouk
@kiddouk Nie, to nie naśladuje „idealnie”. Django nieget_or_create jest bezpieczne dla wątków. To nie jest atomowe. Ponadto Django zwraca flagę True, jeśli instancja została utworzona lub flagę False w przeciwnym razie. get_or_create
Kar
@Kate, jeśli spojrzysz na Django, get_or_createrobi to prawie to samo. To rozwiązanie również zwraca True/Falseflagę sygnalizującą, czy obiekt został utworzony lub pobrany, a także nie jest atomowy. Jednak bezpieczeństwo wątków i aktualizacje atomowe są problemem dla bazy danych, a nie dla Django, Flask lub SQLAlchemy, a zarówno w tym rozwiązaniu, jak i w Django są rozwiązywane przez transakcje w bazie danych.
erik
1
Załóżmy, że dla nowego rekordu podano wartość null dla pola innego niż null, spowoduje to błąd IntegrityError. Cała sprawa się popsuła, teraz nie wiemy, co się właściwie stało i otrzymujemy kolejny błąd, że nie znaleziono żadnego rekordu.
rajat
2
Czy IntegrityErrorsprawa nie powinna powrócić, Falseponieważ ten klient nie utworzył obiektu?
kevmitch
11

Zmodyfikowana wersja doskonałej odpowiedzi Erika

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
  • Użyj transakcji zagnieżdżonej, aby tylko wycofać dodawanie nowego elementu zamiast wycofywać wszystko (zobacz tę odpowiedź, aby użyć transakcji zagnieżdżonych z SQLite)
  • Ruszaj się create_method. Jeśli utworzony obiekt ma relacje i przypisane są do niego członkowie za pośrednictwem tych relacji, jest on automatycznie dodawany do sesji. Np. Utwórz book, który ma user_idi userjako odpowiedni związek, a następnie wykonując book.user=<user object>wewnątrz create_method, dodasz bookdo sesji. Oznacza to, że create_methodmusi znajdować się w środku, withaby skorzystać z ewentualnego wycofania. Zauważ, że begin_nestedautomatycznie uruchamia kolor.

Zauważ, że jeśli używasz MySQL, poziom izolacji transakcji musi być ustawiony na, READ COMMITTEDa nie, REPEATABLE READaby to zadziałało. Django get_or_create (i tutaj ) używa tego samego podstępu, zobacz także dokumentację Django .

Adversus
źródło
Podoba mi się, że pozwala to uniknąć cofania niepowiązanych zmian, jednak IntegrityErrorponowne zapytanie może nadal zakończyć się niepowodzeniem z NoResultFounddomyślnym poziomem izolacji MySQL, REPEATABLE READjeśli sesja wcześniej odpytała model w tej samej transakcji. Najlepszym rozwiązaniem, jakie mogłem wymyślić, jest zadzwonienie session.commit()przed tym zapytaniem, co również nie jest idealne, ponieważ użytkownik może się tego nie spodziewać. Przywoływana odpowiedź nie powoduje tego problemu, ponieważ session.rollback () ma taki sam efekt, jak rozpoczęcie nowej transakcji.
kevmitch
Huh, TIL. Czy umieszczenie zapytania w zagnieżdżonej transakcji zadziała? Masz rację, że commitwnętrze tej funkcji jest prawdopodobnie gorsze niż wykonanie a rollback, mimo że w określonych przypadkach może być do zaakceptowania.
Adversus
Tak, umieszczenie początkowego zapytania w zagnieżdżonej transakcji umożliwia przynajmniej działanie drugiego zapytania. Nadal nie powiedzie się, jeśli użytkownik jawnie zapytał model wcześniej w tej samej transakcji. Zdecydowałem, że jest to dopuszczalne i użytkownik powinien zostać ostrzeżony, aby tego nie robił lub w inny sposób nie łapał wyjątku i decydował, czy commit()sam. Jeśli moje zrozumienie kodu jest poprawne, to właśnie robi Django.
kevmitch
W dokumentacji django mówią, że używa się , 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 wpływów `READ COMMITTED SAVEPOINT`, z którymi czyta REPEATABLE READ. Jeśli nie ma efektu, sytuacja wydaje się nie do rozwiązania, a jeśli skutek, to ostatnie zapytanie może zostać zagnieżdżone?
Adversus
To ciekawe READ COMMITED, może powinienem przemyśleć swoją decyzję, aby nie zmieniać ustawień domyślnych bazy danych. Przetestowałem, że przywrócenie SAVEPOINTzapytania sprzed wykonania zapytania sprawia, że ​​to zapytanie nigdy się nie wydarzyło REPEATABLE READ. Dlatego stwierdziłem, że konieczne jest zawarcie zapytania w klauzuli try w zagnieżdżonej transakcji, aby zapytanie w IntegrityErrorklauzuli except mogło w ogóle działać.
kevmitch
6

Ten przepis SQLALchemy wykonuje zadanie ładnie i elegancko.

Pierwszą rzeczą do zrobienia jest zdefiniowanie funkcji, której dana jest sesja do pracy i powiązanie słownika z funkcją Session (), która śledzi bieżące unikalne klucze.

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

Przykładem wykorzystania tej funkcji może być 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
            )

I wreszcie tworząc unikalny model get_or_create:

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()

Przepis zagłębia się w pomysł i zapewnia różne podejścia, ale użyłem tego z wielkim sukcesem.

jhnwsk
źródło
1
Podoba mi się ten przepis, jeśli tylko jeden obiekt sesji SQLAlchemy może modyfikować bazę danych. Mogę się mylić, ale jeśli inne sesje (SQLAlchemy lub nie) modyfikują bazę danych jednocześnie, nie widzę, w jaki sposób chroni to przed obiektami, które mogły zostać utworzone przez inne sesje, podczas gdy transakcja jest w toku. W takich przypadkach uważam, że rozwiązania opierające się na opróżnianiu po session.add () i obsłudze wyjątków, takie jak stackoverflow.com/a/21146492/3690333, są bardziej niezawodne.
TrilceAC,
3

Najbliższe semantycznie jest prawdopodobnie:

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

nie jestem pewien, jak koszerne jest poleganie na globalnie zdefiniowanym Sessionw sqlalchemy, ale wersja Django nie ma połączenia, więc ...

Zwrócona krotka zawiera instancję i wartość logiczną wskazującą, czy instancja została utworzona (tj. Jest fałszywa, jeśli odczytujemy instancję z bazy danych).

Django get_or_createjest często używane, aby upewnić się, że dane globalne są dostępne, więc zatwierdzam w możliwie najwcześniejszym momencie.

thebjorn
źródło
Powinno to działać tak długo, jak Sesja jest tworzona i śledzona przez scoped_session, co powinno zaimplementować zarządzanie sesjami bezpieczne dla wątków (czy istniało to w 2014?).
cowbert
2

Trochę uprościłem @Kevin. rozwiązanie pozwalające uniknąć zawijania całej funkcji w instrukcji if/ else. W ten sposób jest tylko jeden return, który uważam za czystszy:

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
jmberros
źródło
1

W zależności od przyjętego poziomu izolacji żadne z powyższych rozwiązań nie zadziała. Najlepsze rozwiązanie jakie znalazłem to RAW SQL w postaci:

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

Jest to bezpieczne transakcyjnie niezależnie od poziomu izolacji i stopnia równoległości.

Uwaga: aby było wydajne, dobrze byłoby mieć INDEKS dla unikalnej kolumny.

fcracker79
źródło