Using Generators

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.

Adding Videos and Annotations

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.

images/src/relationships/annotation-relationships.png

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.

Generating Web Interfaces

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:

  • An associated User
  • A creation time for the video
  • A URL of the video location
  • A title
  • The type of the video

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:

  • The name of the context: Multimedia
  • The name of the module that defines the schema: Video
  • The plural form of the schema name: videos
  • Each field, with some type information

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:

images/src/relationships/posts_empty.png

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.

images/src/relationships/posts_new.png

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.

Examining the Generated Context, Controller, and View

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.

Generated Migrations

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.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.225.55.193