Relationships

Relationships define connections between resources. In any application this is the bread and butter of the data modeling.

Setup

We discuss relationships in the context of a simple online shop. Please use Minimal Ash 2.x Setup Guide to generate a new Elixir application. After that create or adapt the following files for a Product resource:

config/config.exs
import Config

config :app, :ash_apis, [App.Shop]
lib/app/shop.ex
defmodule App.Shop do
  use Ash.Api

  resources do
    resource App.Shop.Product (1)
  end
end
1We always have to create an internal API and include all resources in it.
lib/app/shop/resources/product.ex
defmodule App.Shop.Product do
  use Ash.Resource, data_layer: Ash.DataLayer.Ets

  attributes do
    uuid_primary_key :id
    attribute :name, :string
    attribute :price, :decimal
  end

  actions do
    defaults [:create, :read, :update, :destroy]
  end

  code_interface do
    define_for App.Shop
    define :create
    define :read
    define :by_id, get_by: [:id], action: :read
    define :by_name, get_by: [:name], action: :read
    define :update
    define :destroy
  end
end

belongs_to

The belongs_to macro defines a relationship between two resources. In our shop example, a Product belongs to a Category.

+----------+      +-------------+
| Category |      | Product     |
+----------+      +-------------+
| id       |<-----| category_id |
| name     |      | id          |
|          |      | name        |
|          |      | price       |
+----------+      +-------------+

Do you like video tutorials? Have a look at "belongs_to in 2 minutes" in our @elixir-phoenix-ash YouTube Channel.

We need a new Category resource:

lib/app/shop/resources/category.ex
defmodule App.Shop.Category do
  use Ash.Resource, data_layer: Ash.DataLayer.Ets

  attributes do
    uuid_primary_key :id
    attribute :name, :string
  end

  actions do
    defaults [:create, :read, :update, :destroy]
  end

  code_interface do
    define_for App.Shop
    define :create
    define :read
    define :by_id, get_by: [:id], action: :read
    define :by_name, get_by: [:name], action: :read
    define :update
    define :destroy
  end
end

And we need to add it to the internal API:

lib/app/shop.ex
defmodule App.Shop do
  use Ash.Api

  resources do
    resource App.Shop.Product
    resource App.Shop.Category
  end
end

To configure the belongs_to relationship to Category we add one line to the Product resource:

lib/app/shop/resources/product.ex
defmodule App.Shop.Product do
  use Ash.Resource, data_layer: Ash.DataLayer.Ets

  attributes do
    uuid_primary_key :id
    attribute :name, :string
    attribute :price, :decimal
  end

  relationships do (1)
    belongs_to :category, App.Shop.Category do (2)
      attribute_writable? true (3)
    end
  end

  actions do
    defaults [:create, :read, :update, :destroy]
  end

  code_interface do
    define_for App.Shop
    define :create
    define :read
    define :by_id, get_by: [:id], action: :read
    define :by_name, get_by: [:name], action: :read
    define :update
    define :destroy
  end
end
1The relationships macro defines relationships between resources.
2The source_attribute is defined as :<relationship_name>_id of the type :uuid on the source resource and the destination_attribute is assumed to be :id. To override those defaults have a look at https://hexdocs.pm/ash/relationships.html and https://ash-hq.org/docs/dsl/ash-resource#relationships-belongs_to
3By default the attribute category_id is not writable (see https://ash-hq.org/docs/dsl/ash-resource#relationships-belongs_to-attribute_writable-). To make it writable we need to set attribute_writable? to true. Only than we can create a Product with a Category in on call.
$ iex -S mix
iex(1)> alias App.Shop.Product (1)
App.Shop.Product
iex(2)> alias App.Shop.Category
App.Shop.Category
iex(3)> fruits = Category.create!(%{name: "Fruits"}) (2)
#App.Shop.Category<
  __meta__: #Ecto.Schema.Metadata<:loaded>,
  id: "91cb42d8-45c2-451d-8261-72ae4d94a3c6",
  name: "Fruits",
  ...
>
iex(4)> orange = Product.create!(%{
                   name: "Orange",
                   price: 0.15,
                   category_id: fruits.id
                 }) (3)
#App.Shop.Product<
  category: #Ash.NotLoaded<:relationship>,
  __meta__: #Ecto.Schema.Metadata<:loaded>,
  id: "6870b44b-67ed-4186-97ed-bbfffd1fc2a0",
  name: "Orange",
  price: Decimal.new("0.15"),
  category_id: "91cb42d8-45c2-451d-8261-72ae4d94a3c6",
  ...
>
iex(5)> App.Shop.load(orange, :category) (4)
{:ok,
 #App.Shop.Product<
   category: #App.Shop.Category<
     __meta__: #Ecto.Schema.Metadata<:loaded>,
     id: "91cb42d8-45c2-451d-8261-72ae4d94a3c6",
     name: "Fruits",
     ...
   >,
   __meta__: #Ecto.Schema.Metadata<:loaded>,
   id: "6870b44b-67ed-4186-97ed-bbfffd1fc2a0",
   name: "Orange",
   price: Decimal.new("0.15"),
   category_id: "91cb42d8-45c2-451d-8261-72ae4d94a3c6",
   ...
 >}
iex(6)> orange2 = Product.by_name!("Orange", load: [:category]) (5)
#App.Shop.Product<
  category: #App.Shop.Category<
    __meta__: #Ecto.Schema.Metadata<:loaded>,
    id: "91cb42d8-45c2-451d-8261-72ae4d94a3c6",
    name: "Fruits",
    ...
  >,
  __meta__: #Ecto.Schema.Metadata<:loaded>,
  id: "6870b44b-67ed-4186-97ed-bbfffd1fc2a0",
  name: "Orange",
  price: Decimal.new("0.15"),
  category_id: "91cb42d8-45c2-451d-8261-72ae4d94a3c6",
  ...
>
iex(7)> orange2.category
#App.Shop.Category<
  __meta__: #Ecto.Schema.Metadata<:loaded>,
  id: "91cb42d8-45c2-451d-8261-72ae4d94a3c6",
  name: "Fruits",
  ...
>
iex(8)> orange2.category.name
"Fruits"
1Let’s save a bit of typing by creating shorter Aliases.
2Create a new Category for "Fruits" and store it in the variable fruits.
3Create a new Product for "Orange" which belongs to the Category "Fruits" and store it in the variable orange.
4One way to get the Category of a Product if that wasn’t sideloaded initially.
5Sideload the Category of the Product when fetching The `Product from the database.

Sideload a belongs_to Relationship by Default

In case you always want to sideload the Category of the Product without adding load: [:category] to every call you can do the following:

lib/app/shop/resources/product.ex
defmodule App.Shop.Product do
  use Ash.Resource, data_layer: Ash.DataLayer.Ets

  attributes do
    uuid_primary_key :id
    attribute :name, :string
    attribute :price, :decimal
  end

  relationships do
    belongs_to :category, App.Shop.Category do
      attribute_writable? true
    end
  end

  actions do
    defaults [:create, :update, :destroy] (1)

    read :read do
      primary? true (2)
      prepare build(load: [:category]) (3)
    end
  end

  code_interface do
    define_for App.Shop
    define :create
    define :read
    define :by_id, get_by: [:id], action: :read
    define :by_name, get_by: [:name], action: :read
    define :update
    define :destroy
  end
end
1Don’t forget to remove :read from the defaults when you add a custom read action.
2See https://ash-hq.org/docs/guides/ash/latest/topics/actions#primary-actions
3Always sideload the Category when fetching a Product.

Let’s test it in the iex:

iex(9)> Product.by_name("Orange")
{:ok,
 #App.Shop.Product<
   category: #App.Shop.Category<
     __meta__: #Ecto.Schema.Metadata<:loaded>,
     id: "22ab0824-18ac-4daa-9a13-defd0b8bcd73",
     name: "Fruits",
     ...
   >,
   __meta__: #Ecto.Schema.Metadata<:loaded>,
   id: "24348935-6148-4c75-9bf1-55f74ac9397a",
   name: "Orange",
   price: Decimal.new("0.15"),
   category_id: "22ab0824-18ac-4daa-9a13-defd0b8bcd73",
   ...
 >}

has_many

Think of has_many as the opposite site of a belongs_to relationship. In our shop example a Category has many Products.

+----------+      +-------------+
| Category |      | Product     |
+----------+      +-------------+
| id       |----->| category_id |
| name     |      | id          |
|          |      | name        |
|          |      | price       |
+----------+      +-------------+

Do you like video tutorials? Have a look at "has_many in 2m 19s" in our @elixir-phoenix-ash YouTube Channel.

Using the belongs_to example and setup we can now add a has_many relationship to the Category resource:

lib/app/shop/resources/category.ex
defmodule App.Shop.Category do
  use Ash.Resource, data_layer: Ash.DataLayer.Ets

  attributes do
    uuid_primary_key :id
    attribute :name, :string
  end

  relationships do
    has_many :products, App.Shop.Product (1)
  end

  actions do
    defaults [:create, :read, :update, :destroy]
  end

  code_interface do
    define_for App.Shop
    define :create
    define :read
    define :by_id, get_by: [:id], action: :read
    define :by_name, get_by: [:name], action: :read
    define :update
    define :destroy
  end
end
1The has_many macro defines a relationship between two resources. In our example, a Category has many Products. For that to work we need a Product resource. By default, the source_attribute is assumed to be :id and destination_attribute defaults to <snake_cased_last_part_of_module_name>_id. To override those defaults have a look at https://hexdocs.pm/ash/relationships.html and https://ash-hq.org/docs/dsl/ash-resource#relationships-has_many

Let’s play with the new relationship:

$ iex -S mix
Compiling 1 file (.ex)
Erlang/OTP 26 [...]

Interactive Elixir (1.15.5) [...]
iex(1)> alias App.Shop.Product
App.Shop.Product
iex(2)> alias App.Shop.Category
App.Shop.Category
iex(3)> fruits = Category.create!(%{name: "Fruits"}) (1)
#App.Shop.Category<
  products: #Ash.NotLoaded<:relationship>,
  __meta__: #Ecto.Schema.Metadata<:loaded>,
  id: "c77919cf-0a28-4394-96f1-28f70f1d748a",
  name: "Fruits",
  ...
>
iex(4)> Product.create!(%{name: "Orange", category_id: fruits.id}) (2)
#App.Shop.Product<
  category: #Ash.NotLoaded<:relationship>,
  __meta__: #Ecto.Schema.Metadata<:loaded>,
  id: "3ec1c834-70a8-403d-8814-3070c77b525e",
  name: "Orange",
  price: nil,
  category_id: "c77919cf-0a28-4394-96f1-28f70f1d748a",
  ...
>
iex(5)> Product.create!(%{name: "Banana", category_id: fruits.id})
#App.Shop.Product<
  category: #Ash.NotLoaded<:relationship>,
  __meta__: #Ecto.Schema.Metadata<:loaded>,
  id: "460d8cfa-2dad-4da0-95db-45012aa33621",
  name: "Banana",
  price: nil,
  category_id: "c77919cf-0a28-4394-96f1-28f70f1d748a",
  ...
>
iex(6)> App.Shop.load(fruits, :products) (3)
{:ok,
 #App.Shop.Category<
   products: [
     #App.Shop.Product<
       category: #Ash.NotLoaded<:relationship>,
       __meta__: #Ecto.Schema.Metadata<:loaded>,
       id: "3ec1c834-70a8-403d-8814-3070c77b525e",
       name: "Orange",
       price: nil,
       category_id: "c77919cf-0a28-4394-96f1-28f70f1d748a",
       ...
     >,
     #App.Shop.Product<
       category: #Ash.NotLoaded<:relationship>,
       __meta__: #Ecto.Schema.Metadata<:loaded>,
       id: "460d8cfa-2dad-4da0-95db-45012aa33621",
       name: "Banana",
       price: nil,
       category_id: "c77919cf-0a28-4394-96f1-28f70f1d748a",
       ...
     >
   ],
   __meta__: #Ecto.Schema.Metadata<:loaded>,
   id: "c77919cf-0a28-4394-96f1-28f70f1d748a",
   name: "Fruits",
   aggregates: %{},
   calculations: %{},
   ...
 >}
iex(7)> Category.by_name!("Fruits", load: [:products]) (4)
#App.Shop.Category<
  products: [
    #App.Shop.Product<
      category: #Ash.NotLoaded<:relationship>,
      __meta__: #Ecto.Schema.Metadata<:loaded>,
      id: "3ec1c834-70a8-403d-8814-3070c77b525e",
      name: "Orange",
      price: nil,
      category_id: "c77919cf-0a28-4394-96f1-28f70f1d748a",
      ...
    >,
    #App.Shop.Product<
      category: #Ash.NotLoaded<:relationship>,
      __meta__: #Ecto.Schema.Metadata<:loaded>,
      id: "460d8cfa-2dad-4da0-95db-45012aa33621",
      name: "Banana",
      price: nil,
      category_id: "c77919cf-0a28-4394-96f1-28f70f1d748a",
      ...
    >
  ],
  __meta__: #Ecto.Schema.Metadata<:loaded>,
  id: "c77919cf-0a28-4394-96f1-28f70f1d748a",
  name: "Fruits",
  ...
>
1Create a category for fruits.
2Create two products and assign them to the fruits category.
3Load the products for the fruits category.
4Sideload all the products for the fruits category.

Sideload a has_many Relationship by Default

In case you always want to sideload all products of a category without adding load: [:category] to every call you can do the following:

lib/app/shop/resources/category.ex
defmodule App.Shop.Category do
  use Ash.Resource, data_layer: Ash.DataLayer.Ets

  attributes do
    uuid_primary_key :id
    attribute :name, :string
  end

  relationships do
    has_many :products, App.Shop.Product
  end

  actions do
    defaults [:create, :update, :destroy] (1)

    read :read do
      primary? true (2)
      prepare build(load: [:products]) (3)
    end
  end

  code_interface do
    define_for App.Shop
    define :create
    define :read
    define :by_id, get_by: [:id], action: :read
    define :by_name, get_by: [:name], action: :read
    define :update
    define :destroy
  end
end
1Don’t forget to remove :read from the defaults when you add a custom read action.
2See https://ash-hq.org/docs/guides/ash/latest/topics/actions#primary-actions
3Always sideload all products when fetching a Category.

Let’s test it in the iex:

iex(17)> Category.by_name!("Fruits").products |> Enum.map(& &1.name)
["Orange", "Banana"]

many_to_many

many_to_many is a special case of has_many where the relationship is symmetric. That means that the relationship is defined on both sides of the relationship. A good example is a Tag resource that can be assigned to multiple (or none) Product resources and a Product resource has multiple (or none) Tag resources assigned to it.

+---------+     +------------+     +--------+
| Product |     | ProductTag |     | Tag    |
+---------+     +------------+     +--------+
| id      |<--->| product_id |     | name   |
| name    |     | tag_id     |<--->| id     |
| price   |     |            |     |        |
+---------+     +------------+     +--------+

Do you like video tutorials? Have a look at "many_to_many Resources in Ash Framework Tutorial (4m 22s)" in our @elixir-phoenix-ash YouTube Channel.

Setup

We start with a clean slate. A fresh Ash app. Please move or delete an already existing Ash app if you have one under the directory name app. Feel free to copy and paste the following lines in your terminal or do it step by step following relationship setup.

mix new --sup app && cd app
awk '/defp deps do/,/\[/ {
       if ($0 ~ /\[/) {
           print $0;
           print "{:ash, \"~> 2.15.8\"}";
           next;
       }
   } 1' mix.exs > mix.exs.tmp
mv mix.exs.tmp mix.exs
mix deps.get
echo '[
  import_deps: [:ash],
  inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"]
]' > .formatter.exs
mkdir config
echo 'import Config
config :app, :ash_apis, [App.Shop]' > config/config.exs
mix format

We need the following resources for our many_to_many example:

  • Tag

  • Product

  • ProductTag (which will be the glue between the two other resources)

Please create the following files:

lib/app/shop/resources/tag.ex
defmodule App.Shop.Tag do
  use Ash.Resource, data_layer: Ash.DataLayer.Ets

  attributes do
    uuid_primary_key :id
    attribute :name, :string
  end

  relationships do
    many_to_many :products, App.Shop.Product do
      through App.Shop.ProductTag
      source_attribute_on_join_resource :tag_id
      destination_attribute_on_join_resource :product_id
    end (1)
  end

  actions do
    defaults [:read, :update, :destroy]

    create :create do
      primary? true
      argument :products, {:array, :map} (2)

      change manage_relationship(:products,
               type: :append_and_remove,
               on_no_match: :create
             ) (3)
    end
  end

  code_interface do
    define_for App.Shop
    define :create
    define :read
    define :by_id, get_by: [:id], action: :read
    define :by_name, get_by: [:name], action: :read
    define :update
    define :destroy
  end
end
1The configuration for the many_to_many relationship to product.
2Now we can use the products: [] argument to do this: Tag.create!(%{name: "Sweet", products: [apple, cherry]})
3This uses the products: [] argument to create a Tag resource with a list of Product resources assigned to it.
lib/app/shop/resources/product.ex
defmodule App.Shop.Product do
  use Ash.Resource, data_layer: Ash.DataLayer.Ets

  attributes do
    uuid_primary_key :id
    attribute :name, :string
    attribute :price, :decimal
  end

  relationships do
    many_to_many :tags, App.Shop.Tag do (1)
      through App.Shop.ProductTag
      source_attribute_on_join_resource :product_id
      destination_attribute_on_join_resource :tag_id
    end
  end

  actions do
    defaults [:read, :update, :destroy]

    create :create do
      primary? true
      argument :tags, {:array, :map} (2)

      change manage_relationship(:tags,
               type: :append_and_remove,
               on_no_match: :create
             ) (3)
    end
  end

  code_interface do
    define_for App.Shop
    define :create
    define :read
    define :by_id, get_by: [:id], action: :read
    define :by_name, get_by: [:name], action: :read
    define :update
    define :destroy
  end
end
1The configuration for the many_to_many relationship to tag.
2Now we can use the tags: [] argument to do this: Product.create!(%{name: "Banana", tags: [sweet, tropical]})
3This uses the tags: [] argument to create a Product resource with a list of Tag resources assigned to it.
lib/app/shop/resources/product_tag.ex
defmodule App.Shop.ProductTag do
  use Ash.Resource, data_layer: Ash.DataLayer.Ets

  actions do
    defaults [:create, :read, :destroy] (1)
  end

  relationships do (2)
    belongs_to :product, App.Shop.Product do
      primary_key? true
      allow_nil? false
    end

    belongs_to :tag, App.Shop.Tag do
      primary_key? true
      allow_nil? false
    end
  end
end
1No need for an :update action. Entries in the join table are immutable. You can delete but not change them.
2The glue between the Product and the Tag resources.

Finally we have to add the Tag and ProductTag resources to the App.Shop API module.

lib/app/shop.ex
defmodule App.Shop do
  use Ash.Api

  resources do
    resource App.Shop.Product
    resource App.Shop.ProductTag
    resource App.Shop.Tag
  end
end

Now we have a working many_to_many relationship between Tag and Product.

Example in the iex

Let’s use the following data for our example.

Ash does use UUIDs. I use integer IDs in the example table because it’s easier to read for humans.
Product:              Tag:
+----+--------+       +----+----------+
| id | name   |       | id | Name     |
+----+--------+       +----+----------+
| 1  | Apple  |       | 1  | Sweet    |
| 2  | Banana |       | 2  | Tropical |
| 3  | Cherry |       | 3  | Red      |
+----+--------+       +----+----------+

ProductTag:
+-----------+-------+
| product_id| tag_id|
+-----------+-------+
| 1         | 1     |  (Apple is Sweet)
| 1         | 3     |  (Apple is Red)
| 2         | 1     |  (Banana is Sweet)
| 2         | 2     |  (Banana is Tropical)
| 3         | 3     |  (Cherry is Red) (1)
+-----------+-------+
1Not a complete list of all real world combinations. I am aware that cherries can be sweet too. 😉

Open the iex and create all the products with their tags.

$ iex -S mix
iex(1)> alias App.Shop.Tag
iex(2)> alias App.Shop.Product
iex(3)> sweet = Tag.create!(%{name: "Sweet"})
iex(4)> tropical = Tag.create!(%{name: "Tropical"})
iex(5)> red = Tag.create!(%{name: "Red"})
iex(6)> Product.create!(%{name: "Apple", tags: [sweet, red]})
iex(7)> Product.create!(%{name: "Banana", tags: [sweet, tropical]})
iex(8)> Product.create!(%{name: "Cherry", tags: [red]})

Now we can read all products with their tags and vice versa.

iex(9)> Product.read!(load: [:tags]) |>
...(9)> Enum.map(fn product ->
...(9)>   %{
...(9)>     product_name: product.name,
...(9)>     tag_names: Enum.map(product.tags, & &1.name)
...(9)>   }
...(9)> end)
[
  %{product_name: "Banana", tag_names: ["Sweet", "Tropical"]},
  %{product_name: "Apple", tag_names: ["Sweet", "Red"]},
  %{product_name: "Cherry", tag_names: ["Red"]}
]

iex(10)> Tag.read!(load: [:products]) |>
...(10)> Enum.map(fn tag ->
...(10)>   %{
...(10)>     tag_name: tag.name,
...(10)>     product_names: Enum.map(tag.products, & &1.name)
...(10)>   }
...(10)> end)
[
  %{tag_name: "Tropical", product_names: ["Banana"]},
  %{tag_name: "Red", product_names: ["Cherry", "Apple"]},
  %{tag_name: "Sweet", product_names: ["Apple", "Banana"]}
]

many_to_many sideloading by default

Be default Ash will not load the join table entries. You can change this with the :load option in the :read action:

lib/app/product.ex
  # [...]

  actions do
    defaults [:update, :destroy] (1)

    read :read do
      primary? true
      prepare build(load: [:tags]) (2)
    end

    create :create do
      primary? true
      argument :tags, {:array, :map}

      change manage_relationship(:tags,
               type: :append_and_remove,
               on_no_match: :create
             )
    end
  end

  # [...]
1Don’t forget to remove :read here.
2Always sideload the tags relationship.
lib/app/product.ex
  # [...]

  actions do
    defaults [:update, :destroy] (1)

    read :read do
      primary? true
      prepare build(load: [:products]) (2)
    end

    create :create do
      primary? true
      argument :products, {:array, :map}

      change manage_relationship(:products,
               type: :append_and_remove,
               on_no_match: :create
             )
    end
  end

  # [...]
1Don’t forget to remove :read here.
2Always sideload the products relationship.

Let’s use it in the iex console:

$ iex -S mix
iex(1)> alias App.Shop.Tag
iex(2)> alias App.Shop.Product
iex(3)> sweet = Tag.create!(%{name: "Sweet"})
iex(4)> tropical = Tag.create!(%{name: "Tropical"})
iex(5)> red = Tag.create!(%{name: "Red"})
iex(6)> Product.create!(%{name: "Apple", tags: [sweet, red]})
iex(7)> Product.create!(%{name: "Banana", tags: [sweet, tropical]})
iex(8)> Product.create!(%{name: "Cherry", tags: [red]})

iex(9)> Product.read! |> (1)
...(9)> Enum.map(fn product ->
...(9)>   %{
...(9)>     product_name: product.name,
...(9)>     tag_names: Enum.map(product.tags, & &1.name)
...(9)>   }
...(9)> end)
[
  %{product_name: "Banana", tag_names: ["Sweet", "Tropical"]},
  %{product_name: "Apple", tag_names: ["Sweet", "Red"]},
  %{product_name: "Cherry", tag_names: ["Red"]}
]

iex(10)> Tag.read! |> (2)
...(10)> Enum.map(fn tag ->
...(10)>   %{
...(10)>     tag_name: tag.name,
...(10)>     product_names: Enum.map(tag.products, & &1.name)
...(10)>   }
...(10)> end)
[
  %{tag_name: "Tropical", product_names: ["Banana"]},
  %{tag_name: "Red", product_names: ["Cherry", "Apple"]},
  %{tag_name: "Sweet", product_names: ["Apple", "Banana"]}
]
1We don’t have to specify load: [:tags] here because we set it as the default in the :read action.
2We don’t have to specify load: [:tags] here because we set it as the default in the :read action.

Update many_to_many relationships

Sometimes we want to update the tags of a product resource. It feels most natural to do it via the update action of the product resource. For that to work we have to define a custom :update action that will update the tags relationship. We can more or less copy the code from the :create action for that:

lib/app/shop/resources/product.ex
  # [...]
  actions do
    defaults [:read, :destroy]

    create :create do
      primary? true
      argument :tags, {:array, :map}

      change manage_relationship(:tags,
               type: :append_and_remove,
               on_no_match: :create
             )
    end

    update :update do (1)
      primary? true
      argument :tags, {:array, :map}

      change manage_relationship(:tags,
               type: :append_and_remove,
               on_no_match: :create
             )
    end
  end
  # [...]
1Same as the :create action just with :update.

Let’s use it in the iex console. We first create a product with two tags and than we update it to have only one tag:

$ iex -S mix
iex(1)> alias App.Shop.Tag
iex(2)> alias App.Shop.Product
iex(3)> good_deal = Tag.create!(%{name: "Good deal"})
iex(4)> yellow = Tag.create!(%{name: "Yellow"})
iex(5)> Product.create!(%{name: "Banana", tags: [yellow, good_deal]}) (1)
iex(6)> Product.by_name!("Banana", load: [:tags]).tags |> Enum.map(& &1.name) (2)
["Yellow", "Good deal"]
iex(7)> banana = Product.by_name!("Banana") (3)
iex(8)> Product.update!(banana, %{tags: [yellow]}) (4)
iex(9)> Product.by_name!("Banana", load: [:tags]).tags |> Enum.map(& &1.name) (5)
["Yellow"]
1Create a new product with two tags.
2Query the just created product and print the two tag names.
3Store the product in the variable banana for later use.
4Update the product to have only one tag.
5Double check that the product really only has one tag.

The between resource ProductTag is automatically updated. And by update I mean that one entry was deleted.

Unique Tags

We don’t want to have multiple tags with the same name. But right now this is possible:

$ iex -S mix
iex(1)> alias App.Shop.Tag
iex(2)> Tag.create!(%{name: "Yellow"}).id
"d206b758-253d-4f06-9773-5423ae1f6027"
iex(3)> Tag.create!(%{name: "Yellow"}).id
"5d66386c-bb02-4a8e-bf2a-5457477a6da2"
iex(4)> Tag.create!(%{name: "Yellow"}).id
"3497214e-83a0-43bd-b087-143af5ef8c37"
iex(5)> Tag.read! |> Enum.map(& &1.name)
["Yellow", "Yellow", "Yellow"]

We can fix this with identities in the resource:

lib/app/shop/resources/tag.ex
defmodule App.Shop.Tag do
  use Ash.Resource, data_layer: Ash.DataLayer.Ets

  attributes do
    uuid_primary_key :id
    attribute :name, :string
  end

  identities do
    # identity :unique_name, [:name] (1)

    identity :name, [:name] do (2)
      pre_check_with App.Shop (3)
    end
  end
  # [...]
1Use this if you use a PostgreSQL database. Don’t forget to run a mix ash.codegen after you added it.
2Use this if your use a ETS data layer like we do in this example.
3Since ETS doesn’t support unique indexes we have to check for uniqueness before we create it.

Now we can not create multiple tags with the same name anymore:

$ iex -S mix
iex(1)> alias App.Shop.Tag
iex(2)> Tag.create!(%{name: "Yellow"}).id
"f03e163f-5a17-4ea4-b708-f2089234d642"
iex(3)> Tag.create!(%{name: "Yellow"}).id
** (Ash.Error.Invalid) Input Invalid

* name: has already been taken
    (ash 2.14.18) lib/ash/api/api.ex:2179: Ash.Api.unwrap_or_raise!/3
iex(3)> Tag.create(%{name: "Yellow"}).id
** (KeyError) key :id not found in: {:error,
 %Ash.Error.Invalid{
   errors: [
     %Ash.Error.Changes.InvalidChanges{
       fields: [:name],
       message: "has already been taken",
       [...]

add_tag action

Sometimes it is useful to have an add_tag argument that creates and adds a new tag to a new product in one go ( e.g. create!(%{name: "Banana", add_tag: %{name: "Yellow"}})):

lib/app/shop/resources/product.ex
  # [...]
  actions do
    defaults [:read, :destroy, :update]

    create :create do (1)
      primary? true
      argument :tags, {:array, :map}

      argument :add_tag, :map do
        allow_nil? true
      end

      change manage_relationship(:tags,
               type: :append_and_remove,
               on_no_match: :create
             )

      change manage_relationship(
               :add_tag,
               :tags,
               type: :create
             )
    end
  end
  [...]
1You can copy-paste the code for update :update do too if you want to be able to add tags to existing products.

Let’s test it:

$ iex -S mix
iex(1)> App.Shop.Product.create!(%{name: "Banana", add_tag: %{name: "Yellow"}})
#App.Shop.Product<
  tags: [
    #App.Shop.Tag<
      products: #Ash.NotLoaded<:relationship>,
      products_join_assoc: #Ash.NotLoaded<:relationship>,
      __meta__: #Ecto.Schema.Metadata<:loaded>,
      id: "9b95f8cf-9f95-409a-81d3-b6a66e470d2b",
      name: "Yellow",
      ...
    >
  ],
  tags_join_assoc: #Ash.NotLoaded<:relationship>,
  __meta__: #Ecto.Schema.Metadata<:loaded>,
  id: "52049582-c3cb-458c-bbac-0ba36e57e234",
  name: "Banana",
  price: nil,
  ...
>

has_one

has_one is similar to belongs_to except that the reference attribute is on the destination resource, instead of the source.

Let’s assume we run special promotions in our shop (so and so many percent rebate off). But each product can only have one promotion and each promotion can only be used for one product.

+---------+      +------------+
| Product |      | Promotion  |
+---------+      +------------+
| id      |----->| product_id |
| name    |      | id         |
| price   |      | rebate     |
+---------+      +------------+

Let’s adjust our source code accordingly:

lib/app/shop.ex
defmodule App.Shop do
  use Ash.Api

  resources do
    resource App.Shop.Product
    resource App.Shop.Category
    resource App.Shop.Promotion
  end
end
lib/app/shop/resources/promotion.ex
defmodule App.Shop.Promotion do
  use Ash.Resource, data_layer: Ash.DataLayer.Ets

  attributes do
    uuid_primary_key :id
    attribute :name, :string
    attribute :rebate, :integer
    attribute :product_id, :uuid
  end

  relationships do
    belongs_to :product, App.Shop.Product do
      attribute_writable? true
    end
  end

  actions do
    defaults [:create, :read, :update, :destroy]
  end

  code_interface do
    define_for App.Shop
    define :create
    define :read
    define :by_id, get_by: [:id], action: :read
    define :by_name, get_by: [:name], action: :read
    define :update
    define :destroy
  end
end
lib/app/shop/resources/product.ex
defmodule App.Shop.Product do
  use Ash.Resource, data_layer: Ash.DataLayer.Ets

  attributes do
    uuid_primary_key :id
    attribute :name, :string
    attribute :price, :decimal
  end

  relationships do
    belongs_to :category, App.Shop.Category do
      attribute_writable? true
    end

    has_one :promotion, App.Shop.Promotion
  end

  actions do
    defaults [:create, :read, :update, :destroy]
  end

  code_interface do
    define_for App.Shop
    define :create
    define :read
    define :by_id, get_by: [:id], action: :read
    define :by_name, get_by: [:name], action: :read
    define :update
    define :destroy
  end
end

Let’s use it in the iex console:

$ iex -S mix
Erlang/OTP 26 [...]

Interactive Elixir (1.15.5) [...]
iex(1)> alias App.Shop.Product
App.Shop.Product
iex(2)> alias App.Shop.Promotion
App.Shop.Promotion
iex(3)> orange = Product.create!(%{name: "Orange", price: 0.2})
#App.Shop.Product<
  promotion: #Ash.NotLoaded<:relationship>,
  category: #Ash.NotLoaded<:relationship>,
  __meta__: #Ecto.Schema.Metadata<:loaded>,
  id: "c9e9b4ba-408f-4c42-b1e0-e8b3799d5b1f",
  name: "Orange",
  price: Decimal.new("0.2"),
  category_id: nil,
  ...
>
iex(4)> {:ok, promotion} = Promotion.create(%{name: "15% off", rebate: 15, product_id: orange.id})
{:ok,
 #App.Shop.Promotion<
   product: #Ash.NotLoaded<:relationship>,
   __meta__: #Ecto.Schema.Metadata<:loaded>,
   id: "68901cef-f2c5-46bb-a737-d6c248d36347",
   name: "15% off",
   rebate: 15,
   product_id: "c9e9b4ba-408f-4c42-b1e0-e8b3799d5b1f",
   ...
 >}
iex(5)> App.Shop.load(orange, :promotion) (1)
{:ok,
 #App.Shop.Product<
   promotion: #App.Shop.Promotion<
     product: #Ash.NotLoaded<:relationship>,
     __meta__: #Ecto.Schema.Metadata<:loaded>,
     id: "68901cef-f2c5-46bb-a737-d6c248d36347",
     name: "15% off",
     rebate: 15,
     product_id: "c9e9b4ba-408f-4c42-b1e0-e8b3799d5b1f",
     ...
   >,
   category: #Ash.NotLoaded<:relationship>,
   __meta__: #Ecto.Schema.Metadata<:loaded>,
   id: "c9e9b4ba-408f-4c42-b1e0-e8b3799d5b1f",
   name: "Orange",
   price: Decimal.new("0.2"),
   category_id: nil,
   ...
 >}
1We have to load the promotion explicitly. It is not loaded by default.

If you haven’t missed has_one in your life so fare I wouldn’t use it at all. belongs_to is probably the better choice.