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/...を
アントンタラセンコ

回答:


96

それは基本的にそれを行う方法であり、すぐに利用できる近道はありません。

もちろん、一般化することもできます。

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).first()」を読んだところで、「session.Query(model.filter_by(** kwargs))。first()」を読んだ方がいいと思います
pkoch

3
このスレッドが機会を得る前に別のスレッドがインスタンスを作成しないように、これをロックする必要がありますか?
EoghanM、

2
@EoghanM:通常、セッションはスレッドローカルなので、これは問題になりません。SQLAlchemyセッションは、スレッドセーフであることを意図していません。
Wolph、2011年

5
@WolpH同じレコードを同時に作成しようとしている別のプロセスである可能性があります。Djangoのget_or_createの実装を見てください。整合性エラーをチェックし、一意の制約の適切な使用に依存します。
Ivan Virabyan、2012年

1
@IvanVirabyan:@EoghanMがセッションインスタンスについて話していると思いました。その場合try...except IntegrityError: instance = session.Query(...)session.addブロックの周りにあるはずです。
ウルフ

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
私のように検索する人にとって、これは行がまだ存在しない場合に行を作成する適切なソリューションです。
スペンサーラスバン

3
セッションに新しいインスタンスを追加する必要はありませんか?それ以外の場合、呼び出しコードでsession.commit()を発行しても、新しいインスタンスがセッションに追加されないため、何も起こりません。
CadentOrange 2013年

1
これありがとう。これは非常に便利で、将来の使用に備えて要旨を作成しました。gist.github.com/jangeador/e7221fc3b5ebeeac9a08
jangeador

コードを配置する必要がある場所?、実行コンテキストエラーを解決する
Victor Alvarado

7
引数としてセッションを渡すことを考えると、commit(または少なくともflush代わりにaのみを使用すること)を回避する方が良い場合があります。これにより、セッションの制御はこのメソッドの呼び出し元に委ねられ、時期尚早のコミットを発行するリスクがなくなります。また、の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が言うことに比べて、このソリューションは(セッションをコミット/フラッシュすることにより)競合状態を防ぎ、Djangoの動作を完全に模倣するため、優れたソリューションです。
kiddouk 2014年

@kiddoukいいえ、「完全に」模倣したものではありません。Django get_or_createはスレッドセーフではありません。それはアトミックではありません。また、Django get_or_createは、インスタンスが作成された場合はTrueフラグを返し、そうでない場合はFalseフラグを返します。
Kar

@ケイト、Djangoを見ると、get_or_createほとんど同じことをしています。このソリューションは、True/Falseオブジェクトが作成またはフェッチされたかどうかを示すフラグも返します。また、アトミックではありません。ただし、スレッドセーフとアトミック更新は、Django、Flask、SQLAlchemyではなく、データベースの問題であり、このソリューションとDjangoの両方で、データベースのトランザクションによって解決されます。
erik

1
新しいレコードに対してnull以外のフィールドにnull値が指定されたとすると、IntegrityErrorが発生します。全体がめちゃくちゃになりますが、実際には何が起こったのかわからず、レコードが見つからないという別のエラーが発生します。
rajat

2
このクライアントはオブジェクトを作成しなかったので、IntegrityErrorケースは返されFalseませんか?
kevmitch 2016年

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_idを作成し、内部で行うとセッションに追加されます。これは、最終的なロールバックの恩恵を受けるには、それが内部にある必要があることを意味します。フラッシュが自動的にトリガーされることに注意してください。userbook.user=<user object>create_methodbookcreate_methodwithbegin_nested

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


これにより、無関係な変更がロールバックされないようにするのが好きですが、セッションが以前に同じトランザクションでモデルをクエリした場合、MySQLのデフォルトの分離レベルでIntegrityError再クエリが失敗する可能性がNoResultFoundありますREPEATABLE READ。私が思いつくことができる最良の解決策は、session.commit()このクエリの前に呼び出すことです。これは、ユーザーが期待しない可能性があるため、理想的でもありません。session.rollback()には新しいトランザクションを開始するのと同じ効果があるため、参照される回答にはこの問題はありません。
kevmitch 2016年

ああ、TIL。ネストされたトランザクションにクエリを配置することはできますか?あなたしている右のcommitこの関数の内部が行うよりも間違いなく悪化しているrollback具体的なユースケースのためにそれを許容することができたとしても、。
Adversus

はい、ネストされたトランザクションに最初のクエリを配置すると、少なくとも2番目のクエリが機能するようになります。ただし、ユーザーが同じトランザクションで以前に明示的にモデルをクエリした場合でも、失敗します。これは許容できると判断しました。これを行わないように警告するか、例外をキャッチしてcommit()自分自身で判断する必要があります。コードに対する私の理解が正しければ、これがDjangoが行うことです。
kevmitch 2016年

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。したがって、IntegrityErrorexcept句のクエリがまったく機能するように、クエリをネストされたトランザクションのtry句で囲む必要があることがわかりました。
kevmitch 2016年

6

このSQLALchemyレシピは、仕事を素晴らしくエレガントに行います。

最初に、処理するセッションが与えられ、ディクショナリを現在の一意のキーを追跡する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):
    __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バージョンは接続を取りません...

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

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


これは、セッションが作成されscoped_session、スレッドセーフなセッション管理を実装するによって追跡される限り機能します(これは2014年に存在しましたか?)。
カウバート

2

@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

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

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

これは、分離レベルと並列処理の度合いが何であれ、トランザクション上安全です。

注意:効率的にするために、一意の列にINDEXを設定することをお勧めします。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.