15
votes

I've got two models, Song and Vote, where songs has many votes. I want to select all songs and count the number of votes for each.

The index action in the SongController, generated using the mix gen task, has been modified to this:

def index(conn, _params) do
  query = from s in Song, select: %{id: s.id, name: s.name, artist: s.artist} 
  songs = Repo.all(query)
  render(conn, "index.html", songs: songs)
end

In this case songs contains a list of lists. But in the orginal, generated function, songs = Repo.all(Song) it is a list of Song structs.

This means that the song_path functions in the template break with the following error message: maps cannot be converted to_param. A struct was expected, got: %{artist: "Stephen", id: 3, name: "Crossfire"}

Of course, what I really want to do is to somehow add a num_votes field to the select statement, and then somehow make a corresponding field to the Song struct?

2
I see that Hex.pm solves a similar problem (number of downloads of a package) by having completely separate lists. (Controller, View)Torstein

2 Answers

19
votes

First we should add a virtual field to the song schema so that it can be used to store the num_votes result:

defmodule Song do
  use Ecto.Schema

  schema "songs" do
    field :num_votes, :integer, virtual: true
    ...
  end
end

Using a combination of Ecto.Query.select/3, Ecto.Query.join/5 and Ecto.Query.API.count/1 we can add the counts to the map you are using to select from the query:

  query = from s in Song,
    left_join: v in assoc(:votes),
    select: %{id: s.id, name: s.name, artist: s.artist, num_votes: count(v.id)} 

We can then use Kernel.struct to convert each item to a struct:

  songs =
    query
    |> Repo.all()
    |> Enum.map(fn(song) -> struct(Song, song) end)

This returns a list of song structs that can be used in the view.

1
votes

One interesting thing to note is that structs are actually just dicts with a __struct__ key set to the module name they are a part of. Because of this you can turn a normal Struct into a Dict by simply removing the __struct__ key.

iex(1)> defmodule M do
...(1)> defstruct [:a, :b]
...(1)> end

iex(2)> Map.delete(%M{}, :__struct__)
%{a: nil, b: nil}

(reference: https://groups.google.com/forum/#!topic/elixir-lang-talk/2xQqFOZSvk0)

However you want to go the other direction so it's easy to just add it in the same manner using Map.add. Please note to get this to work all the keys must be there, even if you're just setting them to nil.

So for the other part of you're question. There's probably some fancy SQL way to get the counts. I would recommend you do that. I for one would probably just hack it together in elixir using a join and then Enum.maping over it and replacing the counts with an integer rather than a list. Here's an article about how to do the joins: http://blog.plataformatec.com.br/2015/08/working-with-ecto-associations-and-embeds/.

I leave it to you as to how to do that.