¿SQLAlchemy tiene un equivalente de get_or_create de Django?

160

Quiero obtener un objeto de la base de datos si ya existe (según los parámetros proporcionados) o crearlo si no existe.

Django get_or_create(o fuente ) hace esto. ¿Hay un atajo equivalente en SQLAlchemy?

Actualmente lo estoy escribiendo explícitamente así:

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
fuente
44
Para aquellos que solo desean agregar un objeto si aún no existe, ver session.merge: stackoverflow.com/questions/12297156/…
Anton Tarasenko

Respuestas:

96

Esa es básicamente la forma de hacerlo, no hay atajos disponibles AFAIK.

Puedes generalizarlo, por supuesto:

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
fuente
2
Creo que cuando lees "session.Query (model.filter_by (** kwargs) .first ()", deberías leer "session.Query (model.filter_by (** kwargs)). First ()".
pkoch
3
¿Debería haber un bloqueo alrededor de esto para que otro hilo no cree una instancia antes de que este hilo tenga la oportunidad de hacerlo?
EoghanM
2
@EoghanM: Normalmente su sesión sería local, por lo que esto no importará. La sesión SQLAlchemy no debe ser segura para subprocesos.
Wolph
55
@WolpH puede ser otro proceso tratando de crear el mismo registro simultáneamente. Mire la implementación de Django de get_or_create. Comprueba si hay errores de integridad y depende del uso adecuado de restricciones únicas.
Ivan Virabyan
1
@IvanVirabyan: asumí que @EoghanM estaba hablando de la instancia de la sesión. En ese caso, debería haber una try...except IntegrityError: instance = session.Query(...)vuelta a la session.addmanzana.
Wolph
109

Siguiendo la solución de @WoLpH, este es el código que funcionó para mí (versión simple):

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 esto, puedo obtener o crear cualquier objeto de mi modelo.

Supongamos que mi objeto modelo es:

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

Para obtener o crear mi objeto escribo:

myCountry = get_or_create(session, Country, name=countryName)
Kevin
fuente
3
Para aquellos de ustedes que buscan como yo, esta es la solución adecuada para crear una fila si aún no existe.
Spencer Rathbun
3
¿No necesita agregar la nueva instancia a la sesión? De lo contrario, si emite un session.commit () en el código de llamada, no sucederá nada ya que la nueva instancia no se agrega a la sesión.
CadentOrange
1
Gracias por esto. He encontrado esto tan útil que creé una idea general para su uso futuro. gist.github.com/jangeador/e7221fc3b5ebeeac9a08
jangeador
donde necesito poner el código ?, me sale el error de contexto de ejecución?
Victor Alvarado
77
Dado que pasa la sesión como argumento, podría ser mejor evitar el commit(o al menos usar solo un flushen su lugar). Esto deja el control de la sesión a la persona que llama de este método y no se arriesgará a emitir una confirmación prematura. Además, usar en one_or_none()lugar de first()podría ser un poco más seguro.
exhuma
52

He estado jugando con este problema y terminé con una solución bastante sólida:

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

Acabo de escribir una publicación de blog bastante expansiva sobre todos los detalles, pero algunas ideas de por qué usé esto.

  1. Se descomprime en una tupla que te dice si el objeto existió o no. Esto a menudo puede ser útil en su flujo de trabajo.

  2. La función brinda la capacidad de trabajar con @classmethodfunciones de creador decoradas (y atributos específicos para ellas).

  3. La solución protege contra las condiciones de carrera cuando tienes más de un proceso conectado al almacén de datos.

EDITAR: He cambiado session.commit()a session.flush()como se explica en esta publicación de blog . Tenga en cuenta que estas decisiones son específicas del almacén de datos utilizado (Postgres en este caso).

EDIT 2: He actualizado usando un {} como valor predeterminado en la función, ya que este es el típico problema de Python. Gracias por el comentario , Nigel! Si tiene curiosidad sobre este problema, consulte esta pregunta de StackOverflow y esta publicación de blog .

erik
fuente
1
En comparación con lo que dice Spencer , esta solución es la buena, ya que evita las condiciones de carrera (al cometer / enjuagar la sesión, tenga cuidado) e imita perfectamente lo que hace Django.
kiddouk
@kiddouk No, no imita "perfectamente". Django's noget_or_create es seguro para subprocesos. No es atómico. Además, Django's devuelve un indicador True si se creó la instancia o un indicador False de lo contrario. get_or_create
Kar
@Kate si miras a Django, get_or_createhace casi exactamente lo mismo. Esta solución también devuelve el True/Falseindicador para indicar si el objeto fue creado o recuperado, y tampoco es atómico. Sin embargo, las actualizaciones atómicas y de seguridad de subprocesos son una preocupación para la base de datos, no para Django, Flask o SQLAlchemy, y tanto en esta solución como en Django, se resuelven mediante transacciones en la base de datos.
erik
1
Supongamos que se proporciona un valor nulo a un campo no nulo para un nuevo registro, aumentará IntegrityError. Todo se complica, ahora no sabemos qué sucedió realmente y tenemos otro error, que no se encuentra ningún registro.
rajat
2
¿No debería IntegrityErrorvolver el caso Falseya que este cliente no creó el objeto?
kevmitch
11

Una versión modificada de la excelente respuesta de 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
  • Use una transacción anidada para revertir solo la adición del nuevo elemento en lugar de revertir todo (consulte esta respuesta para usar transacciones anidadas con SQLite)
  • Mover create_method. Si el objeto creado tiene relaciones y se le asignan miembros a través de esas relaciones, se agrega automáticamente a la sesión. Por ejemplo, crear un book, que tiene user_idy usercomo relación correspondiente, luego hacer book.user=<user object>dentro de create_methodagregará booka la sesión. Esto significa que create_methoddebe estar adentro withpara beneficiarse de una eventual reversión. Tenga en cuenta que begin_nestedactiva automáticamente una descarga.

Tenga en cuenta que si usa MySQL, el nivel de aislamiento de la transacción debe establecerse en READ COMMITTEDlugar de que REPEATABLE READesto funcione. Get_or_create de Django (y aquí ) usa la misma estratagema, consulte también la documentación de Django .

Adversus
fuente
Me gusta que esto evite revertir cambios no relacionados, sin embargo, la IntegrityErrornueva consulta aún puede fallar NoResultFoundcon el nivel de aislamiento predeterminado de MySQL REPEATABLE READsi la sesión había consultado previamente el modelo en la misma transacción. La mejor solución que se me ocurre es llamar session.commit()antes de esta consulta, lo que tampoco es ideal ya que el usuario puede no esperarlo. La respuesta a la que se hace referencia no tiene este problema ya que session.rollback () tiene el mismo efecto de comenzar una nueva transacción.
kevmitch
Huh, TIL. ¿Poner la consulta en una transacción anidada funcionaría? Tiene razón en que commitdentro de esta función podría decirse que es peor que hacer una rollback, aunque para casos de uso específicos puede ser aceptable.
Adversus el
Sí, poner la consulta inicial en una transacción anidada hace que al menos sea posible que la segunda consulta funcione. Sin embargo, seguirá fallando si el usuario consulta explícitamente el modelo antes en la misma transacción. He decidido que esto es aceptable y se debe advertir al usuario que no haga esto o que, de lo contrario, tome la excepción y decida si commit()lo hace. Si mi comprensión del código es correcta, esto es lo que hace Django.
kevmitch
En la documentación de django dicen que usan las , 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 lecturas de influencias `READ COMMITTED SAVEPOINT` REPEATABLE READ. Si no tiene ningún efecto, la situación parece insalvable, si tiene efecto, ¿podría anidarse la última consulta?
Adversus
Eso es interesante READ COMMITED, tal vez debería repensar mi decisión de no tocar los valores predeterminados de la base de datos. He probado que la restauración de una SAVEPOINTantes de que se realizara una consulta hace que esa consulta nunca suceda REPEATABLE READ. Por lo tanto, me pareció necesario incluir la consulta en la cláusula try en una transacción anidada para que la consulta en la IntegrityErrorcláusula except pueda funcionar.
kevmitch
6

Esta receta de SQLALchemy hace el trabajo agradable y elegante.

Lo primero que debe hacer es definir una función a la que se le asigna una sesión para trabajar, y asocia un diccionario con la sesión () que realiza un seguimiento de las claves únicas actuales .

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 ejemplo de utilización de esta función sería en 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
            )

Y finalmente creando el modelo get_or_create único:

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 receta profundiza en la idea y ofrece diferentes enfoques, pero he usado esta con gran éxito.

jhnwsk
fuente
1
Me gusta esta receta si solo un objeto SQLAlchemy Session puede modificar la base de datos. Podría estar equivocado, pero si otras sesiones (SQLAlchemy o no) modifican la base de datos al mismo tiempo, no veo cómo esto protege contra los objetos que podrían haber sido creados por otras sesiones mientras la transacción está en curso. En esos casos, creo que las soluciones que dependen del lavado después de session.add () y el manejo de excepciones como stackoverflow.com/a/21146492/3690333 son más confiables.
TrilceAC
3

La semántica más cercana es probablemente:

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

no estoy seguro de cuán kosher es confiar en una definición global Sessionen sqlalchemy, pero la versión de Django no toma una conexión, así que ...

La tupla devuelta contiene la instancia y un booleano que indica si se creó la instancia (es decir, es Falso si leemos la instancia de la base de datos).

Django a get_or_createmenudo se usa para asegurarse de que haya datos globales disponibles, por lo que me comprometo lo antes posible.

thebjorn
fuente
esto debería funcionar siempre que se cree y realice un seguimiento de la sesión scoped_session, lo que debería implementar la administración de sesión segura para subprocesos (¿existía esto en 2014?).
cowbert
2

Simplifiqué un poco a @Kevin. solución para evitar envolver toda la función en una declaración if/ else. De esta manera solo hay uno return, que encuentro más limpio:

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
fuente
1

Dependiendo del nivel de aislamiento que adoptó, ninguna de las soluciones anteriores funcionaría. La mejor solución que he encontrado es un SQL RAW en la siguiente forma:

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

Esto es transaccionalmente seguro sea cual sea el nivel de aislamiento y el grado de paralelismo.

Cuidado: para que sea eficiente, sería conveniente tener un ÍNDICE para la columna única.

fcracker79
fuente