When we left off last, we had sat down and gained a thorough knowledge of controllers and the entire connection model. We began with the internal request, going through the router, hitting the glue of the controller, and finally wiring up data and displaying it back to the user via our views and templates. All of this is great by itself, but if we don't have somewhere to store and retrieve data, our application is largely decorative and not terribly functional. We're going to change that by implementing a means of getting our data and putting it back into a database.
Before we can dive too far into storing our data, however, we need to understand the model behind how Ecto takes the information from the database and presents it to the application at large. Ecto, the database library that we'll be using in our project, relies on the concepts of Schemas and Contexts. Through the combination of these techniques, we can safely separate out the side effects of working with a database that may be changing constantly due to interactions with our application. Let's take a few minutes first, however, to understand how contexts and schemas interact with each other and what roles they take in the data interaction layer of our application.
In this chapter, we'll take a deep dive into the topics most directly facing the backend of our application. Specifically, we'll be taking on:
These are all necessary to really understand how the database side of things glues the rest of our application together over time. By the end of the chapter, you should have a thorough working knowledge of all of the database-specific parts of your application and how they all fit together!
The first thing to start understanding is where and how schemas define the shape of the data in your database. Schemas help describe what tables your application uses behind the scenes and what fields exist to Ecto; the Schemas themselves do not define the overall structure to the database itself.
This helps describe the columns and define what types each of the columns are (for example, a string, an integer, or a reference to another table). The important thing to note about schemas in Ecto is what they are intended to do: separate the ideas of data from operations. By keeping our schemas very specific to understanding, describing, and translating the data, we can keep our applications largely side-effect-free when interacting with the database!
Before we can take a really deep dive into schemas, we should start by talking about what our initial voting data model should look like! Let's take a look at the code we introduced for our data model in the controller that we wrote in the last chapter:
poll = %{
title: "My First Poll",
options: [
{"Option 1", 0},
{"Option 2", 5},
{"Option 3", 1}
]
} Based on this, we can probably decide that our model for our Vote concept in the database can be a pretty simple thing. We have the Vote itself, which has a title attached to it, and then the options that people can decide between. We're going to make an assumption here that you're using the default database choice for Phoenix applications, Postgres (but it shouldn't change much regardless of your database choice!). So, a very simple database table model would be:
| Poll | Title Options (reference to another table |
| Option | Poll ID (reference to the poll this option is attached to) Title Votes |
This creates two separate tables: Polls and Options. The Polls table will store all of the Polls themselves and then the options table will store the possible vote choices and their current scores. The Options table will store a reference back to the original poll (we'll get into this later when we start talking about associations). This is typically referred to as a "one to many" relationship between the two tables; a Poll can have many Options, but an Option can only have one Poll. This also means that the referencing of different tables only needs to take place on the Option, not on the Poll.
Given our newfound understanding of our complex object, we need to create our database table. Now, there are a few ways to do this. For example, we can use Phoenix and Ecto's built-in generators to give us a skeleton for this, but since we're trying to learn and understand the underlying systems that are needed for our application, we’ll start by NOT using generators to build our initial skeletons; later on, we’ll dive a little bit into using the generators and how to use them.
We'll get into a habit of learning what we can from the various help commands that exist in mix and IEx, so let's do the same. We can search for any of the Ecto commands that exist for mix with the following:
$ mix help | grep ecto
mix ecto # Prints Ecto help information
mix ecto.create # Creates the repository storage
mix ecto.drop # Drops the repository storage
mix ecto.dump # Dumps the repository database structure
mix ecto.gen.migration # Generates a new migration for the repo
mix ecto.gen.repo # Generates a new repository
mix ecto.load # Loads previously dumped database structure
mix ecto.migrate # Runs the repository migrations
mix ecto.migrations # Displays the repository migration status
mix ecto.rollback # Rolls back the repository migrations
mix phx.new.ecto # Creates a new Ecto project within an umbrella project
Typically any generator will have a .gen as part of the command itself. We've already run mix ecto.create to create our database, and we don't need more help information, nor do we want to drop or dump anything from our database. We don't need to load, and we have nothing to migrate yet, so that also crosses mix ecto.migrate and mix ecto.migrations off our list. Finally, we see mix phx.new.ecto, which talks about creating a new Ecto project within an umbrella project, which also doesn't fit into what we're trying to accomplish here, so that leaves us with our two generator commands.
mix ecto.gen.repo creates a new repository for our application, which we don't need, so we're going to work with mix ecto.gen.migration to create a migration for our application! One of my favorite parts of working with Elixir and Phoenix is how entirely fantastic the documentation that is built-in to almost every single command is, so let's take a look at the documentation for the migration generator and learn how to use it. To get help for anything in mix remember that we can prefix help to any of the commands; let's run mix help ecto.gen.migration:
$ mix help ecto.gen.migration
mix ecto.gen.migration
Generates a migration.
The repository must be set under :ecto_repos in the current app configuration
or given via the -r option.
## Examples
mix ecto.gen.migration add_posts_table
mix ecto.gen.migration add_posts_table -r Custom.Repo
The generated migration filename will be prefixed with the current timestamp in
UTC which is used for versioning and ordering.
By default, the migration will be generated to the "priv/YOUR_REPO/migrations"
directory of the current application but it can be configured to be any
subdirectory of priv by specifying the :priv key under the repository
configuration.
This generator will automatically open the generated file if you have
ECTO_EDITOR set in your environment variable.
## Command line options
• -r, --repo - the repo to generate migration for
Location: _build/dev/lib/ecto/ebin
Okay, this is a pretty simple command! Based on the help documentation, we can see that what we want to do is create a migration that creates a new table, which we’ve referred to previously as Polls.
Based on that, let's create our new migration, add_polls_table:
$ mix ecto.gen.migration add_polls_table
* creating priv/repo/migrations
* creating priv/repo/migrations/20171005161434_add_polls_table.exs
By default, that will give us a mostly blank file consisting of:
defmodule Vocial.Repo.Migratio...