| name | ash-relationships |
| description | Defining, loading, and managing relationships between Ash resources |
Relationships
Relationships describe connections between resources and are a core component of Ash. Define relationships in the relationships block of a resource.
Best Practices for Relationships
- Be descriptive with relationship names (e.g., use
:authored_postsinstead of just:posts) - Configure foreign key constraints in your data layer if they have them (see
referencesin AshPostgres) - Always choose the appropriate relationship type based on your domain model
Relationship Types
For Polymorphic relationships, you can model them using Ash.Type.Union; see the "Polymorphic Relationships" guide for more information.
relationships do
# belongs_to - adds foreign key to source resource
belongs_to :owner, MyApp.User do
allow_nil? false
attribute_type :integer # defaults to :uuid
end
# has_one - foreign key on destination resource
has_one :profile, MyApp.Profile
# has_many - foreign key on destination resource, returns list
has_many :posts, MyApp.Post do
filter expr(published == true)
sort published_at: :desc
end
# many_to_many - requires join resource
many_to_many :tags, MyApp.Tag do
through MyApp.PostTag
source_attribute_on_join_resource :post_id
destination_attribute_on_join_resource :tag_id
end
end
The join resource must be defined separately:
defmodule MyApp.PostTag do
use Ash.Resource,
data_layer: AshPostgres.DataLayer
attributes do
uuid_primary_key :id
# Add additional attributes if you need metadata on the relationship
attribute :added_at, :utc_datetime_usec do
default &DateTime.utc_now/0
end
end
relationships do
belongs_to :post, MyApp.Post, primary_key?: true, allow_nil?: false
belongs_to :tag, MyApp.Tag, primary_key?: true, allow_nil?: false
end
actions do
defaults [:read, :destroy, create: :*, update: :*]
end
end
Loading Relationships
# Using code interface options (preferred)
post = MyDomain.get_post!(id, load: [:author, comments: [:author]])
# Complex loading with filters
posts = MyDomain.list_posts!(
query: [load: [comments: [filter: [is_approved: true], limit: 5]]]
)
# Manual query building (for complex cases)
MyApp.Post
|> Ash.Query.load(comments: MyApp.Comment |> Ash.Query.filter(is_approved == true))
|> Ash.read!()
# Loading on existing records
Ash.load!(post, :author)
Prefer to use the strict? option when loading to only load necessary fields on related data.
MyApp.Post
|> Ash.Query.load([comments: [:title]], strict?: true)
Managing Relationships
There are two primary ways to manage relationships in Ash:
1. Using change manage_relationship/2-3 in Actions
Use this when input comes from action arguments:
actions do
update :update do
# Define argument for the related data
argument :comments, {:array, :map} do
allow_nil? false
end
argument :new_tags, {:array, :map}
# Link argument to relationship management
change manage_relationship(:comments, type: :append)
# For different argument and relationship names
change manage_relationship(:new_tags, :tags, type: :append)
end
end
2. Using Ash.Changeset.manage_relationship/3-4 in Custom Changes
Use this when building values programmatically:
defmodule MyApp.Changes.AssignTeamMembers do
use Ash.Resource.Change
def change(changeset, _opts, context) do
members = determine_team_members(changeset, context.actor)
Ash.Changeset.manage_relationship(
changeset,
:members,
members,
type: :append_and_remove
)
end
end
Quick Reference - Management Types
:append- Add new related records, ignore existing:append_and_remove- Add new related records, remove missing:remove- Remove specified related records:direct_control- Full CRUD control (create/update/destroy):create- Only create new records
Quick Reference - Common Options
on_lookup: :relate- Look up and relate existing recordson_no_match: :create- Create if no match foundon_match: :update- Update existing matcheson_missing: :destroy- Delete records not in inputvalue_is_key: :name- Use field as key for simple values
For comprehensive documentation, see the Managing Relationships section.
Examples
Creating a post with tags:
MyDomain.create_post!(%{
title: "New Post",
body: "Content here...",
tags: [%{name: "elixir"}, %{name: "ash"}] # Creates new tags
})
# Updating a post to replace its tags
MyDomain.update_post!(post, %{
tags: [tag1.id, tag2.id] # Replaces tags with existing ones by ID
})