Ecto is a data validation and database persistence Elixir library. This is the backbone of database operations in the Phoenix Webframework, this is the analog to ActiveRecord within the Rails ecosystem.

Transactions in Ectohacks

Ecto well supports database transactions via the Multi module. You can setup arbitrary functions and run them in sequence with good error handling support1:

 
step_1 = fn repo, _ ->
  %Dog{name: "hello"}
  |> repo.insert()
end
 
step_2 = fn repo, %{step_1_call: dog} ->
  %DogCollar{dog_id: dog.id}
  |> Repo.insert
end
 
Multi.new
|> Multi.run(:step_1_call, step_1)
|> Multi.run(:step_2_call, step_2)
|> Repo.transaction

Notice that the results from the first call are packed into the map argument to the function of the next call under the name that the step was run (:step1call).

Partial Unique Index (Boolean column) in Ecto Migration

It’s useful to be able to say that for a given column pair, there can be only one of one kind of a pair of attributes. For example, consider a team has many mambas but it can only have one captain.

Assume a structure like

 
defmodule Player do
  schema "players" do
    field :name, :string
    field :is_captain, :boolean, default: false
    belongs_to :team, Team
 
    timestamps()
  end
end
 
defmodule Team do
  schema "teams" do
    field :name, :string
    has_many :players, Player
 
    timestamps()
  end
end

Now we want to enforce that for a given, there is only one player at a time who can be the captain (is_captain true).

In the migration that creates the players:

create table(:players) do
  add :is_captain, :boolean, default: false, null: false
  add :name, :string
  add :team_id, references(:teams,  on_delete: :nothing)
 
  timestamps()
end
 
create index(:players, [:team_id])
 
create unique_index(:players, [:team_id, :is_captain],
         name: "player_team_captain_index",
         where: "is_captain IS TRUE"
       )

Importantly is the where clause on the unique index which makes the constrain unique for team id where the iscaptain is true. Note that you cannot just do where: is_captain because the index definition is finicky in postgres. Also, is_captain = true will not work.

Finally, you can setup the changeset to properly catch this uniqueness conflict:

def changeset(player, attrs) do
  player
  |> cast(attrs, [:name, :team_id, :is_captain])
  |> cast_assoc(:team)
  |> unique_constraint([:team_id, :is_captain], name: "player_team_captain_index")
end