Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to turn Ecto select queries into structs in Phoenix?

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?

like image 486
Torstein Avatar asked Feb 28 '16 04:02

Torstein


People also ask

What can you do with ecto's query engine?

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.

Does ecto support SQL queries?

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.

How do I use Ecto with Phoenix?

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.

How do I use ecto and PostgreSQL with Elixir?

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.


2 Answers

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.

like image 59
Gazler Avatar answered Sep 22 '22 06:09

Gazler


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.

like image 21
JustGage Avatar answered Sep 23 '22 06:09

JustGage