frappe.qb获取查询

[["status", "in", ["Open", "Pending"]]]

not in NOT IN {"role": ["not in", ["Guest"]]} [["role", "not in", ["Guest"]]] is IS NULL IS NOT NULL {"customer": ["is", "set"]} {"email": ["is", "not set"]} [["customer", "is", "set"]] [["email", "is", "not set"]] descendants of {"parent_account": ["descendants of", "Assets"]} [["parent_account", "descendants of", "Assets"]] ancestors of {"location": ["ancestors of", "Room 101"]} [["location", "ancestors of", "Room 101"]] not descendants of {"category": ["not descendants of", "Internal"]} [["category", "not descendants of", "Internal"]] not ancestors of {"territory": ["not ancestors of", "West Coast"]} [["territory", "not ancestors of", "West Coast"]] is set is not set IS NOT NULL IS NULL link_fieldname.target_fieldname child_table_fieldname.target_fieldname distinct=True lft rgt filters 'and' 'or' query .run() as_iterator=True as_iterator=True as_dict=True as_list=True frappe.db.unbuffered_cursor() order_by group_by limit offset distinct=True ignore_permissions frappe.qb.get_query ignore_permissions=True ignore_permissions=False ignore_permissions=False if_owner fields filters group_by order_by link_field.target_field child_field.target_field ignore_permissions=False fields filters frappe.PermissionError group_by frappe.PermissionError order_by frappe.PermissionError link_field.target_field child_field.target_field frappe.qb.get_query ignore_permissions=False __CODEBLOCK_156__ __CODEBLOCK_157__ __CODEBLOCK_158__ __CODEBLOCK_159__ __CODEBLOCK_160__ __CODEBLOCK_161__ __CODEBLOCK_162__ __CODEBLOCK_163__ __CODEBLOCK_164__ __CODEBLOCK_165__ __CODEBLOCK_166__ __CODEBLOCK_167__ __CODEBLOCK_168__ __CODEBLOCK_169__ __CODEBLOCK_170__ __CODEBLOCK_171__ __CODEBLOCK_172__ __CODEBLOCK_173__ __CODEBLOCK_174__ __CODEBLOCK_175__ __CODEBLOCK_176__ __CODEBLOCK_177__ __CODEBLOCK_178__ __CODEBLOCK_179__ __CODEBLOCK_180__ __CODEBLOCK_181__ __CODEBLOCK_182__ __CODEBLOCK_183__ __CODEBLOCK_184__ __CODEBLOCK_185__ __CODEBLOCK_186__ __CODEBLOCK_187__ __CODEBLOCK_188__ __CODEBLOCK_189__ __CODEBLOCK_190__ __CODEBLOCK_191__ __CODEBLOCK_192__ __CODEBLOCK_193__ __CODEBLOCK_194__ __CODEBLOCK_195__ __CODEBLOCK_196__ __CODEBLOCK_197__ __CODEBLOCK_198__ __CODEBLOCK_199__ __CODEBLOCK_200__ __CODEBLOCK_201__ __CODEBLOCK_202__ __CODEBLOCK_203__ __CODEBLOCK_204__ __CODEBLOCK_205__ __CODEBLOCK_206__ __CODEBLOCK_207__ __CODEBLOCK_208__ __CODEBLOCK_209__ __CODEBLOCK_210__ __CODEBLOCK_211__ __CODEBLOCK_212__ __CODEBLOCK_213__ __CODEBLOCK_214__ __CODEBLOCK_215__ __CODEBLOCK_216__ __CODEBLOCK_217__ __CODEBLOCK_218__ __CODEBLOCK_219__ __CODEBLOCK_220__ __CODEBLOCK_221__ __CODEBLOCK_222__ __CODEBLOCK_223__ __CODEBLOCK_224__ __CODEBLOCK_225__ __CODEBLOCK_226__ __CODEBLOCK_227__ __CODEBLOCK_228__ __CODEBLOCK_229__ __CODEBLOCK_230__ __CODEBLOCK_231__ __CODEBLOCK_232__ __CODEBLOCK_233__ __CODEBLOCK_234__ __CODEBLOCK_235__ __CODEBLOCK_236__ __CODEBLOCK_237__ __CODEBLOCK_238__ __CODEBLOCK_239__ __CODEBLOCK_240__ __CODEBLOCK_241__ __CODEBLOCK_242__ __CODEBLOCK_243__ __CODEBLOCK_244__ __CODEBLOCK_245__ __CODEBLOCK_246__ __CODEBLOCK_247__ __CODEBLOCK_248__ __CODEBLOCK_249__ __CODEBLOCK_250__ __CODEBLOCK_251__ __CODEBLOCK_252__ __CODEBLOCK_253__ __CODEBLOCK_254__ __CODEBLOCK_255__ __CODEBLOCK_256__ __CODEBLOCK_257__ __CODEBLOCK_258__ __CODEBLOCK_259__ __CODEBLOCK_260__ __CODEBLOCK_261__ __CODEBLOCK_262__ __CODEBLOCK_263__ __CODEBLOCK_264__ __CODEBLOCK_265__ __CODEBLOCK_266__ __CODEBLOCK_267__ __CODEBLOCK_268__ __CODEBLOCK_269__ __CODEBLOCK_270__ __CODEBLOCK_271__ __CODEBLOCK_272__ __CODEBLOCK_273__ __CODEBLOCK_274__ __CODEBLOCK_275__ __CODEBLOCK_276__ __CODEBLOCK_277__ __CODEBLOCK_278__ __CODEBLOCK_279__ __CODEBLOCK_280__ __CODEBLOCK_281__ __CODEBLOCK_282__ __CODEBLOCK_283__ __CODEBLOCK_284__ __CODEBLOCK_285__ __CODEBLOCK_286__ __CODEBLOCK_287__ __CODEBLOCK_288__ __CODEBLOCK_289__ __CODEBLOCK_290__ __CODEBLOCK_291__ __CODEBLOCK_292__ __CODEBLOCK_293__ __CODEBLOCK_294__ __CODEBLOCK_295__ __CODEBLOCK_296__ __CODEBLOCK_297__ __CODEBLOCK_298__ __CODEBLOCK_299__ __CODEBLOCK_300__ __CODEBLOCK_301__ __CODEBLOCK_302__ __CODEBLOCK_303__ __CODEBLOCK_304__ __CODEBLOCK_305__ __CODEBLOCK_306__ __CODEBLOCK_307__ __CODEBLOCK_308__ __CODEBLOCK_309__ __CODEBLOCK_310__ __CODEBLOCK_311__ __CODEBLOCK_312__ __CODEBLOCK_313__ __CODEBLOCK_314__ __CODEBLOCK_315__ __CODEBLOCK_316__ __CODEBLOCK_317__ __CODEBLOCK_318__ __CODEBLOCK_319__ __CODEBLOCK_320__ __CODEBLOCK_321__ __CODEBLOCK_322__ __CODEBLOCK_323__ __CODEBLOCK_324__ __CODEBLOCK_325__ __CODEBLOCK_326__ __CODEBLOCK_327__ __CODEBLOCK_328__ __CODEBLOCK_329__ __CODEBLOCK_330__ __CODEBLOCK_331__ __CODEBLOCK_332__ __CODEBLOCK_333__ __CODEBLOCK_334__ __CODEBLOCK_335__ __CODEBLOCK_336__ __CODEBLOCK_337__ __CODEBLOCK_338__ __CODEBLOCK_339__ __CODEBLOCK_340__ __CODEBLOCK_341__ __CODEBLOCK_342__ __CODEBLOCK_343__ __CODEBLOCK_344__ __CODEBLOCK_345__ __CODEBLOCK_346__ __CODEBLOCK_347__ __CODEBLOCK_348__ __CODEBLOCK_349__ __CODEBLOCK_350__ __CODEBLOCK_351__ __CODEBLOCK_352__ __CODEBLOCK_353__ __CODEBLOCK_354__ __CODEBLOCK_355__ __CODEBLOCK_356__ __CODEBLOCK_357__ __CODEBLOCK_358__ __CODEBLOCK_359__ __CODEBLOCK_360__ __CODEBLOCK_361__ __CODEBLOCK_362__ __CODEBLOCK_363__ __CODEBLOCK_364__ __CODEBLOCK_365__ __CODEBLOCK_366__ __CODEBLOCK_367__ __CODEBLOCK_368__ __CODEBLOCK_369__ __CODEBLOCK_370__ __CODEBLOCK_371__ __CODEBLOCK_372__ __CODEBLOCK_373__ __CODEBLOCK_374__ __CODEBLOCK_375__ __CODEBLOCK_376__ __CODEBLOCK_377__ __CODEBLOCK_378__ __CODEBLOCK_379__ __CODEBLOCK_380__ __CODEBLOCK_381__ __CODEBLOCK_382__ __CODEBLOCK_383__ __CODEBLOCK_384__ __CODEBLOCK_385__ __CODEBLOCK_386__ __CODEBLOCK_387__ __CODEBLOCK_388__ __CODEBLOCK_389__ __CODEBLOCK_390__ __CODEBLOCK_391__ __CODEBLOCK_392__ __CODEBLOCK_393__ __CODEBLOCK_394__ __CODEBLOCK_395__ __CODEBLOCK_396__ __CODEBLOCK_397__ __CODEBLOCK_398__ __CODEBLOCK_399__ __CODEBLOCK_400__ __CODEBLOCK_401__ __CODEBLOCK_402__ __CODEBLOCK_403__ __CODEBLOCK_404__ __CODEBLOCK_405__ __CODEBLOCK_406__ __CODEBLOCK_407__ __CODEBLOCK_408__ __CODEBLOCK_409__ __CODEBLOCK_410__ __CODEBLOCK_411__ __CODEBLOCK_412__ __CODEBLOCK_413__ __CODEBLOCK_414__ __CODEBLOCK_415__ __CODEBLOCK_416__ __CODEBLOCK_417__ __CODEBLOCK_418__ __CODEBLOCK_419__ __CODEBLOCK_420__ __CODEBLOCK_421__ __CODEBLOCK_422__ __CODEBLOCK_423__ __CODEBLOCK_424__ __CODEBLOCK_425__ __CODEBLOCK_426__ __CODEBLOCK_427__ __CODEBLOCK_428__ __CODEBLOCK_429__ __CODEBLOCK_430__ __CODEBLOCK_431__ __CODEBLOCK_432__ __CODEBLOCK_433__ __CODEBLOCK_434__ __CODEBLOCK_435__ __CODEBLOCK_436__ __CODEBLOCK_437__ __CODEBLOCK_438__ __CODEBLOCK_439__ __CODEBLOCK_440__ __CODEBLOCK_441__ __CODEBLOCK_442__ __CODEBLOCK_443__ __CODEBLOCK_444__ __CODEBLOCK_445__ __CODEBLOCK_446__ __CODEBLOCK_447__ __CODEBLOCK_448__ __CODEBLOCK_449__ __CODEBLOCK_450__ __CODEBLOCK_451__ __CODEBLOCK_452__ __CODEBLOCK_453__ __CODEBLOCK_454__ __CODEBLOCK_455__ __CODEBLOCK_456__ __CODEBLOCK_457__ __CODEBLOCK_458__ __CODEBLOCK_459__ __CODEBLOCK_460__ __CODEBLOCK_461__ __CODEBLOCK_462__ __CODEBLOCK_463__ __CODEBLOCK_464__ __CODEBLOCK_465__ __CODEBLOCK_466__ __CODEBLOCK_467__ __CODEBLOCK_468__ __CODEBLOCK_469__ __CODEBLOCK_470__ __CODEBLOCK_471__ __CODEBLOCK_472__ __CODEBLOCK_473__ __CODEBLOCK_474__ __CODEBLOCK_475__ __CODEBLOCK_476__ __CODEBLOCK_477__ __CODEBLOCK_478__ __CODEBLOCK_479__ __CODEBLOCK_480__ __CODEBLOCK_481__ __CODEBLOCK_482__ __CODEBLOCK_483__ __CODEBLOCK_484__ __CODEBLOCK_485__ __CODEBLOCK_486__ __CODEBLOCK_487__ __CODEBLOCK_488__ __CODEBLOCK_489__ __CODEBLOCK_490__ __CODEBLOCK_491__ __CODEBLOCK_492__ __CODEBLOCK_493__ __CODEBLOCK_494__ __CODEBLOCK_495__ __CODEBLOCK_496__ __CODEBLOCK_497__ __CODEBLOCK_498__ __CODEBLOCK_499__ __CODEBLOCK_500__ __CODEBLOCK_501__ __CODEBLOCK_502__ __CODEBLOCK_503__ __CODEBLOCK_504__ __CODEBLOCK_505__ __CODEBLOCK_506__ __CODEBLOCK_507__ __CODEBLOCK_508__ <td style="text-align:left [["status", "in", ["Open", "Pending"]]] not in NOT IN {"role": ["not in", ["Guest"]]} [["role", "not in", ["Guest"]]] is IS NULLIS NOT NULL {"customer": ["is", "set"]}{"email": ["is", "not set"]} [["customer", "is", "set"]][["email", "is", "not set"]] descendants of (嵌套集) {"parent_account": ["descendants of", "Assets"]} [["parent_account", "descendants of", "Assets"]] ancestors of (嵌套集) {"location": ["ancestors of", "Room 101"]} [["location", "ancestors of", "Room 101"]] not descendants of (嵌套集) {"category": ["not descendants of", "Internal"]} [["category", "not descendants of", "Internal"]] not ancestors of (嵌套集) {"territory": ["not ancestors of", "West Coast"]} [["territory", "not ancestors of", "West Coast"]]

关于 is set / is not set 的说明: 这些用于检查字段是否有值(分别为 IS NOT NULLIS NULL)。

按链接文档字段筛选

您可以使用点号表示法根据链接文档中的字段进行筛选:link_fieldname.target_fieldname

# Get Sales Orders where the linked Customer's territory is 'North America'
query = frappe.qb.get_query(
    "Sales Order",
    fields=["name", "customer"],
    filters={"customer.territory": "North America"} # Filter on linked field
)
north_america_orders = query.run(as_dict=True)

按子表字段筛选

您可以使用点号表示法根据子表记录中的值筛选父记录:child_table_fieldname.target_fieldname

# Get Sales Orders that contain 'Item A' in their items table
# Use distinct=True to ensure each Sales Order appears only once
query = frappe.qb.get_query(
    "Sales Order",
    fields=["name", "customer"],
    filters={"items.item_code": "Item A"}, # Filter based on child table field
    distinct=True
)
orders_with_item_a = query.run(as_dict=True)

重要提示: 当基于子表字段进行筛选时,如果您只需要唯一的父记录,请使用 distinct=True

嵌套集筛选

对于树形结构的 DocType(使用 lftrgt 列,如科目、地区、仓库等),您可以使用特殊筛选器:

# Get all accounts under 'Assets'
query = frappe.qb.get_query(
    "Account",
    fields=["name"],
    filters={"parent_account": ["descendants of", "Assets"]}
)

# Get the parent territories of 'West Coast'
query = frappe.qb.get_query(
    "Territory",
    fields=["name"],
    filters={"parent_territory": ["ancestors of", "West Coast"]}
)

逻辑运算符(AND/OR)

对于复杂条件,请将您的 filters 构建为列表,并使用 'and''or' 组合条件。

# Find users who are enabled AND have first name 'Admin'
filters_and = [
    ["enabled", "=", 1],
    "and",
    ["first_name", "=", "Admin"],
]
query = frappe.qb.get_query("User", filters=filters_and)

# Find users who have first name 'Admin' OR 'Guest'
filters_or = [
    ["first_name", "=", "Admin"],
    "or",
    ["first_name", "=", "Guest"],
]
query = frappe.qb.get_query("User", filters=filters_or)

# Combine AND and OR (use nested lists for grouping)
# Find users who are enabled AND (have first name 'Admin' OR 'Guest')
filters_nested = [
    ["enabled", "=", 1],
    "and",
    [
        ["first_name", "=", "Admin"],
        "or",
        ["first_name", "=", "Guest"],
    ]
]
query = frappe.qb.get_query("User", filters=filters_nested)

查询执行

基本执行

一旦您获得了 query 对象,请使用 .run() 执行它:

# Returns a list of tuples by default
results = query.run()

# Returns a list of dictionaries
results = query.run(as_dict=True)

# Returns a list of lists
results = query.run(as_list=True)

# If selecting a single field, returns a flat list of values
results = query.run(pluck=True)

# Print the generated SQL query and execution time
results = query.run(debug=True)

获取 SQL 字符串

您可以在不执行的情况下获取生成的 SQL 字符串:

# Get the SQL string with values directly substituted (for debugging)
sql_string = query.get_sql()
print(sql_string)
# Example Output: SELECT `name`, `email` FROM `tabUser` WHERE `first_name`='Admin'

对大型数据集使用迭代器

处理大型数据集时,请使用 as_iterator=True 逐行处理结果,而无需将所有内容加载到内存中:

# Process a large number of tasks without loading all into memory
query = frappe.qb.get_query(
    "Task",
    fields=["name", "subject", "status"],
    filters={"status": "Open"}
)

# Use unbuffered_cursor for optimal memory usage with the iterator
with frappe.db.unbuffered_cursor():
    task_iterator = query.run(as_iterator=True, as_dict=True)

    processed_count = 0
    for task in task_iterator:
        # Process each task dictionary one by one
        print(f"Processing Task: {task['name']} - {task['subject']}")
        processed_count += 1
        if processed_count % 1000 == 0:
            print(f"Processed {processed_count} tasks...")

要求:

  • 您必须将 as_iterator=Trueas_dict=Trueas_list=True 一起使用
  • 为获得最佳内存效率,请在 frappe.db.unbuffered_cursor() 上下文管理器中使用

排序、分组和分页

排序结果

使用 order_by 参数对结果进行排序:

# Order users by creation date, ascending
query = frappe.qb.get_query("User", fields=["name", "creation"], order_by="creation asc")

# Order by multiple fields
query = frappe.qb.get_query(
    "Sales Invoice",
    fields=["name", "customer", "grand_total"],
    order_by="customer asc, grand_total desc"
)

分组结果

使用 group_by 进行聚合:

# Count invoices per customer
query = frappe.qb.get_query(
    "Sales Invoice",
    fields=["customer", {"COUNT": "'*'", "as": "invoice_count"}],
    filters={"docstatus": 1},
    group_by="customer"
)
results = query.run(as_dict=True)
# results: [{'customer': 'Cust A', 'invoice_count': 5}, {'customer': 'Cust B', 'invoice_count': 3}, ...]

分页

使用 limitoffset 进行分页:

# Get the first 10 users
query = frappe.qb.get_query("User", limit=10)

# Get the next 10 users (page 2)
query = frappe.qb.get_query("User", limit=10, offset=10)

去重结果

使用 distinct=True 获取唯一行:

# Get distinct customers from submitted Sales Invoices
query = frappe.qb.get_query(
    "Sales Invoice",
    fields=["customer"],
    filters={"docstatus": 1},
    distinct=True
)

权限

ignore_permissions 标志

默认情况下,frappe.qb.get_query 忽略权限(ignore_permissions=True)。要强制执行权限,请设置 ignore_permissions=False

# This query bypasses all permission checks (default behavior)
query_ignore = frappe.qb.get_query("DocType", fields=["name"], filters={"istable": 1})

# This query enforces standard Frappe permissions for the current user
query_enforce = frappe.qb.get_query(
    "DocType",
    fields=["name"],
    filters={"istable": 1},
    ignore_permissions=False # Explicitly enable permission checks
)

try:
    results = query_enforce.run()
except frappe.PermissionError:
    print("User does not have permission to read DocType!")

权限的应用方式

ignore_permissions=False 时:

  1. 角色权限: 根据用户的角色检查其是否具有“读取”或“选择”权限。
  2. 用户权限: 应用为 DocType 和链接的 DocType 定义的用户权限(允许/限制)。
  3. 共享: 包含明确共享给用户的文档。
  4. 所有者约束: 如果角色权限仅授予 if_owner 访问权限,则查询会将结果限制为用户拥有的文档。
  5. 权限查询条件: 应用通过 Hooks 或服务器脚本定义的条件。
  6. 字段级安全: 筛选所选的 fields,如果用户没有权限级别访问权限,则不允许使用 filtersgroup_byorder_by 中使用的字段。同时检查 link_field.target_fieldchild_field.target_field 表示法中的字段。

字段级安全

ignore_permissions=False 时:

  • fields 仅包含用户最大允许权限级别下可访问的字段。请求不可访问的字段将静默移除该字段的选择。
  • filters 筛选仅允许在用户有权访问的字段上进行。尝试筛选不可访问的字段将引发 frappe.PermissionError
  • group_by 分组仅允许在用户有权访问的字段上进行。尝试按不可访问的字段分组将引发 frappe.PermissionError
  • order_by 仅允许对用户有权限访问的字段进行排序。尝试按无权限访问的字段排序将引发 frappe.PermissionError
  • 链接表和子表字段: 在上述任何子句中使用 link_field.target_fieldchild_field.target_field 表示法时,系统会同时检查链接/子字段本身的权限,以及链接/子 DocType 中目标字段的权限。
# Assume 'published' field in Blog Post has permlevel 1
# User '[email protected]' only has permlevel 0 access

# This works, but 'published' field is silently removed from results
query = frappe.qb.get_query(
    "Blog Post",
    fields=["name", "title", "published"], # 'published' requested but inaccessible
    ignore_permissions=False,
    user="[email protected]"
)
# result will contain 'name' and 'title', but NOT 'published'

# This FAILS because filtering on 'published' is not allowed for this user
try:
    query = frappe.qb.get_query(
        "Blog Post",
        fields=["name"],
        filters={"published": 1}, # Filtering on restricted field
        ignore_permissions=False,
        user="[email protected]"
    )
    query.run()
except frappe.PermissionError as e:
    print(f"Permission error: {e}")

指定用户和父级上下文

# Check permissions for a specific user
query = frappe.qb.get_query(
    "Task",
    ignore_permissions=False,
    user="[email protected]" # Check permissions for this user
)

# Provide parent context for child DocTypes
query = frappe.qb.get_query(
    "Sales Order Item",
    fields=["item_code", "qty"],
    filters={"parent": "SO-00001"},
    ignore_permissions=False,
    parent_doctype="Sales Order" # Specify parent context for permission checks
)

高级功能

使用 Pypika 对象

对于涉及子查询、高级条件或字典语法中不可用函数的复杂场景,您可以直接使用 Pypika 对象。

字段中的 Pypika 对象

from frappe.query_builder import Field, functions, Query

# Define Pypika objects
user_table = frappe.qb.DocType("User")
todo_table = frappe.qb.DocType("ToDo")

# Build a subquery to count open ToDos for each user
open_todo_subquery = (
    Query.from_(todo_table)
    .select(functions.Count("*"))
    .where(todo_table.owner == user_table.name) # Correlated subquery
    .where(todo_table.status == "Open")
).as_("open_todos_count")

# Use Pypika objects in fields
query = frappe.qb.get_query(
    "User",
    fields=[
        user_table.name,
        user_table.email,
        open_todo_subquery # Using the subquery object
    ],
    filters={"user_type": "System User"}
)
users_with_counts = query.run(as_dict=True)

过滤器中的 Pypika 对象

from frappe.query_builder import Field, functions

# Define Pypika objects
task_table = frappe.qb.DocType("Task")
modified_field = task_table.modified
creation_field = task_table.creation
subject_field = task_table.subject
status_field = task_table.status

# Build complex criterion
complex_filter = (
    (modified_field > creation_field) & (functions.Length(subject_field) > 10)
) | (status_field == "Cancelled")

# Use the Criterion object in filters
query = frappe.qb.get_query(
    "Task",
    fields=["name", "subject", "status", "creation", "modified"],
    filters=complex_filter
)
results = query.run(as_dict=True)

记录锁定

用于数据库事务中,防止其他事务修改特定行:

基本锁定

# Lock specific Stock Ledger Entries
query = frappe.qb.get_query(
    "Stock Ledger Entry",
    fields=["name", "qty_after_transaction"],
    filters={"item_code": "ITEM001", "warehouse": "WH001"},
    for_update=True # Adds FOR UPDATE clause, will wait if rows are locked
)
entries = query.run(as_dict=True)

跳过已锁定行

# Skip rows that are already locked by another transaction
query = frappe.qb.get_query(
    "ToDo",
    fields=["name", "description"],
    filters={"status": "Pending"},
    limit=5,
    order_by="creation asc",
    for_update=True,
    skip_locked=True # Skip locked rows
)
available_tasks = query.run(as_dict=True)

非阻塞锁定尝试

# Fail immediately if rows are already locked
try:
    query = frappe.qb.get_query(
        "System Settings",
        fields=["name"],
        filters={"name": "System Settings"},
        for_update=True,
        wait=False # Don't wait for locks
    )
    settings = query.run(as_dict=True)
except Exception as e:
    print(f"Could not acquire lock immediately: {e}")

安全注意事项

frappe.qb.get_query 在设计时充分考虑了安全性:

  • 字段验证: 字段名称会按照严格的模式进行验证,以防止 SQL 注入。
  • 参数化: 过滤器值由数据库驱动程序进行参数化处理。
  • 权限执行: 使用 ignore_permissions=False 可充分利用 Frappe 强大的权限系统。

请始终确保,如果用于构建过滤器键或字段名称的任何动态值来自不受信任的来源,都经过适当的清理。始终依赖将用户输入作为过滤器值传递。

由 Claude Sonnet 4 编写。经人工审核。