What's the easiest way to populate ecto's virtual field with database data

Hey guys! I have an Album module with an Ecto schema. The schema has a cover_url virtual field that depends on the cover column to return the url for the cover.

I have this code that uses a function on the module to build the url. Now the issue is that album is not passed to the function. It raises an error instead stating: variable "album" does not exist and is being expanded to "album()", please use parentheses to remove the ambiguity or change the variable name

The function that builds the url looks like this:
Screen Shot 2020-06-08 at 7.54.21 PM

This stuff is so easy to do in other framework such as Laravel. I’m building my first big app in Elixir/Phoenix in part to learn the platform. I realize that this platform makes most hard things in other languages/tools easy and some easy things hard. Thanks for you help!

It’s really hard to read that tiny text on a phone. Can you paste your code in proper code blocks?

I think a fairly conventional approach to this would be to first query for the album/s and then decorate the returned structs. The nice thing about this is that it is very flexible. Here is an example decorating a list of albums.

from(Album)
|> Repo.all()
|> Enum.map(&add_cover_url/1)
end

You could decorate a single album that was already queried with add_cover_url(album).

4 Likes

Hey sorry about that.

  def album_with_cover_url do
    query = from a in Album, as: :album, limit: 1, select: [%{a | cover_url: ^make_cover_url(album)}]

    Repo.one!(query)
  end```

And this:

```elixir
 def make_cover_url(%Album{} = album) do
    if album.cover do
      "https://" <> album.img_bucket <> "/" <> album.cover
    else
      @default_poster_url
    end
  end

hey thanks for replying! Here’s what I got for now:

def with_cover_url(%Album{} = album) do
    %{album | cover_url: make_cover_url(album)}
end

It’s taking an album and adding the cover_url to the struct map. So it’s similar to your approach I guess.

Yep, the key thing being that it is operating on an album that has already been queried. You’ve probably heard this already, but the elixir community and coding conventions tend toward explicitness over magic. It has taken some getting used to for me coming from Rails, but this is the sort of thing that you won’t think twice about after doing it a few times. And when you come back to it a few months from now, it will still be easy to follow.

3 Likes

Yes that makes sense. Thanks a lot!

An additional option for some cases is Ecto.Query.select_merge which can populate virtual fields on your schema directly at read-time if the desired content can be calculated via SQL with fragment. This is something that you should measure the performance impact of but is generally within tolerances for queries that don’t produce an enormous number of rows.

9 Likes

Thanks for your input! I’ll look at it to learn more. Thanks again!

Tangentially, when posting code, please always use text instead of images. Text allows those who are helping to easily make suggested edits without retyping everything, and it’s also more accessible across a variety of screen sizes and resolutions.

7 Likes

OK got it. Thanks!

This was very helpful to me @shanesveller, select_merge with a fragment is exactly what I wanted!

Took a little fumbling around to figure out exactly what to do, since none of the docs nor linked docs describe using fragments this way, so for reference:

defmodule Answer do
  use Ecto.Schema

  schema "answer" do
    field :word, :string
    field :length, :integer, virtual: true
  end
end

Repo.insert(%Answer{word: "SWANS"})

Repo.one(
  from answer in Answer,
  select_merge: %{length: fragment("length(word)")}
)
#=> %Answer{word: "SWANS", length: 5}
7 Likes