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
1 | The relationships macro defines relationships between resources. |
2 | The 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 |
3 | By 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
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<
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]
.