查询数据

本文介绍使用 SDK for Python查询 Dataverse 数据的方法。 可以使用结构化查询语言(SQL)和基于 OData 的 API 查询数据。

Python 开发人员在继续阅读本文之前,应先阅读 Getting started,了解 Python SDK。

QueryBuilder

QueryBuilder 是查询记录的建议方法。 它提供了一个流畅的、类型安全的接口,用于自动生成正确的 OData 查询。 无需记住 OData 筛选器语法。

# Fluent query builder (recommended)
from PowerPlatform.Dataverse.models import col

for record in (client.query.builder("account")
               .select("name", "revenue")
               .where(col("statecode") == 0)
               .where(col("revenue") > 1000000)
               .order_by("revenue", descending=True)
               .top(100)
               .page_size(50)
               .execute()):
    print(f"{record['name']}: {record['revenue']}")

QueryBuilder 自动处理值格式、列名大小写和 OData 语法。 使用 col() 和标准Python运算符生成筛选器表达式。

# Get results as a pandas DataFrame (consolidates all pages)
df = (client.query.builder("account")
      .select("name", "telephone1")
      .where(col("statecode") == 0)
      .top(100)
      .execute()
      .to_dataframe())
print(f"Got {len(df)} accounts")
# Comparison filters using col() expressions
query = (client.query.builder("contact")
         .where(col("statecode") == 0)                        # statecode eq 0
         .where(col("revenue") > 1000000)                     # revenue gt 1000000
         .where(col("name").contains("Corp"))                 # contains(name, 'Corp')
         .where(col("statecode").in_([0, 1]))                 # Microsoft.Dynamics.CRM.In(...)
         .where(col("revenue").between(100000, 500000))       # revenue ge 100000 and revenue le 500000
         .where(col("telephone1").is_null())                  # telephone1 eq null
         )

字符串筛选器

使用 .contains()、.startswith() 和 .endswith() 进行子字符串匹配。 或者将 .like() 与 % 通配符模式结合使用。 SDK 会将该模式编译为最接近的等效筛选器。

query = (client.query.builder("contact")
         .where(col("email").contains("outlook.com"))    # contains(email, 'outlook.com')
         .where(col("lastname").startswith("Smith"))      # startswith(lastname, 'Smith')
         .where(col("firstname").endswith("son")))        # endswith(firstname, 'son')
模式表单 Example 等效筛选器
val% like("Contoso%") startswith
%val like("%Ltd") endswith
%val% like("%Corp%") 包含
无通配符 like("Contoso") 精确相等性
query = client.query.builder("account").where(col("name").like("Contoso%"))

注释

含有内部通配符的模式(例如 "Con%oso")会导致 ValueError。 对于这些模式,请使用 client.query.fetchxml() 或 client.query.sql()。

否定和设置成员身份

每个正运算符都有一个否定的对应项: .is_not_null(), .not_in()和 .not_between()。

query = (client.query.builder("contact")
         .where(col("telephone1").is_not_null())          # telephone1 ne null
         .where(col("statecode").not_in([2, 3]))          # not in set
         .where(col("creditlimit").not_between(0, 100)))  # outside range

原始 OData 转义通道

当您需要 col() 不支持的 OData 函数或语法时,请使用 raw()。 它是没有对应类型化等效形式的 OData 表达式的预设回退方式。

from PowerPlatform.Dataverse.models import col, raw

query = (client.query.builder("account")
         .where(col("statecode") == 0)
         .where(raw("Microsoft.Dynamics.CRM.Today(PropertyName='createdon')")))

对于复杂逻辑(OR、NOT、分组),请使用 &、 |、 ~:

from PowerPlatform.Dataverse.models import col

# OR conditions: (statecode = 0 OR statecode = 1) AND revenue > 100k
for record in (client.query.builder("account")
               .select("name", "revenue")
               .where(((col("statecode") == 0) | (col("statecode") == 1))
                      & (col("revenue") > 100000))
               .execute()):
    print(record["name"])

# NOT, between, and in operators
for record in (client.query.builder("account")
               .where(col("statecode") != 2)                       # NOT inactive
               .where(col("revenue").between(100000, 500000))      # revenue in range
               .execute()):
    print(record["name"])

带格式的值和注释

此示例演示如何请求本地化标签、货币符号和显示名称。

# Get formatted values (choice labels, currency, lookup names) — via query builder
for record in (client.query.builder("account")
               .select("name", "statecode", "revenue")
               .include_formatted_values()
               .execute()):
    status = record["statecode@OData.Community.Display.V1.FormattedValue"]
    print(f"{record['name']}: {status}")

# Get formatted values — via records.list() / records.retrieve() include_annotations param
result = client.records.list(
    "account",
    select=["name", "statecode"],
    include_annotations="OData.Community.Display.V1.FormattedValue",
)
for record in result:
    label = record.get("statecode@OData.Community.Display.V1.FormattedValue")
    print(f"{record['name']}: {label}")

record = client.records.retrieve(
    "account", account_id,
    select=["name", "statuscode"],
    include_annotations="OData.Community.Display.V1.FormattedValue",
)
if record:
    print(record.get("statuscode@OData.Community.Display.V1.FormattedValue"))

若要请求所有可用的注解,请在构建器上调用 .include_annotations()(其默认值为 "*"),或将 records.list() 传递给 records.retrieve() 和 include_annotations="*"。

builder = (client.query.builder("account")
           .select("name", "_ownerid_value")
           .include_annotations())   # defaults to "*"

展开导航属性

使用带选项的嵌套 expand 方法,通过 $select、$filter、$orderby 和 $top 展开导航属性。

from PowerPlatform.Dataverse.models import ExpandOption

# Expand related tasks with filtering and sorting
for record in (client.query.builder("account")
               .select("name")
               .expand(ExpandOption("Account_Tasks")
                       .select("subject", "createdon")
                       .filter("contains(subject,'Task')")
                       .order_by("createdon", descending=True)
                       .top(5))
               .execute()):
    print(record["name"], record.get("Account_Tasks"))

Paging

使用 execute_pages() 可通过完整的构建器选项(如筛选、排序和格式化后的值)流式传输大型结果集。 对于更简单的基于字符串的 OData 筛选器查询,请使用 records.list() 和 records.list_pages() 用作快捷方式。

# Preferred: query.builder().execute_pages() — stream one page at a time, memory stays flat
# Supports composable filters, sorting, formatted values, and expand with nested selects
for page_num, page in enumerate(
    client.query.builder("account")
    .select("accountid", "name", "revenue")
    .where(col("statecode") == 0)
    .order_by("name")
    .page_size(500)        # optional: override Dataverse default (~5000/page)
    .execute_pages()
):
    print(f"Page {page_num + 1}: {len(page)} records")
    for record in page:
        print(f"  {record['name']}")

# Simple shortcut: records.list() — automatic paging, all records in memory
# Use for basic filter+select queries; string OData filter only (no composable expressions)
result = client.records.list(
    "account",
    filter="statecode eq 0",
    select=["name", "revenue"],
    orderby=["name asc"],          # optional sort
    top=500,                       # bounds total records returned and number of HTTP round-trips
    page_size=200,                 # optional: hint Dataverse default page size
)
for record in result:
    print(record["name"])

# Simple streaming shortcut: records.list_pages() — same params as records.list(), yields one page at a time
for page_num, page in enumerate(
    client.records.list_pages("account", filter="statecode eq 0", select=["name"], orderby=["name asc"])
):
    print(f"Page {page_num + 1}: {len(page)} records")
    for record in page:
        print(record["name"])

注释

execute(by_page=True) 和 execute(by_page=False) 均已弃用,并会发出 UserWarning。 将其替换为 execute_pages()(流式)或普通 execute()(即时)。 QueryBuilder.to_dataframe() 也已弃用 - 请改用 .execute().to_dataframe() 。

迁移工具会自动重写所有这些调用。 通过运行 pip install PowerPlatform-Dataverse-Client[migration] 和运行 dataverse-migrate path/to/your/scripts/来安装迁移工具。 或者,在开发环境检出中执行 python -m PowerPlatform.Dataverse.migration.migrate_v0_to_v1。

记录计数

若要获取记录数,请在请求中包含 $count=true。

# Via query builder
results = (client.query.builder("account")
           .where(col("statecode") == 0)
           .count()
           .execute())

# Via records.list() — count=True adds $count=true to the OData request
results = client.records.list("account", filter="statecode eq 0", count=True)

FetchXML 查询

调用 client.query.fetchxml() 将返回一个非活动的 FetchXmlQuery 对象。 在调用 .execute() 或 .execute_pages() 之前,该方法不会发起 HTTP 请求。

xml = """
<fetch>
  <entity name="account">
    <attribute name="name"/>
    <attribute name="revenue"/>
    <filter><condition attribute="statecode" operator="eq" value="0"/></filter>
  </entity>
</fetch>
"""

# .execute() — blocking, fetches all pages and returns a single QueryResult
result = client.query.fetchxml(xml).execute()
df = result.to_dataframe()

# .execute_pages() — streaming, yields one QueryResult per HTTP page
# Use count="N" in the FetchXML <fetch> element to set page size
for page_num, page in enumerate(client.query.fetchxml(xml).execute_pages()):
    print(f"Page {page_num + 1}: {len(page)} records")
    for record in page:
        print(record["name"])

重要

一旦 URL 编码,FetchXML 字符串不得超过 32,768 个字符。 具有许多属性或条件的查询通常达到此限制。 如果到达它,请通过减少属性和条件的数量来简化查询。 通过设置 count 元素上的 <fetch> 属性来控制页面大小,例如 <fetch count="200">。

简单列表快捷方式

该 records.list() 调用接受基本查询的原始 OData 筛选器字符串。 对于简单筛选器和选择之外的任何内容,请使用 client.query.builder(),它提供可组合的筛选器、格式化值和嵌套扩展。

# records.list() shortcut — raw OData filter string, all records loaded into memory
# Column names in filter must be lowercase logical names
for record in client.records.list(
    "account",
    select=["name"],
    filter="statecode eq 0",
    top=100,
):
    print(record["name"])

# Discover navigation property names for $expand (metadata-discovery helper, kept at GA)
nav_props = client.query.odata_expands("account")  # → list of navigation property metadata

# Expand navigation properties using the query builder
from PowerPlatform.Dataverse.models import ExpandOption
for record in (client.query.builder("contact")
               .select("fullname")
               .expand(ExpandOption("parentcustomerid_account").select("name"))
               .execute()):
    acct = record.get("parentcustomerid_account") or {}
    print(f"{record['fullname']} -> {acct.get('name')}")

# Build @odata.bind for lookup fields (deprecated helper, still functional with DeprecationWarning)
bind = client.query.odata_bind("contact", "account", account_id)
# Returns: {"parentcustomerid_account@odata.bind": "/accounts(guid)"}
client.records.create("contact", {"firstname": "Jane", **bind})

使用 SQL 查询数据

使用 Python SDK client.query.sql 方法将 SQL 查询发送到 Dataverse Web API ?sql= 的参数。 有关详细信息,请参阅 使用 SQL 通过 Dataverse Web API 查询数据。

支持的 SQL

Dataverse 提供支持以下 T-SQL 子集的只读 SQL 接口。

功能 / 特点 支持的语法
选择 SELECT、 SELECT DISTINCTSELECT TOP N (0–5000)
Joins INNER JOIN、 LEFT JOIN (多表)
筛选 WHERE,带有 =、!=、<、>、>=、LIKE、AND、<=、NOT IN、OR、IN、IS NULL、IS NOT NULL、BETWEEN、嵌套括号
分组和聚合 GROUP BY、COUNT(*)、SUM()、AVG()、MIN()、MAX()
排序和分页 ORDER BY [ASC\|DESC]、OFFSET n ROWS FETCH NEXT m ROWS ONLY

注释

每个命令必须包含单个 SELECT 语句。 Dataverse 不支持具有多个结果集的命令,例如 SELECT name FROM account; SELECT fullname FROM contact。

重要

SQL 查询必须满足以下要求:

  • WHERE 只能是一个布尔表达式树,其中叶是二进制运算符(=、 >等 like),其中一个参数是直接列引用,另一个是常量。
  • TOP 仅允许整数文本。
  • ORDER BY 只能引用列,不允许任何复杂的表达式。

不支持以下语句或功能:SELECT *、子查询、CTE、HAVING、、UNION、CROSS JOINFULL JOINRIGHT JOIN、、CASECOALESCE窗口函数、字符串/日期/数学函数,以及除 或以外的DECLAREALTER TABLESELECTINSERTUPDATEDELETE任何语句。

对于不受支持的查询,会在发送请求之前引发 ValidationError。 客户端无法验证但会被服务器拒绝的查询会引发 HttpError。

SQL 示例

以下示例代码演示 Python 中的 SQL 查询。

# Basic query
results = client.query.sql(
    "SELECT TOP 10 accountid, name FROM account WHERE statecode = 0"
)

# JOINs and aggregates work
results = client.query.sql(
    "SELECT a.name, COUNT(c.contactid) as cnt "
    "FROM account a "
    "JOIN contact c ON a.accountid = c.parentcustomerid "
    "GROUP BY a.name"
)

# SQL results directly as a DataFrame
df = client.dataframe.sql(
    "SELECT name, revenue FROM account ORDER BY revenue DESC"
)

# Discover columns from metadata (schema-discovery helper, kept at GA)
cols_meta = client.query.sql_columns("account")
col_names = [c["LogicalName"] for c in cols_meta]

# Build queries using the discovered column names
sql = f"SELECT TOP 10 {', '.join(col_names[:5])} FROM account"
df = client.dataframe.sql(sql)

另请参阅