我想从数据库中获得一个对象,如果它已经存在(基于提供的参数)或创建它,如果它不存在。
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
埃里克精彩回答的修改版
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 a nested transaction to only roll back the addition of the new item instead of rolling back everything (See this answer to use nested transactions with SQLite)
Move create_method. If the created object has relations and it is assigned members through those relations, it is automatically added to the session. E.g. create a book, which has user_id and user as corresponding relationship, then doing book.user=<user object> inside of create_method will add book to the session. This means that create_method must be inside with to benefit from an eventual rollback. Note that begin_nested automatically triggers a flush.
注意,如果使用MySQL,事务隔离级别必须设置为READ COMMITTED,而不是REPEATABLE READ。Django的get_or_create(和这里)使用了相同的策略,请参阅Django文档。
埃里克精彩回答的修改版
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 a nested transaction to only roll back the addition of the new item instead of rolling back everything (See this answer to use nested transactions with SQLite)
Move create_method. If the created object has relations and it is assigned members through those relations, it is automatically added to the session. E.g. create a book, which has user_id and user as corresponding relationship, then doing book.user=<user object> inside of create_method will add book to the session. This means that create_method must be inside with to benefit from an eventual rollback. Note that begin_nested automatically triggers a flush.
注意,如果使用MySQL,事务隔离级别必须设置为READ COMMITTED,而不是REPEATABLE READ。Django的get_or_create(和这里)使用了相同的策略,请参阅Django文档。
根据所采用的隔离级别,上述解决方案都不起作用。
我发现的最好的解决方案是一个RAW SQL在以下形式:
INSERT INTO table(f1, f2, unique_f3)
SELECT 'v1', 'v2', 'v3'
WHERE NOT EXISTS (SELECT 1 FROM table WHERE f3 = 'v3')
无论隔离级别和并行度如何,这都是事务安全的。
注意:为了提高效率,明智的做法是为唯一的列使用INDEX。
语义上最接近的可能是:
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版本不需要连接所以…
返回的元组包含实例和一个布尔值,表示实例是否已创建(例如,如果从db中读取实例则为False)。
Django的get_or_create通常用于确保全局数据可用,所以我尽可能在最早的时候提交。
我一直在研究这个问题,并最终得到了一个相当强大的解决方案:
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
我只是写了一篇关于所有细节的相当广泛的博客文章,但有一些关于我为什么使用它的想法。
它解包到一个元组,该元组告诉您对象是否存在。这在您的工作流中通常是有用的。
该函数提供了使用@classmethod修饰的创建者函数(以及特定于它们的属性)的能力。
当有多个进程连接到数据存储时,该解决方案可以防止Race Conditions。
编辑:我已经将session.commit()更改为session.flush(),如本文所述。注意,这些决策是特定于所使用的数据存储的(在本例中是Postgres)。
编辑2:我在函数中使用{}作为默认值进行更新,因为这是典型的Python陷阱。谢谢你的评论,奈杰尔!如果你对这个问题感到好奇,看看这个StackOverflow的问题和这篇博客文章。