i-n-g-m-a-r

i-n-g-m-a-r

Ecto vs polymorphism: to embed or not to embed

Hi everyone,

I am wondering if I’m on the right track here.

My Phoenix project doesn’t involve any html, so I only use pipe_through :api and channels.
There’s a context module (Accounts) in between the database layer and calling code (mostly channels).
Accounts consist of users (email, password), roles (name) and profiles (user, role, metadata).
Here’s the thing: I’d like to use a jsonb field to store profile metadata, like “first_name” for persons.
Also I’d like profiles to reference users as well as roles, so that a profile is a user/role combination with metadata.

I ended up implementing the following (abbreviated).

defmodule MyApp.Repo.Migrations.CreateProfiles do
  use Ecto.Migration
  def change do
    create table(:profiles) do
      add :user_id, references(:users, on_delete: :nothing), null: false
      add :role_id, references(:roles, on_delete: :nothing), null: false
      add :metadata, :jsonb, null: false, default: "{}"
      timestamps()
    end
    create unique_index(:profiles, [:user_id, :role_id], name: :user_profile)
  end
end

defmodule MyApp.Accounts.Profile do
  # ...
  schema "profiles" do
    field :user_id, :id
    field :role_id, :id
    field :metadata, :map
    timestamps()
  end
  # ...
end

defmodule MyApp.Accounts.PersonProfile do
  # ...
  @primary_key false
  embedded_schema do
    field :first_name, :string
    field :last_name, :string
  end
  # ...
end

defmodule MyApp.Accounts.PaymentProfile do
  # ...
  @primary_key false
  embedded_schema do
    field :payment_method, :string
  end
  # ...
end

defmodule MyApp.Accounts do
  # ...
  def create_person_profile(%User{id: user_id}, %Role{id: role_id, name: "person"}, %{} = params) do
    %PersonProfile{}
    |> PersonProfile.changeset(params)
    |> create_profile(user_id, role_id)
  end

  def create_payment_profile(%User{id: user_id}, %Role{id: role_id, name: "payment"}, %{} = params) do
    %PaymentProfile{}
    |> PaymentProfile.changeset(params)
    |> create_profile(user_id, role_id)
  end

  defp get_map(%Ecto.Changeset{valid?: true} = profile) do
    profile
    |> Ecto.Changeset.apply_changes
    |> Map.from_struct
  end

  defp create_profile(%Ecto.Changeset{valid?: true} = profile, user_id, role_id) do
    %Profile{user_id: user_id, role_id: role_id}
    |> Profile.changeset(%{"metadata" => get_map(profile)})
    |> Repo.insert
  end

  defp create_profile(%Ecto.Changeset{} = profile, _, _), do: {:error, profile}
  # ...
end

So my calling code would call Accounts.create_person_profile/3 in order to create a person profile.
The metadata would be validated by Accounts.PersonProfile.changeset/2 using the embedded_schema.
Private function Accounts.create_profile/3 would then persist the actual profile where metadata is just a map.

I am avoiding Ecto.Schema.embeds_one/3 because it would break polymorphism.

Should this implementation be considered bad practise or is just fine to use Ecto like this.

Cheers,

Ingmar

Most Liked

blatyo

blatyo

Conduit Core Team

Polymorphism is useful when one thing can be substituted for another. In your example, it doesn’t look to me like that is the case with a payment profile and a person profile. It appears as if you’re putting them in the same table because they happen to share some of the same fields. If that is the case, I would probably model this as two separate tables with no shared code. As they grow, they’re likely to have divergent behaviors.

Last Post!

i-n-g-m-a-r

i-n-g-m-a-r

I have worked quite a lot with jsonb columns, though not in combination with Ecto.
provided an index is used (gin/jsonb_path_ops) querying performance is pretty decent.
if I would want to query on metadata only I would not use jsonb though.

jsonb works very well together with “regular” selects, something like:

where role = 'dog owner' and metadata @> '{"breed":"Siberian Husky"}'::jsonb

when having multiple breeds is allowed, indexed querying becomes more complex.

thx again for your time and feedback.

Where Next?

Popular in Questions Top

hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
jononomo
For some reason my phoenix channels are working for me in my local dev environment, but as soon as I deploy via Docker, I get a 403 error...
New
PeterCarter
There are pre-rolled solutions for other frameworks that do work. However, Phoenix does not seem to have these. Have people had good expe...
New
joeerl
Hello again - after a longish gap I’ve decided I really must dig into Elixir and see what’s been happening here - so I have a few questio...
New
fireproofsocks
Forgive me if this is obvious, but how does one delete a database record WITHOUT selecting it first? Ecto.Repo — Ecto v3.14.0 has exampl...
New
aalberti333
As the title describes, I’m trying to run Enum.map() over a list of key/value pairs, where the value is a map. My data looks like this: ...
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID<0.412.0> terminating ** (Postgrex.Error) FATAL...
New

Other popular topics Top

vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
joaquinalcerro
Hi there, I am working with Ecto-Postgresql and I need to call all of the records from a specific table but the table has 40,000 records...
New
hariharasudhan94
I would like to know what is the best IDE for elixir development?
New
chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 31586 112
New
greenz1
I have a phoenix application from which a user can download multiple(5-6) files of size 1MB. I couldn’t find anything related to sending ...
New
saif
Hello everyone, Long time lurker first time poster here. I’ve recently begun working on Elixir full-time again! :raised_hands: It’s been...
New

We're in Beta

About us Mission Statement