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?
In addition to a natural Elixir DSL, Ecto's query engine gives us multiple great features, such as SQL injection protection and compile-time optimization of queries. Let's try it out. First, we imported [ Ecto.Query ], which imports the from/2 macro of Ecto's Query DSL.
We're not just limited to basic querying – Ecto includes a full-fledged query DSL for advanced SQL generation. In addition to a natural Elixir DSL, Ecto's query engine gives us multiple great features, such as SQL injection protection and compile-time optimization of queries.
Let's get started! Phoenix uses Ecto to provide builtin support to the following databases: Newly generated Phoenix projects include Ecto with the PostgreSQL adapter by default. You can pass the --database option to change or --no-ecto flag to exclude this.
Once we have Ecto and PostgreSQL installed and configured, the easiest way to use Ecto is to generate an Ecto schema through the phx.gen.schema task. Ecto schemas are a way for us to specify how Elixir data types map to and from external sources, such as database tables.
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.
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.map
ing 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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With