python - instalar - ¿Tiene SQLAlchemy un equivalente de get_or_create de Django?
sqlalchemy python install (8)
Quiero obtener un objeto de la base de datos si ya existe (según los parámetros proporcionados) o crearlo si no existe.
get_or_create
(o source ) de Django 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
Dependiendo del nivel de aislamiento que adoptó, ninguna de las soluciones anteriores funcionaría. La mejor solución que he encontrado es un RAW SQL 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 seguro para las transacciones, cualquiera que sea el nivel de aislamiento y el grado de paralelismo.
Cuidado: para que sea eficiente, sería aconsejable tener un ÍNDICE para la columna única.
El semántico más cercano 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 qué tan kosher es confiar en una Session
definida globalmente en sqlalchemy, pero la versión de Django no tiene conexión, así que ...
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 la base de datos).
get_or_create
de Django se usa a menudo para asegurarse de que los datos globales estén disponibles, por lo que me comprometo lo antes posible.
Esa es básicamente la forma de hacerlo, no hay un acceso directo disponible AFAIK.
Podrías 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
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(), False
Acabo de escribir una entrada de blog bastante amplia sobre todos los detalles, pero algunas ideas bastante claras de por qué usé esto.
Se desempaqueta en una tupla que le dice si el objeto existió o no. Esto a menudo puede ser útil en su flujo de trabajo.
La función ofrece la posibilidad de trabajar con funciones de creador decoradas con
@classmethod
(y atributos específicos para ellas).La solución protege contra las Condiciones de Carrera cuando tiene 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 del blog . Tenga en cuenta que estas decisiones son específicas para el almacén de datos utilizado (Postgres en este caso).
EDITAR 2: He actualizado utilizando {} como un valor predeterminado en la función, ya que es típico de Python Gotcha. Gracias por el comentario , Nigel! Si tiene curiosidad por esta pregunta, consulte esta pregunta de y esta publicación del blog .
He simplificado un poco @Kevin. Solución para evitar envolver toda la función en una sentencia if
/ else
. De esta manera solo hay una return
, que me parece más limpia:
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
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, soy capaz de 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)
Una versión modificada de la excelente answer 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 artículo en lugar de revertir todo (consulte esta answer para usar transacciones anidadas con SQLite)
- Mueve
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 unbook
, que tengauser_id
yuser
como relación correspondiente, luego hacerbook.user=<user object>
dentro decreate_method
agregarábook
a la sesión. Esto significa quecreate_method
debe estar dentro para beneficiarse de un posible retroceso. Tenga en cuenta quebegin_nested
desencadena automáticamente una descarga.
Tenga en cuenta que si utiliza MySQL, el nivel de aislamiento de la transacción debe establecerse en READ COMMITTED
lugar de REPEATABLE READ
para que esto funcione. get_or_create de Django (y here ) usa la misma estratagema, vea también la documentation Django.
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 da una sesión para trabajar, y asociar 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 la utilización de esta función sería en una mezcla:
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 único modelo 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 receta profundiza en la idea y proporciona diferentes enfoques, pero he utilizado esta con gran éxito.