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",
   ...
 >}