本文介绍使用 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)