Check out the Alchemist's Edition
View source on GitHub
Now this patter works really well when we want to chain queries together.
Let’s say we wanted to add the ability to filter our movies based on their genre - how could we do that?
Here we have our movie app and it’s got a new feature that helps us filter the movies displayed.
Currently we can use it to return any of our movies released after a certain year.
Now one of the great features of using Ecto for is for composable queries.
Let’s walk through how this is working and how simply we can add additional filters to it.
From our editor let’s open our movies
Here is our search form. We’re using our connection, and making a request to our
movie_path. And by using
as: :search we’re grouping the fields for this form in our params under the
Right now we only have one field -
released_after which is a select field populated with a list of years.
And if we open our
movie_view.ex and we can see the
released_after_years function returns our list of the years displayed in our dropdown.
Now when this form is submitted a request is made to the
index action on our
movie_controller.ex module, let’s open that module.
And you can see we’re using
get_in to get the
released_after field and assigning it to the variable
We’re then creating a pipeline where we we take our
movie and pass it into the
Movie. released_after function along with the year.
This returns an Ecto query which is then piped into
Repo.all, returning all movies that match the query.
Let’s take a closer look at the query.
We’ll open our
We’re using a guard clause, so when no search is performed and
nil we just return our query.
Otherwise we use Ecto to query for all movies where the year is greater than or equal to the year passed in.
This works because Ecto queries can take either the Module OR the query.
The value on the right-side of
in needs to implement the
For our example here, this can either be an
Ecto.Query or our
Movie module, since modules in Elixir are really Atoms.
So another way we could look this would be to remove the query, and use
Movie in it’s place.
This pattern works really well when we want to chain queries together. So let’s change our function back to use the variable
And now let’s say we now wanted to add the ability to filter on genres, how could we do that?
First we need to get all of our movie genres.
So let’s create another function
all_genres, and we’ll allow it to accept a query as well.
Then we’ll create an Ecto query to select all distinct genres from our movies.
… def all_genres(query) do from movie in query, distinct: movie.genre, select: movie.genre end …
Then let’s go back to our
And let’s update our
index function to use our new function to get our genres.
And then we’ll pass our
genres into our
assigns so access them in our template.
… def index do year = get_in(params, ["search", "released_after"]) movies = Movie |> Movie.released_after(year) |> Repo.all() genres = Movie |> Movie.all_genres() |> Repo.all() render(conn, "index.html", movies: movies, genres: genres) end …
So let’s go back to our movie
And we’ll add another field to our search form.
Let’s make it a
select called genre. And then for the values, we’ll use our
@genres list from the assigns
And we’ll include a prompt of
Now when we submit our search form it will pass over our genre too.
<%= form_for @conn, movie_path(@conn, :index), [method: :get, as: :search], fn f -> %> <%= label(f, :released_after, "After") %> <%= select(f, :released_after, released_after_years, prompt: "Year") %> <%= label(f, :genre, "Genre") %> <%= select(f, :genre, @genres, prompt: "Choose genre") %> <%= submit "Filter" %> <% end %>
Let’s go back to our
movie_controller.ex module and now we can get our
genre from the params.
Now that we have our genre, we need to use it.
Let’s create another query. We’ll open our
movie.ex module. And we’ll add another function. Let’s call it
by_genre that will take a query and a genre.
The we can write our Ecto query to get all movies that match the given
We’ll also need to add a function to guard against when no
genre is selected. And in those cases we’ll just return the query.
… def by_genre(query, genre) when is_nil(genre) or byte_size(genre) == 0 do query end def by_genre(query, genre) do from movie in query, where: movie.genre == ^genre end …
Now that we have our query, let’s go back to our
And we’ll compose our query by placing it along with our other query in the pipeline.
… def index(conn, params) do year = get_in(params, ["search", "released_after"]) genre = get_in(params, ["search", "genre"]) movies = Movie |> Movie.released_after(year) |> Movie.by_genre(genre) |> Repo.all() genres = Movie |> Movie.all_genres() |> Repo.all() render(conn, "index.html", movies: movies, genres: genres) end …
With that we can open our browser.
And now we can filter our movies by genre and by year.