To dig into Ecto, we’re going to have to define relationships, and for that we need to extend the domain of our application. That’s great, because our application is going to need those features. Let’s define our problem in a little more detail.
The rumbl application will let users choose a video. Then, they can attach their comments, in real time. Users can play back these videos with comments over time. See what it looks like in the figure.
Users create videos. Then, users can create annotations on those videos. If you’ve ever seen Mystery Science Theater 3000, you know exactly what we’re going for. In that show, some robots sat on the bottom of the screen, throwing in their opinions about bad science fiction.
Here’s how it’s going to work. Rather than building everything by hand as we did with the Accounts context and its User schema, we’re going to use generators to build the skeleton—including the migration, context, controllers, and templates to bootstrap the process for us. It’s going to happen fast, and we’re going to move through the boilerplate quickly, so be sure to follow closely.
Phoenix includes two Mix tasks to bootstrap web interfaces. phx.gen.html creates a simple HTTP scaffold with HTML pages, and phx.gen.json does the same for a REST-based API using JSON. They give you a simple scaffold for a traditional web-based application with CRUD (create, read, update, and delete) operations. You get migrations, a basic context, controllers, and templates for simple CRUD operations of a resource, as well as tests so you can hit the ground running. You won’t write all your Phoenix code this way, but the generators are a great way to get up and running quickly. They can also help new users learn how the Phoenix layers fit together in the context of a working application.
Our application allows users to annotate videos in real time. We know up-front that we’ll need a video resource, but we need to figure out where it will live within our application. When you’re organizing code, think contexts first. Rumbl enables users to interact around videos in real time, and we can imagine a future expansion to real-time conversations around all types of multimedia-–-images, books, etc.—so a Multimedia context will give us a nice place to group this functionality.
Now that we know where videos will live, we’ll start with a few fields, including:
Later, our application will let users decorate these videos with annotations. But first, we need users to be able to create and show their videos. Let’s use the phx.gen.html Mix task to generate our resource, like this:
| $ mix phx.gen.html Multimedia Video videos user_id:references:users |
| url:string title:string description:text |
| |
| * creating lib/rumbl_web/controllers/video_controller.ex |
| * creating lib/rumbl_web/templates/video/edit.html.eex |
| * creating lib/rumbl_web/templates/video/form.html.eex |
| * creating lib/rumbl_web/templates/video/index.html.eex |
| * creating lib/rumbl_web/templates/video/new.html.eex |
| * creating lib/rumbl_web/templates/video/show.html.eex |
| * creating lib/rumbl_web/views/video_view.ex |
| * creating test/rumbl_web/controllers/video_controller_test.exs |
| * creating lib/rumbl/multimedia/video.ex |
| * creating priv/repo/migrations/20180408024739_create_videos.exs |
| * creating lib/rumbl/multimedia.ex |
| * injecting lib/rumbl/multimedia.ex |
| * creating test/rumbl/multimedia_test.exs |
| * injecting test/rumbl/multimedia_test.exs |
| |
| Add the resource to your browser scope in lib/rumbl_web/router.ex: |
| |
| resources "/videos", VideoController |
All of the preceding files should look familiar, because you wrote a similar stack of code for the user accounts layer by hand. Let’s break that command down. Following the mix phx.gen.html command, we have:
This mix command may be more verbose than you’ve seen elsewhere. In some frameworks, you might use simple one-time generator commands, which leave it up to the framework to inflect plural and singular forms as requests come and go. It ends up adding complexity to the framework, and indirectly, to your application. At the end of the day, you save only a few keystrokes every once in a while. Such generators optimize the wrong thing.
Sometimes it pays to be explicit. For all things internal, Phoenix frees you from memorizing unnecessary singular and plural conventions by consistently using singular forms in schemas, controllers, and views in most cases. In your application boundaries, such as URLs and table names, you provide a bit more information, because you can use pluralized names. Since creating plural forms is imperfect and rife with exceptions, the generator command is the perfect place to tell Phoenix exactly what we need.
It’s time to follow up on the remaining instructions printed by the generator. First, we need to add the route to lib/rumbl_web/router.ex:
| resources "/videos", VideoController |
The question is: in which pipeline? Let’s review what we know and come back to that question shortly.
Our Multimedia.Video is a REST resource, and these routes work just like the ones we created for Accounts.User. As with the index and show actions in UserController, we also want to restrict the video actions to logged-in users. We’ve already written the code for authentication in the user controller. Let’s recap that now:
| defp authenticate(conn, _opts) do |
| if conn.assigns.current_user do |
| conn |
| else |
| conn |
| |> put_flash(:error, "You must be logged in to access that page") |
| |> redirect(to: Routes.page_path(conn, :index)) |
| |> halt() |
| end |
| end |
To share this function between routers and controllers, move it to RumblWeb.Auth, call it authenticate_user for clarity, make it public (use def instead of defp), import our controller functions for put_flash and redirect, and alias our router helpers:
| import Phoenix.Controller |
| alias RumblWeb.Router.Helpers, as: Routes |
| |
| def authenticate_user(conn, _opts) do |
| if conn.assigns.current_user do |
| conn |
| else |
| conn |
| |> put_flash(:error, "You must be logged in to access that page") |
| |> redirect(to: Routes.page_path(conn, :index)) |
| |> halt() |
| end |
| end |
You might be tempted to import RumblWeb.Router.Helpers instead of defining an alias, but hold off on that impulse. The router depends on Rumbl.Auth so importing the router helpers in Rumbl.Auth would lead to a circular dependency and compilation would fail.
Save the auth.ex file. Since that module provides services our entire application will use, we’ll want to make it easier to integrate. An import should do the trick. First, let’s share authenticate_user function across all controllers and routers. We will write import RumblWeb.Auth, only: [authenticate_user: 2], where the number 2 is the number of arguments expected by authenticate_user. Crack open lib/rumbl_web.ex and make this change to your controller function:
| def controller do |
| quote do |
| use Phoenix.Controller, namespace: RumblWeb |
| |
| import Plug.Conn |
| import RumblWeb.Gettext |
| import RumblWeb.Auth, only: [authenticate_user: 2] # New import |
| alias RumblWeb.Router.Helpers, as: Routes |
| end |
| end |
In the same file, make a similar change to your router function:
| def router do |
| quote do |
| use Phoenix.Router |
| import Plug.Conn |
| import Phoenix.Controller |
| import RumblWeb.Auth, only: [authenticate_user: 2] # New import |
| end |
| end |
Next, in UserController, we want to use the newly imported function. Rename authenticate to authenticate_user, like this:
| plug :authenticate_user when action in [:index, :show] |
Now, back to the router. Let’s define a new scope called /manage containing the video resources. This scope pipes through the browser pipeline and our newly imported authenticate_user function, like this:
| scope "/manage", RumblWeb do |
| pipe_through [:browser, :authenticate_user] |
| |
| resources "/videos", VideoController |
| end |
pipe_through can work with a single pipeline, and it also supports a list of them. Furthermore, because pipelines are also plugs, we can use authenticate_user directly in pipe_through.
We now have a whole group of actions that allow the users to manage content. In a business application, many of those groups of tasks would have a policy, or checklist. Our combination of plugs with pipe_through allows developers to mix and match those policies at will. You can use these techniques for any group of users that share your plug’s policies, whether they are admins or anonymous users. Applications can use as many plugs and pipelines as they need to do a job, organizing them in scopes.
We’re almost ready to give the generated code a try, but first we need to run the last of the generator’s instructions. Go ahead and update the database by running migrations:
| $ mix ecto.migrate |
| Compiling 24 files (.ex) |
| Generated rumbl app |
| [info] == Running Rumbl.Repo.Migrations.CreateVideos.change/0 forward |
| [info] create table videos |
| [info] create index videos_user_id_index |
Next start your server:
| $ mix phx.server |
And we’re all set. The migration created the new video table and an index to keep it fast. Head over to your browser and visit http://localhost:4000/manage/videos as a logged-in user. We see an empty list of videos:
Now take it for a test drive. Click “New video” to create a video. We see the generated form for a new video in the figure.
Fill out the form and click “Save”. The application should create your video and redirect. We’re not yet scoping our video lists to a given user, but we still have a great start. We know that code generators like this one aren’t unique, that dozens of other tools and languages do the same. Still, it’s a useful exercise that can rapidly ramp up your understanding of Phoenix and even Elixir. Let’s take a quick glance at what was generated.
The generated controller is complete. It contains the full spectrum of REST actions. The Multimedia context handles all of our heavy lifting.
The view looks like an empty module, but at this point we already know that it will pick all templates in lib/rumbl_web/templates/video and transform them into functions, such as render("index.html", assigns):
| defmodule RumblWeb.VideoView do |
| use RumblWeb, :view |
| end |
Take some time and read through the template files in lib/rumbl_web/templates/video/ to see how Phoenix uses forms, links, and other HTML functions. There’s no magic with Phoenix. Everything is explicit so you can see exactly what each function does. With the application boilerplate generated, we can shift our focus to Ecto relationships, starting with the generated migration.
First let’s take a look at the generated Multimedia context in lib/rumbl/multimedia.ex:
| defmodule Rumbl.Multimedia do |
| import Ecto.Query, warn: false |
| alias Rumbl.Repo |
| alias Rumbl.Multimedia.Video |
| |
| def list_videos do |
| Repo.all(Video) |
| end |
| |
| def get_video!(id), do: Repo.get!(Video, id) |
| |
| def create_video(attrs \ %{}) do |
| %Video{} |
| |> Video.changeset(attrs) |
| |> Repo.insert() |
| end |
| |
| def update_video(%Video{} = video, attrs) do |
| video |
| |> Video.changeset(attrs) |
| |> Repo.update() |
| end |
| |
| def delete_video(%Video{} = video) do |
| Repo.delete(video) |
| end |
| |
| def change_video(%Video{} = video) do |
| Video.changeset(video, %{}) |
| end |
| end |
The Accounts context we wrote by hand is similar, so this context should look familiar to you. It contains a logical grouping of functions you can use to work with our videos. After all, grouping like functions is what contexts is all about. Let’s shift to the migrations code that will interact directly with the database to create our schema.
Let’s open up the video migration in priv/repo/migrations:
| def change do |
| create table(:videos) do |
| add :url, :string |
| add :title, :string |
| add :description, :text |
| add :user_id, references(:users, on_delete: :nothing) |
| |
| timestamps() |
| end |
| |
| create index(:videos, [:user_id]) |
| end |
Phoenix generates a migration for all the fields that we passed on the command line, like the migration we created by hand for our users table. You can see that our generator made effective use of the type hints we provided. In relational databases, primary keys, such as our automatically generated id field, identify rows. Foreign keys, such as our user_id field, point from one table to the primary key in another one. At the database level, this foreign key lets the database get in on the act of maintaining consistency across our two relationships. Ecto is helping us to do the right thing.
The change function handles two database changes: one for migrating up and one for migrating down. A migration up applies a migration, and a migration down reverts it. This way, if you make a mistake and need to move a single migration up or down, you can do so.
For example, let’s say you meant to add a view_count field to your generated create_video migration before you migrated the database up. You could create a new migration that adds your new field. Since you haven’t pushed your changes upstream yet, you can roll back, make your changes, and then migrate up again. First, you’d roll back your changes:
| $ mix ecto.rollback |
| [info] == Running Rumbl.Repo.Migrations.CreateVideos.change/0 backward |
| [info] drop index videos_user_id_index |
| [info] drop table videos |
| [info] == Migrated in 0.0s |
We verify that our database was fully migrated up. Then we run mix ecto.rollback to undo our CreateVideos migration. At this point, we could add our missing view_count field. We don’t need a view_count at the moment, so let’s migrate back up and carry on:
| $ mix ecto.migrate |
| [info] == Running Rumbl.Repo.Migrations.CreateVideos.change/0 forward |
| [info] create table videos |
| [info] create index videos_user_id_index |
The migration sets up the basic relationships between our tables and—now that we’ve migrated back up—we’re ready to leverage those relationships in our schemas.
18.225.55.193