Computed Fields for an Ecto Model

Other than defining a method in the application model module (that accepts the model as a parameter), is it possible to define a computed field in an ecto schema or model?

For example, say we have a field ‘title’ (Mr., Mrs., Dr. etc.) and a field ‘name’, and we’d like a display field ‘display_name’ - which includes the concatenated ‘title’ (if present), with ‘name’. (e.g. Dr. James Gordon).

The concatenated field should also be lazy evaluated, so that there is no impact on Repo.all(model).

Thoughts?

2 Likes

I think here is relevant discussion https://github.com/elixir-ecto/ecto/issues/405

1 Like

I assume you are using Phoenix Framework. If so, the correct place to add this display_name property is a view. Think of it as a decorator.

3 Likes

thanks @sztosz - I’d actually read this thread quickly, before posting here. The OP on that thread also raised the serialization issue, ensuring that the computed field appears in JSON as well. The after_load callback has been removed from Ecto 2.0. The ‘Store’ implementation suggested there looks like a valid approach. https://github.com/elixir-ecto/ecto/issues/405#issuecomment-231563686

1 Like

thanks @hubertlepicki - in this case the ‘model’ will still need to be passed back to the view helper method as a parameter correct?

1 Like

How did you end up solving your original problem? Still struggling with something similar.

1 Like

I am not using views. (Server side templates are so 2011). The original question is totally relevant.

Server side templates are so 2011

Wow.

8 Likes

I would probably add virtual: true field to the schema and a function that maybe populates it.

schema "users" do
  field :title, :string
  field :name, :string
  field :display_name, :string, virtual: true
end

@spec maybe_populate_display_name(%User{}) :: %User{}
def maybe_populate_display_name(%User{title: nil, name: nil} = user) do
  user # that's why it's maybe
end
def maybe_populate_display_name(%User{title: nil, name: name} = user) do
  %{user | display_name: name}
end
def maybe_populate_display_name(%User{title: title, name: nil} = user) do
  %{user | display_name: title} # because i'm so silly
end
def maybe_populate_display_name(%User{title: title, name: name} = user) do
  %{user | display_name: title <> " " <> name} # however I would prefer iolist here
end

Actually, I think @outlog’s approach below is better.

5 Likes

or do it in your serializer eg defimpl Poison.Encoder or similar…

2 Likes

You use Phoenix views even when you serve JSON API. Or other kind of API, or you can actually use them in different ways you want and they are very convenient way of providing sort of decorator to your structs.

2 Likes

I’m confused, this seems like the right idea, but how is this called? Do we have to use User.maybe_populate_display_name(user) every-time we want to populated a user struct to do user.display_name? I’m thinking that, with the exception of the requirement of lazy loading it may be best to create it in the changeset and store it in the db. Thoughts?

Current convention is to encapsulate all calls to the Repo inside the context module. So internally all those context get functions would need to use maybe_populate_display_name/1 - everybody else is supposed to be using the context (Accounts) functions - not the Repo functions; so it really isn’t that big of an issue.

4 Likes

I’ve been solving this kind of problem with virtual fields. Populating this fields with values can be done as @peerreynders explains.

Later if the number of virtual fields that must be decorated grows you can also take a look at Decoratex, which may also be of some help.

3 Likes