SQLAlchemy ha un equivalente di get_or_create di Django?


160

Voglio ottenere un oggetto dal database se esiste già (in base ai parametri forniti) o crearlo in caso contrario.

Django get_or_create(o fonte ) fa questo. Esiste un collegamento equivalente in SQLAlchemy?

Attualmente sto scrivendo esplicitamente in questo modo:

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
Per coloro che vogliono solo aggiungere oggetto se non esiste ancora, vedi session.merge: stackoverflow.com/questions/12297156/...
Anton Tarasenko

Risposte:


96

Questo è fondamentalmente il modo di farlo, non esiste un collegamento disponibile AFAIK.

Potresti generalizzarlo naturalmente:

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
Penso che dove leggi "session.Query (model.filter_by (** kwargs) .first ()", dovresti leggere "session.Query (model.filter_by (** kwargs)).
First

3
Dovrebbe esserci un blocco attorno a questo in modo che un altro thread non crei un'istanza prima che questo thread abbia la possibilità di farlo?
EoghanM,

2
@EoghanM: normalmente la tua sessione sarebbe threadlocal, quindi non importa. La sessione SQLAlchemy non è pensata per essere thread-safe.
Wolph,

5
@ WolpH può essere un altro processo che prova a creare lo stesso record contemporaneamente. Guarda l'implementazione di get_or_create di Django. Verifica la presenza di errori di integrità e si basa sull'uso corretto di vincoli univoci.
Ivan Virabyan,

1
@IvanVirabyan: supponevo che @EoghanM stesse parlando dell'istanza della sessione. In tal caso dovrebbe esserci un try...except IntegrityError: instance = session.Query(...)intorno al session.addblocco.
Wolph,

109

Seguendo la soluzione di @WoLpH, questo è il codice che ha funzionato per me (versione semplice):

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

Con questo, sono in grado di ottenere_o_creare qualsiasi oggetto del mio modello.

Supponiamo che il mio oggetto modello sia:

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

Per ottenere o creare il mio oggetto scrivo:

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

3
Per quelli di voi che cercano come me, questa è la soluzione corretta per creare una riga se non esiste già.
Spencer Rathbun,

3
Non è necessario aggiungere la nuova istanza alla sessione? Altrimenti se si emette session.commit () nel codice chiamante, non accadrà nulla poiché la nuova istanza non viene aggiunta alla sessione.
CadentOrange

1
Grazie per questo. Ho trovato questo così utile che ne ho creato una sintesi per un uso futuro. gist.github.com/jangeador/e7221fc3b5ebeeac9a08
jangeador

dove devo inserire il codice ?, ottengo un errore nel contesto di esecuzione?
Victor Alvarado,

7
Dato che si passa la sessione come argomento, potrebbe essere meglio evitare il commit(o almeno usare solo un flushinvece). Questo lascia il controllo della sessione al chiamante di questo metodo e non rischierà di emettere un commit prematuro. Inoltre, l'utilizzo al one_or_none()posto di first()potrebbe essere leggermente più sicuro.
exhuma,

52

Ho giocato con questo problema e ho finito con una soluzione abbastanza solida:

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

Ho appena scritto un post sul blog abbastanza ampio su tutti i dettagli, ma alcune idee abbastanza sul perché l'ho usato.

  1. Si decomprime in una tupla che ti dice se l'oggetto esiste o no. Questo può spesso essere utile nel tuo flusso di lavoro.

  2. La funzione offre la possibilità di lavorare con @classmethod funzioni di creatore decorate (e attributi specifici per esse).

  3. La soluzione protegge dalle condizioni di gara quando si dispone di più di un processo collegato all'archivio dati.

EDIT: Ho cambiato session.commit()a session.flush()come spiegato in questo post del blog . Si noti che queste decisioni sono specifiche per il datastore utilizzato (Postgres in questo caso).

EDIT 2: Ho aggiornato utilizzando un {} come valore predefinito nella funzione in quanto si tratta del tipico gotcha di Python. Grazie per il commento , Nigel! Se sei curioso di questo gotcha, dai un'occhiata a questa domanda StackOverflow e a questo post sul blog .


1
Rispetto a quanto dice Spencer , questa soluzione è la migliore in quanto previene le condizioni di Gara (impegnandosi / svuotando la sessione, attenzione) e imita perfettamente ciò che Django fa.
kiddouk,

@kiddouk No, non imita "perfettamente". Django's nonget_or_create è thread-safe. Non è atomico. Inoltre, Django restituisce un flag True se l'istanza è stata creata o altrimenti un flag False. get_or_create
Kar

@Kate se guardi Django get_or_createfa quasi esattamente la stessa cosa. Questa soluzione restituisce anche il True/Falseflag per segnalare se l'oggetto è stato creato o recuperato e non è anche atomico. Tuttavia, la sicurezza dei thread e gli aggiornamenti atomici sono una preoccupazione per il database, non per Django, Flask o SQLAlchemy, e sia in questa soluzione che in Django sono risolti dalle transazioni sul database.
erik

1
Supponiamo che a un campo non nullo sia stato fornito un valore nullo per un nuovo record, genererà IntegrityError. Il tutto viene incasinato, ora non sappiamo cosa sia effettivamente successo e riceviamo un altro errore, che non viene trovato alcun record.
rajat,

2
Il caso non dovrebbe IntegrityErrortornare Falsepoiché questo client non ha creato l'oggetto?
kevmitch,

11

Una versione modificata dell'ottima risposta di erik

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
  • Utilizzare una transazione nidificata per ripristinare solo l'aggiunta del nuovo elemento anziché ripristinare tutto (vedere questa risposta per utilizzare le transazioni nidificate con SQLite)
  • Sposta create_method. Se l'oggetto creato ha relazioni e gli vengono assegnati membri tramite tali relazioni, viene automaticamente aggiunto alla sessione. Ad esempio creare un book, che ha user_ide usercome relazione corrispondente, quindi facendo book.user=<user object>all'interno di create_methodsi aggiungerà bookalla sessione. Ciò significa che create_methoddeve essere all'interno withper beneficiare di un eventuale rollback. Si noti che begin_nestedinnesca automaticamente un flush.

Si noti che se si utilizza MySQL, il livello di isolamento della transazione deve essere impostato READ COMMITTEDpiuttosto che REPEATABLE READper farlo funzionare. Get_or_create (e qui ) di Django usa lo stesso stratagemma, vedi anche la documentazione di Django .


Mi piace che ciò eviti il ​​rollback delle modifiche non correlate, tuttavia la IntegrityErrorquery potrebbe non riuscire NoResultFoundcon il livello di isolamento predefinito di MySQL REPEATABLE READse la sessione avesse precedentemente eseguito una query sul modello nella stessa transazione. La migliore soluzione che potrei trovare è quella di chiamare session.commit()prima di questa query, che non è l'ideale poiché l'utente potrebbe non aspettarselo. La risposta a cui si fa riferimento non presenta questo problema poiché session.rollback () ha lo stesso effetto dell'avvio di una nuova transazione.
kevmitch,

Eh, TIL. Metterebbe la query in una transazione nidificata funzionerebbe? Hai ragione sul fatto che commitall'interno di questa funzione è probabilmente peggio che fare una rollback, anche se per casi d'uso specifici può essere accettabile.
Adversus,

Sì, l'inserimento della query iniziale in una transazione nidificata rende almeno possibile il funzionamento della seconda query. Avrà comunque esito negativo se l'utente ha richiesto esplicitamente il modello prima nella stessa transazione. Ho deciso che questo è accettabile e l'utente dovrebbe semplicemente essere avvisato di non farlo o altrimenti prendere l'eccezione e decidere se da commit()soli. Se la mia comprensione del codice è corretta, questo è ciò che fa Django.
Kevinev

Nella documentazione di django dicono di , 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 leggere le influenze di "LEGGI IMPEGNATO SAVEPOINT" REPEATABLE READ. Se nessun effetto, la situazione sembra inalterabile, se si potesse annidare l'ultimo effetto?
Adversus,

È interessante READ COMMITED, forse dovrei ripensare la mia decisione di non toccare le impostazioni predefinite del database. Ho testato che il ripristino di una versione SAVEPOINTprecedente a una query fa sì che quella query non si verifichi mai REPEATABLE READ. Pertanto, ho trovato necessario racchiudere la query nella clausola try in una transazione nidificata in modo che la query nella IntegrityErrorclausola tranne possa funzionare affatto.
Kevinev

6

Questa ricetta SQLALchemy fa il lavoro piacevole ed elegante.

La prima cosa da fare è definire una funzione a cui viene assegnata una Sessione con cui lavorare e associa un dizionario alla Sessione () che tiene traccia delle attuali chiavi univoche .

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

Un esempio di utilizzo di questa funzione sarebbe in un 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
            )

E infine creando il modello unico 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()

La ricetta approfondisce l'idea e fornisce approcci diversi ma l'ho usata con grande successo.


1
Mi piace questa ricetta se solo un singolo oggetto Sessione SQLAlchemy può modificare il database. Potrei sbagliarmi, ma se altre sessioni (SQLAlchemy o meno) modificano il database contemporaneamente, non vedo come questo protegge dagli oggetti che potrebbero essere stati creati da altre sessioni mentre la transazione è in corso. In questi casi, penso che le soluzioni che si basano sul flushing dopo session.add () e sulla gestione delle eccezioni come stackoverflow.com/a/21146492/3690333 siano più affidabili.
TrilceAC,

3

Il semanticamente più vicino è probabilmente:

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

non sono sicuro di quanto sia kosher fare affidamento su una Sessionsqlalchemy definita globalmente , ma la versione Django non ha una connessione quindi ...

La tupla restituita contiene l'istanza e un valore booleano che indica se l'istanza è stata creata (ovvero è False se leggiamo l'istanza dal db).

Django get_or_createviene spesso utilizzato per assicurarsi che siano disponibili dati globali, quindi mi impegno al più presto possibile.


questo dovrebbe funzionare fino a quando la sessione viene creata e tracciata da scoped_session, che dovrebbe implementare la gestione della sessione thread-safe (esisteva nel 2014?).
cowbert,

2

Ho semplificato leggermente @Kevin. soluzione per evitare di racchiudere l'intera funzione in un'istruzione if/ else. In questo modo ce n'è solo uno return, che trovo più pulito:

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

A seconda del livello di isolamento adottato, nessuna delle soluzioni sopra avrebbe funzionato. La migliore soluzione che ho trovato è un SQL RAW nella seguente forma:

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

Ciò è sicuro dal punto di vista delle transazioni qualunque sia il livello di isolamento e il grado di parallelismo.

Attenzione: per renderlo efficiente, sarebbe saggio disporre di un INDICE per la colonna unica.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.