Identities
Identities do just one thing: They tell Ash that a certain attribute or combination of attributes is unique. Which means that a value of that attribute can only exist once in the database. This can be useful for things like usernames, email addresses, etc. Ash uses this information to do some background magic (e.g. it creates a unique index in a PostgreSQL database).
| Identities are not part of the validation process. They are checked before the validation. |
ETS
If you are using ETS - the non persisting database we use for most of
our examples - you have to implement a pre_check_with callback in
the resource because ETS does not offer a unique index like PostgreSQL
does.
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 do
allow_nil? false
public? true
end
attribute :price, :decimal do
allow_nil? false
public? true
end
end
identities do
identity :unique_name, [:name] do
pre_check_with App.Shop (1)
end
end
actions do
default_accept [:name, :price]
defaults [:create, :read, :update, :destroy]
end
end
| 1 | pre_check_with App.Shop tells Ash to run a read via that domain
before writing, so duplicates fail with a clean changeset error
instead of a runtime crash. |
PostgreSQL
With AshPostgres the database enforces uniqueness, so you only need
the identity line (no pre_check_with):
[...]
identities do
identity :unique_name, [:name] (1)
end
[...]
| 1 | Named unique_name, makes name unique across the products
table. |
Don’t forget to run mix ash.codegen <migration_name> and
mix ash_postgres.migrate after adding an identity so the unique
index lands in the database.
|
Test in iex
Let’s try to create a product with the same name twice:
$ iex -S mix
iex(1)> App.Shop.create_product!(%{name: "Banana", price: Decimal.new("0.1")})
%App.Shop.Product{
id: "321070d5-6cec-4054-a99a-c5036a80e7d0",
name: "Banana",
price: Decimal.new("0.1"),
__meta__: #Ecto.Schema.Metadata<:loaded>
}
iex(2)> App.Shop.create_product!(%{name: "Banana", price: Decimal.new("0.2")})
** (Ash.Error.Invalid) Invalid Error
* name: has already been taken
(ash 3.24.3) lib/ash/code_interface.ex:...: Ash.CodeInterface.create_act!/4
Identities with multiple attributes
Sometimes you have to combine multiple attributes. I can not think of a good example in our product resource. So let’s discuss this in an imaginary resource for flight reservations. A passenger can book a flight which is represented by a flight number and a date. We want to make sure that a passenger can only book the same flight once per day.
defmodule App.Airline.Reservation do
use Ash.Resource,
data_layer: Ash.DataLayer.Ets,
domain: App.Airline,
otp_app: :app
attributes do
uuid_primary_key :id
attribute :passenger_id, :integer, public?: true
attribute :flight_number, :string, public?: true
attribute :date, :date, public?: true
end
actions do
default_accept [:passenger_id, :flight_number, :date]
defaults [:create, :read, :update, :destroy]
end
identities do
# identity :unique_booking, [:passenger_id, :flight_number, :date] (1)
identity :unique_booking, [:passenger_id, :flight_number, :date] do
pre_check_with App.Airline
end
end
end
| 1 | This would be the PostgreSQL version. |
Case Insensitive Identities
Sometimes you want to make sure that an attribute is unique but you don’t want to care about the case. For example you want to make sure that an email address is unique but you don’t want to care about the case.
In those cases you can use the :ci_string type. It is a string that is stored in the database as a string but it is compared case insensitive.
defmodule App.Shop.Customer do
use Ash.Resource,
data_layer: Ash.DataLayer.Ets,
domain: App.Shop,
otp_app: :app
attributes do
uuid_primary_key :id
attribute :name, :string do
allow_nil? false
public? true
end
attribute :email, :ci_string do
allow_nil? false
public? true
end
end
identities do
# identity :unique_email, [:email] (1)
identity :unique_email, [:email] do
pre_check_with App.Shop
end
end
actions do
default_accept [:name, :email]
defaults [:create, :read, :update, :destroy]
end
end
| 1 | Use this version for PostgreSQL. |
|
PostgreSQL users have to add the citext extension. See the AshPostgres.Repo behaviour. lib/app/repo.ex
After that change run |