0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PythonとSQLiteで二重取込を防ぐ、変更行は止める

0
Posted at

ターミナルで同じ取込を2回動かす。1回目は1件登録、2回目は0件。ここまでは順調でした。ところが、同じ受注IDの金額を変えても、エラーなしで0件になった。

「重複したら無視する」だけの処理を、僕は業務の取込には使いたくありません。再実行で増やさないことと、訂正データを握りつぶさないことを、セットで確かめます。

架空の受注データで再現しました。CSVを読んだ後の登録部分に使う例です。IDは文字列、金額は整数で渡します。

SQLiteの重複無視には、内容の照合がない

ON CONFLICT(id) DO NOTHINGは、同じIDがあれば挿入を見送ります。既存行の金額が一致するかまでは調べません。

初回が1,000円、再取込が1,100円でも、IDが同じなら通り過ぎる。件数だけをログに残していると、同じファイルを流したのか、訂正を捨てたのか区別がつきません。

そこで、挿入を見送った行だけ既存の金額と照合します。一致すれば再取込として無視。違えば例外を出し、その回の取込を全部取り消す方針にしました。

受注IDが新規なら仮登録、既存なら金額を照合する流れ。金額違いで今回の仮登録をすべて取り消し、全行を問題なく読み終えた場合だけ確定する。

途中まで入った行も取り消すコード

Python 3.12以降が必要です。CPython 3.14.8、SQLite 3.54.1で実行しました。import_check.pyに保存し、python3 import_check.pyで動かせます。

import sqlite3

db = sqlite3.connect(":memory:", autocommit=False)
db.execute("""
CREATE TABLE orders (
    id TEXT PRIMARY KEY NOT NULL,
    yen INTEGER NOT NULL
)
""")
db.commit()

def take(rows):
    added = 0
    with db:
        for key, yen in rows:
            cur = db.execute(
                "INSERT INTO orders VALUES (?, ?) "
                "ON CONFLICT(id) DO NOTHING", (key, yen)
            )
            if cur.rowcount == 1:
                added += 1
            else:
                old = db.execute(
                    "SELECT yen FROM orders WHERE id = ?", (key,)
                ).fetchone()
                if old != (yen,):
                    raise ValueError(f"{key}: 金額が既存行と違います")
    return added

print("初回:", take([("A001", 1000)]))
print("再取込:", take([("A001", 1000)]))
try:
    take([("A002", 2000), ("A001", 1100)])
except ValueError as e:
    print(e)
print("保存済み:", db.execute("SELECT * FROM orders ORDER BY id").fetchall())
db.close()

出力は次のとおりでした。

初回: 1
再取込: 0
A001: 金額が既存行と違います
保存済み: [('A001', 1000)]

3回目はA002を挿入した後、A001の金額違いを見つけました。最後の一覧にA002が残っていないところまで確認します。

僕が最初に見たのは、エラーが出るかだけでした。先に入った行まで確かめないと、半分だけ登録された取込を見逃します。

例外を受け取る位置で、保存結果が変わる

autocommit=Falseでトランザクションを使い、with db:を正常に抜ければ確定、例外で抜ければ取り消します。実行環境の一次資料も確認しました。help(sqlite3.Connection.__exit__)に、この確定と取り消しの動作が記載されています。

exceptはwith db:の外側です。内側で例外を握りつぶし、正常終了させると、途中まで挿入した行が確定し得ます。エラーを表示する位置は好みの問題ではありません。

空のIDも別途弾きたいところです。NOT NULLで拒めるのはNoneに対応するNULLで、空文字は通ります。取込元で空白しか入っていないIDを許すかは、先に決める必要があります。

この例が照合するのは金額だけです。納品先や品目も扱うなら、それらも比較対象に足します。明細が複数ある受注では、受注IDと明細番号の組をキーにします。会社名をキーにすると、別の受注まで重複扱いになります。

実データは接続先を:memory:からファイル名へ変え、再実行時も同じDBを開きます。既存行の訂正手順は別途必要です。

自動取込を任せる範囲が広がるほど、失敗後に同じデータを流す機会も増えると見ています。成功件数だけでは、その再実行を評価できません。新規、同内容の再送、内容が変わった再送で、残る行をそれぞれ確かめられます。手元の取込処理にもこの3パターンを入れ、エラーの後に保存された行まで確認してください。

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?