Run this notebook

Use Livebook to open this notebook and explore new ideas.

It is easy to get started, on your machine or the cloud.

Click below to open and run it in your Livebook at .

(or change your Livebook location)

<!-- livebook:{"persist_outputs":true} --> # Define Polymorphic Relationships ```elixir Mix.install([{:ash, "~> 3.0"}], consolidate_protocols: false) ``` <!-- livebook:{"output":true} --> ``` :ok ``` ## Introduction Something that comes up in more complex domains is the idea of "polymorphic relationships". For this example, we will use the concept of a `BankAccount`, which can be either a `SavingsAccount` or a `CheckingAccount`. All accounts have an `account_number` and `transactions`, but checkings & savings accounts might have their own specific information. For example, a `SavingsAccount` has an `interest_rate`, and a `CheckingAccount` has many `debit_card`s. Ash does not support polymorphic relationships defined _as relationships_, but you can accomplish similar functionality via [calculations](documentation/topics/resources/calculations.md) with the type `Ash.Type.Union`. For this tutorial, we will have a dedicated resource called `BankAccount`. I suggest taking that approach, as many things down the road will be simplified. With that said, you don't necessarily need to do that when there is no commonalities between the types. Instead of setting up the polymorphism on the `BankAccount` resource, you would define relationships to `SavingsAccount` and `CheckingAccount` directly. This tutorial is not attempting to illustrate good design of accounting systems. We make many concessions for the sake of the simplicity of our example. ## Defining our Resources ```elixir defmodule BankAccount do use Ash.Resource, domain: Finance, data_layer: Ash.DataLayer.Ets actions do defaults [:read, :destroy, create: [:account_number, :type]] end attributes do uuid_primary_key :id attribute :account_number, :integer, allow_nil?: false attribute :type, :atom, constraints: [one_of: [:checking, :savings]] end # calculations do # calculate :implementation, AccountImplementation, GetAccountImplementation do # allow_nil? false # end # end relationships do has_one :checking_account, CheckingAccount has_one :savings_account, SavingsAccount end end defmodule CheckingAccount do use Ash.Resource, domain: Finance, data_layer: Ash.DataLayer.Ets actions do defaults [:read, :destroy, create: [:bank_account_id]] end attributes do uuid_primary_key :id end identities do identity :unique_bank_account, [:bank_account_id], pre_check?: true end relationships do belongs_to :bank_account, BankAccount do allow_nil? false end end end defmodule SavingsAccount do use Ash.Resource, domain: Finance, data_layer: Ash.DataLayer.Ets actions do defaults [:read, :destroy, create: [:bank_account_id]] end attributes do uuid_primary_key :id end identities do identity :unique_bank_account, [:bank_account_id], pre_check?: true end relationships do belongs_to :bank_account, BankAccount do allow_nil? false end end end defmodule Finance do use Ash.Domain, validate_config_inclusion?: false resources do resource BankAccount resource SavingsAccount resource CheckingAccount end end ``` <!-- livebook:{"output":true} --> ``` {:module, Finance, <<70, 79, 82, 49, 0, 0, 44, ...>>, [ Ash.Domain.Dsl.Resources.Resource, Ash.Domain.Dsl.Resources.Options, %{opts: [], entities: [%Ash.Domain.Dsl.ResourceReference{...}, ...]} ]} ``` We haven't implemented the polymorphic part yet, but lets create a few of the above resources to show how they relate. Below we create a `BankAccount` for checkings, and a `BankAccount` for savings, and connect them to their "specific" types, i.e `CheckingAccount` and `SavingsAccount`. We load the data, you can see that one `BankAccount` has a `:checking_account` but no `:savings_account`. For the other, the opposite is the case. ```elixir bank_account1 = Ash.create!(BankAccount, %{account_number: 1, type: :checking}) bank_account2 = Ash.create!(BankAccount, %{account_number: 2, type: :savings}) checking_account = Ash.create!(CheckingAccount, %{bank_account_id: bank_account1.id}) savings_account = Ash.create!(SavingsAccount, %{bank_account_id: bank_account2.id}) [bank_account1, bank_account2] |> Ash.load!([:checking_account, :savings_account]) ``` <!-- livebook:{"output":true} --> ``` [ #BankAccount< implementation: #Ash.NotLoaded<:calculation, field: :implementation>, savings_account: nil, checking_account: #CheckingAccount< bank_account: #Ash.NotLoaded<:relationship, field: :bank_account>, __meta__: #Ecto.Schema.Metadata<:loaded>, id: "60f585f6-f352-410e-8c09-09ae49448851", bank_account_id: "21d27a6c-49b8-4984-a1b7-f2ef030626af", aggregates: %{}, calculations: %{}, ... >, __meta__: #Ecto.Schema.Metadata<:loaded>, id: "21d27a6c-49b8-4984-a1b7-f2ef030626af", account_number: 1, type: :checking, aggregates: %{}, calculations: %{}, ... >, #BankAccount< implementation: #Ash.NotLoaded<:calculation, field: :implementation>, savings_account: #SavingsAccount< bank_account: #Ash.NotLoaded<:relationship, field: :bank_account>, __meta__: #Ecto.Schema.Metadata<:loaded>, id: "d2bcc1cc-d709-418d-a9ff-0d21fd7d667b", bank_account_id: "4d8dc988-3e09-4efb-a643-d822013abfba", aggregates: %{}, calculations: %{}, ... >, checking_account: nil, __meta__: #Ecto.Schema.Metadata<:loaded>, id: "4d8dc988-3e09-4efb-a643-d822013abfba", account_number: 2, type: :savings, aggregates: %{}, calculations: %{}, ... > ] ``` ## Defining our union type Below we define an `Ash.Type.NewType`. This allows defining a new type that is the combination of an existing type and custom constraints. ```elixir defmodule AccountImplementation do use Ash.Type.NewType, subtype_of: :union, constraints: [ types: [ checking: [ type: :struct, constraints: [instance_of: CheckingAccount] ], savings: [ type: :struct, constraints: [instance_of: SavingsAccount] ] ] ] end ``` <!-- livebook:{"output":true} --> ``` {:module, AccountImplementation, <<70, 79, 82, 49, 0, 0, 44, ...>>, :ok} ``` ## Defining the calculation Next, we'll define a calculation resolves to the specific type of any given account. ```elixir defmodule GetAccountImplementation do use Ash.Resource.Calculation def load(_, _, _) do [:checking_account, :savings_account] end # This ensures that all attributes are selected by default # on the related loads we depend on. def strict_loads?, do: false def calculate(records, _, _) do Enum.map(records, &(&1.checking_account || &1.savings_account)) end end ``` <!-- livebook:{"output":true} --> ``` {:module, GetAccountImplementation, <<70, 79, 82, 49, 0, 0, 13, ...>>, {:calculate, 3}} ``` ## Adding the calculation to our resource Finally, we'll add the calculation to our `BankAccount` resource! For those following along with the LiveBook, go back up and uncomment the commented out calculation. Now we can load `:implementation` and see that, for one account, it resolves to a `CheckingAccount` and for the other it resolves to a `SavingsAccount`. ```elixir bank_account1 = Ash.create!(BankAccount, %{account_number: 1, type: :checking}) bank_account2 = Ash.create!(BankAccount, %{account_number: 2, type: :savings}) checking_account = Ash.create!(CheckingAccount, %{bank_account_id: bank_account1.id}) savings_account = Ash.create!(SavingsAccount, %{bank_account_id: bank_account2.id}) [bank_account1, bank_account2] |> Ash.load!([:implementation]) ``` <!-- livebook:{"output":true} --> ``` [ #BankAccount< implementation: #CheckingAccount< bank_account: #Ash.NotLoaded<:relationship, field: :bank_account>, __meta__: #Ecto.Schema.Metadata<:loaded>, id: "1d1a7b6c-dd08-4b8c-9769-2c1155a41a40", bank_account_id: "ce370381-303e-49dc-9950-a05b5052e7f8", aggregates: %{}, calculations: %{}, ... >, savings_account: #Ash.NotLoaded<:relationship, field: :savings_account>, checking_account: #Ash.NotLoaded<:relationship, field: :checking_account>, __meta__: #Ecto.Schema.Metadata<:loaded>, id: "ce370381-303e-49dc-9950-a05b5052e7f8", account_number: 1, type: :checking, aggregates: %{}, calculations: %{}, ... >, #BankAccount< implementation: #SavingsAccount< bank_account: #Ash.NotLoaded<:relationship, field: :bank_account>, __meta__: #Ecto.Schema.Metadata<:loaded>, id: "e537116d-c5b8-414a-a350-fa9b2afe0a4e", bank_account_id: "c4739158-2a1f-4de8-bccb-ee1d1ee9e602", aggregates: %{}, calculations: %{}, ... >, savings_account: #Ash.NotLoaded<:relationship, field: :savings_account>, checking_account: #Ash.NotLoaded<:relationship, field: :checking_account>, __meta__: #Ecto.Schema.Metadata<:loaded>, id: "c4739158-2a1f-4de8-bccb-ee1d1ee9e602", account_number: 2, type: :savings, aggregates: %{}, calculations: %{}, ... > ] ``` ## Taking it further One of the best things about using `Ash.Type.Union` is how it is *integrated*. Every extension (provided by the Ash team) supports working with unions. For example: * [Working with Unions in AshPhoenix.Form](https://hexdocs.pm/ash_phoenix/union-forms.html) * [AshGraphql & AshJsonApi Unions](https://hexdocs.pm/ash_graphql/use-unions-with-graphql.html) You can also synthesize filterable fields with calculations. For example, if you wanted to allow filtering `BankAccount` by `:interest_rate`, and that field only existed on `SavingsAccount`, you might have a calculation like this on `BankAccount`: <!-- livebook:{"force_markdown":true} --> ```elixir calculate :interest_rate, :decimal, expr( if type == :savings do savings_account.interest_rate else 0 end ) ``` This would allow usage like the following: <!-- livebook:{"force_markdown":true} --> ```elixir BankAccount |> Ash.Query.filter(interest_rate > 0.01) |> Ash.read!() ```
See source

Have you already installed Livebook?

If you already installed Livebook, you can configure the default Livebook location where you want to open notebooks.
Livebook up Checking status We can't reach this Livebook (but we saved your preference anyway)
Run notebook

Not yet? Install Livebook in just a minute

Livebook is open source, free, and ready to run anywhere.

Run on your machine

with Livebook Desktop

Run in the cloud

on select platforms

To run on Linux, Docker, embedded devices, or Elixir’s Mix, check our README.

PLATINUM SPONSORS
SPONSORS
Code navigation with go to definition of modules and functions Read More ×