Recommendations for Virtual Attributes in Ecto

Not sure we’re talking the same thing then.

Me and the others are saying “it’s okay if you transform your Ecto schema object to something else before rendering”, and you are saying “it’s not okay to add extra stages (and maybe even virtual attributes) to the Ecto schemas”, maybe? Maybe I am misreading.

You seem to be saying “not only is it obvious, but expected.” And I don’t think the docs/examples suggest that. I felt like I was going “outside the norm” by doing so.

I agree that it might be best practice (either via a completely separate embedded_schema or virtual attributes), but the docs/examples seem to suggest the norm is directly using schema attributes in forms. There is no emphasis on separating presentation and backend data.

There’s maybe misunderstanding going on? I think what you are describing is something like you store first_name and last_name but have a form with only full_name which you need to split apart and rejoin? Am I getting that right?

Yeah, fair, I wasn’t very specific, true. Not saying it’s expected; I’m saying you shouldn’t be worried when you have to. You know?

Just go give you an example: you want to disable objects in your app and you just store disabled_at in their DB table.

But in your UI you just want to show a greyed out text (and disable interaction) if an object is disabled. The UI doesn’t care when was the thing disabled.

So you can:

  1. Make a virtual attribute;
  2. Inline the check right inside your template;
  3. Replace the model attribute disabled_at with disabled e.g. in your view;
  4. Have a convenience function e.g. is_enabled(thing) and use that in the view or the template.

I usually go for No. 4 but I’ve stumbled upon a good amount of projects that utilized No. 3.

2 Likes

Well, I think we’re off topic now, but the original question was “what’s the best way to populate the virtual fields in this schema?”:

defmodule UserProfile do
  schema "user_profile" do
    field :first_name, :string
    field :last_name, :string
    field :full_name, :virtual
    field :age, :integer
    field :profile_image, :string
    field :profile_image_url, :virtual
  end
end

And my suggestion was to simply have a “preload virtual fields” function, something like this:

Ecto.Query.from(u in UserProfile, ...)
|> Repo.all()
|> Repo.preload(stuff)
|> UserProfile.preload_virtual([:full_name, :profile_image_url])

My rational is that people are accustom to Repo.preload after querying records, so just continue along that pattern for virtual fields.

Whats the recommended way to populate virtual attributes in a scalable way?

I think the method above is as scalable as calling Repo.preload everywhere… :slight_smile:

1 Like

Strictly from an efficiency standpoint, my solution only performs one query with no post-processing so I would recommend that over preloading. While preloading is common, query composition is a big part of what makes Ecto so great and shouldn’t be thought of as anything special (I’m paraphrasing a bit to avoid saying one should use composition even if I do believe it).

1 Like

That’s a pretty cool approach. I wouldn’t have thought of that on my own.

I almost replied asking about non-db data, but I guess you can inject anything using fragment, a la:

def populate_virtual_fields(query) do
  base_url = Application.get_env(:my_app, :profile_image_url)
  from u in query,
    select_merge: %{profile_image_url: fragment("concat(?, '/ ', ?)", ^base_url, u.id)}
end
2 Likes