Schemas And Migrations
Mix.install([
{:jason, "~> 1.4"},
{:kino, "~> 0.9", override: true},
{:youtube, github: "brooklinjazz/youtube"},
{:hidden_cell, github: "brooklinjazz/hidden_cell"},
{:ecto, "~> 3.9.5"}
])
Navigation
Home Report An Issue Blog: SearchBlog: MigrationReview Questions
Upon completing this lesson, a student should be able to answer the following questions.
- How do you generate a new migration?
- How do you create or alter a table in the database?
- How do you create, alter, or remove a field from a table in the database?
- Why might you validate data in the schema vs the migration?
Overview
In Elixir, migrations and schemas are used to manage and manipulate data stored in a database.
Migrations are modules that define changes to be made to the structure of a database. A migration can create, modify, or delete tables and columns in a database, and can be used to update the structure of the database to match the latest version of the application.
Schemas are modules that define the structure of data that will be stored in a database. A schema specifies the fields that a record in the database will have, and the data types of those fields.
When using migrations and schemas together in Elixir, the schema is typically used to define the structure of the data that will be stored in the database, and the migration is used to create and modify the tables and columns in the database to match the schema.
For example, if you have a schema that defines a users table with name, email, and password_hash fields, you might create a migration that creates a users table in the database with the same fields as defined in the schema. Then, you can use the schema to insert, update, and query records in the users table.
Migrations and schemas can be used together to make it easier to manage and manipulate data in a database. By defining the structure of the data in a schema and the structure of the database in a migration, you can ensure that the data in the database is always consistent with the schema and the latest version of the application.
Ecto is a library for the Elixir programming language that is used for working with databases. Ecto.Schema
is a module within Ecto that defines how data is mapped to and from a database. Ecto.Migration
is a module that is used for defining and running database migrations, which are a way to change the structure of a database over time in a predictable and organized manner. Ecto.Changeset is a module that is used to apply changes to data in a database, ensuring that the changes are valid and conform to the constraints defined in the associated Ecto.Schema
.
Here is an example of how these three modules might be used together:
Schema
First, you would define your Ecto.Schema
, which specifies the structure of the data you want to store in the database. For example:
defmodule MyApp.User do
use Ecto.Schema
schema "users" do
field :name, :string
field :email, :string
field :age, :integer
timestamps()
end
end
This schema defines a MyApp.User
module that can be used to represent a user in a database. The schema specifies that the user data will be stored in a table named users, and it defines three fields: name
, email
, and age
. Each field has a corresponding data type, such as :string
for the name
and email
fields, and :integer
for the age
field.
The timestamps/0
function is also called at the end of the schema, which will add inserted_at
and updated_at
fields to the users
table. These fields will be automatically updated by Ecto to track the creation and modification timestamps for each user record.
With this schema in place, you can use the MyApp.User
module to define and manipulate user data in your Elixir application. Ecto will automatically convert between Elixir data structures and database records, making it easy to work with user data in a consistent and efficient way.
Migration
Next, you would use Ecto.Migration
to define and run a migration for the “users” table in the database. This would involve creating a new module for the migration, in which you would use Ecto.Migrate to define the changes you want to make to the database structure:
defmodule MyApp.Repo.Migrations.CreateUser do
use Ecto.Migration
def change do
create table(:users) do
add :name, :string
add :email, :string
end
end
end
A database migration is a way of changing the structure or content of a database over time. This can include things like adding or removing tables, columns, or rows; changing the data types or constraints of existing columns; or updating the data in existing rows.
Database migrations are often used in software development to evolve the structure of a database as the underlying software changes and grows over time. They can help ensure that the database remains consistent and correct, even as the software and its requirements evolve.
In general, database migrations involve writing code that specifies the changes to be made to the database. This code is typically run using a tool or library designed for managing database migrations, which will automatically apply the changes to the database in a safe and orderly manner. This can help ensure that the migration process is consistent, repeatable, and reversible, which is important for maintaining the integrity of the database over time.
You can use any of the Elixir Primitive Types and they will be converted to the appropriate Field Type for your database.
To run this migration, you would use the mix ecto.migrate
task provided by Ecto. This task will automatically apply the migration to the database, creating the users table and adding the name
, email
, and age
columns to it.
It’s important to note that Ecto will track which migrations have been applied to the database, and it will only apply migrations that haven’t been run before. This means that you can safely run the mix ecto.migrate
task multiple times, and it will only apply new or unapplied migrations. This can be very useful for managing the evolution of your database over time.
Migration Generator
To generate an Ecto migration in Elixir, you can use the mix ecto.gen.migration
command. This command is provided by the Ecto library, and it can be used to create a new Ecto migration module, which you can use to define and apply changes to your database.
To generate an Ecto migration, you can run the following command from the root directory of your Elixir project:
mix ecto.gen.migration
Replace ` with the desired name for your migration. This name should be a descriptive and unique identifier for the migration, such as
adduserstableor
update_product_prices. When you run this command, Ecto will generate a new migration module and place it in the
priv/repo/migrations/directory of your project. The generated module will contain some basic skeleton code for defining and applying changes to your database. You can then edit this module to add the specific changes that you want to make. Once your migration is ready, you can apply it to your database using the
mix ecto.migrateMix task provided by Ecto. This task will automatically apply any unapplied migrations to your database, ensuring that it stays up-to-date and consistent with your application code. ## Schema Generator
mix phx.gen.schemais a Mix task provided by Phoenix. It is used to generate a new schema file for a given table in the database. This can save time and reduce the potential for errors when creating a schema, as it allows you to quickly generate the basic structure of the schema based on the structure of the existing database table. To use
mix phx.gen.schema, you need to provide the name of the schema you want to generate, as well as the name of the database table that the schema will be based on. For example, if you have a table called
usersin your database and you want to generate a schema called
User, you can use the following command: ``` mix phx.gen.schema User users ``` This will generate a new schema file called
user.exin the
lib/my_app/directory (assuming the default directory structure for an Elixir project). The file will contain the basic structure of the schema, with fields for each column in the users table. You can then customize the schema as needed, adding additional fields or modifying the types of the existing fields. You can also include fields in the initial creation of a schema. ``` mix ecto.gen.schema User users name:string email:string age:integer ``` This would generate the following schema. ```elixir defmodule MyApp.User do use Ecto.Schema import Ecto.Changeset schema "users" do field :age, :integer field :email, :string field :name, :string timestamps() end @doc false def changeset(user, attrs) do user |> cast(attrs, [:name, :email, :age]) |> validate_required([:name, :email, :age]) end end ``` And the following migration. ```elixir defmodule MyApp.Repo.Migrations.CreateUsers do use Ecto.Migration def change do create table(:users) do add :name, :string add :email, :string add :age, :integer timestamps() end end end ``` ## Manipulating Tables Ecto provides functions for manipulating tables in a database. Here are a few common functions: * [create/2](https://hexdocs.pm/ecto_sql/Ecto.Migration.html#create/2): creates a new table in the database * [alter/2](https://hexdocs.pm/ecto_sql/Ecto.Migration.html#alter/2): modifies an existing table in the database * [rename/2](https://hexdocs.pm/ecto_sql/Ecto.Migration.html#rename/2): renames an existing table in the database * [drop/2](https://hexdocs.pm/ecto_sql/Ecto.Migration.html#drop/2): removes an existing table from the database ## Manipulating Fields To alter a table in an Elixir Ecto migration, you can use the
alter/2function provided by Ecto. This function allows you to make changes to an existing table in the database, such as adding or removing columns, changing data types, or modifying constraints. [Ecto.Migration](https://hexdocs.pm/ecto_sql/Ecto.Migration.html) provides functions for manipulating fields in a table. Here are a few to get you started. * [add/3](https://hexdocs.pm/ecto_sql/Ecto.Migration.html#add/3) adds a new field to the table. * [modify/2](https://hexdocs.pm/ecto_sql/Ecto.Migration.html#modify/3) modifies an existing field in the table. * [remove/2](https://hexdocs.pm/ecto/Ecto.Migration.html#remove/1) removes an existing field from the table. Here is an example migration that uses some of these Ecto functions to manipulate fields on a table: ```elixir defmodule MyApp.Repo.Migrations.AlterUsersTable do use Ecto.Migration def change do alter table(:users) do add :phone, :string modify :email, :string, unique: true modify :name, :string, null: false remove :age end end end ``` In this example, the
alter/2function is used to make several changes to the users table in the database. The
add/2function is called to add a new phone column to the table, with a data type of :string. The
modify/3function is then used to update the email and name columns, adding a unique constraint to the email column and making the name column non-nullable. Finally, the
remove/1function is called to remove the age column from the table. Once this migration is run using the
mix ecto.migrateMix task, the changes will be applied to the users table in the database, resulting in a new phone column, a modified email column, a modified name column, and a removed age column. It's important to note that Ecto will only apply unapplied migrations, so you can safely run the ecto.migrate task multiple times without worrying about making the same changes to the database more than once. This can be very useful for managing the evolution of your database over time. ## Ensure Schemas Reflect Migrations Elixir schemas and migrations work together to define and manage the structure and content of a database. Schemas are used to define the shape and structure of data in a database, while migrations are used to apply changes to the database over time. When we alter our database table with a migration, we also have to modify the schema to reflect those changes. For example, if we want to modify our user table to add a
phonefield, remove the
agefield, modify the
emailfield to be unique, and modify the
namefield to not be null, our schema should also reflect these changes otherwise we'll encounter issues when reading or writing to the database. ```elixir defmodule MyApp.User do use Ecto.Schema schema "users" do field :name, :string field :email, :string # add the phone field field :phone, :string # remove the age field # field :age, :integer timestamps() end # Modify changeset to reflect database constraints. def changeset(user, params \\ %{}) do user |> cast(params, [:name, :email, :phone]) |> validate_required([:name]) |> validate_format(:email, ~r/@/) |> unique_constraint(:email) end end ``` If our schema uses
Ecto.Changesetto validate data in a
changeset/2function, we may also want to modify any validations to reflect the database constraints. ## Database Constraints Ecto provides the ability to add constraints to your database tables using migrations. Constraints are rules that specify the allowed values for fields in your table, such as the minimum and maximum values for numeric fields, or the maximum length for string fields. To add constraints to your table using Ecto, you can use the [constraint/3](https://hexdocs.pm/ecto_sql/Ecto.Migration.html#constraint/3) function in your migration. This function takes the name of the constraint as its first argument, followed by the options for the constraint. Here is an example migration that adds some constraints to a
userstable: ```elixir defmodule MyApp.Repo.Migrations.AddConstraints do use Ecto.Migration def change do alter table(:users) do constraint(:users_email_unique, unique: true) constraint(:users_age_non_negative, check: "age >= 0") constraint(:users_name_length, check: "char_length(name) <= 100") end end end ``` This migration adds three constraints to the
userstable. The first constraint, called
users_email_unique, ensures that the
emailfield in the table has unique values. The second constraint, called
users_age_non_negative, ensures that the
agefield in the table is non-negative. The third constraint, called
users_name_length, ensures that the
namefield in the table has a maximum length of 100 characters. In Ecto, it is generally recommended to enforce constraints in both the migration and the schema. This provides two layers of protection against invalid data being inserted into your database. The migration is responsible for defining the structure of the database, including the fields and constraints for each table. By adding constraints to the migration, you can ensure that the structure of the database is correct and that the fields in each table have the correct data types and constraints. The schema, on the other hand, is responsible for defining the structure of the data that will be stored in the database. By adding constraints to the schema, you can ensure that the data being inserted into the database conforms to the constraints you have defined. This can prevent invalid data from being inserted into the database, and can help ensure the integrity of your data. Therefore, it is generally recommended to enforce constraints in both the migration and the schema. This provides a robust and reliable way to ensure that the data in your database is valid and consistent. ## Rollbacks In an Elixir Ecto migration, the
upand
downfunctions are used to define the changes that will be made to the database when the migration is run. The
upfunction contains the code that will be executed to apply the changes, and the down function contains the code that will be executed to undo those changes. Here's an example of how you might use the
upand
downfunctions in a migration: ```elixir defmodule MyApp.Repo.Migrations.CreateUsersTable do use Ecto.Migration def up do create table(:users) do add :name, :string add :email, :string add :password_hash, :string timestamps() end end def down do drop table(:users) end end ``` In this example, the
upfunction uses the
createEcto function to create a new
userstable in the database. The
downfunction uses the
dropEcto function to delete the users table from the database. When this migration is run using the
mix ecto.migratecommand, the
upfunction will be executed and the
userstable will be created in the database. If you ever need to undo the changes made by this migration, you can run the
mix ecto.rollbackcommand, which will run the
downfunction and delete the
userstable from the database. In general, the
upand
downfunctions should be used together to define a set of changes that can be applied to the database and then undone if necessary. This allows you to easily roll back changes if you encounter any problems or need to make changes to your database schema. ## Ecto Commands In Elixir, the
mix ectocommands are used to manage an Ecto project. Here is a list of all the mix ecto. commands and a brief description of what each one does: *
mix ecto.create: This command creates a new database for an Ecto project. *
mix ecto.drop: This command drops the database for an Ecto project. *
mix ecto.gen.migration: This command generates a new migration file for an Ecto project. *
mix ecto.gen.schema: This command generates a new schema file for an Ecto project. *
mix ecto.migrate: This command runs all pending migrations for an Ecto project. *
mix ecto.rollback: This command rolls back the latest migration for an Ecto project. *
mix ecto.reset: This command rolls back all migrations and then runs them again for an Ecto project. *
mix ecto.setup: This command sets up a new Ecto project by creating the database and running any pending migrations. *
mix ecto.version: This command prints the current version of Ecto that is installed in the project. These commands can be used to perform various tasks related to managing an Ecto project, such as creating and dropping the database, generating and running migrations, and resetting the database schema. ## Your Turn ### Generate Todo List Application We're going to create a todo-list application to learn more about Ecto schemas and migrations. Generate a phoenix project. ``` mix phx.new todo_list ``` Create the Postgres database. ``` mix ecto.create ``` Ensure the project compiles and all tests pass. ``` mix test ``` We can generate both the migration and the schema with a single command. Note that we can create these files manually, generators are purely for convenience. ``` mix phx.gen.schema TodoItems.TodoItem todo_items title:string ``` This command generated the create todo items migration in
priv/repo/migrations. ```elixir defmodule TodoList.Repo.Migrations.CreateTodoItems do use Ecto.Migration def change do create table(:todo_items) do add :title, :string timestamps() end end end ``` The command also generated the
TodoItemschema in
lib/todo_list/todo_items/```elixir defmodule TodoList.TodoItems.TodoItem do use Ecto.Schema import Ecto.Changeset schema "todo_items" do field :title, :string timestamps() end @doc false def changeset(todo_item, attrs) do todo_item |> cast(attrs, [:title]) |> validate_required([:title]) end end ``` ### Run Migrations And Create A TodoItem Record Run migrations to add the
todo_itemstable to your database. ``` mix ecto.migrate ``` Open the IEx shell so we can ensure our schema and migration work as expected. ```elixir $ iex -S mix iex> alias TodoList.TodoItems.TodoItem iex> changeset = TodoItem.changeset(%TodoItem{}, %{title: "finish schemas and migrations reading"}) iex> TodoList.Repo.insert(changeset) iex> TodoList.Repo.all(TodoItem) ``` This Elixir code performs the following actions: 1. It uses the alias keyword to create an alias for the TodoList.TodoItems.TodoItem module. This allows the code to refer to the module using the shorter TodoItem alias. 2. It uses the
TodoItem.changeset/2function to create a changeset for a new
TodoItemrecord. The changeset is created by passing the empty
TodoItemstruct
%TodoItem{}and a map of field values
%{title: “finish schemas and migrations reading”}to the changeset/2 function. 3. It uses the
TodoList.Repo.insert/1function to insert the new
TodoItemrecord into the database. This function is called by passing the changeset created in the previous step as an argument. 4. It uses the
TodoList.Repo.all/1function to fetch all
TodoItemrecords from the database. This function is called by passing the
TodoItemmodule as an argument. In summary, this code creates a new
TodoItemrecord in the database and then fetches all TodoItem records from the database. You should see a
TodoItem` record similar to the following:
```elixir
[debug] QUERY OK source=”todo_items” db=0.3ms queue=0.5ms idle=1322.0ms
SELECT t0.”id”, t0.”title”, t0.”inserted_at”, t0.”updated_at” FROM “todo_items” AS t0 []
↳ :erl_eval.do_apply/7, at: erl_eval.erl:744
[
%TodoList.TodoItems.TodoItem{
__meta: #Ecto.Schema.Metadata<:loaded, “todo_items”>,
id: 1,
inserted_at: ~N[2022-12-13 05:56:42],
title: “finish schemas and migrations reading”,
updated_at: ~N[2022-12-13 05:56:42]
}
]
### Add A `completed` Field Now that we have a working `TodoItem` schema and migration, let's add a `:completed` boolean field. First, we'll create a new migration. Once again, we can create this file manually, but it's more convenient to use a generator.
mix ecto.gen.migration add_completed_to_todo_items
In the generated `priv/repo/migrations/TIMESTAMP_add_completed_to_todo_items` migration, alter the `todo_items` table to include a `completed` boolean field. We're going to make `completed` a required field so it cannot be `null`.
elixir
defmodule TodoList.Repo.Migrations.AddCompletedToTodoItems do
use Ecto.Migration
def change do
alter table(:todo_items) do
add :completed, :boolean, null: false
end
end
end
Run migrations. You'll notice an error because the existing database data has TodoItem records with no completed field.
$ mix ecto.migrate
19:29:06.032 [info] == Running 20221211032143 TodoList.Repo.Migrations.AddCompletedToTodoItems.change/0 forward
19:29:06.035 [info] alter table todo_items
* (Postgrex.Error) ERROR 23502 (not_null_violation) column “completed” contains null values
There are multiple ways to resolve this issue: 1. In database where we don't need to preserve existing data (such as our local development environment), we could simply drop the database with `mix ecto.drop` then re-create the database with `mix ecto.create` and migrate the database with `mix ecto.migrate`. To accomplish all of the above we could simply run `mix ecto.reset`. This is far simpler, and perfectly fine in the early-stages of a project before deployment. 2. In a database where we do need to preserve existing data, we can add the `completed` field, populate the `completed` field in the database, then add the `null: false` constraint to the `completed` field. We're going to pretend we need to preserve existing data for the sake of learning the process. Modify our existing migration to the following:
elixir
defmodule TodoList.Repo.Migrations.AddCompletedToTodoItems do
use Ecto.Migration
import Ecto.Query
def change do
alter table(:todo_items) do
add :completed, :boolean
end
# Immediately alter the todo_items table to ensure the completed
field exists.
flush()
from(“todo_items”, update: [set: [completed: false]])
|> TodoList.Repo.update_all([])
alter table(:todo_items) do
modify :completed, :boolean, null: false
end
end
end
The code above defines an Ecto migration that adds a `completed` field to the `todo_items` table in a database. The migration starts by using the `alter` function to add a new `completed` field to the todo_items table. The `completed` field has a data type of `boolean`, which means it can store either true or false values. Next, the `flush/1` function is called to immediately execute the changes and add the `completed` field to the `todo_items` table. Then, the `from/2` function is used to update all records in the `todo_items` table. The `update` option is used to specify that the `set` function should be used to set the `completed` field to `false` for all records in the table. Finally, the `alter` function is used again to modify the `completed` field. The `modify` function is used to set the `null` option to `false`, which means that the `completed` field cannot be empty. This ensures that all records in the todo_items table have a value for the `completed` field. In summary, this code adds a new `completed` field to the `todo_items` table, sets the `completed` field to false for all existing records in the table, and ensures that the `completed` field cannot be empty. Now we should be able to migrate the database without any issue.
mix ecto.migrate
Since we've added a new `completed` field with a migration, we also have to update the corresponding schema.
elixir
defmodule TodoList.TodoItems.TodoItem do
use Ecto.Schema
import Ecto.Changeset
schema “todo_items” do
field :title, :string
field :completed, :boolean
timestamps()
end
@doc false
def changeset(todo_item, attrs) do
todo_item
|> cast(attrs, [:title, :completed])
|> validate_required([:title, :completed])
end
end
Test your solution in the IEx shell.
elixir
$ iex -S mix
iex> alias TodoList.TodoItems.TodoItem
iex> changeset = TodoItem.changeset(%TodoItem{}, %{title: “finish schemas and migrations reading”, completed: true})
iex> TodoList.Repo.insert(changeset)
iex> TodoList.Repo.all(TodoItem)
```
## Further Reading
Consider the following resource(s) to deepen your understanding of the topic.
Elixir Schools: Ecto Basics
Programming Ecto by Darin Wilson and Eric Meadows-Jönsson
HexDocs: Ecto.Migration
* HexDocs: Ecto.Schema
## Commit Your Progress
DockYard Academy now recommends you use the latest Release rather than forking or cloning our repository.
Run git status
to ensure there are no undesirable changes.
Then run the following in your command line from the curriculum
folder to commit your progress.
$ git add . $ git commit -m "finish Schemas And Migrations reading" $ git push
We’re proud to offer our open-source curriculum free of charge for anyone to learn from at their own pace.
We also offer a paid course where you can learn from an instructor alongside a cohort of your peers.
We will accept applications for the June-August 2023 cohort soon.
## Navigation
Home
https://github.com/DockYard-Academy/curriculum/issues/new?assignees=&labels=&template=issue.md&title=Schemas And Migrations”>Report An Issue
Blog: Search
Blog: Migration