02-DataLayer(ETS)

Now it is time to enable data persistence. When we configure a persistent data-layer - Ash makes many new features available - these include:

  • Unique Resources (Identities)
  • Resource Queries (at the data layer)
  • Data Calculations (at the Data Layer)
  • Relationships between Resources
  • Aggregate Queries between Resource Relationships

We will start with simple persistance (ETS) an OTP based memory based storage The we will switch to using postgreSQL for long-term storage.

ETS is wonderful for experimenting and prototyping without having to do migrations, etc.

ETS Persistence

In order to read, update and generally execute queries we will add persistance. We will start with an OTP based method using (ETS) in memory persistance. In a separate tutorial we will switch to PostgreSQL.

ETS is an in-memory (OTP based) way to persist data (we will work with PostgreSQL later). Once we have persisted data we can explore relationships.

To add ETS to the Data Layer we need to change the line use Ash.Resource to:

# lib/support/resources/user.ex
defmodule Support.User do
  use Ash.Resource,
    data_layer: Ash.DataLayer.Ets
  # ...
end

Lets try this out - we will create several user and then query for them.

iex -S mix phx.server

customer = (
  Support.User
  |> Ash.Changeset.for_create(
      :new_customer, %{first_name: "Ratna", last_name: "Sönam", email: "nyima@example.com"}
    )
  |> Support.AshApi.create!()
)
employee = (
  Support.User
  |> Ash.Changeset.for_create(
      :new_employee, %{first_name: "Nyima", last_name: "Sönam", email: "nyima@example.com",
                       department_name: "Office Actor", account_type: :employee}
    )
  |> Support.AshApi.create!()
)
admin = (
  Support.User
  |> Ash.Changeset.for_create(
      :new_admin, %{first_name: "Karma", last_name: "Sönam", email: "karma@example.com",
                    department_name: "Office Admin", account_type: :employee, admin: true}
    )
  |> Support.AshApi.create!()
)

# now we should be able to 'read' all our users:
Support.AshApi.read!(Support.User)

Identity (Uniqueness)

In order to ensure that the email is a unique identifier - we use the identities feature. We can test that one or multiple attributes build a unique identity.

This feature behaves differently depending on the Data Layer in use. In particular, from the docs, we see Ash.DataLayer.Ets will actually require you to set pre_check_with since the ETS data layer has no built in support for unique constraints.

For a single

  identities do
    identity :email, [:email], pre_check_with: Support.AshApi
    identity :names, [:first_name, :middle_name, :last_name], pre_check_with: Support.AshApi
  end

Let’s test the uniqueness of the email attribute - identity :email, [:email], pre_check_with: Support.AshApi

iex -S mix phx.server

customer = (
  Support.User
  |> Ash.Changeset.for_create(
      :new_customer, %{first_name: "Ratna", last_name: "Sönam", email: "ratna@example.com"}
    )
  |> Support.AshApi.create!()
)
employee = (
  Support.User
  |> Ash.Changeset.for_create(
      :new_employee, %{first_name: "Nyima", last_name: "Sönam", email: "ratna@example.com",
                       department_name: "Office Actor", account_type: :employee}
    )
  |> Support.AshApi.create!()
)
# we should get this error
** (Ash.Error.Invalid) Input Invalid

* email: has already been taken.

# But the following should work
employee = (
  Support.User
  |> Ash.Changeset.for_create(
      :new_employee, %{first_name: "Nyima", last_name: "Sönam", email: "nyima@example.com",
                       department_name: "Office Actor", account_type: :employee}
    )
  |> Support.AshApi.create!()
)

Let’s test the uniqueness of the all the name attributes - identity :names, [:first_name, :middle_name, :last_name], pre_check_with: Support.AshApi

iex -S mix phx.server

employee1 = (
  Support.User
  |> Ash.Changeset.for_create(
      :new_employee, %{first_name: "Nyima", last_name: "Sönam", email: "nyima@example.com",
                       department_name: "Office Actor"}
    )
  |> Support.AshApi.create!()
)
employee2 = (
  Support.User
  |> Ash.Changeset.for_create(
      :create, %{first_name: "Nyima", last_name: "Sönam", email: "nyima_karma@example.com",
                       department_name: "Office Actor", account_type: :employee}
    )
  |> Support.AshApi.create!()
)
# we should get this error
** (Ash.Error.Invalid) Input Invalid

* email: has already been taken.

# But the following should work
employee = (
  Support.User
  |> Ash.Changeset.for_create(
      :new_employee, %{first_name: "Nyima", last_name: "Sönam", email: "nyima@example.com",
                       department_name: "Office Actor", account_type: :employee}
    )
  |> Support.AshApi.create!()
)

We can also now read back from our DataLayer and we should now be able to read back the users we have created with:

Support.User
|> Support.AshApi.read!()