erpnext-errors-database
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseERPNext Database - Error Handling
ERPNext 数据库 - 错误处理
This skill covers error handling patterns for database operations. For syntax, see .
erpnext-databaseVersion: v14/v15/v16 compatible
本技能涵盖数据库操作的错误处理模式。语法相关内容请参考。
erpnext-database版本:兼容v14/v15/v16
Database Exception Types
数据库异常类型
┌─────────────────────────────────────────────────────────────────────┐
│ FRAPPE DATABASE EXCEPTIONS │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ frappe.DoesNotExistError │
│ └─► Document not found (get_doc, get_value with strict) │
│ │
│ frappe.DuplicateEntryError │
│ └─► Unique constraint violation (insert, rename) │
│ │
│ frappe.LinkExistsError │
│ └─► Cannot delete - linked documents exist │
│ │
│ frappe.ValidationError │
│ └─► General validation failure │
│ │
│ frappe.TimestampMismatchError │
│ └─► Concurrent edit detected (modified since load) │
│ │
│ frappe.db.InternalError │
│ └─► Database-level error (deadlock, connection lost) │
│ │
│ frappe.QueryTimeoutError (v15+) │
│ └─► Query exceeded timeout limit │
│ │
└─────────────────────────────────────────────────────────────────────┘┌─────────────────────────────────────────────────────────────────────┐
│ FRAPPE DATABASE EXCEPTIONS │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ frappe.DoesNotExistError │
│ └─► 文档未找到(使用get_doc、带strict参数的get_value时触发) │
│ │
│ frappe.DuplicateEntryError │
│ └─► 唯一约束冲突(插入、重命名操作时触发) │
│ │
│ frappe.LinkExistsError │
│ └─► 无法删除 - 存在关联文档 │
│ │
│ frappe.ValidationError │
│ └─► 通用验证失败 │
│ │
│ frappe.TimestampMismatchError │
│ └─► 检测到并发编辑(加载后已被修改) │
│ │
│ frappe.db.InternalError │
│ └─► 数据库级错误(死锁、连接丢失) │
│ │
│ frappe.QueryTimeoutError (v15+) │
│ └─► 查询超出超时限制 │
│ │
└─────────────────────────────────────────────────────────────────────┘Main Decision: Error Handling by Operation
核心决策:按操作类型处理错误
┌─────────────────────────────────────────────────────────────────────────┐
│ WHAT DATABASE OPERATION? │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ► frappe.get_doc() / frappe.get_cached_doc() │
│ └─► Can raise DoesNotExistError │
│ └─► Check with frappe.db.exists() first OR catch exception │
│ │
│ ► doc.insert() / frappe.new_doc().insert() │
│ └─► Can raise DuplicateEntryError (unique constraints) │
│ └─► Can raise ValidationError (mandatory fields, custom validation) │
│ │
│ ► doc.save() │
│ └─► Can raise ValidationError │
│ └─► Can raise TimestampMismatchError (concurrent edit) │
│ │
│ ► doc.delete() / frappe.delete_doc() │
│ └─► Can raise LinkExistsError (linked documents) │
│ └─► Use force=True to ignore links (careful!) │
│ │
│ ► frappe.db.sql() / frappe.qb │
│ └─► Can raise InternalError (syntax, deadlock, connection) │
│ └─► Always use parameterized queries │
│ │
│ ► frappe.db.set_value() / doc.db_set() │
│ └─► Silently fails if record doesn't exist │
│ └─► No validation triggered │
│ │
└─────────────────────────────────────────────────────────────────────────┘┌─────────────────────────────────────────────────────────────────────────┐
│ 执行的数据库操作类型? │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ► frappe.get_doc() / frappe.get_cached_doc() │
│ └─► 可能抛出DoesNotExistError │
│ └─► 先使用frappe.db.exists()检查 或 捕获异常 │
│ │
│ ► doc.insert() / frappe.new_doc().insert() │
│ └─► 可能抛出DuplicateEntryError(违反唯一约束) │
│ └─► 可能抛出ValidationError(必填字段缺失、自定义验证失败) │
│ │
│ ► doc.save() │
│ └─► 可能抛出ValidationError │
│ └─► 可能抛出TimestampMismatchError(并发编辑) │
│ │
│ ► doc.delete() / frappe.delete_doc() │
│ └─► 可能抛出LinkExistsError(存在关联文档) │
│ └─► 使用force=True参数忽略关联(需谨慎使用!) │
│ │
│ ► frappe.db.sql() / frappe.qb │
│ └─► 可能抛出InternalError(语法错误、死锁、连接问题) │
│ └─► 始终使用参数化查询 │
│ │
│ ► frappe.db.set_value() / doc.db_set() │
│ └─► 若记录不存在则静默失败 │
│ └─► 不会触发任何验证 │
│ │
└─────────────────────────────────────────────────────────────────────────┘Error Handling Patterns
错误处理模式
Pattern 1: Safe Document Fetch
模式1:安全获取文档
python
undefinedpython
undefinedOption A: Check first (preferred for expected missing docs)
选项A:先检查(适用于预期可能缺失的文档)
if frappe.db.exists("Customer", customer_name):
customer = frappe.get_doc("Customer", customer_name)
else:
frappe.throw(_("Customer '{0}' not found").format(customer_name))
if frappe.db.exists("Customer", customer_name):
customer = frappe.get_doc("Customer", customer_name)
else:
frappe.throw(_("客户 '{0}' 不存在").format(customer_name))
Option B: Try/except (preferred when doc usually exists)
选项B:Try/Except捕获(适用于文档通常存在的场景)
try:
customer = frappe.get_doc("Customer", customer_name)
except frappe.DoesNotExistError:
frappe.throw(_("Customer '{0}' not found").format(customer_name))
try:
customer = frappe.get_doc("Customer", customer_name)
except frappe.DoesNotExistError:
frappe.throw(_("客户 '{0}' 不存在").format(customer_name))
Option C: Get with default (for optional lookups)
选项C:带默认值获取(适用于可选查询)
customer = frappe.db.get_value("Customer", customer_name, "*", as_dict=True)
if not customer:
# Handle missing - no error raised
customer = {"customer_name": "Unknown", "credit_limit": 0}
undefinedcustomer = frappe.db.get_value("Customer", customer_name, "*", as_dict=True)
if not customer:
# 处理缺失情况 - 不会抛出错误
customer = {"customer_name": "未知", "credit_limit": 0}
undefinedPattern 2: Safe Document Insert
模式2:安全插入文档
python
def create_customer(data):
"""Create customer with duplicate handling."""
try:
doc = frappe.get_doc({
"doctype": "Customer",
"customer_name": data.get("name"),
"customer_type": data.get("type", "Company")
})
doc.insert()
return {"success": True, "name": doc.name}
except frappe.DuplicateEntryError:
# Already exists - return existing
existing = frappe.db.get_value("Customer", {"customer_name": data.get("name")})
return {"success": True, "name": existing, "existing": True}
except frappe.ValidationError as e:
return {"success": False, "error": str(e)}python
def create_customer(data):
"""创建客户并处理重复情况。"""
try:
doc = frappe.get_doc({
"doctype": "Customer",
"customer_name": data.get("name"),
"customer_type": data.get("type", "Company")
})
doc.insert()
return {"success": True, "name": doc.name}
except frappe.DuplicateEntryError:
# 已存在 - 返回现有记录
existing = frappe.db.get_value("Customer", {"customer_name": data.get("name")})
return {"success": True, "name": existing, "existing": True}
except frappe.ValidationError as e:
return {"success": False, "error": str(e)}Pattern 3: Safe Document Delete
模式3:安全删除文档
python
def delete_customer(customer_name):
"""Delete customer with link handling."""
if not frappe.db.exists("Customer", customer_name):
frappe.throw(_("Customer '{0}' not found").format(customer_name))
try:
frappe.delete_doc("Customer", customer_name)
return {"success": True}
except frappe.LinkExistsError as e:
# Get linked documents for user info
linked = get_linked_documents("Customer", customer_name)
frappe.throw(
_("Cannot delete customer. Linked documents exist:<br>{0}").format(
"<br>".join([f"• {l['doctype']}: {l['name']}" for l in linked[:10]])
)
)python
def delete_customer(customer_name):
"""删除客户并处理关联文档情况。"""
if not frappe.db.exists("Customer", customer_name):
frappe.throw(_("客户 '{0}' 不存在").format(customer_name))
try:
frappe.delete_doc("Customer", customer_name)
return {"success": True}
except frappe.LinkExistsError as e:
# 获取关联文档信息供用户查看
linked = get_linked_documents("Customer", customer_name)
frappe.throw(
_("无法删除客户。存在以下关联文档:<br>{0}").format(
"<br>".join([f"• {l['doctype']}: {l['name']}" for l in linked[:10]])
)
)Pattern 4: Concurrent Edit Handling
模式4:并发编辑处理
python
def update_document(doctype, name, updates):
"""Update with concurrent edit detection."""
try:
doc = frappe.get_doc(doctype, name)
doc.update(updates)
doc.save()
return {"success": True}
except frappe.TimestampMismatchError:
# Document was modified by another user
frappe.throw(
_("This document was modified by another user. Please refresh and try again."),
title=_("Concurrent Edit Detected")
)
except frappe.DoesNotExistError:
frappe.throw(_("Document not found"))python
def update_document(doctype, name, updates):
"""更新文档并检测并发编辑。"""
try:
doc = frappe.get_doc(doctype, name)
doc.update(updates)
doc.save()
return {"success": True}
except frappe.TimestampMismatchError:
# 文档已被其他用户修改
frappe.throw(
_("该文档已被其他用户修改,请刷新后重试。"),
title=_("检测到并发编辑")
)
except frappe.DoesNotExistError:
frappe.throw(_("文档未找到"))Pattern 5: Batch Operations with Error Isolation
模式5:批量操作的错误隔离
python
def bulk_update_items(items_data):
"""Bulk update with per-item error handling."""
results = {"success": [], "failed": []}
for item_data in items_data:
item_code = item_data.get("item_code")
try:
if not frappe.db.exists("Item", item_code):
results["failed"].append({
"item": item_code,
"error": "Item not found"
})
continue
doc = frappe.get_doc("Item", item_code)
doc.update(item_data)
doc.save()
results["success"].append(item_code)
except frappe.ValidationError as e:
results["failed"].append({
"item": item_code,
"error": str(e)
})
except Exception as e:
frappe.log_error(frappe.get_traceback(), f"Bulk update error: {item_code}")
results["failed"].append({
"item": item_code,
"error": "Unexpected error"
})
return resultspython
def bulk_update_items(items_data):
"""批量更新并处理单条记录的错误。"""
results = {"success": [], "failed": []}
for item_data in items_data:
item_code = item_data.get("item_code")
try:
if not frappe.db.exists("Item", item_code):
results["failed"].append({
"item": item_code,
"error": "商品不存在"
})
continue
doc = frappe.get_doc("Item", item_code)
doc.update(item_data)
doc.save()
results["success"].append(item_code)
except frappe.ValidationError as e:
results["failed"].append({
"item": item_code,
"error": str(e)
})
except Exception as e:
frappe.log_error(frappe.get_traceback(), f"批量更新错误: {item_code}")
results["failed"].append({
"item": item_code,
"error": "意外错误"
})
return resultsPattern 6: Safe SQL Query
模式6:安全SQL查询
python
def get_sales_report(customer, from_date, to_date):
"""Safe SQL query with error handling."""
try:
# ALWAYS use parameterized queries
result = frappe.db.sql("""
SELECT
customer,
SUM(grand_total) as total,
COUNT(*) as count
FROM `tabSales Invoice`
WHERE customer = %(customer)s
AND posting_date BETWEEN %(from_date)s AND %(to_date)s
AND docstatus = 1
GROUP BY customer
""", {
"customer": customer,
"from_date": from_date,
"to_date": to_date
}, as_dict=True)
return result[0] if result else {"total": 0, "count": 0}
except frappe.db.InternalError as e:
frappe.log_error(frappe.get_traceback(), "Sales Report Query Error")
frappe.throw(_("Database error. Please try again or contact support."))See:for more error handling patterns.references/patterns.md
python
def get_sales_report(customer, from_date, to_date):
"""带错误处理的安全SQL查询。"""
try:
# 始终使用参数化查询
result = frappe.db.sql("""
SELECT
customer,
SUM(grand_total) as total,
COUNT(*) as count
FROM `tabSales Invoice`
WHERE customer = %(customer)s
AND posting_date BETWEEN %(from_date)s AND %(to_date)s
AND docstatus = 1
GROUP BY customer
""", {
"customer": customer,
"from_date": from_date,
"to_date": to_date
}, as_dict=True)
return result[0] if result else {"total": 0, "count": 0}
except frappe.db.InternalError as e:
frappe.log_error(frappe.get_traceback(), "销售报表查询错误")
frappe.throw(_("数据库错误,请重试或联系支持人员。"))参考:更多错误处理模式请查看。references/patterns.md
Transaction Handling
事务处理
Automatic Transaction Management
自动事务管理
python
undefinedpython
undefinedFrappe wraps each request in a transaction
Frappe会为每个请求自动包裹事务
On success: auto-commit
成功时:自动提交
On exception: auto-rollback
发生异常时:自动回滚
def validate(self):
# All changes are in ONE transaction
self.calculate_totals()
frappe.db.set_value("Counter", "main", "count", 100)
if error_condition:
frappe.throw("Error") # EVERYTHING rolls backundefineddef validate(self):
# 所有修改都在同一个事务中
self.calculate_totals()
frappe.db.set_value("Counter", "main", "count", 100)
if error_condition:
frappe.throw("错误") # 所有操作都会回滚undefinedManual Savepoints (Advanced)
手动保存点(高级用法)
python
def complex_operation():
"""Use savepoints for partial rollback."""
# Create savepoint
frappe.db.savepoint("before_risky_op")
try:
risky_database_operation()
except Exception:
# Rollback only to savepoint
frappe.db.rollback(save_point="before_risky_op")
frappe.log_error(frappe.get_traceback(), "Risky Op Failed")
# Continue with alternative approach
safe_alternative_operation()python
def complex_operation():
"""使用保存点实现部分回滚。"""
# 创建保存点
frappe.db.savepoint("before_risky_op")
try:
risky_database_operation()
except Exception:
# 仅回滚到保存点
frappe.db.rollback(save_point="before_risky_op")
frappe.log_error(frappe.get_traceback(), "高风险操作失败")
# 继续执行替代方案
safe_alternative_operation()Scheduler/Background Jobs
调度器/后台任务
python
def background_task():
"""Background jobs need explicit commit."""
try:
for record in records:
process_record(record)
# REQUIRED in background jobs
frappe.db.commit()
except Exception:
frappe.db.rollback()
frappe.log_error(frappe.get_traceback(), "Background Task Error")python
def background_task():
"""后台任务需要显式提交。"""
try:
for record in records:
process_record(record)
# 后台任务中必须显式提交
frappe.db.commit()
except Exception:
frappe.db.rollback()
frappe.log_error(frappe.get_traceback(), "后台任务错误")Critical Rules
关键规则
✅ ALWAYS
✅ 必须遵守
- Check existence before get_doc - Or catch DoesNotExistError
- Use parameterized SQL queries - Never string formatting
- Handle DuplicateEntryError on insert - Unique constraints
- Commit in scheduler/background jobs - No auto-commit
- Log database errors with context - Include query/doc info
- Use db.exists() for existence checks - Not try/except get_doc
- 获取文档前先检查存在性 - 或者捕获DoesNotExistError
- 使用参数化SQL查询 - 绝不使用字符串拼接
- 插入时处理DuplicateEntryError - 处理唯一约束冲突
- 调度器/后台任务中显式提交 - 无自动提交机制
- 记录带上下文的数据库错误 - 包含查询/文档信息
- 使用db.exists()检查存在性 - 而非通过try/except捕获get_doc的异常
❌ NEVER
❌ 严禁操作
- Don't use string formatting in SQL - SQL injection risk
- Don't commit in controller hooks - Breaks transaction
- Don't ignore DoesNotExistError silently - Handle or log
- Don't assume db.set_value() succeeded - No error on missing doc
- Don't catch generic Exception for database ops - Catch specific types
- SQL查询中使用字符串拼接 - 存在SQL注入风险
- 在控制器钩子中提交事务 - 会破坏事务逻辑
- 静默忽略DoesNotExistError - 必须处理或记录
- 假设db.set_value()一定成功 - 记录不存在时会静默失败
- 数据库操作中捕获通用Exception - 应捕获特定异常类型
Quick Reference: Exception Handling
快速参考:异常处理
python
undefinedpython
undefinedDoesNotExistError - Document not found
DoesNotExistError - 文档未找到
try:
doc = frappe.get_doc("Customer", name)
except frappe.DoesNotExistError:
frappe.throw(_("Customer not found"))
try:
doc = frappe.get_doc("Customer", name)
except frappe.DoesNotExistError:
frappe.throw(_("客户未找到"))
DuplicateEntryError - Unique constraint violation
DuplicateEntryError - 违反唯一约束
try:
doc.insert()
except frappe.DuplicateEntryError:
# Handle duplicate
try:
doc.insert()
except frappe.DuplicateEntryError:
# 处理重复情况
LinkExistsError - Cannot delete linked document
LinkExistsError - 无法删除存在关联的文档
try:
frappe.delete_doc("Customer", name)
except frappe.LinkExistsError:
frappe.throw(_("Cannot delete - linked documents exist"))
try:
frappe.delete_doc("Customer", name)
except frappe.LinkExistsError:
frappe.throw(_("无法删除 - 存在关联文档"))
TimestampMismatchError - Concurrent edit
TimestampMismatchError - 并发编辑
try:
doc.save()
except frappe.TimestampMismatchError:
frappe.throw(_("Document was modified. Please refresh."))
try:
doc.save()
except frappe.TimestampMismatchError:
frappe.throw(_("文档已被修改,请刷新后重试。"))
InternalError - Database-level error
InternalError - 数据库级错误
try:
frappe.db.sql(query)
except frappe.db.InternalError:
frappe.log_error(frappe.get_traceback(), "Database Error")
frappe.throw(_("Database error occurred"))
---try:
frappe.db.sql(query)
except frappe.db.InternalError:
frappe.log_error(frappe.get_traceback(), "数据库错误")
frappe.throw(_("发生数据库错误"))
---Reference Files
参考文件
| File | Contents |
|---|---|
| Complete error handling patterns |
| Full working examples |
| Common mistakes to avoid |
| 文件 | 内容 |
|---|---|
| 完整的错误处理模式 |
| 完整的可运行示例 |
| 需避免的常见错误 |
See Also
另请参阅
- - Database operations syntax
erpnext-database - - Controller error handling
erpnext-errors-controllers - - Server Script error handling
erpnext-errors-serverscripts - - Permission patterns
erpnext-permissions
- - 数据库操作语法
erpnext-database - - 控制器错误处理
erpnext-errors-controllers - - 服务器脚本错误处理
erpnext-errors-serverscripts - - 权限模式
erpnext-permissions