Comparing Rails and Phoenix: Part II
In the first post of this two-part series, we touched on generating a new application and talked about the entry point to each application: the Router. We also discussed at a high level about how Phoenix apps can fit into larger OTP applications.
In this post, we will be looking at the Model, View, and Controller, the parts that comprise a typical MVC framework.
The Model
The Model, that place where your application’s business logic tends to reside and where the schema is defined to be able to talk to the database. Where rows and columns are converted to objects or structs. Here, we’ll explore some of the differences in the model layer between Rails and Phoenix.
The model in Rails
There is a lot of “magic” going on in Rails, and I love it! I purposefully put that word magic in quotes because it isn’t really magic. It’s logic that is defined in the ActiveRecord::Base
class that we get access to as soon as we create a class which inherits from it.
Let’s take a look at our User
class:
class User < ApplicationRecord has_many :tweets has_many :notifications validates :email, :name, :username, :bio, presence: true def to_param username end end
And with that little code, we suddenly have the ability to query the users
table in our database, to validate each object, and to reach out to related objects that we’ve defined through the two has_many
lines.
In Rails, we use the model class to find and create instances of that same class. User.find_by(username: "leigh")
goes to the database and returns us an instance of the User
class that matches the query we gave.
We also don’t have to tell Rails which columns this table has. It figures all of that out on its own. ActiveRecord is an ORM (Object Relational Mapper); mapping relational data in the database to objects in Ruby. You may wonder why I’m going into detail about things that to a seasoned Rails developer are second nature — that is because in Phoenix it’s done differently.
Database migrations in Rails
Here are the migrations which were generated with the scaffold command from earlier in the article.
class CreateUsers < ActiveRecord::Migration[5.0] def change create_table :users do |t| t.string :email t.string :name t.string :username t.string :bio t.timestamps end add_index :users, :email, unique: true add_index :users, :username, unique: true end end
The model in Phoenix
The model layer in Phoenix is actually handled by a great library called Ecto. As we take a look at the User
model in Phoenix, you’ll immediately notice some differences. The first is that we define the different columns and relationships it has in a schema
block. Ecto requires you to be a little bit more explicit about your database schema and how it maps to your Ecto model.
defmodule TwitterPhoenix.User do use TwitterPhoenix.Web, :model schema "users" do field :name, :string field :username, :string field :email, :string field :bio, :string has_many :tweets, TwitterPhoenix.Tweet has_many :notifications, TwitterPhoenix.Notification timestamps end @required_fields ~w(name username email bio) @optional_fields ~w() @doc """ Creates a changeset based on the `model` and `params`. If no params are provided, an invalid changeset is returned with no validation performed. """ def changeset(model, params \\ :empty) do model |> cast(params, @required_fields, @optional_fields) |> unique_constraint(:username) |> unique_constraint(:email) end end
Thankfully, most of that is written for us when we generated the scaffolding for our application, so don’t be afraid or turned off by its verboseness. In fact, it is nice to be able to look at your model and immediately see how it is structured.
Because Elixir is a functional, immutable language, there are no “objects” as you find in Ruby. Validations and casting of the data are defined inside the changeset
method. You provide the existing data in addition to the data you are changing, and it will produce an Ecto.Changeset
struct which contains all of the information needed to validate and save the record.
Unlike Rails where you use the class and object instances to interact with the database, all of that in Ecto is done through a Repo
module. If we look at the update
action of the UserController
, we’ll see two different interactions with Repo
along with some interactions with the changeset
.
def update(conn, %{"id" => username, "user" => user_params}) do # Find a user by its username user = Repo.get_by!(User, username: username) # Generate a changeset using existing user and incoming user_params changeset = User.changeset(user, user_params) # Attempt to save changeset to the database case Repo.update(changeset) do {:ok, user} -> conn |> put_flash(:info, "User updated successfully.") |> redirect(to: user_path(conn, :show, user.username)) {:error, changeset} -> render(conn, "edit.html", user: user, changeset: changeset) end end
First we use the Repo.get_by!
method to find a User by its username. We then produce a changeset
by providing the existing user and the form data (found in user_params
).
With the changeset
we can now use the Repo.update
method to attempt to save these changes to the database. Handling the result is done in Elixir style pattern matching rather than the Rails style if
statements, but the goal is the same.
Database migrations in Phoenix
Here is what migrations look like in Phoenix. They look quite similar to the ones in Rails and should be instantly understandable to someone coming to Phoenix from Rails.
defmodule TwitterPhoenix.Repo.Migrations.CreateUser do use Ecto.Migration def change do create table(:users) do add :name, :string add :username, :string add :email, :string add :bio, :string timestamps end create unique_index(:users, [:username]) create unique_index(:users, [:email]) end end
The Controller
Controllers sit sort of in the middle of a web request. We get to the controller by way of the routing, and it’s the controller’s job to basically convert incoming request (and its params) into a response. It does so by interacting with the Model layer and then passing that data to a view who presents the data as HTML or JSON.
We’re going to be looking at the TweetsController
, more specifically at the index
action, which shows the latest tweets for a specific user, in this case.
Controllers in Rails
With controllers in Rails, we typically end up in a method referred to as the action
. This is the method that the router calls and whose job it is to gather data from the model and pass it to the view. Let’s take a look at the index
action:
class TweetsController < ApplicationController before_action :set_user def index @tweets = @user.tweets.latest end private def set_user @user = User.find_by(username: params[:user_id]) end end
The index
method is quite small, but what’s actually going on here?
In Rails, you can have before_action
filters which happen before the action actually gets called. These can set data (such as the @user
variable) and perform security authorization checks, among other things. We declared that we wanted a before_action
to take place at the top of the controller and defined the actual method itself further below.
Inside the set_user
method, we’re using data that has come to us via the URL. The path which maps to this action looks like /users/:user_id/tweets
, so if we want to grab this user’s latest tweets, we first need to find the user, using their ID (username in this case).
In Rails, we have access to URL params, GET params, and POST data all through a single object, the params
. This may look like just a hash, but it’s a little more complicated than that. We can actually use it to validate our incoming parameters using StrongParameters, which can require certain fields to exist and permit (whitelist) others.
In this case, we are just grabbing the user_id
. Just one note about this is that the user_id
here is actually their username. It could have been redefined to username
in the routing, but I just went with the default naming convention.
Variables are passed to the view by setting them as instance variables (@ variables
) rather than local ones. In this case, rendering happens implicitly by Rails, which knows to look for the index.html
template inside the views/tweets
folder. We can choose to call the render
method if we want more control over the process or something different from the default behavior.
Controllers in Phoenix
If I were to describe the main differences between controllers in Rails and those in Phoenix, I would say that they are a little simpler and a bit more explicit in Phoenix.
The first thing to note in Phoenix is that there is only a single concept which weaves its way through controllers, and that is the same we saw in routing: namely, the plug
. There are no filters, no actions, just plugs.
It may appear very similar because of some nice macros that help define what is commonly done in filters and actions, but these are just plugs. Also, because Elixir is a functional language, there are no objects we have access to, like the request
or params
objects which we have in Rails.
There is a single conn
struct which is passed from plug to plug, containing all of the information of the entire request, including the params. We add or modify this struct as it flows from one plug to another. Take a look at our controller below (I have again removed code which doesn’t relate to the specific use-case):
defmodule TwitterPhoenix.TweetController do use TwitterPhoenix.Web, :controller alias TwitterPhoenix.{UserTweets, User, Tweet} plug :load_user def index(conn, _params) do tweets = UserTweets.latest(conn.assigns[:user]) render(conn, "index.html", tweets: tweets) end defp load_user(conn, _) do user = Repo.get_by!(User, username: conn.params["user_id"]) assign(conn, :user, user) end end
It looks similar to what we saw in Rails. We define what would be a before_action
filter (a plug) called load_user
. This happens before the index
method is called, and we can access the user_id
(username) from the conn
variable to find the user. We then add its information to the conn
struct and send it on its way to the next plug, which happens to be the index
method.
In the index
method, we have to be explicit about rendering the view that we want, passing the variables we want it to have access to.
The View
We’ve made it to the point of the request/response life-cycle where we need to render some HTML that will be returned to the user.
Views in Rails
Views in Rails are pretty self-explanatory. They have access to instance variables which were set in the controller that rendered it, along with a number of other helper methods which come from Rails or which were defined by the user. The default views in Rails are written in ERB
(Embedded Ruby), allowing us to mix HTML with Ruby logic.
html <tbody> <% @tweets.each do |tweet| %> <tr> <td><%= tweet.body %></td> <td><%= tweet.retweet_count %></td> <td><%= tweet.like_count %></td> <td><%= link_to 'Show', user_tweet_path(@user, tweet) %></td> </tr> <% end %> </tbody>
Views in Phoenix
Things change quite a bit when we enter Phoenix.
Here we don’t go from the controller directly to where we write HTML. Phoenix has split things up into “views” and “templates.”
Views are modules who have the job of rendering the template and providing methods to assist with rendering the data. You can almost think of them as “presenters” for the templates, where you might want to present the data differently if you are rendering HTML vs JSON. It provides a bit of separation between the controller who finds the data, ensures the user is authenticated and authorized, and the template which renders the output.
In our case, we aren’t doing anything special with the data at the moment, so it is a module with a single method that helps us display the user’s name:
defmodule TwitterPhoenix.TweetView do use TwitterPhoenix.Web, :view def username(conn) do conn.assigns[:user].username end end
The template which produces the HTML actually ends up looking remarkably similar to how things looked in Rails:
html <tbody> <%= for tweet <- @tweets do %> <tr> <td><%= tweet.body %></td> <td><%= tweet.retweet_count %></td> <td><%= tweet.like_count %></td> <td class="text-right"> <%= link "Show", to: user_tweet_path(@conn, :show, username(@conn), tweet), class: "btn btn-default btn-xs" %> </td> </tr> <% end %> </tbody>
Websockets/Channels
With the imminent release of Rails 5, both Rails and Phoenix have support for websockets and channels by default. This wasn’t the case prior to Rails 5, and you might say that the implementation in Phoenix is still a stronger one.
You may have seen the article that talks about how the Phoenix team achieved two million connections on a single (albeit very large) server. I have yet to see a similar comparison in Rails, although my gut instinct tells me it wouldn’t be able to achieve as many (although I’d love to be proven wrong). That being said, there are very few applications which require this sort of scaling. If you are one of them, congratulations, that’s awesome! For the majority of websites, Rails should perform just fine, and it’s a great addition to the framework.
ActionCable (channels) in Rails requires either Redis or PostreSQL to handle the pub/sub nature of channels. You are welcome to use those with channels in Phoenix as well, but they aren’t required due to the concurrent-by-default nature of the language.
Conclusion
In this article, I tried to touch on the main components of an MVC web framework. As many readers will have realized, there is more that I have left out than I was able to include!
We didn’t get to talk about ecosystems, testing, async processing through queues (or natively in Elixir), email, caching, among other things. But hopefully what I was able to do was to point out a few of the commonalities between these powerful frameworks, as well as other areas where they might differ in terms of their philosophy and/or implementation.
If you are interested in taking a look at the code I used in these examples, here are links to the Phoenix app and the Rails app. If you haven’t tried Phoenix or the Elixir language before, please do! You won’t be disappointed.
Reference: | Comparing Rails and Phoenix: Part II from our WCG partner Florian Motlik at the Codeship Blog blog. |