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. |
Do you like video tutorials? Have a look at "Identities" in our @elixir-phoenix-ash YouTube Channel. |
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 an unique index like PostgreSQL does.
[...]
attributes do
uuid_primary_key :id
attribute :name, :string do
allow_nil? false
end
attribute :price, :decimal do
allow_nil? false
end
end
identities do
identity :unique_name, [:name] do
pre_check_with App.Shop (1)
end
end
[...]
1 | This pre_check is used to check if the identity is unique. |
PostgreSQL
In PostgreSQL you just use the identity
macro to define an identity. It takes two arguments: The name of the identity and a list of attributes that should be unique.
[...]
attributes do
uuid_primary_key :id
attribute :name, :string do
allow_nil? false
end
attribute :price, :decimal do
allow_nil? false
end
end
identities do
identity :unique_name, [:name] (1)
end
[...]
1 | This is the identity. It is called unique_name and it makes sure that the name attribute is unique. |
Don’t forget to run a mix ash.codegen and a mix ecto.migrate after adding an identity. |
Test in iex
Let’s try to create a product with the same name twice:
$ iex -S mix
iex(1)> App.Shop.Product.create!(%{name: "Banana",
price: 0.1})
#App.Shop.Product<
__meta__: #Ecto.Schema.Metadata<:loaded>,
id: "321070d5-6cec-4054-a99a-c5036a80e7d0",
name: "Banana",
...
>
iex(2)> App.Shop.Product.create!(%{name: "Banana",
price: 0.2})
** (Ash.Error.Invalid) Input Invalid
* name: has already been taken
(ash 2.15.8) lib/ash/api/api.ex:2183: Ash.Api.unwrap_or_raise!/3
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 a 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
attributes do
uuid_primary_key :id
attribute :passenger_id, :integer
attribute :flight_number, :string
attribute :date, :date
end
actions do
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
code_interface do
define_for App.Airline
define :create
define :read
define :by_id, get_by: [:id], action: :read
define :update
define :destroy
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.
[...]
attributes do
uuid_primary_key :id
attribute :name, :string do
allow_nil? false
end
attribute :email, :ci_string do
allow_nil? false
end
end
identities do
# identity :unique_email, [:email] (1)
identity :unique_email, [:email] do
pre_check_with App.Shop
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 |