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].