2010-03-30 18 views
110

Quiero obtener un objeto de la base de datos si ya existe (basado en los parámetros proporcionados) o crearlo si no lo hace.¿SQLAlchemy tiene un equivalente de get_or_create de Django?

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

Actualmente estoy escribiendo explícitamente como esto:

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 
+0

Para aquellos que sólo quieren añadir objeto si no existe todavía, ver 'session.merge': https://stackoverflow.com/questions/12297156/fast-way-to-insert-object-if-it-doesnt-exist-with-sqlalchemy/12298306 # 12298306 –

Respuesta

66

Eso es básicamente la forma de hacerlo, no hay acceso directo disponible yo sepa.

Se podría generalizar que 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 
+2

Creo que donde lee "session.Query (model.filter_by (** kwargs) .first() ", debería leer" session.Query (modelo.filtro_by (** kwargs)). first() ". – pkoch

+0

@pkoch: de hecho debería, gracias :) – Wolph

+0

¿Debería haber un bloqueo alrededor de este para que otro hilo no crea una instancia antes de que este hilo tenga la posibilidad de? – EoghanM

75

Siguiendo la solución de @WoLpH, este es el código que trabajó 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, estoy capaz de obtener_o_crear cualquier objeto de mi modelo.

Supongamos que mi modelo de objetos es:

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

Para obtener o crear mi objetivo escribo:

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

Para aquellos que buscan como yo, esta es la solución adecuada para crear una fila si aún no existe. –

+2

¿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 ocurrirá 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 esencia para uso futuro. https://gist.github.com/jangeador/e7221fc3b5ebeeac9a08 – jangeador

6

This SQLALchemy recipe hace el trabajo agradable y elegante.

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

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 la 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 la creación del 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 proporciona diferentes enfoques pero he usado este con gran éxito.

36

He estado jugando con este problema y he terminado con una solución bastante robusta:

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

que acabo de escribir un fairly expansive blog post en todos los detalles, pero algunas ideas bastante de eso utilicé esto.

  1. Se desempaqueta en una tupla que indica si el objeto existe o no. Esto a menudo puede ser útil en su flujo de trabajo.

  2. La función ofrece la posibilidad de trabajar con @classmethod funciones de creador decoradas (y atributos específicos para ellas).

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

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

EDIT 2: He actualizado el uso de un {} como valor predeterminado en la función, ya que esto es típico de Python gotcha. Gracias por the comment, Nigel! Si tienes curiosidad acerca de esto, echa un vistazo a this StackOverflow question y this blog post.

+1

Comparado con lo que dice spencer [dice] (http://stackoverflow.com/questions/2546207/does-sqlalchemy-have-an-equivalent-of-djangos-get- or-create/21146492 # comment11457084_6078058), esta solución es la buena, ya que previene las condiciones de carrera (al comprometer/lavar la sesión, tenga cuidado) e imita perfectamente lo que hace Django. – kiddouk

+1

¡Esta debería ser la respuesta aceptada! –

+0

@kiddouk No, no imita "perfectamente". El 'get_or_create' de Django es * no * seguro para subprocesos. No es atómico.Además, 'get_or_create' de Django devuelve una bandera True si la instancia fue creada o una bandera falsa de lo contrario. – Kar

3

Lo más cerca que semánticamente 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 está seguro de cómo kosher es confiar en un nivel global definido en Session sqlalchemy, pero la versión de Django no tiene una conexión tan ...

La tupla devuelta contiene la instancia y un valor booleano que indica si la instancia se creó (es decir, es False si leemos la instancia desde el archivo db).

Django's get_or_create se usa a menudo para garantizar que los datos globales estén disponibles, por lo que me comprometo lo antes posible.

1

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

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

Ésta es transaccionalmente segura cualquiera que sea el nivel de aislamiento y el grado de paralelismo son.

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

6

Una versión modificada de la excelente Erik answer

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 
  • Utilice un nested transaction sólo a hacer retroceder la adición del nuevo elemento en lugar de hacer retroceder todo (ver este answer utilizar 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. P.ej. crear un book, que tiene user_id y user como relación correspondiente, luego haciendo book.user=<user object> dentro de create_method agregará book a la sesión. Esto significa que create_method debe estar dentro de with para beneficiarse de una reversión final. Tenga en cuenta que begin_nested activa automáticamente una descarga.

Tenga en cuenta que si el uso de MySQL, el nivel de aislamiento se debe establecer en READ COMMITTED en lugar de REPEATABLE READ para que esto funcione. Django's get_or_create (y here) utiliza la misma estratagema, ver también Django documentation.

+0

Me gusta que esto evite deshacer cambios no relacionados, sin embargo la re-consulta 'IntegrityError' aún puede fallar con' NoResultFound' con el nivel de aislamiento predeterminado de MySQL 'REPEATABLE READ' si la sesión había consultado previamente el modelo en la misma transacción. La mejor solución que podría surgir es llamar a 'session.commit()' antes de esta consulta, 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 iniciar una nueva transacción. – kevmitch

+0

Huh, TIL. ¿Funcionaría la consulta en una transacción anidada? Tiene razón en que 'commit' dentro de esta función es posiblemente peor que hacer un 'rollback', aunque para casos de uso específicos puede ser aceptable. – Adversus

+0

Sí, al colocar la consulta inicial en una transacción anidada, al menos es posible que la segunda consulta funcione. Sin embargo, aún fallará si el usuario ha consultado explícitamente el modelo antes en la misma transacción. He decidido que esto es aceptable y que se debe advertir al usuario que no haga esto o que de otra forma atrape la excepción y decida si desea 'commit()'. Si mi comprensión del código es correcta, esto es lo que hace Django. – kevmitch

1

I ligeramente simplificado @Kevin. solución para evitar envolver toda la función en una declaración if/else.De esta manera sólo hay una return, que me parece 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 
Cuestiones relacionadas