SQLAlchemy是否具有与Django的get_or_create等效的功能?


160

我想从数据库中获取一个对象(如果已存在)(基于提供的参数),或者如果不存在则创建它。

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

4
对于那些谁只想补充对象,如果它不存在,请参见session.mergestackoverflow.com/questions/12297156/...
安东塔拉先科

Answers:


96

基本上就是这样做的方法,没有快捷方式可供使用的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
我认为,在这里你读“session.Query(model.filter_by(** kwargs)。首先()”,你应该阅读“session.Query(model.filter_by(** kwargs))第()。”
pkoch

3
是否应该对此进行锁定,以使另一个线程在该线程有机会这样做之前不会创建实例?
EoghanM

2
@EoghanM:通常您的会话是线程本地的,所以这无关紧要。SQLAlchemy会话并不意味着是线程安全的。
沃尔夫,

5
@WolpH可能是尝试同时创建相同记录的另一个过程。查看Django的get_or_create实现。它检查完整性错误,并依赖于对唯一约束的正确使用。
伊万·维拉比扬

1
@IvanVirabyan:我以为@EoghanM在谈论会话实例。在这种情况下,try...except IntegrityError: instance = session.Query(...)session.add块周围应该有一个。
沃尔夫,2012年

109

在@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)

3
对于像我这样搜索的人,这是创建行(如果尚不存在)的正确解决方案。
Spencer Rathbun

3
您是否不需要将新实例添加到会话中?否则,如果在调用代码中发出session.commit(),则不会发生任何事情,因为不会将新实例添加到会话中。
CadentOrange

1
这次真是万分感谢。我发现它是如此有用,以至于我为其创建了要旨。gist.github.com/jangeador/e7221fc3b5ebeeac9a08
jangeador 2014年

我需要在哪里放置代码?,我发现执行上下文错误吗?
维克多·阿尔瓦拉多

7
鉴于您将会话作为参数传递,最好避免使用commit(或至少仅使用a flush代替)。这将会话控制权留给该方法的调用者,并且不会冒发出过早提交的风险。另外,使用one_or_none()代替first()可能会更安全。
exhuma

52

我一直在解决这个问题,并最终得到了一个相当强大的解决方案:

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

我只是写了一篇有关所有详细信息的相当广泛的博客文章,但是对我为什么要使用它的一些颇有想法。

  1. 它解压缩到一个元组,该元组告诉您对象是否存在。这通常在您的工作流程中很有用。

  2. 该功能使您能够使用@classmethod修饰的创建者功能(以及特定于它们的属性)。

  3. 当您有多个进程连接到数据存储时,该解决方案可防止出现竞争状况。

编辑:我已经改变session.commit()session.flush()在解释这个博客帖子。请注意,这些决定特定于所使用的数据存储(在这种情况下为Postgres)。

编辑2:我已在函数中使用{}作为默认值进行了更新,因为这是典型的Python陷阱。谢谢你的评论,奈杰尔!如果您对此问题感到好奇,请查看此StackOverflow问题此博客文章


1
与spencer 所说的相比,该解决方案是一个很好的解决方案,因为它可以防止Race条件(通过提交/刷新会话,要当心),并且可以完美地模仿Django的功能。
kiddouk 2014年

@kiddouk不,它不能“完美地”模仿。Django的get_or_create不是线程安全的。这不是原子的。同样,get_or_create如果创建了实例,则Django会返回True标志,否则返回False标志。
2015年

@Kate,如果您查看Django,get_or_create它几乎完成了完全相同的操作。此解决方案还返回True/False标志,以信号通知对象是已创建还是已获取,并且不是原子的。但是,线程安全和原子更新是数据库的问题,而不是Django,Flask或SQLAlchemy的问题,并且在此解决方案和Django的解决方案中,都由数据库上的事务解决。
erik 2015年

1
假设为新记录提供了非空字段空值,它将引发IntegrityError。整个事情搞砸了,现在我们不知道实际发生了什么,并且遇到另一个错误,即找不到记录。
rajat

2
由于此客户端未创建对象,IntegrityError案件不应该返回False吗?
kevmitch '16

11

埃里克出色答案的修改版

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
  • 使用嵌套事务仅回滚新项的添加,而不回滚所有内容(请参阅此答案以将嵌套事务与SQLite一起使用)
  • 移动create_method。如果创建的对象具有关系,并且通过这些关系为其分配了成员,则它将自动添加到会话中。例如,创建一个book具有user_iduser作为对应关系的,然后在book.user=<user object>里面做create_method将添加book到会话中。这意味着create_method必须在内部with才能从最终回滚中受益。请注意,它会begin_nested自动触发冲洗。

请注意,如果使用MySQL,则必须将事务隔离级别设置为READ COMMITTED而不是REPEATABLE READ此级别。Django的get_or_create(和此处)使用相同的策略,另请参见Django 文档


我喜欢这样做,可以避免回滚无关的更改,但是如果会话以前在同一事务中查询过该模型,则使用MySQL默认隔离级别IntegrityError仍然可能导致重新查询失败。我能想到的最好的解决方案是在此查询之前调用,这也不理想,因为用户可能不希望这样。引用的答案不存在此问题,因为session.rollback()具有启动新事务的相同效果。NoResultFoundREPEATABLE READsession.commit()
kevmitch '16

T,蒂尔。将查询放入嵌套事务中可以吗?您说对commitrollback,尽管可以接受,但可以肯定的是,此函数的内部效果比做一个更差。
Adversus

是的,将初始查询放入嵌套事务中至少可以使第二个查询正常工作。但是,如果用户之前在同一事务中明确查询了模型,它将仍然失败。我认为这是可以接受的,应该警告用户不要这样做,否则应捕获异常并commit()自行决定是否这样做。如果我对代码的理解是正确的,这就是Django要做的。
kevmitch '16

在django 文档中,他们说要使用`READ COMMITTED , 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 SAVEPOINT`影响读取REPEATABLE READ。如果没有效果,那么情况似乎无法挽回,如果没有效果,那么最后一个查询可以嵌套吗?
Adversus

有趣的是READ COMMITED,也许我应该重新考虑不要更改数据库默认值的决定。我已经测试过,SAVEPOINT在进行查询之前从中还原可以使该查询从未发生过REPEATABLE READ。因此,我发现有必要将查询包含在嵌套事务中的try子句中,以使IntegrityErrorexcept子句中的查询可以正常工作。
kevmitch,

6

这个SQLALchemy食谱能很好地完成工作。

首先要做的是定义一个函数,该函数被赋予要使用的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()

配方更深入地介绍了这个想法,并提供了不同的方法,但是我已经成功地使用了这一方法。


1
如果只有一个SQLAlchemy Session对象可以修改数据库,那么我喜欢这个食谱。我可能是错的,但是如果其他会话(无论是否使用SQLAlchemy)同时修改数据库,我看不到如何防止事务正在进行时其他会话可能创建的对象受到保护。在那些情况下,我认为依赖session.add()之后的刷新和stackoverflow.com/a/21146492/3690333之类的异常处理的解决方案更加可靠。
TrilceAC

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

不知道如何依靠Sessionsqlalchemy中的全局定义,但是Django版本没有连接,所以...

返回的元组包含实例和一个布尔值,指示是否创建了实例(即,如果我们从数据库读取实例,则为False)。

get_or_create经常使用Django 来确保全局数据可用,因此我会尽早提交。


只要Session由创建和跟踪scoped_session,该方法就应该起作用,该方法应该实施线程安全的会话管理(是否在2014年存在?)。
Cowbert

2

我稍微简化了@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

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是明智的。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.