Ecto: Shared schema definition/ model alias

In my application I want to have a single entities table, which stores type a and type b entities. type a and type b share the exact same fields, I just set a type field can be "type_a" or "type_b".

For some reasons I don’t want to work with %Entity{} structs, but %TypeA{} and %TypeB{} structs, and I’m now wondering what might be the best way to do this.

In the end I’m looking for a way to create “aliases” for my %Entity{} struct. Here’s what I’ve tried so far:

1. Just define the schema multiple times

defmodule App.Entities.Entity do
  # ...

  schema "entities" do
    field :title, :string
    field :type, :string
    has_one :address, Address
  end

  @doc false
  def changeset(%Entity{} = entity, attrs) do
    entity
    |> cast(attrs, [:title, :type])
    |> validate_required([:title, :type])
  end
end

defmodule App.Entities.TypeA/B do
  # ...

  schema "entities" do
    field :title, :string
    field :type, :string
    has_one :address, Address, foreign_key: :entity_id
  end

  @doc false
  def changeset(%TypeA/B{} = entity, attrs) do
    entity
    |> cast(attrs, [:title, :type])
    |> validate_required([:title, :type])
    |> put_change(:type, "type_a/b")
  end
end

This works fine, but I don’t like the repetitiveness of this approach.

2. Use a separate schema module

defmodule App.Entities.EntitySchema do
  defmacro __using__(_) do
    quote do
      # ...

      schema "entities" do
        field :title, :string
        field :type, :string
        has_one :address, Address, foreign_key: :entity_id
      end
    end
  end
end

defmodule App.Entities.Entity do
  use App.Entities.EntitySchema

  @doc false
  def changeset(%Entity{} = entity, attrs) do
    entity
    |> cast(attrs, [:title, :type])
    |> validate_required([:title, :type])
  end
end

defmodule App.Entities.TypeA/B do
  use App.Entities.EntitySchema

  @doc false
  def changeset(%TypeA/B{} = entity, attrs) do
    entity
    |> Map.put(:__struct__, Entity)
    |> Entity.changeset(attrs)
    |> put_change(:type, "type_a/b")
  end
end

I prefer this approach, but it still seems to be a bit overkill for a “simple alias”.

3. Define structs

I have not tried this so far but it might be possible to use regular Elixir structs:

defmodule App.Entities.TypeA/B do
  defstruct [:id, :title, :type, :address, :created_at, :updated_at]
end

defmodule App.Entities.Entity do
  # ...

  schema "entities" do
    field :title, :string
    field :type, :string
    has_one :address, Address
  end

  @doc false
  def changeset(%TypeA/B{} = entity, attrs) do
    entity
    |> Map.put(:__struct__, Entity)
    |> changeset(attrs)
    |> put_change(:type, "type_a/b")
  end
  
  def changeset(%Entity{} = entity, attrs) do
    entity
    |> cast(attrs, [:title, :type])
    |> validate_required([:title, :type])
  end
end

This seems to be a clean approach as my %TypeA/B{} structs are pure data containers,
but I’m not sure if I would lose some Ecto functionality, say preloads etc.?

Alternatives, Feedback?

How would you solve this problem and why? Maybe there’s a better approach that I haven’t thought of yet :slight_smile:

2 Likes

I’m still not sure how I want to do it, but no. 3 – defining structs – is also possible:

defmodule App.Entities.TypeA/B do
  @entity App.Entities.Entity.__struct__()

  defstruct @entity
            |> Map.keys()
            |> Enum.reduce([], fn key, acc ->
              val = Map.get(@entity, key)
              Keyword.put_new(acc, key, val)
            end)

  def __schema__(key) do
    App.Entities.Entity.__schema__(key)
  end

  def __schema__(key, arg) do
    App.Entities.Entity.__schema__(key, arg)
  end

  def __changeset__ do
    App.Entities.Entity.__changeset__()
  end
end

I’ve moved this into a separate module so I can just do

defmodule App.Entities.TypeA/B do
  use App.Alias, for: App.Entities.Entity
end