Read Actions
View SourceRead actions operate on an Ash.Query
. Read actions always return lists of data. The act of pagination, or returning a single result, is handled as part of the interface, and is not a concern of the action itself. Here is an example of a read action:
# Giving your actions informative names is always a good idea
read :ticket_queue do
# Use arguments to take in values you need to run your read action.
argument :priorities, {:array, :atom} do
constraints items: [one_of: [:low, :medium, :high]]
end
# This action may be paginated,
# and returns a total count of records by default
pagination offset: true, countable: :by_default
# Arguments can be used in preparations and filters
filter expr(status == :open and priority in ^arg(:priorities))
end
For a full list of all of the available options for configuring read actions, see the Ash.Resource.Dsl documentation.
Calling Read Actions
The basic formula for calling a read action looks like this:
Resource
|> Ash.Query.for_read(:action_name, %{argument: :value}, ...opts)
|> Ash.read!()
See below for variations on action calling, and see the code interface guide guide for how to define idiomatic and convenient functions that call your actions.
Ash.get!
The Ash.get!
function is a convenience function for running a read action, filtering by a unique identifier, and expecting only a single result. It is equivalent to the following code:
# action can be omitted to use the primary read action
Ash.get!(Resource, 1, action: :read_action)
# is roughly equivalent to
Resource
|> Ash.Query.filter(id == 1)
|> Ash.Query.limit(2)
|> Ash.Query.for_read(:read_action, %{})
|> Ash.read!()
|> case do
[] -> # raise not found error
[result] -> result
[_, _] -> # raise too many results error
end
Ash.read_one!
The Ash.read_one!
function is a similar convenience function to Ash.get!
, but it does not take a unique identifier. It is useful when you expect an action to return only a single result, and want to enforce that and return a single result.
Ash.read_one!(query)
# is roughly equivalent to
query
|> Ash.Query.limit(2)
|> Ash.read!()
|> case do
[] -> nil
[result] -> result
[_, _] -> # raise too many results error
end
Pagination
Ash provides built-in support for pagination when reading resources and their relationships. You can find more information about this in the pagination guide.
Pagination configuration on default vs custom read actions
The default read action supports keyset pagination automatically. You need to explicitly enable pagination strategies you want to support when defining your own read actions.
What happens when you call Ash.Query.for_read/4
The following steps are performed when you call Ash.Query.for_read/4
.
- Cast input arguments -
Ash.Resource.Dsl.actions.read.argument
- Set default argument values -
Ash.Resource.Dsl.actions.read.argument.default
Add errors for missing required arguments |
Ash.Resource.Dsl.actions.read.argument.allow_nil?
Run query preparations |
Ash.Resource.Dsl.actions.read.prepare
Run query validations |
Ash.Resource.Dsl.actions.read.validate
Add action filter |
Ash.Resource.Dsl.actions.read.filter
What happens when you run the action
These steps are trimmed down, and are aimed at helping users understand the general flow. Some steps are omitted.
- Run
Ash.Query.for_read/3
if it has not already been run - Apply tenant filters for attribute
- Apply pagination options
- Run before action hooks
- Multi-datalayer filter is synthesized. We run queries in other data layers to fetch ids and translate related filters to
(destination_field in ^ids)
- Strict Check & Filter Authorization is run
- Data layer query is built and validated
- Field policies are added to the query
- Data layer query is Run
- Authorizer "runtime" checks are run (you likely do not have any of these)
The following steps happen while(asynchronously) or after the main data layer query has been run
- If paginating and count was requested, the count is determined at the same time as the query is run.
- Any calculations & aggregates that were able to be run outside of the main query are run
- Relationships, calculations, and aggregates are loaded
Customizing Queries When Calling Actions
When calling read actions through code interfaces, you can customize the query using the query
option. This allows you to filter, sort, limit, and otherwise modify the results without manually building queries.
User Input Safety
When accepting query parameters from untrusted sources (like web requests), always use the _input
variants (sort_input
, filter_input
) instead of the regular options.
These functions only allow access to public fields and provide safe parsing of user input.
Query Options via Code Interfaces
The query
option accepts all the options that Ash.Query.build/2
accepts:
# Filtering results
posts = MyApp.Blog.list_posts!(
query: [filter: [status: :published]]
)
# Sorting results
posts = MyApp.Blog.list_posts!(
query: [sort: [published_at: :desc]]
)
# Limiting results
posts = MyApp.Blog.list_posts!(
query: [limit: 10]
)
# Combining multiple query options
posts = MyApp.Blog.list_posts!(
query: [
filter: [status: :published, author_id: author.id],
sort: [published_at: :desc],
limit: 10,
offset: 20
]
)
# Loading related data with query constraints
posts = MyApp.Blog.list_posts!(
query: [
load: [
comments: [
filter: [approved: true],
sort: [created_at: :desc],
limit: 5
]
]
]
)
Handling User Input
When accepting query parameters from user input, use the safe input variants:
# Safe sorting from user input
posts = MyApp.Blog.list_posts!(
query: [sort_input: params["sort"] || "+published_at"]
)
# Safe filtering from user input
posts = MyApp.Blog.list_posts!(
query: [filter_input: params["filter"] || %{}]
)
# Combining user input with application-defined constraints
posts = MyApp.Blog.list_posts!(
query: [
# User-controlled sorting
sort_input: params["sort"],
# User-controlled filtering
filter_input: params["filter"],
# Application-enforced constraints
filter: [archived: false],
limit: 100 # Prevent excessive data fetching
]
)
Default Query Behavior in Actions
You can configure default query behavior in your action definitions:
actions do
read :recent_posts do
# Default sort - overridden if user provides any sort
prepare build(default_sort: [published_at: :desc])
# Always applied filter - cannot be overridden
filter expr(status == :published)
# Default pagination
pagination offset: true, default_limit: 20
end
read :search do
argument :query, :string, allow_nil?: false
# Prepare modifies the query before execution
prepare fn query, _context ->
Ash.Query.filter(query, contains(title, ^query.arguments.query))
end
end
read :user_posts do
argument :email, :string, allow_nil?: false
argument :status, :string, default: "published"
# Validate arguments before processing
validate match(:email, ~r/^[^\s]+@[^\s]+\.[^\s]+$/) do
message "must be a valid email address"
end
validate one_of(:status, ["published", "draft", "archived"])
# Conditional validation - only validate if email is provided
validate present(:email) do
where present(:email)
end
end
end
Building Queries Manually
For more complex scenarios, you can build queries manually before calling the action:
require Ash.Query
# Build a complex query
query =
MyApp.Post
|> Ash.Query.filter(status == :published)
|> Ash.Query.sort(published_at: :desc)
|> Ash.Query.limit(10)
# Execute the query
posts = Ash.read!(query)
# Or use it with a specific action
posts = Ash.read!(query, action: :published_posts)
Common Query Patterns
Pagination
# With page options
posts = MyApp.Blog.list_posts!(
page: [limit: 20, offset: 40]
)
# with a query
MyApp.Post
|> Ash.Query.page(
limit: 20, offset: 40
)
# when calling an action
MyApp.Post
|> Ash.Query.for_read(...)
|> Ash.read!(page: [limit: 20, offste: 40])
Complex Filtering
# Filtering with relationships
posts = MyApp.Blog.list_posts!(
query: [
filter: [
author: [verified: true],
comments_count: [greater_than: 5]
]
]
)
# Using filter expressions (requires building query manually)
query =
MyApp.Post
|> Ash.Query.filter(
status == :published and
(author.verified == true or author.admin == true)
)
Validations on Read Actions
Read actions support validations to ensure query arguments meet your requirements before processing. Most built-in validations work on both changesets and queries.
Supported Validations
The following built-in validations support queries:
action_is
- validates the action nameargument_does_not_equal
,argument_equals
,argument_in
- validates argument valuescompare
- compares argument valuesconfirm
- confirms two arguments matchmatch
- validates arguments against regex patternsnegate
- negates other validationsone_of
- validates arguments are in allowed valuespresent
- validates required arguments are presentstring_length
- validates string argument length
Validation Examples
actions do
read :user_search do
argument :email, :string
argument :role, :string
argument :min_age, :integer
argument :max_age, :integer
# Validate email format
validate match(:email, ~r/^[^\s]+@[^\s]+\.[^\s]+$/) do
message "must be a valid email address"
end
# Validate role is one of allowed values
validate one_of(:role, ["admin", "user", "moderator"])
# Validate age range makes sense
validate compare(:min_age, less_than: :max_age) do
message "minimum age must be less than maximum age"
end
# Conditional validation - only validate email if provided
validate present(:email) do
where present(:email)
end
# Skip expensive validation if query is already invalid
validate expensive_validation() do
only_when_valid? true
end
end
end
Where Clauses
Use where
clauses to conditionally apply validations:
read :conditional_search do
argument :include_archived, :boolean, default: false
argument :archive_reason, :string
# Only validate archive_reason if including archived items
validate present(:archive_reason) do
where argument_equals(:include_archived, true)
end
end
only_when_valid? Option
Use only_when_valid?
to skip validations when the query is already invalid:
read :complex_search do
argument :required_field, :string
# This validation must pass
validate present(:required_field)
# This expensive validation only runs if query is valid so far
validate expensive_external_validation() do
only_when_valid? true
end
end
For detailed information about query capabilities, see:
Ash.Query
module documentation for building queriesAsh.Query.build/2
for all available query options- Write Queries guide for practical examples
- Validations guide for more validation examples