web-dev-qa-db-ja.com

SQLAlchemyの挿入時に重複する主キーを処理する(宣言型)

私のアプリケーションは、スコープ付きセッションとSQLALchemyの宣言型スタイルを使用しています。これはWebアプリであり、多くのDB挿入はタスクスケジューラであるCeleryによって実行されます。

通常、オブジェクトを挿入することを決定するとき、私のコードは次の行に沿って何かをするかもしれません:

_from schema import Session
from schema.models import Bike

pk = 123 # primary key
bike = Session.query(Bike).filter_by(bike_id=pk).first()
if not bike: # no bike in DB
    new_bike = Bike(pk, "shiny", "bike")
    Session.add(new_bike)
    Session.commit()
_

ここでの問題は、これの多くが非同期ワーカーによって行われるため、別の人がその存在を確認している間に、Bikeを_id=123_に挿入することで作業を途中で行うことができることです。この場合、2番目のワーカーは同じ主キーを持つ行を挿入しようとし、SQLAlchemyはIntegrityErrorを発生させます。

私は一生、Session.commit()を交換する以外にこの問題に対処する良い方法を見つけることができません:

_'''schema/__init__.py'''
from sqlalchemy.orm import scoped_session, sessionmaker
Session = scoped_session(sessionmaker())

def commit(ignore=False):
    try:
        Session.commit()
    except IntegrityError as e:
        reason = e.message
        logger.warning(reason)

        if not ignore:
            raise e

        if "Duplicate entry" in reason:
            logger.info("%s already in table." % e.params[0])
            Session.rollback()
_

そして、_Session.commit_があるすべての場所で、行が再び挿入されないことを気にしないschema.commit(ignore=True)があります。

私にとって、これは文字列のチェックのために非常に脆いようです。参考までに、IntegrityErrorが発生すると、次のようになります。

_(IntegrityError) (1062, "Duplicate entry '123' for key 'PRIMARY'")
_

だからもちろん、私が挿入していた主キーは_Duplicate entry is a cool thing_のようなものだったので、実際には主キーの重複が原因ではないIntegrityErrorを見逃す可能性があると思います。

私が使用しているクリーンなSQLAlchemyアプローチを維持するより良いアプローチがありますか(文字列などのステートメントを書き始めるのではなく)。

DbはMySQLです(ただし、ユニットテストではSQLiteを使用するのが好きで、新しいアプローチでその機能を妨げたくありません)。

乾杯!

36
Edwardr

session.merge(bike)の代わりにsession.add(bike)を使用すると、主キーエラーは生成されません。 bikeは、必要に応じて取得および更新または作成されます。

28
sirdodger

すべてのIntegrityErrorを同じ方法で処理する必要があります。トランザクションをロールバックし、オプションで再試行します。一部のデータベースでは、IntegrityErrorの後にそれ以上のことはできません。 2つの競合するトランザクションの開始時に、テーブルのロック、またはデータベースで許可されている場合はよりきめの細かいロックを取得することもできます。

withステートメントを使用してトランザクションを明示的に開始し、自動的にコミット(または例外でロールバック):

from schema import Session
from schema.models import Bike

session = Session()
with session.begin():
    pk = 123 # primary key
    bike = session.query(Bike).filter_by(bike_id=pk).first()
    if not bike: # no bike in DB
        new_bike = Bike(pk, "shiny", "bike")
        session.add(new_bike)
8
joeforker

ここでの主キーは何らかの形で自然であると想定しているため、通常の自動インクリメント手法に頼ることはできません。そのため、問題は実際には、挿入する必要のある一意の列の1つであり、これはより一般的です。

「挿入を試み、失敗時に部分的にロールバックする」場合は、SQLAlchemyでbegin_nested()であるSAVEPOINTを使用します。次のrollback()またはcommit()は、そのSAVEPOINTにのみ作用し、進行中のより大きなスパンには作用しません。

ただし、ここでの全体的なパターンは、実際に回避すべきものにすぎません。ここで本当にやりたいことは、3つのことの1つです。 1.挿入する必要のある同じキーを扱う並行ジョブを実行しないでください。 2.作業中の並行キーで何らかの方法でジョブを同期し、3。いくつかの共通サービスを使用して、ジョブで共有されるこの特定のタイプの新しいレコードを生成します(または、ジョブを実行する前にすべてセットアップすることを確認します)。

考えてみると、いずれの場合でも、高度な分離が行われている#2が発生します。 2つのpostgresセッションを開始します。セッション1:

test=> create table foo(id integer primary key);
NOTICE:  CREATE TABLE / PRIMARY KEY will create implicit index "foo_pkey" for table "foo"
CREATE TABLE
test=> begin;
BEGIN
test=> insert into foo (id) values (1);

セッション2:

test=> begin;
BEGIN
test=> insert into foo(id) values(1);

pK#1の行がロックされているため、セッション2がブロックされます。 MySQLがこれを行うのに十分なスマートかどうかはわかりませんが、それは正しい動作です。 OTOHが別のPKを挿入しようとする場合:

^CCancel request sent
ERROR:  canceling statement due to user request
test=> rollback;
ROLLBACK
test=> begin;
BEGIN
test=> insert into foo(id) values(2);
INSERT 0 1
test=> \q

ブロックせずに問題なく進行します。

ポイントは、この種のPK/UQ競合を行っている場合、セロリのタスクはシリアル化されますとにかく、または少なくともそうすべきです。

4
zzzeek

以下のコードを使用する必要があるsession.add(obj)の代わりに、これははるかにきれいになり、前述のようなカスタムコミット関数を使用する必要はありません。ただし、重複するキーだけでなく、他のキーについても、競合は無視されます。

mysql:

 self.session.execute(insert(self.table, values=values, prefixes=['IGNORE']))

sqlite

self.session.execute(insert(self.table, values=values, prefixes=['OR IGNORE']))
2
rajat