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.

lib/app/shop/resources/product.ex
  [...]
  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
  [...]
1This 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.

lib/app/shop/resources/product.ex
  [...]
  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
  [...]
1This 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.

lib/app/shop/resources/product.ex
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
1This 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.

lib/app/shop/resources/customer.ex
  [...]
  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
  [...]
1Use this version for PostgreSQL.

PostgreSQL users have to add the citext extension. See the AshPostgres.Repo behaviour.

lib/app/repo.ex
defmodule App.Repo do
  use AshPostgres.Repo, otp_app: :app

  def installed_extensions do
    ["citext"]
  end
end

After that change run mix ash.codegen and mix ash_postgres.migrate