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       |
+----------+      +-------------+

We need a new Category resource:

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

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

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

And we need to add code interface definitions to our domain:

lib/app/shop.ex
defmodule App.Shop do
  use Ash.Domain, otp_app: :app

  resources do
    resource App.Shop.Product do
      define :create_product, action: :create
      define :read_products, action: :read
      define :get_product_by_id, action: :read, get_by: :id
      define :get_product_by_name, action: :read, get_by: :name
      define :update_product, action: :update
      define :destroy_product, action: :destroy
    end

    resource App.Shop.Category do
      define :create_category, action: :create
      define :read_categories, action: :read
      define :get_category_by_id, action: :read, get_by: :id
      define :get_category_by_name, action: :read, get_by: :name
      define :update_category, action: :update
      define :destroy_category, action: :destroy
    end
  end
end

To configure the belongs_to relationship to Category we add a relationships block to the Product resource:

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

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

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

  actions do
    defaults [:create, :read, :update, :destroy]
  end
end
1The relationships macro defines relationships between resources.
2The source_attribute is defined as :<relationship_name>_id (category_id in this case) 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
3By default the category_id can be nil. Setting allow_nil? to false makes the relationship required.

Let’s test this in the iex:

$ iex -S mix
iex(1)> # Create a new category
iex(2)> {:ok, fruits} = App.Shop.create_category(%{name: "Fruits"})
{:ok,
 #App.Shop.Category<
   __meta__: #Ecto.Schema.Metadata<:loaded>,
   id: "91cb42d8-45c2-451d-8261-72ae4d94a3c6",
   name: "Fruits",
   ...
 >}
iex(3)> # Create a new product in the "Fruits" category
iex(4)> {:ok, orange} = App.Shop.create_product(%{
                  name: "Orange",
                  price: 0.15,
                  category_id: fruits.id
                })
{:ok,
 #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)> # Load the category relationship for the orange product
iex(6)> {:ok, orange_with_category} = Ash.load(orange, :category)
{: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(7)> # Fetch a product with its category pre-loaded
iex(8)> {:ok, orange2} = App.Shop.get_product_by_name("Orange", load: [:category])
{: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(9)> orange2.category.name
"Fruits"

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 customize the read action:

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

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

  relationships do
    belongs_to :category, App.Shop.Category do
      allow_nil? false
    end
  end

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

    read :read do
      primary? true (2)
      prepare build(load: [:category]) (3)
    end
  end
end
1Don’t include :read in the defaults when you add a custom read action.
2This marks this action as the primary read action for the resource.
3The prepare step always sideloads the Category when fetching a Product.

Let’s test it in the iex:

iex(10)> {:ok, orange} = App.Shop.get_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",
   ...
 >}

Note how the category is automatically loaded even though we didn’t specify load: [:category].