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
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 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.
Popular in Questions
Other popular topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #deployment
- #library
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #channels
- #elixirconf
- #exunit
- #discussion
- #code-sync
- #javascript
- #podcasts
- #onsite
- #dialyzer
- #docker
- #authentication
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #elixir-ls
- #phoenix_html
- #iex
- #blog-post
- #graphql
- #genstage
- #ai
- #websockets
- #supervisor
- #elixirconf-us
- #advent-of-code
- #distillery
- #processes
- #api
- #forms
- #metaprogramming
- #security
- #hex









