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, public?: true
  end

  actions do
    default_accept [:name]
    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, public?: true
    attribute :price, :decimal, public?: true
  end

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

  actions do
    default_accept [:name, :price, :category_id] (5)
    defaults [:create, :read, :update, :destroy]
  end
end
1 The relationships macro defines relationships between resources.
2 By convention Ash creates a source attribute named <relationship>_id (category_id here) of type :uuid, matching the :id primary key on the destination. See https://hexdocs.pm/ash/relationships.html if you need to override those defaults.
3 By default category_id can be nil. Setting allow_nil? to false makes the relationship required.
4 In Ash 3.x relationships are also private by default. Make them public?: true when you want the outside world to see them (including API extensions such as AshJsonApi or AshGraphql).
5 The default create/update actions need to accept the foreign key for the relationship to be settable through the code interface.

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{
   id: "cea38a50-2178-42c0-aad9-2624eec49609",
   name: "Fruits",
   __meta__: #Ecto.Schema.Metadata<:loaded>
 }}
iex(3)> # Create a new product in the "Fruits" category
iex(4)> {:ok, orange} = App.Shop.create_product(%{
                  name: "Orange",
                  price: Decimal.new("0.15"),
                  category_id: fruits.id
                })
{:ok,
 %App.Shop.Product{
   id: "fc36e094-a28d-4f78-860d-0fd1e345c1ce",
   name: "Orange",
   price: Decimal.new("0.15"),
   category_id: "cea38a50-2178-42c0-aad9-2624eec49609",
   category: #Ash.NotLoaded<:relationship, field: :category>,
   __meta__: #Ecto.Schema.Metadata<:loaded>
 }}
iex(5)> # Load the category relationship for the orange product
iex(6)> {:ok, orange_with_category} = Ash.load(orange, :category)
{:ok,
 %App.Shop.Product{
   id: "fc36e094-a28d-4f78-860d-0fd1e345c1ce",
   name: "Orange",
   price: Decimal.new("0.15"),
   category_id: "cea38a50-2178-42c0-aad9-2624eec49609",
   category: %App.Shop.Category{
     id: "cea38a50-2178-42c0-aad9-2624eec49609",
     name: "Fruits",
     __meta__: #Ecto.Schema.Metadata<:loaded>
   },
   __meta__: #Ecto.Schema.Metadata<:loaded>
 }}
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{
   id: "fc36e094-a28d-4f78-860d-0fd1e345c1ce",
   name: "Orange",
   price: Decimal.new("0.15"),
   category_id: "cea38a50-2178-42c0-aad9-2624eec49609",
   category: %App.Shop.Category{
     id: "cea38a50-2178-42c0-aad9-2624eec49609",
     name: "Fruits",
     __meta__: #Ecto.Schema.Metadata<:loaded>
   },
   __meta__: #Ecto.Schema.Metadata<:loaded>
 }}
iex(9)> orange2.category.name
"Fruits"
Ash 3.24+ inspects resources as plain %Module{} structs (the full set of attributes is visible). Older Ash tutorials may still show the opaque #Module<…​> form produced by an earlier custom Inspect implementation.

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, public?: true
    attribute :price, :decimal, public?: true
  end

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

  actions do
    default_accept [:name, :price, :category_id]
    defaults [:create, :update, :destroy] (1)

    read :read do
      primary? true (2)
      prepare build(load: [:category]) (3)
    end
  end
end
1 Don’t include :read in the defaults when you add a custom read action.
2 This marks this action as the primary read action for the resource.
3 The 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{
   id: "24348935-6148-4c75-9bf1-55f74ac9397a",
   name: "Orange",
   price: Decimal.new("0.15"),
   category_id: "22ab0824-18ac-4daa-9a13-defd0b8bcd73",
   category: %App.Shop.Category{
     id: "22ab0824-18ac-4daa-9a13-defd0b8bcd73",
     name: "Fruits",
     __meta__: #Ecto.Schema.Metadata<:loaded>
   },
   __meta__: #Ecto.Schema.Metadata<:loaded>
 }}

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