i-n-g-m-a-r

i-n-g-m-a-r

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

Showing Posts 1 to 8

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.

i-n-g-m-a-r

i-n-g-m-a-r OP

thanks for your response.
in terms of behaviour you are right,
the various profiles don’t substitute one another.
from a data persistance perspective they do,
I can treat each of them the same way.
Also I make sure that a user doesn’t have more than one of each profile type.
And it’s very convenient to retrieve all of them in one go.

My main concern is how to deal with models that are in part well structured with foreign key constraints and so on but also contain unstructured data (nosql, jsonb).
The example I posted shows my attempt to have the best of both worlds using Ecto.

dimitarvp

dimitarvp

You should not use anything because you want it – you should use it because it fits the job. I cannot find a reason to use polymorphism in your case.

In your example it looks like that you need one of the following:

  1. Separate related records, like PersonalProfile and PaymentProfile and others which you should add with time.
  2. Separate top-level keys in a JSONB column, like so:
%{
  "personal_profile": ...
  "payment_profile": ...
}

…and again, whatever other objects you might need in the future.

If having common fields worries you about data duplication then why not just put them in the parent object?

i-n-g-m-a-r

i-n-g-m-a-r OP

hi Dimitar,

tnx for your comments, I appreciate your feedback.
also I agree with you, any particular solution should fit the job.

at this point the question seems to be about whether to use polymorphism at all versus how to do it with Ecto.
I would like to use polymorphism in order to have a solid and coherent interface to handle the same kind of records.
as you suggested I could have just a single record (user) to store all of the metadata related to roles.
I guess maybe it’s a matter of taste, but I’d rather have many specialized records than one big record of everything.
to me it makes sense to store metadata about a relation in the relation (using jsonb, kind of independently).

  • I’m planning on having lots of roles, most of them are presently unknown
  • adding roles should be fairly trivial; having to migrate the database every time would not be
  • also adding metadata fields to profiles should be trivial and easy to validate
  • selecting on roles is more important than selecting on users or profile metadata
  • users and roles will belong to other things like groups and ratings
  • roles should be very flexible, for example “dog owner” could be a role and so could “wikipedia contributor”

so in the end I’m trying to avoid modelling everything and instead have a solid core that is very flexible.
I’m trying to use Ecto to guard the data that is used throughout the application.

dimitarvp

dimitarvp

But again, I do not see why polymorphism is better in your case – I don’t think it is. You haven’t addressed that.

Also if you want to select on roles then I’d say having them as separate table and records to whom Users point (via has_many) is much more practical and likely faster. But I haven’t measured PostgreSQL’s performance on selecting inside a JSONB field, mind you.

Do not overengineer. If you do not know 100% of the requirements beforehand, just go with what will get the job done now. Far too many times I’ve planned and coded for a future that never came.

i-n-g-m-a-r

i-n-g-m-a-r OP

if I would store metadata in the users table as you suggested before,
I would not have to change the current roles table and I could use has_many to point users to roles.
so I would be “merging” users and profiles and I would not need to migrate every time.
that would work just fine.

at this point my user module only knows how to authorize users (changeset > cast > validate > hash password).
I use Guardian to rehydrate a user resource from a JWT.
if I don’t (pre)load all of the metadata at that point (unnecessarily) the %User{} would be incomplete.

so the profiles table provides “lazy loading” as well as structured metadata storage.

of course I agree with you not to overengineer, in my mind I’m not.

dimitarvp

dimitarvp

What you say is fair, but have you checked about the quality and speed of querying by JSONB columns? If you are satisfied with it, then go ahead with your original idea – only modify it slightly to use different keys inside the JSONB as I pointed out above.

i-n-g-m-a-r

i-n-g-m-a-r OP

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.

— All posts loaded —

Where Next? Top

Trending in Questions Top

Blokh
Hey guys, I’ve got a huge CSV ( around 10 GB ) that needs to be processed hourly Do you guys have any suggestions what is the best prac...
New
kszambelanczyk
Hello! Could someone please give me a help/sample code, how to delete a file from s3 using waffle/waffle_ecto from Phoenix app. I creat...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New
psy-q
I’m trying to set up Emacs with elixir-ls via lsp-mode and credo via Flycheck. This should mostly be preconfigured as Flycheck picks up c...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
mcass19
ExRatatui lets you cook up rich terminal UIs in Elixir, powered by Rust’s ratatui via Rustler NIFs. Build interactive terminal applicatio...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge & Solve. They are GUI (Emerge) and State management (S...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews