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