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
1 | The relationships macro defines relationships between resources. |
2 | The 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 |
3 | By 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"
1 | Let’s save a bit of typing by creating shorter Aliases. |
2 | Create a new Category for "Fruits" and store it in the variable fruits . |
3 | Create a new Product for "Orange" which belongs to the Category "Fruits" and store it in the variable orange . |
4 | One way to get the Category of a Product if that wasn’t sideloaded initially. |
5 | Sideload 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
1 | Don’t forget to remove :read from the defaults when you add a custom read action. |
2 | See https://ash-hq.org/docs/guides/ash/latest/topics/actions#primary-actions |
3 | Always 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",
...
>}