Querying data¶
This guide uses the Ticket model from the
first project. Configure and connect the
database and apply the model's migration before executing queries. The model
has subject, description, and resolved fields in addition to its ID.
Build first, execute explicitly¶
filter() and order_by() build a QuerySet without contacting PostgreSQL.
Call an async terminal method such as .all(), .first(), .count(), or
.exists() to execute it. The QuerySet itself is not awaitable.
from app.models import Ticket
query = Ticket.objects.filter(resolved=False).order_by("subject", "id")
tickets = await query.all()
await Ticket.objects.all() returns a list. It is useful for small complete
sets, but does not create a query builder you can subsequently filter or slice.
Retrieve one record¶
from aksara.manager import DoesNotExist
try:
ticket = await Ticket.objects.get(id=ticket_id)
except DoesNotExist:
ticket = None
get() raises MultipleObjectsReturned when more than one row matches. Use a
unique lookup when exactly one result is required. For an optional first match:
first() returns None when empty. get_or_none() also returns a first match
and does not check uniqueness. Use explicit ordering when selection order matters.
Filters and negation¶
Keyword conditions combine with AND. Use Q for grouped OR and negation;
there is no exclude() method on the ordinary Manager/QuerySet.
from aksara import Q
matches = await Ticket.objects.filter(
Q(subject__icontains="login") | Q(description__icontains="login"),
resolved=False,
).all()
other_tickets = await Ticket.objects.filter(~Q(subject="Archived")).all()
Supported ordinary comparisons include gt, gte, lt, lte, in,
isnull, contains, and icontains, applied to appropriate field types.
Do not assume every lookup from another ORM is available. JSON path and
relation lookups have additional type and relationship requirements; see
advanced fields and relations.
Conditional queries¶
Start with filter(), not all(), when building up a query:
query = Ticket.objects.filter()
if search_term:
query = query.filter(subject__icontains=search_term)
if unresolved_only:
query = query.filter(resolved=False)
rows = await query.order_by("subject", "id").limit(20).all()
The builder methods return a new QuerySet. Keep their return value. Filtering data is separate from checking whether the caller may access it. Applications must establish identity, tenant scope, and object policy explicitly.
Pagination¶
Use .limit() and .offset() on the QuerySet. Python slicing is not a
supported query interface. Validate application page inputs before building the
query and cap the page size.
page = 2
page_size = 20
if page < 1 or not 1 <= page_size <= 100:
raise ValueError("Invalid page or page size")
rows = await Ticket.objects.order_by("subject", "id").limit(page_size).offset(
(page - 1) * page_size
).all()
Including a unique tie-breaker makes ordering deterministic for an unchanged dataset. Offset pagination does not promise a stable snapshot across requests while records are being inserted, deleted, or reordered.
Counts, existence, and aggregates¶
from aksara import Count
total = await Ticket.objects.count()
open_count = await Ticket.objects.filter(resolved=False).count()
has_open_tickets = await Ticket.objects.filter(resolved=False).exists()
summary = await Ticket.objects.aggregate(total=Count("*"))
Use .filter().exists() when testing the whole table; the ordinary Manager
has no direct exists() method. Aggregates take named expressions, not a
positional Avg(...). See expressions and transactions
for F, grouped filters, annotations, and supported one-hop aggregates.
Output projection¶
The ordinary Manager/QuerySet has no values() or values_list() methods.
For small bounded results, explicitly shape application output:
rows = await Ticket.objects.order_by("subject", "id").limit(20).all()
public_rows = [{"id": str(row.id), "subject": row.subject} for row in rows]
This Python projection still loads model rows; it is not a SQL column-selection optimization. For generated HTTP responses, configure the supported serializer and field-policy boundary instead.
Related data¶
Use select_related() and get_related() for supported forward relation
loading, and the documented prefetch interfaces for collections. A forward FK
attribute contains the stored identifier, not an awaitable object. The
relationship tutorial adds an Agent to Ticket;
relations explains the corresponding loading APIs.
Writes and concurrency¶
Use .create(), individual .save(), or explicit filtered .update() and
.delete() operations according to their contracts. get_or_create() is a
lookup followed by creation, not an atomic uniqueness guarantee. The ordinary
Manager does not supply update_or_create().
See bulk operations for bulk and upsert behavior and transactions for multi-step atomic work. Neither an ORM query nor a transaction makes email, files, or remote API calls atomic with PostgreSQL.
Raw SQL¶
The ordinary Manager does not offer .raw(). Use the documented Database
connection boundary when application SQL is necessary, with asyncpg $1, $2
parameters for values. Raw SQL must explicitly preserve authorization and
tenant restrictions; it does not inherit ORM query filtering. Keep it on the
active pinned connection when it must participate in an existing transaction.