frappe-errors-database について
Frappe 開発者向けのデータベースエラー診断スキルです。一般的な例外の原因と対処法に加え、トランザクション、クエリ、安全なデータベース操作の指針をまとめています。
主な機能
- 01データベース例外の原因と対処法の対応表
- 02エラー処理パターンの例
- 03コミットとセーブポイントの規則
- 04パラメーター化 SQL とインジェクション対策
- 05クエリ、接続、並行処理の問題整理
活用シーン
- 01Frappe のデータベース例外の診断
- 02安全な frappe.db.sql() クエリの作成
- 03バッチ処理でのトランザクションとデッドロック対応
- 04レコード欠落や同時編集の確認
インストール
コマンドをコピーしてターミナルで実行します。
$
npx skills add https:/ / github.com/ impertio-studio/ frappe_claude_skill_package --skill frappe-errors-databaseインストール後、Claude Code で/frappe-errors-databaseを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/anti-patterns.mdexamples.mdpatterns.md詳細
Frappe データベースエラーの診断と解決
相互参照:frappe-core-database(API 構文)、frappe-errors-controllers(コントローラーのエラー)。
エラーと修正方法の対応表
| エラー/例外 | HTTP | 原因 | 修正方法 |
|---|---|---|---|
DuplicateEntryError | 409 | 挿入/名前変更時の一意制約違反 | 先に存在を確認するか、例外を捕捉して既存のレコードを返します |
DoesNotExistError | 404 | レコードが存在しない状態での get_doc() | 先に frappe.db.exists() を使用するか、例外を捕捉します |
LinkValidationError | 417 | リンクフィールドが存在しないレコードを指している | 保存前にリンク先の存在を検証します |
LinkExistsError | N/A | リンクされたドキュメントがあるため削除できない | リンクされたドキュメントをユーザーに表示します。force=True は慎重に使用します |
MandatoryError | 417 | 保存時に必須フィールドが空 | 挿入/保存前にすべての必須フィールドを設定します |
TimestampMismatchError | N/A | 同時編集を検出した(modified が変更された) | ドキュメントを再読み込みして再試行するか、ユーザーに更新を促します |
CharacterLengthExceededError | 417 | 文字列がフィールドの最大長/DB 列サイズを超えている | 入力を切り詰めるか、フィールド長を増やします |
DataTooLongException | 417 | 値が DB 列の格納容量を超えている | CharacterLengthExceededError と同様です |
InReadOnlyMode | 503 | 読み取り専用モード中に書き込みを試みた | frappe.flags.in_import またはサイト設定を確認します |
QueryTimeoutError | N/A | クエリが制限時間を超えた[v15+] | インデックスを追加し、結果セットを縮小して、ページネーションを使用します |
QueryDeadlockError | N/A | 2 つのトランザクションが互いの処理を待機している | バックオフを入れて再試行し、トランザクションの範囲を縮小します |
TooManyWritesError | N/A | 1 回のリクエストで過剰な書き込みが発生した | 操作をバッチ処理し、バックグラウンドジョブを使用します |
InternalError(gone away) | N/A | MariaDB 接続が切断された | frappe.db.connect() で再接続します |
InternalError(too many) | N/A | 接続プールが枯渇した | max_connections を確認し、アイドル状態の接続を閉じます |
ValidationError | 417 | 保存時の一般的な検証エラー | エラーメッセージを確認し、フィールド値を修正します |
| SQL 構文エラー | N/A | frappe.db.sql() のパラメーター形式が誤っている | 辞書と %(name)s を使用します。タプルと %s は使用しません |
例外の階層
Exception
├── frappe.ValidationError (HTTP 417)
│ ├── frappe.MandatoryError
│ ├── frappe.LinkValidationError
│ ├── frappe.CharacterLengthExceededError
│ ├── frappe.DataTooLongException
│ ├── frappe.UniqueValidationError
│ ├── frappe.UpdateAfterSubmitError
│ └── frappe.DataError
├── frappe.DoesNotExistError (HTTP 404)
├── frappe.DuplicateEntryError (HTTP 409) ← inherits NameError
├── frappe.TimestampMismatchError
├── frappe.LinkExistsError
├── frappe.QueryTimeoutError
├── frappe.QueryDeadlockError
├── frappe.TooManyWritesError
├── frappe.InReadOnlyMode (HTTP 503)
└── frappe.db.InternalError ← MariaDB/Postgres driver error
frappe.db.sql() のパラメーター形式
# ❌ WRONG — %s with positional tuple (works but fragile)
frappe.db.sql("SELECT * FROM `tabItem` WHERE name = %s", ("ITEM-001",))
# ❌ WRONG — f-string or .format() — SQL INJECTION!
frappe.db.sql(f"SELECT * FROM `tabItem` WHERE name = '{item_name}'")
frappe.db.sql("SELECT * FROM `tabItem` WHERE name = '{}'".format(item_name))
# ❌ WRONG — bare % operator
frappe.db.sql("SELECT * FROM `tabItem` WHERE name = '%s'" % item_name)
# ✅ CORRECT — named parameters with dict (ALWAYS use this)
frappe.db.sql(
"SELECT * FROM `tabItem` WHERE name = %(name)s AND warehouse = %(wh)s",
{"name": item_name, "wh": warehouse},
as_dict=True
)
# ✅ CORRECT — frappe.qb (query builder, no injection risk)
Item = frappe.qb.DocType("Item")
result = (
frappe.qb.from_(Item)
.select(Item.name, Item.item_name)
.where(Item.warehouse == warehouse)
.run(as_dict=True)
)
ルール:パラメーターには必ず辞書と %(name)s を使用してください。SQL 値の文字列フォーマットは絶対に使用しないでください。
get_value が None を返す場合:例外ではありません
# ❌ DANGEROUS — get_value returns None, not raises
credit = frappe.db.get_value("Customer", "CUST-001", "credit_limit")
if credit > 1000: # TypeError: '>' not supported between NoneType and int
pass
# ✅ CORRECT — handle None explicitly
credit = frappe.db.get_value("Customer", "CUST-001", "credit_limit")
if credit is None:
frappe.throw(_("Customer not found"))
credit = credit or 0 # Default to 0 if field is empty
# ✅ CORRECT — get_value with as_dict for multiple fields
data = frappe.db.get_value("Customer", "CUST-001",
["credit_limit", "disabled"], as_dict=True)
if not data: # None when record not found
frappe.throw(_("Customer not found"))
if data.disabled:
frappe.throw(_("Customer is disabled"))
メソッドごとの主な動作:
| メソッド | レコードが見つからない場合 | フィールドが空の場合 |
|---|---|---|
get_doc() | DoesNotExistError を発生させる | フィールドのデフォルト値を返す |
get_value() | None を返す | None または "" を返す |
get_all() | [] を返す | 結果に含まれる |
exists() | False を返す | N/A |
set_value() | 何もせずに終了する | N/A |
db.sql() | [] または () を返す | 結果に含まれる |
各例外タイプの処理
DuplicateEntryError
# Pattern: Insert with duplicate handling
def create_or_get(doctype, data):
try:
doc = frappe.get_doc({"doctype": doctype, **data})
doc.insert()
return doc
except frappe.DuplicateEntryError:
# Race condition safe: someone else created it
name = frappe.db.get_value(doctype, data, "name")
return frappe.get_doc(doctype, name)
TimestampMismatchError
# Pattern: Concurrent edit detection
try:
doc = frappe.get_doc("Sales Invoice", name)
doc.update(updates)
doc.save()
except frappe.TimestampMismatchError:
frappe.throw(
_("Document modified by another user. Please refresh and try again."),
title=_("Concurrent Edit")
)
LinkValidationError と MandatoryError
# Pattern: Pre-validate before save
def safe_create_invoice(data):
errors = []
# Check mandatory fields
if not data.get("customer"):
errors.append(_("Customer is required"))
if not data.get("items"):
errors.append(_("At least one item is required"))
# Check link validity
if data.get("customer"):
if not frappe.db.exists("Customer", data["customer"]):
errors.append(_("Customer '{0}' not found").format(data["customer"]))
if errors:
frappe.throw("<br>".join(errors))
doc = frappe.get_doc({"doctype": "Sales Invoice", **data})
doc.insert()
return doc
CharacterLengthExceededError
# Pattern: Truncate before save
def safe_set_description(doc, description):
max_len = 140 # Match field length in DocType
if len(description) > max_len:
description = description[:max_len - 3] + "..."
frappe.msgprint(_("Description truncated to {0} characters").format(max_len))
doc.description = description
QueryTimeoutError[v15+]
# Pattern: Paginated query to avoid timeout
def get_large_report(filters):
try:
return frappe.db.sql(query, filters, as_dict=True)
except frappe.QueryTimeoutError:
frappe.log_error(frappe.get_traceback(), "Report Query Timeout")
frappe.throw(
_("Report too large. Please narrow your date range or add filters."),
title=_("Query Timeout")
)
InReadOnlyMode
# Pattern: Check before write
def safe_write(doctype, name, field, value):
if frappe.flags.in_import:
frappe.db.set_value(doctype, name, field, value)
return
try:
frappe.db.set_value(doctype, name, field, value)
except frappe.InReadOnlyMode:
frappe.log_error(f"Write blocked: {doctype}/{name}", "Read-Only Mode")
frappe.throw(_("System is in read-only mode. Please try again later."))
トランザクションのデッドロック
# ❌ CAUSES DEADLOCKS — long transaction with many writes
def process_all():
for inv in frappe.get_all("Sales Invoice", limit=10000):
doc = frappe.get_doc("Sales Invoice", inv.name)
doc.custom_field = "value"
doc.save() # Each save locks rows; other processes wait
# ✅ CORRECT — batch with commits to release locks
def process_all():
invoices = frappe.get_all("Sales Invoice", limit=10000)
BATCH = 100
for i in range(0, len(invoices), BATCH):
for inv in invoices[i:i + BATCH]:
frappe.db.set_value("Sales Invoice", inv.name, "custom_field", "value")
frappe.db.commit() # Release locks after each batch
# ✅ CORRECT — retry on deadlock
import time
def with_deadlock_retry(func, max_retries=3):
for attempt in range(max_retries):
try:
return func()
except frappe.QueryDeadlockError:
if attempt < max_retries - 1:
frappe.db.rollback()
time.sleep(0.5 * (attempt + 1))
else:
raise
MariaDB の接続切断/接続数超過
# Pattern: Connection recovery
def reliable_operation():
try:
return frappe.db.sql("SELECT 1")
except frappe.db.InternalError as e:
msg = str(e).lower()
if "gone away" in msg or "lost connection" in msg:
frappe.db.connect() # Reconnect
return frappe.db.sql("SELECT 1")
if "too many connections" in msg:
frappe.log_error("Too many DB connections", "Connection Pool")
frappe.throw(_("Server busy. Please try again in a moment."))
raise # Unknown InternalError — re-raise
予防策:
- MariaDB の設定で
wait_timeoutを設定します(デフォルトは 28800s) max_connectionsの設定がワークロードに適していることを確認します- 本番環境では接続プーリングを使用します(Gunicorn ワーカー)
トランザクションのルール
コミットするタイミング
| コンテキスト | 自動コミット? | 手動コミット? |
|---|---|---|
| Web リクエスト(POST/PUT) | はい | いいえ |
| コントローラーフック(validate、on_update) | はい | いいえ |
| doc_events フック | はい | いいえ |
| スケジューラータスク | いいえ | 必ず実行 |
| バックグラウンドジョブ(frappe.enqueue) | いいえ | 必ず実行 |
| bench execute | いいえ | 必ず実行 |
部分ロールバック用のセーブポイント
def complex_operation():
frappe.db.savepoint("before_risky")
try:
risky_database_operation()
except Exception:
frappe.db.rollback(save_point="before_risky")
safe_alternative() # Continue with fallback
# Transaction hooks [v15+]
frappe.db.after_commit.add(lambda: send_notification())
frappe.db.after_rollback.add(lambda: cleanup_files())
SQL インジェクションの防止
# ❌ INJECTION VULNERABLE — all of these
frappe.db.sql(f"SELECT * FROM `tabItem` WHERE name = '{user_input}'")
frappe.db.sql("SELECT * FROM `tabItem` WHERE name = '%s'" % user_input)
frappe.db.sql("SELECT * FROM `tabItem` WHERE name = '{}'".format(user_input))
# ❌ ALSO VULNERABLE — in permission_query_conditions
def query_conditions(user):
return f"owner = '{user}'" # Unescaped!
# ✅ SAFE — parameterized query
frappe.db.sql("SELECT * FROM `tabItem` WHERE name = %(name)s", {"name": user_input})
# ✅ SAFE — frappe.db.escape() for dynamic SQL (permission hooks)
def query_conditions(user):
return f"owner = {frappe.db.escape(user)}"
# ✅ SAFE — query builder
Item = frappe.qb.DocType("Item")
frappe.qb.from_(Item).where(Item.name == user_input).run()
# ✅ SAFE — ORM methods
frappe.get_all("Item", filters={"name": user_input})
frappe.db.get_value("Item", user_input, "item_name")
db.set_value のサイレントな失敗
# ❌ DANGEROUS — no error if record doesn't exist
frappe.db.set_value("Customer", "NONEXISTENT", "status", "Active")
# Returns without error! No rows updated.
# ✅ ALWAYS verify existence before set_value
if not frappe.db.exists("Customer", customer_name):
frappe.throw(_("Customer '{0}' not found").format(customer_name))
frappe.db.set_value("Customer", customer_name, "status", "Active")
# Note: set_value skips validate/on_update hooks
# Use doc.save() when you need validation to run
重要なルール
必ず行うこと
frappe.db.sql()では%(name)sの辞書パラメーターを使用します。文字列フォーマットは絶対に使用しないでくださいget_doc()の前にfrappe.db.exists()で確認するか、DoesNotExistErrorを捕捉します- すべての
insert()呼び出しでDuplicateEntryErrorを処理します - API 内のすべての
save()でTimestampMismatchErrorを処理します - スケジューラーとバックグラウンドジョブでは
frappe.db.commit()を呼び出します - 大量のクエリはページネーションします。
limitパラメーターを使用します - 使用する前に、
get_value()の結果がNoneでないことを確認します - 動的な SQL 文字列では
frappe.db.escape()を使用します
絶対に行わないこと
- SQL の値に文字列フォーマット(
f""、.format()、%)を使用する - コントローラーフックや
doc_events内でfrappe.db.commit()を呼び出す Exceptionを無条件に捕捉してpassする。ログを記録するか、具体的な例外を再送出しますdb.set_value()が成功したと決めつける。レコードが存在しない場合、エラーなしで失敗します- データベースの生のエラーメッセージをユーザーに公開する。詳細はログに記録し、一般的なメッセージを表示します
limitなしで上限のないクエリを実行する。メモリ不足やタイムアウトのリスクがあります
クイックリファレンス:例外処理
try:
doc = frappe.get_doc("Customer", name)
except frappe.DoesNotExistError:
frappe.throw(_("Not found"))
try:
doc.insert()
except frappe.DuplicateEntryError:
existing = frappe.db.get_value("Customer", filters, "name")
except frappe.MandatoryError as e:
frappe.throw(_("Missing required field: {0}").format(e))
try:
doc.save()
except frappe.TimestampMismatchError:
frappe.throw(_("Document modified. Please refresh."))
except frappe.CharacterLengthExceededError:
frappe.throw(_("Text too long for field"))
try:
frappe.delete_doc("Customer", name)
except frappe.LinkExistsError:
frappe.throw(_("Cannot delete — linked documents exist"))
try:
frappe.db.sql(query, values)
except frappe.QueryTimeoutError: # [v15+]
frappe.throw(_("Query too slow. Add filters."))
except frappe.QueryDeadlockError:
frappe.db.rollback() # Retry with backoff
except frappe.db.InternalError as e:
frappe.log_error(frappe.get_traceback(), "DB Error")
参考ファイル
| ファイル | 内容 |
|---|---|
references/patterns.md | すべての DB 操作に関する完全なエラー処理パターン |
references/examples.md | エラー処理を含む、完全に動作するサンプル |
references/anti-patterns.md | よくある誤りと、誤った例・正しい例の組み合わせ |
関連項目
frappe-core-database— データベース API の構文とクエリビルダーfrappe-errors-controllers— コントローラーのエラー処理frappe-errors-hooks— フックのエラー処理frappe-core-permissions— 権限のパターン
コメント
Markdown 対応