Ectogram: Generating The Users Table
14 min readNow that we have our project all set up and Ecto customized we can begin generating migrations for our database. Coming from frontend land I don’t have much experience with performing migrations so this will be great practice. For those out there that don’t know a migration is a snapshot of what the data in your database looks like at that point in time. In our case this is the first migration and we will be adding a table to it called users
. We define what the users table looks like by defining columns on the table. A row in that table could end up looking something like the below:
With Ecto we have te ability to generate migrations from the command-line using mix
, but we also have the ability to apply migrations and rollback if necessary. I am sure I am not the only one out there who has forgotten to apply migrations to the local database after pulling in changes on a git tree, this is where rollback can come in handy. We will take a look at mix ecto.rollback
a little later when we have more migrations to work with. If you take a look at our mix.exs
you can see when we run the ecto.setup
command we apply all of our migrations and then seed the database. This comes in particularly handy if you have someone new joining your team. Documentation can tell that user:
After cloning the repository and confirming you have
elixir
anderlang
on your machine just runmix setup
.
As long as migrations are checked into version control, which they should be! Your team can stay up-to-date and your database in harmony.
Generating The Users Table
With Ecto’s command-line tools we can generate a template for our migrations:
As per the documentation from Ecto.Migration there are three functions we can use here change/0
, down/0
, and up/0
. change/0
essentially is an up/0
which applies the changes made in it’s function body to the database. We could add several tables, associations, constraints, or indexes. As long as what we have written is in congruence with our previous migrations and the database engine we selected to use (Postgres in our case), then we should have no problems. The majority of the code for this section is very similar to the code generated when using phx.gen.auth
. Hats off to Aaron Renner for the great job on this generator that is now a stable part of Phoenix 1.6 and onwards. The down/0
on the other hand is for undoing changes that the up or change made. This will become clearer when we work with mix ecto.rollback
. In the mean time we will add the below to the migration template:
To go over some of the code above we define our columns as the first value after the add/3
function. It follows as the :term
we want to name our column, the :type
our column should be, and any further options represented as a keyword list. It might be helpful to see it written as:
As seen in the example we can define if a column should be nullable or not and we can even define the default value a row should be created with if no value is given in the query.
We might touch on it later on in this series, but there is also the ability to define columns with enums using Ecto.Enum. I am a big fan of using enums for things like statuses, roles, or any other state that is stored in the database that can be more than two state.
You might notice we can create indexes to speed up the read times on our queries. We can also add constraints. We create a simple constraint that will check to see if the user registering to use Ectogram is 18 or older. To do that we write some SQL in the check
field.
For anyone not familiar with that SQL what is happening is we are passing the given
birth_date
to theAGE
function in Postgres. This returns an object with keys likeyear
,month
, etc where the values are numbers. TheDATE_PART
function then cherry picks the desired part of the object for us. An example would be my birth year would evaluate to 33 when passed through these functions and then ran against the>= 18
comparison. Postgres has some pretty slick features!
With the migration written we can go ahead and apply our changes to the database by running mix ecto.migrate
:
And with that we have written and applied our first migration with Ecto! We can verify that our migration was applied in a few different manners. We can query the database via iex
, psql
, or an SQL client:
We can see that we actually have two tables; our users
table and a table who’s sole purpose is to track our migrations. That timestamp that you see added to the name of your migration becomes a key that references when the migration was applied to the database.
Adding The User Schema
The schema is a representation of a row (singular) in our users table in a manner which is friendly in the language or framework we are working in. No one really wants to write a bunch of SQL queries daily and this is why all those ORMs (Object Relational Maps) exist across all languages. The Ecto.Schema
is that tool in elixir:
You can see we model our schema in a very similar manner to our migration. You might notice we added a password
field that was not present in our migration. You can add lot and lots of fields to your schema that will never be added to the actual database. First since our migration doesn’t have a column for password
it won’t be added anyways. Perhaps we have :first_name
and :last_name
in our database, but our frontend wants to expose on the form the concept of :full_name
. We could have :full_name
on our schema and have code that splits the string and sends the respective values to the correct column.
A cool feature to point out is the redact: true
option. Especially in development you will notice SQL queries are logged out to the terminal via the debug logger. If you remove the redact option you will see the plain text password in your terminal when the query is executed. With this option in place that field will be “redacted” or removed from the logs which is great if we are sending logs to a third party service to monitor our application.
Before we wrap up this session don’t forget to add the following to your .iex.exs
file:
Now the user schema will be available to us in iex
at the top level.
Adding Bcrypt
We will need to add one dependency before moving forward. We will be making use of the popular bcrypt
password hashing library. Add the following to your mix.exs
and then install the dependency:
Working With Changesets
Ecto introduced me to the concept of a changeset. A changeset is:
A changeset is a way in which Ecto takes in user data and validates that the data is as we want it before allowing it to be written to the database.
An example of a changeset at work would be if we do not give an email because it is marked as a non-nullable field Ecto will return an error message stating that you cannot leave this field empty. So we try again and submit the number 1. Ecto again throws and error and says that is an invalid format. This goes on and on until the correct format and type is met.
In the below registration_changeset/3
we cast the incoming data to the %User{}
struct with essentially a whitelist of fields. If we don’t include a field in this list it will not be cast to the schema. Trust me I learned this the hard way when I forgot to add :url
to the @required_fields
attribute . Then we begin our validation checks. Ecto.Changeset
comes with many pre-baked validation functions. If you need to run many different checks on a piece of data it’s common practice to group those checks into a function by the name of the data. For instance our validate_password/2
below checks to see that a password was given to it, the min and max length of the string, and various regexes are ran against the string to verify it fits the specific format we are requiring of the user. The key thing to remember when creating your own custom validation functions is that the function should take in a changeset and return a changeset so it can continue to be used in a pipeline.
For brevity I removed a lot of the validations that are being ran to condense this section down you can view the whole file here.
The first thing that happens is that we cast the input (attrs
) against our data. In the case of registration_changeset/3
our data is the %User{}
struct which is empty. That call to cast/3
will ONLY cast the keys that we permit it too. That is where the concatenation of @required_fields
and @optional_fields
comes in. These are the keys we are telling Ecto to apply as pending changes.
Cool tidbit I learned recently was that instead of creating the attribute
@required_fields
as a list of atoms you can instead use the~w
sigil which is for word lists add the words you want to use in a non-comma separated list, and append thea
at the end. This will generate that same list of atoms.
The very next thing we do is get into validating the data and checking the various constraints. You can see that in validate_birth_date/1
we explicitly check the constraint we defined in our migration :eighteen_or_older_only
.
Seeding The Database
We will make use of the popular faker
library for generating a bunch of random data for our database. One thing to note is that random data needs to fit within the validation and constraints of our database. That is why you will see so many manipulations of the Faker seed data
With our first migration and schema under our belt we can now move forward with building Ectogram into a the Instagram clone it is trying to become! However before we do that we will get our feet wet with testing in Elixir so we can verify we haven’t missed any edge cases in our implementations. All of the code for this section can be found in PR#2.
Related Articles
Ectogram: Testing Ecto
Part 4 in the Ectogram series where I cut my teeth on testing the user schema with ExUnit.
Ectogram: Setting Up Ecto
Part 2 in the Ectogram series where I setup the project and make customizations to Ecto.
Ectogram: Introduction
A clone of the popular social media platform, Instagram, written in Elixir & Ecto.
Cody is a Christian, USN Veteran, Jayhawk, and an American expat living outside of Bogotá, Colombia. He is currently looking for new opportunities in the tech industry.