Consultar datos

En este artículo se describen los métodos para consultar datos de Dataverse mediante el SDK para Python. Puede consultar datos mediante el lenguaje de consulta estructurado (SQL) y las API basadas en OData.

Los desarrolladores de Python deberían primero familiarizarse con el SDK para Python leyendo Primeros pasos antes de continuar con este artículo.

QueryBuilder

QueryBuilder es la manera recomendada de consultar registros. Proporciona una interfaz fluida con comprobación de tipos que genera automáticamente consultas OData válidas. No es necesario recordar la sintaxis del filtro OData.

# Fluent query builder (recommended)
from PowerPlatform.Dataverse.models.filters 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 gestiona automáticamente el formato de los valores, el uso de mayúsculas y minúsculas en los nombres de columna y la sintaxis de OData. Compile expresiones de filtro con col() y operadores de Python estándar.

# 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
         )

Filtros de cadena

Use .contains(), .startswith()y .endswith() para la coincidencia de subcadenas. O bien, use .like() con un % patrón de caracteres comodín. El SDK compila ese patrón en el filtro equivalente más cercano.

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')
Formulario de patrones Example Filtro equivalente
val% like("Contoso%") empieza con
%val like("%Ltd") termina con
%val% like("%Corp%") contains
Sin carácter comodín like("Contoso") igualdad exacta
query = client.query.builder("account").where(col("name").like("Contoso%"))

Note

Patrones con caracteres comodín interiores, como "Con%oso" generar un ValueError. Use client.query.fetchxml() o client.query.sql() para esos patrones.

Negación y pertenencia a conjuntos

Cada operador positivo tiene un homólogo negado: .is_not_null(), .not_in()y .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

Vía de escape de OData sin procesar

Cuando necesite una función o sintaxis de OData que col() no cubra, use raw(). Es la alternativa predeterminada para las expresiones OData que no tienen un equivalente tipado.

from PowerPlatform.Dataverse.models.filters import col, raw

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

Para la lógica compleja (OR, NOT, agrupación), componga expresiones con &, , |~:

from PowerPlatform.Dataverse.models.filters 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"])

Valores con formato y anotaciones

En este ejemplo se muestra cómo solicitar etiquetas localizadas, símbolos de moneda y nombres para mostrar.

# 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"))

Para solicitar todas las anotaciones disponibles, llame a .include_annotations() en el generador (por defecto, es "*"), o pase include_annotations="*" a records.list() y records.retrieve().

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

Expandir las propiedades de navegación

Utilice la expansión anidada con opciones para expandir las propiedades de navegación mediante $select, $filter, $orderby y $top.

from PowerPlatform.Dataverse.models.query_builder 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

Use execute_pages() para transmitir conjuntos de resultados grandes con opciones completas del generador, como filtrar, ordenar y aplicar formato a valores. Para consultas de filtro OData basadas en cadenas más sencillas, use records.list() y records.list_pages() como accesos directos.

# 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"])

Note

Tanto execute(by_page=True) como execute(by_page=False) están obsoletos y generan un UserWarning. Reemplácelas por execute_pages() (streaming) o por execute() sin formato (eager). QueryBuilder.to_dataframe() también está en desuso: use .execute().to_dataframe() en su lugar.

La herramienta de migración vuelve a escribir todas estas llamadas automáticamente. Instale la herramienta de migración ejecutando pip install PowerPlatform-Dataverse-Client[migration] y luego ejecute dataverse-migrate path/to/your/scripts/. Como alternativa, ejecute python -m PowerPlatform.Dataverse.migration.migrate_v0_to_v1 para los checkouts de desarrollo.

Recuento de registros

Para obtener un recuento de registros, incluya $count=true en la solicitud.

# 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)

Consultas FetchXML

La llamada a client.query.fetchxml() devuelve un objeto inert FetchXmlQuery . El método no realiza una solicitud HTTP hasta que se llama a .execute() o .execute_pages().

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"])

Importante

La cadena FetchXML, una vez codificada con dirección URL, no debe superar los 32 768 caracteres. Las consultas con muchos atributos o condiciones suelen alcanzar este límite. Si se llega a ella, simplifique la consulta reduciendo el número de atributos y condiciones. Controle el tamaño de página estableciendo el count atributo en el <fetch> elemento, por ejemplo <fetch count="200">.

Acceso directo de lista simple

La records.list() llamada acepta una cadena de filtro OData sin procesar para las consultas básicas. Para cualquier caso que vaya más allá de un filtro y una selección simples, use client.query.builder(), que proporciona filtros combinables, valores con formato y expansión anidada.

# 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.query_builder 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})

Consulta de datos mediante SQL

Dataverse proporciona una interfaz de solo lectura a un conjunto limitado de comandos SQL SELECT . Admite combinaciones JOIN de SQL, agregados, GROUP BY, DISTINCT y paginación con OFFSET FETCH.

También puede acceder a la capacidad de consulta SQL mediante el parámetro ?sql= de la API web de Dataverse. Este enfoque permite que el código escrito en lenguajes distintos de Python acceda a datos de Dataverse. Para obtener más información, consulte Uso de SQL para consultar datos con la API web de Dataverse.

Importante

La compatibilidad con SQL se limita a las consultas de solo lectura. Es posible que no se admita combinaciones complejas, subconsultas y determinadas funciones SQL. La consulta SQL debe seguir el subconjunto admitido:

  • WHERE solo puede ser un árbol de expresión booleano donde las hojas son operadores binarios (=, >, like, etc.) con uno de los argumentos que son una referencia de columna directa y otra que es una constante.
  • TOP solo permite un literal entero.
  • ORDERBY solo puede hacer referencia a columnas y no permite expresiones complejas.

Sintaxis sql admitida

La interfaz SQL admite el siguiente subconjunto de T-SQL.

Feature Sintaxis admitida
Seleccionar SELECT, , SELECT DISTINCTSELECT TOP N (0–5000)
Joins INNER JOIN, LEFT JOIN (varias tablas)
Filtrado WHERE con =, !=, >, >=, LIKE, IS NULL, IN, <=, <, IS NOT NULL, BETWEEN, NOT IN, AND, OR, paréntesis anidados
Agrupación y agregación GROUP BY, COUNT(*), SUM(), AVG(), , MIN(), MAX()
Ordenación y paginación ORDER BY [ASC\|DESC], OFFSET n ROWS FETCH NEXT m ROWS ONLY

No se admiten las siguientes sentencias o características: SELECT *, subconsultas, CTE, HAVING, CROSS JOIN, RIGHT JOIN, UNION, FULL JOIN, CASE, COALESCE, funciones de ventana, funciones de cadena, fecha y matemáticas, y cualquier sentencia de escritura (INSERT, UPDATE, DELETE). Las consultas no compatibles o de escritura provocan ValidationError antes de que se envíe la solicitud. Las consultas que el cliente no puede validar, pero que el servidor rechaza, generan HttpError.

En el código de ejemplo siguiente se muestra una consulta SQL en Python.

# 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)

Consulte también