Adding Fields to an Ecto Model in Phoenix
I recently needed to add some fields to an Ecto model that I had generated with mix phoenix.gen.html [...]
, and to use the new fields in a Phoenix app. Google did not immediately serve up a simple tutorial, so here it is!
This assumes you have generated a new Phoenix app and then generated a Users model, like so:
$ mix phoenix.new my_app
Fetch and install dependencies? [Yn] Y
$ cd my_app
$ git init && git add . && git commit -m "Initial commit of generated Phoenix app"
$ curl http://[your-id].mit-license.org > LICENSE
$ git add LICENSE && git commit -m "Add MIT License"
$ mix phoenix.gen.html User users name:string email:string
$ git add . && git commit -m "Add generated User model"
# edit web/router.ex as instructed in output
$ git add . && git commit -m "Add users resources to browser scope"
$ mix ecto.create
$ mix ecto.migrate
$ mix phoenix.server
If you need more information on any of the steps, they are covered in detail in Phoenix and Ecto from mix new to Heroku, or feel free to ask in the comments below.
Before moving on, visit http://localhost:4000/users and make sure you can add, edit, and delete a user.
After discovering that I needed to add some fields to the User model, I checked to see if there was something like rails generate migration NAME [field[:type]...]
.
Not quite (yet?), but there is mix ecto.gen.migration
which will generate a skeleton for you to fill in. You can read about it in the Phoenix Mix Tasks docs docs, or in the Ecto docs.
Note: If you search for something related to Phoenix and end up at a URL that contains a version number such as /v0.10.0/
, be sure to delete that bit of the URL and reload the page so that you are looking at the latest version of the docs.
Generate a Migration
Let’s generate a migration to add fields to our User model.
$ mix ecto.gen.migration add_fields_to_users
* creating priv/repo/migrations
* creating priv/repo/migrations/20150727000247_add_fields_to_users.exs
$ git add . && git commit -m "Add generated add_fields_to_users migration"
If we open the [datetime]_add_fields_to_users.exs
file it says it created, we’ll see this:
defmodule MyApp_726605.Repo.Migrations.AddFieldsToUsers do
use Ecto.Migration
def change do
end
end
It’s up to us to tell it what needs to be changed. We can get a hint by looking at the other file in the migrations directory, named [datetime]_create_user.exs
:
defmodule MyApp_726605.Repo.Migrations.CreateUser do
use Ecto.Migration
def change do
create table(:users) do
add :name, :string
add :email, :string
timestamps
end
end
end
We want essentially the same thing with alter table
instead of create
, and with our new fields and types. Let’s update the add_fields_to_users migration with this:
@@ -2,5 +2,11 @@ defmodule MyApp_726605.Repo.Migrations.AddFieldsToUsers do
use Ecto.Migration
def change do
+ alter table(:users) do
+ add :user_id, :string
+ add :access_token, :binary
+ add :access_token_expires_at, :datetime
+ add :refresh_token, :binary
+ end
end
end
$ git add . && git commit -m "Update add_fields_to_users migration"
Run the Migration
Before we run the migration, let’s have a look at the database in the Postgres psql
console:
# \list
# \connect my_app_726605_dev
# \d
# \d users
Table "public.users"
Column | Type | Modifiers
-------------+-----------------------------+----------------------------------------------------
id | integer | not null default nextval('users_id_seq'::regclass)
name | character varying(255) |
email | character varying(255) |
inserted_at | timestamp without time zone | not null
updated_at | timestamp without time zone | not null
Now we can run the migration…
$ mix ecto.migrate
20:22:42.939 [info] == Running MyApp_726605.Repo.Migrations.AddFieldsToUsers.change/0 forward
20:22:42.939 [info] alter table users
20:22:42.956 [info] == Migrated in 0.1s
… and have another look at the users table:
# \d users
Table "public.users"
Column | Type | Modifiers
-------------------------+-----------------------------+----------------------------------------------------
id | integer | not null default nextval('users_id_seq'::regclass)
name | character varying(255) |
email | character varying(255) |
inserted_at | timestamp without time zone | not null
updated_at | timestamp without time zone | not null
user_id | character varying(255) |
access_token | bytea |
access_token_expires_at | timestamp without time zone |
refresh_token | bytea |
Indexes:
"users_pkey" PRIMARY KEY, btree (id)
What if we made a mistake? If so, we can roll back with:
$ mix ecto.rollback
If you now look at the database table, the additional fields will be gone. (If you did the rollback, re-run the migration to put the fields back in place.)
Update Phoenix App
Now that our new fields exist in the database, let’s look at what’s needed to use them in our Phoenix app.
These aren’t fields that will need to be edited directly, (they will be coming from another application after the user grants us access to their data via OAuth,) but we can display the user_id on the ‘show’ page.
Let’s edit the template at web/templates/user/show.html.eex
:
@@ -12,6 +12,12 @@
<%= @user.email %>
</li>
+ <li>
+ <strong>User ID:</strong>
+ <%= @user.user_id %>
+ </li>
+
+
</ul>
<%= link "Back", to: user_path(@conn, :index) %>
If we add a user and then click the ‘Show’ link, we’ll get an error. We need to add the user_id to the model. While we’re at it, let’s add the other fields and make them all optional. In web/models/user.ex
:
@@ -4,12 +4,16 @@ defmodule MyApp_726605.User do
schema "users" do
field :name, :string
field :email, :string
+ field :user_id, :string
+ field :access_token, :binary
+ field :access_token_expires_at, Ecto.DateTime
+ field :refresh_token, :binary
timestamps
end
- @required_fields ~w(name email)
- @optional_fields ~w()
+ @required_fields ~w()
+ @optional_fields ~w(name email user_id access_token access_token_expires_at refresh_token)
(Note that if you do not add the new fields to either @required_fields
or @optional_fields
, they will be ignored when you attempt to update the database.)
Now, visiting http://localhost:4000/users, adding a user and clicking ‘Show’ will work – the User ID label will be displayed along with no value since the field is empty.
git add . && git commit -m "Add new fields to model. Make all fields optional. Add user_id to show template."
Next let’s simulate adding a user after they return from the OAuth flow and have granted us access to their data. In reality there will be other libraries and a separate controller involved, but we’ll just add a new function to the page controller.
In web/page_controller.ex
, add this above the current index
function:
alias MyApp_726605.User
def index(conn, %{"test" => _}) do
changeset = User.changeset(%User{},
%{name: "Amy Smith",
email: "amy@example.com",
user_id: "ABC123",
access_token: "fjlsfj2l34h2lh2l432lj",
refresh_token: "l4l2k34h2l234k2h97sf",
access_token_expires_at: {{2015, 12, 31}, {12, 00, 00}}
})
Repo.insert!(changeset)
render conn, "index.html"
end
This must be added above the current index
method due to the pattern match. This function definition will match on a request that contains ‘test’ as a parameter. (The underscore means that we don’t care what the value is, just that it is present.) If there is no ‘test’ parameter, then it will continue on and run the original index
function that does not care about the parameters.
Now if you visit http://localhost:4000/?test you will get routed to this function instead of the default index function, and the console log will show:
[debug] Processing by MyApp_726605.PageController.index/2
Parameters: %{"format" => "html", "test" => nil}
Pipelines: [:browser]
[debug] BEGIN [] OK query=0.3ms
[debug] INSERT INTO "users" ("access_token", "access_token_expires_at", "email", "inserted_at", "name", "refresh_token", "updated_at", "user_id") VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING "id" ["fjlsfj2l34h2lh2l432lj", {{2015, 12, 31}, {12, 0, 0, 0}}, "amy@example.com", {{2015, 7, 27}, {11, 18, 47, 0}}, "Amy Smith", "l4l2k34h2l234k2h97sf", {{2015, 7, 27}, {11, 18, 47, 0}}, "ABC123"] OK query=0.7ms
[debug] COMMIT [] OK query=0.7ms
You can see that values for all of the fields are filled in, and there were no errors. If you return to http://localhost:4000/users and click ‘Show’ for the last one in the list, you should see that user id displayed:
$ git add . && git commit -m "Simulate adding a user to the database after the OAuth flow"
The code for this example is available at https://github.com/wsmoak/my_app_726605/tree/20150727
Copyright 2015 Wendy Smoak - This post first appeared on http://wsmoak.github.io and is licensed CC BY-NC.