我想从数据库中获取一个对象(如果它已经存在)(基于提供的参数),或者如果不存在则创建它。
Django的get_or_create
(或source)就是这么做的。 SQLAlchemy中是否有等效的快捷方式?
我目前正在明确地写出来:
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 :(得分:93)
根据@WoLpH的解决方案,这是适合我的代码(简单版本):
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
有了这个,我就可以get_or_create我的模型的任何对象。
假设我的模型对象是:
class Country(Base):
__tablename__ = 'countries'
id = Column(Integer, primary_key=True)
name = Column(String, unique=True)
为了获取或创建我的对象,我写道:
myCountry = get_or_create(session, Country, name=countryName)
答案 1 :(得分:83)
这基本上是这样做的方式,AFAIK没有随时可用的快捷方式。
你可以将其概括为:
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 :(得分:47)
我一直在玩这个问题,最终找到了一个相当强大的解决方案:
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
我刚刚在所有细节上写了一个fairly expansive blog post,但对于我使用它的原因有几点了解。
它解压缩到一个元组,告诉你对象是否存在。这在您的工作流程中通常很有用。
该功能可以使用@classmethod
装饰的创建者功能(以及特定于它们的属性)。
当您有多个进程连接到数据存储区时,该解决方案可以防止竞争条件。
编辑:我已将session.commit()
更改为session.flush()
,如this blog post中所述。请注意,这些决策特定于所使用的数据存储区(在本例中为Postgres)。
编辑2:我使用{}作为函数中的默认值进行了更新,因为这是典型的Python问题。感谢the comment,奈杰尔!如果您对此问题感到好奇,请查看this StackOverflow question和this blog post。
答案 3 :(得分:9)
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
create_method
。如果创建的对象具有关系并且通过这些关系为其分配成员,则会自动将其添加到会话中。例如。创建book
,其中user_id
和user
作为对应关系,然后在book.user=<user object>
内执行create_method
会将book
添加到会话中。这意味着create_method
必须位于with
内才能从最终回滚中受益。请注意,begin_nested
会自动触发刷新。请注意,如果使用MySQL,则必须将事务隔离级别设置为READ COMMITTED
而不是REPEATABLE READ
才能使其生效。 Django的get_or_create(和here)使用相同的策略,另请参阅Django documentation。
答案 4 :(得分:5)
This SQLALchemy recipe让工作变得优雅和优雅。
要做的第一件事是定义一个给予Session使用的函数,并将字典与Session()相关联,Session()跟踪当前唯一键。
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
利用此功能的一个例子是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
)
最后创建独特的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()
这个方法深入探讨了这个想法并提供了不同的方法,但我已经用这个方法取得了巨大的成功。
答案 5 :(得分:3)
最接近语义可能是:
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
不确定在sqlalchemy中依赖全局定义的Session
是多么犹豫,但是Django版本没有连接所以......
返回的元组包含实例和一个布尔值,表示实例是否已创建(即如果我们从数据库中读取实例,则它为False)。
Django&#39; get_or_create
通常用于确保全局数据可用,因此我尽可能早地提交。
答案 6 :(得分:1)
根据您采用的隔离级别,上述解决方案都不起作用。 我找到的最佳解决方案是以下列形式的RAW SQL:
INSERT INTO table(f1, f2, unique_f3)
SELECT 'v1', 'v2', 'v3'
WHERE NOT EXISTS (SELECT 1 FROM table WHERE f3 = 'v3')
无论隔离级别和并行度如何,这都是事务安全的。
注意:为了提高效率,为独特列设置INDEX是明智的。
答案 7 :(得分:1)
我略微简化了@Kevin。避免将整个函数包装在if
/ else
语句中的解决方案。这样,只有一个return
,我觉得更清洁:
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
答案 8 :(得分:1)
有一个Python软件包,它具有@erik的解决方案以及update_or_create()
的版本。 https://github.com/enricobarzetti/sqlalchemy_get_or_create
答案 9 :(得分:0)
我经常遇到的一个问题是当一个字段有一个最大长度(比如 STRING(40)
)并且你想用一个大长度的字符串执行一个 get or create
时,上述解决方案将失败.
基于上述解决方案,这是我的方法:
from sqlalchemy import Column, String
def get_or_create(self, add=True, flush=True, commit=False, **kwargs):
"""
Get the an entity based on the kwargs or create an entity with those kwargs.
Params:
add: (default True) should the instance be added to the session?
flush: (default True) flush the instance to the session?
commit: (default False) commit the session?
kwargs: key, value pairs of parameters to lookup/create.
Ex: SocialPlatform.get_or_create(**{'name':'facebook'})
returns --> existing record or, will create a new record
---------
NOTE: I like to add this as a classmethod in the base class of my tables, so that
all data models inherit the base class --> functionality is transmitted across
all orm defined models.
"""
# Truncate values if necessary
for key, value in kwargs.items():
# Only use strings
if not isinstance(value, str):
continue
# Only use if it's a column
my_col = getattr(self.__table__.columns, key)
if not isinstance(my_col, Column):
continue
# Skip non strings again here
if not isinstance(my_col.type, String):
continue
# Get the max length
max_len = my_col.type.length
if value and max_len and len(value) > max_len:
# Update the value
value = value[:max_len]
kwargs[key] = value
# -------------------------------------------------
# Make the query...
instance = session.query(self).filter_by(**kwargs).first()
if instance:
return instance
else:
# Max length isn't accounted for here.
# The assumption is that auto-truncation will happen on the child-model
# Or directtly in the db
instance = self(**kwargs)
# You'll usually want to add to the session
if add:
session.add(instance)
# Navigate these with caution
if add and commit:
try:
session.commit()
except IntegrityError:
session.rollback()
elif add and flush:
session.flush()
return instance