OData Query Builder
Query Options
Build powerful OData queries with a clean, type-safe API
from easy_acumatica import QueryOptions, F
options = QueryOptions(
filter=F.Amount > 1000,
select=["CustomerID", "Amount", "Status"],
expand=["Details"],
order_by="Amount desc",
top=50
)
Overview
The QueryOptions
class provides a clean, type-safe way to build OData queries for the Acumatica REST API. It handles all the complex URL encoding and parameter formatting, letting you focus on your business logic.
Pro tip: QueryOptions works seamlessly with the F factory for building filters. All parameters are optional and can be combined as needed.
Quick Start Examples
python
from easy_acumatica import QueryOptions
# Get top 10 customers
options = QueryOptions(top=10)
customers = client.customers.get_list(options)
# Get specific fields only
options = QueryOptions(
select=["CustomerID", "CustomerName", "Email"]
)
customers = client.customers.get_list(options)
Parameters Reference
All QueryOptions parameters with examples:
Advanced Usage Patterns
Dynamic Query Building
Build queries dynamically based on runtime conditions
python
def build_customer_query(
status=None,
min_balance=None,
customer_class=None,
include_contacts=False,
page=1,
page_size=20
):
"""Build a dynamic customer query based on parameters."""
# Start with base options
options_dict = {
"select": ["CustomerID", "CustomerName", "Balance", "Status"],
"order_by": "CustomerName",
"top": page_size,
"skip": (page - 1) * page_size
}
# Build filter conditions
conditions = []
if status:
conditions.append(F.Status == status)
if min_balance is not None:
conditions.append(F.Balance >= min_balance)
if customer_class:
conditions.append(F.CustomerClass == customer_class)
# Combine conditions with AND
if conditions:
filter_expr = conditions[0]
for condition in conditions[1:]:
filter_expr = filter_expr & condition
options_dict["filter"] = filter_expr
# Add expansion if requested
if include_contacts:
options_dict["expand"] = ["MainContact", "Contacts"]
options_dict["select"].extend(["MainContact.Email", "MainContact.Phone"])
return QueryOptions(**options_dict)
# Usage examples
query1 = build_customer_query(status="Active", min_balance=5000)
query2 = build_customer_query(customer_class="RETAIL", include_contacts=True, page=2)
query3 = build_customer_query() # No filters, just pagination
Warning: Always validate user inputs before building queries to prevent injection attacks.
Efficient Pagination
Implement efficient pagination for large datasets
python
class PaginatedQuery:
"""Helper class for paginated queries."""
def __init__(self, client, service_name, page_size=50):
self.client = client
self.service = getattr(client, service_name)
self.page_size = page_size
def get_all(self, base_options=None):
"""Get all records using pagination."""
all_records = []
skip = 0
while True:
# Build options for this page
options_dict = {
"top": self.page_size,
"skip": skip,
"order_by": "id" # Consistent ordering required
}
# Merge with base options
if base_options:
if hasattr(base_options, 'to_dict'):
base_dict = base_options.to_dict()
else:
base_dict = base_options
options_dict.update(base_dict)
options = QueryOptions(**options_dict)
# Get page
page = self.service.get_list(options)
if not page:
break
all_records.extend(page)
# Check if we got a full page
if len(page) < self.page_size:
break
skip += self.page_size
return all_records
def get_page(self, page_number, base_options=None):
"""Get a specific page of results."""
options_dict = {
"top": self.page_size,
"skip": (page_number - 1) * self.page_size,
"order_by": "id"
}
if base_options:
if hasattr(base_options, 'to_dict'):
base_dict = base_options.to_dict()
else:
base_dict = base_options
options_dict.update(base_dict)
options = QueryOptions(**options_dict)
return self.service.get_list(options)
# Usage
paginator = PaginatedQuery(client, "customers", page_size=100)
# Get all active customers
base_options = QueryOptions(filter=F.Status == "Active")
all_active = paginator.get_all(base_options)
# Get specific page
page_3 = paginator.get_page(3, base_options)
Warning: Large datasets can impact performance. Consider using server-side filtering instead of retrieving all records.
Performance Optimization
Optimize queries for maximum performance
python
# BAD: Retrieving all fields when you only need a few
bad_options = QueryOptions(
filter=F.Status == "Active"
)
# This returns ALL fields for each record
# GOOD: Select only needed fields
good_options = QueryOptions(
filter=F.Status == "Active",
select=["CustomerID", "CustomerName", "Email", "Balance"]
)
# Much smaller payload, faster response
# GOOD: Expand only what you need
good_options = QueryOptions(
expand=["MainContact"],
select=[
"CustomerID",
"CustomerName",
"MainContact.Email",
"MainContact.Phone"
]
)
# BAD: Client-side filtering
all_customers = client.customers.get_list()
active = [c for c in all_customers if c.Status == "Active"]
# GOOD: Server-side filtering
options = QueryOptions(filter=F.Status == "Active")
active = client.customers.get_list(options)
# Monitoring query performance
import time
def measure_query(options):
start = time.time()
results = client.customers.get_list(options)
duration = time.time() - start
print(f"Query took {duration:.2f} seconds")
print(f"Retrieved {len(results)} records")
if hasattr(results, '__sizeof__'):
size = sys.getsizeof(results) / 1024
print(f"Payload size: ~{size:.1f} KB")
return results
# Test different approaches
measure_query(QueryOptions(top=10)) # Baseline
measure_query(QueryOptions(top=10, select=["CustomerID"])) # Minimal
Copying and Modifying Queries
Create variations of existing queries
python
# Base query
base_options = QueryOptions(
select=["CustomerID", "CustomerName", "Balance"],
order_by="CustomerName"
)
# Method 1: Using copy() method
active_options = base_options.copy(
filter=F.Status == "Active"
)
# Method 2: Convert to dict and modify
base_dict = base_options.to_dict()
base_dict["filter"] = F.Status == "Inactive"
inactive_options = QueryOptions(**base_dict)
# Method 3: Build query templates
class QueryTemplates:
@staticmethod
def customer_summary(status=None, min_balance=None):
"""Standard customer summary query."""
options = {
"select": [
"CustomerID",
"CustomerName",
"Balance",
"CreditLimit",
"Status"
],
"order_by": "Balance desc"
}
filters = []
if status:
filters.append(F.Status == status)
if min_balance is not None:
filters.append(F.Balance >= min_balance)
if filters:
filter_expr = filters[0]
for f in filters[1:]:
filter_expr = filter_expr & f
options["filter"] = filter_expr
return QueryOptions(**options)
@staticmethod
def customer_details(customer_id):
"""Detailed customer query."""
return QueryOptions(
filter=F.CustomerID == customer_id,
expand=[
"MainContact",
"DefaultLocation",
"CreditTerms",
"Contacts",
"Locations"
]
)
# Usage
summary = QueryTemplates.customer_summary(status="Active", min_balance=1000)
details = QueryTemplates.customer_details("CUST001")
QueryOptions Methods
Available methods on QueryOptions instances:
Method | Description | Returns | Example |
---|---|---|---|
to_dict() | Convert QueryOptions to a dictionary | Dict[str, Any] | options.to_dict() |
to_params() | Convert to URL parameters with $ prefixes | Dict[str, str] | options.to_params() |
copy(**kwargs) | Create a copy with updated parameters | QueryOptions | options.copy(top=50) |
Filter Functions Reference
Need help with filter functions?
The filter parameter accepts Filter objects created with the F factory, which provides a comprehensive set of functions for building OData queries.
Quick Examples
python
from easy_acumatica import QueryOptions, F
# Basic comparisons
options = QueryOptions(filter=F.Status == "Active")
options = QueryOptions(filter=(F.Amount > 1000) & (F.Amount <= 5000))
# String functions
options = QueryOptions(filter=F.CustomerName.startswith("ABC"))
options = QueryOptions(filter=F.Email.contains("@company.com"))
# Date functions
options = QueryOptions(filter=F.CreatedDate.year() == 2024)
options = QueryOptions(filter=(F.Date >= "2024-01-01") & (F.Date < "2024-07-01"))
# Complex filters
options = QueryOptions(
filter=(
(F.Status == "Active") &
(F.CustomerClass.in_(["WHOLESALE", "RETAIL"])) &
(F.Balance > 0)
)
)