SQLAlchemy是否有相当于Django的get_or_create?

时间:2010-03-30 14:57:31

标签: python django sqlalchemy

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

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

10 个答案:

答案 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,但对于我使用它的原因有几点了解。

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

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

  3. 当您有多个进程连接到数据存储区时,该解决方案可以防止竞争条件。

  4. 编辑:我已将session.commit()更改为session.flush(),如this blog post中所述。请注意,这些决策特定于所使用的数据存储区(在本例中为Postgres)。

    编辑2:我使用{}作为函数中的默认值进行了更新,因为这是典型的Python问题。感谢the comment,奈杰尔!如果您对此问题感到好奇,请查看this StackOverflow questionthis 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
  • 使用nested transaction仅回滚新项目的添加而不是回滚所有内容(请参阅此answer以使用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(和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