web-dev-qa-db-ja.com

SQLAlchemyにはDjangoのget_or_createと同等のものがありますか?

既に存在する場合はデータベースからオブジェクトを取得します(提供されたパラメーターに基づいて)。存在しない場合は作成します。

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
142
FogleBird

それは基本的にそれを行う方法です、すぐに入手できるショートカットはありません。

もちろんそれを一般化できます:

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
86
Wolph

@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):
    __table= 'countries'
    id = Column(Integer, primary_key=True)
    name = Column(String, unique=True)

オブジェクトを取得または作成するには、次のように記述します。

myCountry = get_or_create(session, Country, name=countryName)
96
Kevin.

私はこの問題で遊んでいて、かなり堅牢なソリューションになりました:

_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. オブジェクトが存在するかどうかを知らせるTupleに展開します。これは多くの場合、ワークフローで役立ちます。

  2. この関数は、_@classmethod_で装飾された作成者関数(およびそれらに固有の属性)を操作する機能を提供します。

  3. このソリューションは、データストアに複数のプロセスが接続されている場合に競合状態から保護します。

編集: このブログ投稿 で説明されているように、session.commit()session.flush()に変更しました。これらの決定は、使用するデータストア(この場合はPostgres)に固有であることに注意してください。

編集2:これは典型的なPythonの落とし穴であるため、関数のデフォルト値として{}を使用して更新しました。 コメント 、ナイジェルに感謝します!この落とし穴に興味がある場合は、 このStackOverflowの質問このブログ投稿

49
erik

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
  • ネストされたトランザクション を使用して、すべてをロールバックするのではなく、新しいアイテムの追加のみをロールバックします(SQLiteでネストされたトランザクションを使用するには answer を参照)
  • create_methodを移動します。作成されたオブジェクトにリレーションがあり、それらのリレーションを介してメンバーが割り当てられている場合、それは自動的にセッションに追加されます。例えば。対応する関係としてuser_idbookを持つuserを作成し、book.user=<user object>内でcreate_methodを実行すると、bookがセッションに追加されます。これは、create_methodwith内になければならないことを意味します。 begin_nestedは自動的にフラッシュをトリガーすることに注意してください。

MySQLを使用する場合、これを機能させるには、トランザクション分離レベルをREAD COMMITTEDではなくREPEATABLE READに設定する必要があることに注意してください。 Djangoの get_or_create (および here )は同じ戦略を使用しています。Django documentation も参照してください。

10
Adversus

このSQLALchemyのレシピ は、素敵でエレガントな仕事をします。

最初に行うことは、作業するセッションを指定し、現在のuniqueキーを追跡する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

この機能を利用する例は、ミックスインにあります。

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):
    __table= '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
jhnwsk

意味的に最も近いのは次のとおりです。

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バージョンは接続を取得しません...

返されるTupleには、インスタンスと、インスタンスが作成されたかどうかを示すブール値が含まれます(つまり、dbからインスタンスを読み取る場合はFalseです)。

Djangoのget_or_createは、グローバルデータが利用可能であることを確認するためによく使用されるため、可能な限り早い段階でコミットしています。

3
thebjorn

採用した分離レベルに応じて、上記のソリューションはどれも機能しません。私が見つけた最良の解決策は、次の形式のRAW SQLです。

INSERT INTO table(f1, f2, unique_f3) 
SELECT 'v1', 'v2', 'v3' 
WHERE NOT EXISTS (SELECT 1 FROM table WHERE f3 = 'v3')

これは、分離レベルと並列度に関係なく、トランザクション的に安全です。

注意:効率的にするためには、一意の列にINDEXを設定するのが賢明です。

1
fcracker79

@Kevinを少し単純化しました。 if/elseステートメントで関数全体をラップしないようにするソリューション。この方法では、returnが1つしかありません。

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
jmberros