frappe-core-database について
Frappe 開発者向けのデータベースおよびドキュメント操作リファレンスです。ORM、データベース API、クエリビルダー、キャッシュ、トランザクションを扱い、権限の違いや SQL インジェクション対策も説明します。
主な機能
- 01ドキュメントの CRUD 操作
- 02権限適用・非適用の一覧クエリ
- 03クエリビルダーとパラメーター化 SQL
- 04ドキュメントおよび Redis キャッシュ
- 05トランザクション管理とバージョン差分
活用シーン
- 01Frappe ドキュメントの CRUD 実装
- 02ページ分割・条件付きクエリの作成
- 03結合クエリと集計レポートの構築
- 04SQL インジェクションやトランザクションのリスク確認
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ impertio-studio/ frappe_claude_skill_package --skill frappe-core-databaseインストール後、Claude Code で/frappe-core-databaseを入力して読み込まれたか確認します。
同梱ファイル
SKILL.md と一緒にインストールされる参考資料やスクリプト。
references/anti-patterns.mdcaching-patterns.mdexamples.mdmethods-reference.mdquery-patterns.md詳細
Frappe データベース操作
クイックリファレンス
| 操作 | メソッド | 権限 |
|---|---|---|
| ドキュメントを取得 | frappe.get_doc(doctype, name) | あり |
| キャッシュされたドキュメント | frappe.get_cached_doc(doctype, name) | なし |
| 新しいドキュメント | frappe.new_doc(doctype) | — |
| 挿入 | doc.insert() | あり |
| 保存 | doc.save() | あり |
| ドキュメントを削除 | frappe.delete_doc(doctype, name) | あり |
| リスト(権限あり) | frappe.db.get_list(doctype, ...) | あり |
| リスト(権限なし) | frappe.get_all(doctype, ...) | なし |
| 単一フィールド | frappe.db.get_value(doctype, name, field) | なし |
| 単一 DocType | frappe.db.get_single_value(doctype, field) | なし |
| キャッシュされた値 | frappe.db.get_value(..., cache=True) | なし |
| 直接更新 | frappe.db.set_value(doctype, name, field, val) | なし |
| 直接更新 | doc.db_set(field, value) | なし |
| 存在確認 | frappe.db.exists(doctype, name) | なし |
| 件数 | frappe.db.count(doctype, filters) | なし |
| 行を削除 | frappe.db.delete(doctype, filters) | なし |
| 生 SQL | frappe.db.sql(query, values, as_dict) | なし |
| クエリビルダー | frappe.qb.from_(doctype).select(...) | なし |
「権限」=ありは、ユーザー権限フィルターが自動的に適用されることを意味します。
判断ツリー
What do you need?
│
├─ Create / Update / Delete a document?
│ ├─ With validations + hooks → frappe.get_doc() + .insert()/.save()/.delete()
│ └─ Direct DB (no hooks) → frappe.db.set_value() or doc.db_set()
│
├─ Read a single document?
│ ├─ Need full object with methods → frappe.get_doc()
│ ├─ Read-only, rarely changes → frappe.get_cached_doc()
│ └─ Only need 1-2 fields → frappe.db.get_value()
│
├─ List of documents?
│ ├─ Respect user permissions → frappe.db.get_list()
│ └─ System/admin context → frappe.get_all()
│
├─ Single DocType value?
│ └─ frappe.db.get_single_value('Settings', 'field')
│
├─ Check existence?
│ └─ frappe.db.exists() — NEVER use get_doc in try/except
│
├─ Complex query (JOINs, aggregates)?
│ ├─ Cross-DB compatible → frappe.qb (Query Builder)
│ └─ DB-specific SQL → frappe.db.sql() with parameters
│
└─ DB-specific logic?
└─ frappe.db.multisql({'mariadb': q1, 'postgres': q2})
ルール:常に最も抽象度の高いレベルを使用してください:ORM > データベース API > クエリビルダー > 生 SQL。
ORM:ドキュメント操作
ドキュメントを取得
doc = frappe.get_doc('Sales Invoice', 'SINV-00001')
# Single DocType (no name needed)
settings = frappe.get_doc('System Settings')
# Cached (read-only, for rarely-changing docs)
company = frappe.get_cached_doc('Company', 'My Company')
# Last created
last_task = frappe.get_last_doc('Task', filters={'status': 'Open'})
ドキュメントを作成
doc = frappe.get_doc({
'doctype': 'Task',
'subject': 'Review report',
'status': 'Open'
})
doc.insert()
# Alternative
doc = frappe.new_doc('Task')
doc.subject = 'Review report'
doc.insert()
ドキュメントを更新
# Via ORM — triggers validate, on_update, etc.
doc = frappe.get_doc('Task', 'TASK-001')
doc.status = 'Completed'
doc.save()
# Direct DB — SKIPS all validations and hooks
frappe.db.set_value('Task', 'TASK-001', 'status', 'Completed')
# Direct DB on loaded doc
doc.db_set('status', 'Completed')
doc.db_set('status', 'Completed', update_modified=False)
doc.db_set({'status': 'Completed', 'priority': 'High'})
ドキュメントを削除
frappe.delete_doc('Task', 'TASK-001')
# Also removes linked Communications, Comments, etc.
挿入フラグ
doc.insert(
ignore_permissions=True, # Bypass permission check
ignore_links=True, # Skip link validation
ignore_if_duplicate=True, # No error on duplicate
ignore_mandatory=True # Skip required field check
)
ルール:文書化された理由がない限り、複数の無視フラグを同時に使用しないでください。追加するフラグは、いずれもデータの整合性を弱めます。
データベース API:読み取り
get_value
# Single field → scalar
status = frappe.db.get_value('Task', 'TASK-001', 'status')
# Multiple fields → tuple
subject, status = frappe.db.get_value('Task', 'TASK-001', ['subject', 'status'])
# As dict
data = frappe.db.get_value('Task', 'TASK-001', ['subject', 'status'], as_dict=True)
# With filters instead of name
status = frappe.db.get_value('Task', {'project': 'PROJ-001'}, 'status')
# Cached (for values that rarely change)
country = frappe.db.get_value('Company', 'MyCompany', 'country', cache=True)
get_single_value
timezone = frappe.db.get_single_value('System Settings', 'time_zone')
get_list / get_all
# get_list — applies user permissions
tasks = frappe.db.get_list('Task',
filters={'status': 'Open'},
fields=['name', 'subject', 'assigned_to'],
order_by='creation desc',
start=0,
page_length=50
)
# get_all — NO permission check (same API, different default)
all_tasks = frappe.get_all('Task', filters={'status': 'Open'})
# pluck — returns flat list of single field
names = frappe.get_all('Task', filters={'status': 'Open'}, pluck='name')
# Returns: ['TASK-001', 'TASK-002', ...]
exists / count
exists = frappe.db.exists('User', '[email protected]')
exists = frappe.db.exists('User', {'email': '[email protected]'})
total = frappe.db.count('Task')
open_count = frappe.db.count('Task', {'status': 'Open'})
フィルター演算子
{'status': 'Open'} # =
{'status': ['!=', 'Cancelled']} # !=
{'amount': ['>', 1000]} # >
{'amount': ['>=', 1000]} # >=
{'status': ['in', ['Open', 'Working']]} # IN
{'status': ['not in', ['Cancelled', 'Closed']]} # NOT IN
{'date': ['between', ['2024-01-01', '2024-12-31']]} # BETWEEN
{'subject': ['like', '%urgent%']} # LIKE
{'description': ['is', 'set']} # IS NOT NULL
{'description': ['is', 'not set']} # IS NULL
フィルターの組み合わせ
# AND — all conditions in one dict
filters = {'status': 'Open', 'priority': 'High'}
# AND — list format (allows duplicate fields)
filters = [['status', '=', 'Open'], ['priority', '=', 'High']]
# OR — separate parameter
or_filters = {'priority': 'Urgent', 'status': 'Overdue'}
データベース API:書き込み
set_value
# Single field
frappe.db.set_value('Task', 'TASK-001', 'status', 'Closed')
# Multiple fields
frappe.db.set_value('Task', 'TASK-001', {'status': 'Closed', 'priority': 'Low'})
# Without updating modified timestamp
frappe.db.set_value('Task', 'TASK-001', 'status', 'Closed', update_modified=False)
delete / truncate
# Delete with filters (DML — can be rolled back)
frappe.db.delete('Error Log', {'creation': ['<', '2024-01-01']})
# Truncate (DDL — CANNOT be rolled back)
frappe.db.truncate('Error Log')
bulk_update [v15+]
frappe.db.bulk_update('Task', {
'TASK-001': {'status': 'Closed'},
'TASK-002': {'status': 'Closed'}
}, chunk_size=100)
生 SQL:必ずパラメーター化する
# ✅ CORRECT — parameterized query
results = frappe.db.sql("""
SELECT name, subject FROM `tabTask`
WHERE status = %(status)s AND owner = %(owner)s
""", {'status': 'Open', 'owner': frappe.session.user}, as_dict=True)
重大:SQL 内では、f 文字列、% 書式指定、文字列連結を絶対に使用しないでください。SQL インジェクションの防止を参照してください。
戻り値の型
frappe.db.sql(query) # Tuple of tuples (default)
frappe.db.sql(query, as_dict=True) # List of dicts
frappe.db.sql(query, as_list=True) # List of lists
テーブル名
バッククォートで囲んだ tab プレフィックスを必ず使用してください:`tabSales Invoice`, `tabTask`
データベース固有の SQL
frappe.db.multisql({
'mariadb': "SELECT IFNULL(field, 0) FROM `tabDoc`",
'postgres': "SELECT COALESCE(field, 0) FROM `tabDoc`"
})
クエリビルダー(frappe.qb)[v14+]
クエリビルダーは内部で PyPika を使用します。パラメーター化された SQL を自動的に生成します。
Task = frappe.qb.DocType('Task')
results = (
frappe.qb.from_(Task)
.select(Task.name, Task.subject, Task.status)
.where(Task.status == 'Open')
.orderby(Task.creation, order='desc')
.limit(10)
).run(as_dict=True)
JOIN
SI = frappe.qb.DocType('Sales Invoice')
Customer = frappe.qb.DocType('Customer')
results = (
frappe.qb.from_(SI)
.inner_join(Customer).on(SI.customer == Customer.name)
.select(SI.name, SI.grand_total, Customer.customer_name)
.where(SI.docstatus == 1)
).run(as_dict=True)
集計
from frappe.query_builder.functions import Count, Sum, Avg
stats = (
frappe.qb.from_(Task)
.select(Task.status, Count(Task.name).as_('count'))
.groupby(Task.status)
).run(as_dict=True)
OR 条件
customers = frappe.qb.DocType('Customer')
results = (
frappe.qb.from_(customers)
.select(customers.name)
.where(
(customers.territory == 'US') | (customers.territory == 'UK')
)
).run(as_dict=True)
生成された SQL を確認
query = frappe.qb.from_(Task).select('*').where(Task.name == 'X')
sql, params = query.walk() # Returns (sql_string, param_dict)
sql_str = query.get_sql() # Returns SQL string
サブクエリ、ImportMapper、ConstantColumn、カスタム関数については、
references/query-patterns.mdを参照してください。
キャッシュ
ドキュメントキャッシュ
doc = frappe.get_cached_doc('Company', 'My Company') # Full document
val = frappe.db.get_value('Company', 'X', 'country', cache=True) # Single value
Redis キャッシュ
frappe.cache.set_value('key', data, expires_in_sec=3600)
data = frappe.cache.get_value('key')
frappe.cache.delete_value('key')
@redis_cache デコレーター
from frappe.utils.caching import redis_cache
@redis_cache(ttl=300)
def get_dashboard_data(user):
return expensive_calculation(user)
# Invalidate
get_dashboard_data.clear_cache()
ハッシュ操作、無効化戦略、ベストプラクティスについては、
references/caching-patterns.mdを参照してください。
トランザクション管理
フレームワークがトランザクションを自動的に管理します:
| コンテキスト | コミット | ロールバック |
|---|---|---|
| POST/PUT リクエスト | 成功後 | 捕捉されない例外が発生した場合 |
| GET リクエスト | なし | — |
| バックグラウンドジョブ | 成功後 | 例外発生時 |
| パッチ | 成功後 | 例外発生時 |
手動トランザクション(ほとんどの場合不要)
frappe.db.savepoint('before_payment')
try:
# operations...
frappe.db.commit()
except Exception:
frappe.db.rollback(save_point='before_payment')
トランザクションフック [v15+]
frappe.db.after_commit.add(sync_to_external_system)
frappe.db.after_rollback.add(cleanup_external_state)
SQL インジェクションの防止
重大なセキュリティルール:ユーザー入力を SQL 文字列に絶対に埋め込まないでください。
# ❌ VULNERABLE — SQL injection risk
frappe.db.sql(f"SELECT * FROM `tabUser` WHERE name = '{user_input}'")
frappe.db.sql("SELECT * FROM `tabUser` WHERE name = '%s'" % user_input)
frappe.db.sql("SELECT * FROM `tabUser` WHERE name = " + user_input)
# ✅ SAFE — parameterized query
frappe.db.sql("SELECT * FROM `tabUser` WHERE name = %(name)s", {'name': user_input})
# ✅ SAFE — ORM / Query Builder (always parameterized)
frappe.get_all('User', filters={'name': user_input})
User = frappe.qb.DocType('User')
frappe.qb.from_(User).select('*').where(User.name == user_input).run()
ルール:frappe.db.sql() を使用しなければならない場合は、辞書と %(param)s プレースホルダーを必ず使用してください。新しいコードでは、生 SQL よりクエリビルダー(frappe.qb)を必ず優先してください。
バージョン間の違い
| 機能 | v14 | v15 | v16 |
|---|---|---|---|
| クエリビルダー(frappe.qb) | はい | はい | はい |
| トランザクションフック | いいえ | はい | はい |
bulk_update | いいえ | はい | はい |
run=False の戻り値 | SQL 文字列 | SQL 文字列 | クエリビルダーオブジェクト |
| 集計フィールドの構文 | 文字列 | 文字列 | 辞書 |
v16 の破壊的変更
# v14/v15 — string aggregates
fields=['count(name) as count']
# v16 — dict aggregates
fields=[{'COUNT': 'name', 'as': 'count'}]
# v14/v15 — run=False returns SQL string
sql = frappe.db.get_list('Task', run=False)
# v16 — run=False returns Query Builder object
qb_obj = frappe.db.get_list('Task', run=False)
sql = qb_obj.get_sql()
重要なルールのまとめ
- 絶対に SQL で文字列フォーマットを使わないでください — 必ずパラメーター化クエリを使用してください
- 絶対にコントローラーフック(validate、on_update など)の内部で
frappe.db.commit()を呼び出さないでください - 必ずリストクエリをページネーションしてください —
page_lengthパラメーターを使用してください - 必ずフィールドを指定してください — 本番環境では絶対に
fields=['*']を使用しないでください - 存在確認には必ず
frappe.db.exists()を使用してください — get_doc と try/except を絶対に使用しないでください - 生の SQL ではテーブル名の先頭に
tabを必ず付けてください:`tabSales Invoice` - ドキュメント化された正当な理由がない限り、複数の ignore フラグを絶対に使用しないでください
- N+1 クエリを回避するため、必ず一括取得を使用してください
- 新しいコードでは、
frappe.db.sql()よりもfrappe.qbを必ず優先してください - ロールバックできないことを理解せずに
frappe.db.truncate()を絶対に使用しないでください
クエリビルダー:専用スキル
複雑なクエリ(結合、集計、サブクエリ、データベース間の互換性)については、frappe-syntax-query-builder を参照してください。
- frappe.db メソッド(このスキル) — シンプルな CRUD、get_value、get_list、存在確認
- frappe.qb(query-builder スキル) — 結合、GROUP BY、HAVING、サブクエリ、データベース間の関数
- frappe.db.sql — qb では表現できない非常に複雑な SQL(必ずパラメーター化してください)
参照ファイル
- methods-reference.md — すべてのデータベースメソッドとドキュメントメソッドの API シグネチャー
- query-patterns.md — クエリビルダーのパターン、サブクエリ、ImportMapper、カスタム関数
- caching-patterns.md — Redis キャッシュ、@redis_cache、ハッシュ操作、無効化
- examples.md — 実際のパターン:CRUD、レポート、一括処理、トランザクション
- anti-patterns.md — SQL インジェクション、N+1、コミットの誤り、その他 10 個のアンチパターン