Ecto Exclusive Belongs to Aka Arc
Table of Contents
Intro
I had to implement a exclusive belongs to for a phoenix app of mine…
I found this very interesting post on the subject but I had to implement it with ecto. For my future self and for others here is the recipe.
exclusive-belongs-to-aka-exclusive-arc-
Diagram
How it will look.
Migration
defmodule ExclusiveArc.Repo.Migrations.CreateAcls do
use Ecto.Migration
def change do
create table(:acls) do
add :document_id, references(:documents, on_delete: :delete_all)
add :image_id, references(:images, on_delete: :delete_all)
add :file_id, references(:files, on_delete: :delete_all)
add :report_id, references(:reports, on_delete: :delete_all)
# acl is a simple text here is not the point of the til
add :level, :text, null: false
timestamps()
end
# ensure the it's exclusive
check_exclusive = """
(
(document_id IS NOT NULL)::integer +
(image_id IS NOT NULL)::integer +
(file_id IS NOT NULL)::integer +
(report_id IS NOT NULL)::integer
) = 1
"""
create constraint(:acls, :check_exclusive, check: check_exclusive)
create index(:acls, [:document_id], unique: true, where: "document_id IS NOT NULL")
create index(:acls, [:image_id], unique: true, where: "image_id IS NOT NULL")
create index(:acls, [:file_id], unique: true, where: "file_id IS NOT NULL")
create index(:acls, [:report_id], unique: true, where: "report_id IS NOT NULL")
end
end
Schema
defmodule ExclusiveArc.ACL do
use Ecto.Schema
alias Ecto.Changeset
alias ExclusiveArc.Medias.{Document, Image, File, Report}
schema "acls" do
belongs_to :document, Document
belongs_to :image, Image
belongs_to :file, File
belongs_to :report, Report
field :level, :string
timestamps()
end
@doc false
def changeset(acl, attrs, media) do
acl
|> Changeset.cast(attrs, [:level])
|> Changeset.validate_required([:level])
|> assoc_media(media)
# Last check if only one media was associated it's impossible to trigger
# with this changeset but it will help in case of changes later in the project
|> Changeset.check_constraint(:check_exclusive, name: :check_exclusive)
end
# Using pattern matching to associate the media and check the foreign key and unique constraint
defp assoc_media(changeset, %Document{} = document) do
changeset
|> Changeset.put_assoc(:document, document)
|> Changeset.foreign_key_constraint(:document_id)
|> Changeset.unique_constraint(:document_id)
end
defp assoc_media(changeset, %Image{} = image) do
changeset
|> Changeset.put_assoc(:image, image)
|> Changeset.foreign_key_constraint(:image_id)
|> Changeset.unique_constraint(:image_id)
end
defp assoc_media(changeset, %File{} = file) do
changeset
|> Changeset.put_assoc(:file, file)
|> Changeset.foreign_key_constraint(:file_id)
|> Changeset.unique_constraint(:file_id)
end
defp assoc_media(changeset, %Report{} = report) do
changeset
|> Changeset.put_assoc(:report, report)
|> Changeset.foreign_key_constraint(:report_id)
|> Changeset.unique_constraint(:report_id)
end
end
Repository demo
Read other posts