Mix.install([
{:youtube, github: "brooklinjazz/youtube"},
{:hidden_cell, github: "brooklinjazz/hidden_cell"},
{:tested_cell, github: "brooklinjazz/tested_cell"},
{:utils, path: "#{__DIR__}/../utils"}
])
Ensure you type the ea
keyboard shortcut to evaluate all Elixir cells before starting. Alternatively you can evaluate the Elixir cells as you read.
Ecto is a library we typically use to communicate with a database. We'll learn more about databases in future lessons, but for now, we'll see how Ecto helps to interact with data.
In addition to being the layer that we use to communicate with the database, Ecto is powerful for validating data. So far, you've created structs with their own (limited) data validation.
Structs allow you to enforce keys and write validation functions or guards.
defmodule Person do
@enforce_keys [:name, :age]
defstruct @enforce_keys
def new(%{name: name, age: age}) when is_binary(name) and is_integer(age) do
{:ok,
%Person{
name: name,
age: age
}}
end
def new(_), do: {:error, :invalid_params}
end
Then the struct typically returns an {ok, struct}
tuple or an {:error, message}
tuple.
# Valid params
{:ok, %Person{age: 20, name: "valid name"}} = Person.new(%{name: "valid name", age: 20})
# Invalid params
{:error, :invalid_params} = Person.new(%{name: ["invalid name"], age: 20})
However, this is pretty clunky, especially as the struct gets larger and validations grow in complexity.
Structs don't have any built-in data validation other than enforcing keys. Ecto helps us validate data with changeset structs.
An Ecto.Changeset
struct allows us to validate data and return helpful error messages when something is invalid.
A changeset describes the expected shape of your data and ensures that data conforms to the desired shape.
flowchart
n[username]
User --> n --> s1[string]
n --> l[less than 40 characters]
n --> gr[greater than 3 characters]
User --> password --> s2[string]
password --> g[greater than 10 characters]
User --> a[accepted terms of service] --> true
To create a changeset, we need to know.
- the
initial_data
. - the expected data validation as
types
. - the attempted change from
params
. - the keys of
params
tocast/3
into data that conforms to the changeset.
flowchart
i[initial_data]
t[types]
p[params]
k[keys]
ca[cast/3]
c[Ecto.Changeset]
i --> ca
t --> ca
p --> ca
k --> ca
ca --> c
We use changesets to validate new data or validate a change to some already validated data.
Here we use the primitive types :string
and :integer
. For a full list of
the allowable types, see the Primitive Types table.
initial_data = %{}
params = %{name: "Peter", age: 22}
types = %{name: :string, age: :integer}
keys = [:name, :age]
changeset = Ecto.Changeset.cast({initial_data, types}, params, keys)
We've now bound the changeset
variable to an Ecto.Changeset
struct.
The changeset contains:
- changes: the
params
we want to update the source data. - data: the source data for the changeset.
- valid?: whether or not the desired change breaks any validation.
- errors: a keyword list of errors for any changes.
- action: you can ignore this for now and manually set it to
:update
. You can see the documentation for why.
flowchart
e[Ecto.Changeset]
a[action]
d[data]
v[valid?]
c[changes]
er[errors]
e --> a
e --> d
e --> v
e --> c
e --> er
The changeset will store the errors as a keyword list, and valid?
will be false when the parameters
are invalid.
invalid_params = %{name: 2, age: "hello"}
invalid_changeset = Ecto.Changeset.cast({initial_data, types}, invalid_params, keys)
You'll notice that the changeset stores errors
in a keyword list. Both the name
and age
are invalid,
and each stores a different error message.
We can also add additional validations to the changeset. For a full list of functions, see the Ecto Validation Functions.
These validation functions change the errors
according to what they validate.
initial_data = %{}
# Notice we are missing age.
params = %{name: "Peter"}
types = %{name: :string, age: :integer}
keys = [:name, :age]
changeset =
Ecto.Changeset.cast({initial_data, types}, params, keys)
|> Ecto.Changeset.validate_required(:age)
Let's take the User
example from above. A User
should have a username
between 3
and 40
characters,
a password greater than 10
characters, and should accept our :terms_and_conditions
.
initial_data = %{}
# intentionally left empty to show errors
params = %{}
types = %{username: :string, password: :string, terms_and_conditions: :boolean}
keys = [:username, :password, :terms_and_conditions]
invalid_changeset =
{initial_data, types}
|> Ecto.Changeset.cast(params, keys)
|> Ecto.Changeset.validate_required([:username, :password])
|> Ecto.Changeset.validate_length(:password, min: 10)
|> Ecto.Changeset.validate_length(:username, min: 3, max: 40)
|> Ecto.Changeset.validate_acceptance(:terms_and_conditions)
When the user enters the correct params
, the changeset will be valid.
initial_data = %{}
params = %{username: "Peter", password: "secret_spider", terms_and_conditions: true}
types = %{username: :string, password: :string, terms_and_conditions: :boolean}
keys = [:username, :password, :terms_and_conditions]
valid_changeset =
Ecto.Changeset.cast({initial_data, types}, params, keys)
|> Ecto.Changeset.validate_required([:username, :password])
|> Ecto.Changeset.validate_length(:password, min: 10)
|> Ecto.Changeset.validate_length(:username, min: 3, max: 40)
|> Ecto.Changeset.validate_acceptance(:terms_and_conditions)
To apply changes, we can use Ecto.Changeset.apply_changes/1
, which will only apply the changes if the changes are valid.
Ecto.Changeset.apply_action(valid_changeset, :update)
If the changes are not valid, we will receive an error.
Ecto.Changeset.apply_action(invalid_changeset, :update)
It's common to use this pattern to create validated data or handle the error.
case Ecto.Changeset.apply_action(valid_changeset, :update) do
{:ok, data} -> data
{:error, message} -> message
end
Use Ecto.Changeset
to validate some parameters to create a blog.
A blog should have:
- A required
:title
string field. - A required
:body
string field. - A list of tags no more than 5.
We often store the associated changeset and validations inside of a struct. The struct typically has a changeset/2
function
to create the changeset and a function new/1
that creates a struct. This allows us to use a changeset to validate
the creation of a struct.
We've reorganized the same code as above into a User
struct rather than a generic map. Everything mostly remains unchanged other than putting types
into an @types
module attribute and using Map.keys(@types)
to
get the list of keys.
defmodule User do
defstruct [:username, :password, :terms_and_conditions]
@types %{username: :string, password: :string, terms_and_conditions: :boolean}
def changeset(%User{} = user, params) do
{user, @types}
|> Ecto.Changeset.cast(params, Map.keys(@types))
|> Ecto.Changeset.validate_required([:username, :password])
|> Ecto.Changeset.validate_length(:password, min: 10)
|> Ecto.Changeset.validate_length(:username, min: 3, max: 40)
|> Ecto.Changeset.validate_acceptance(:terms_and_conditions)
end
def new(params) do
%User{}
|> changeset(params)
|> Ecto.Changeset.apply_action(:update)
end
end
User.new(%{username: "Peter Parker", password: "secret_spider", terms_and_conditions: true})
We're reusing Ecto.Changeset
a lot, so it may be worth importing it.
defmodule User do
import Ecto.Changeset
defstruct [:username, :password, :terms_and_conditions]
@types %{username: :string, password: :string, terms_and_conditions: :boolean}
def changeset(%User{} = user, params) do
{user, @types}
|> cast(params, Map.keys(@types))
|> validate_required([:username, :password])
|> validate_length(:password, min: 10)
|> validate_length(:username, min: 3, max: 40)
|> validate_acceptance(:terms_and_conditions)
end
def new(params) do
%User{}
|> changeset(params)
|> apply_action(:update)
end
end
In the Elixir cell below, create a Book
schemaless changeset struct.
A book should have:
- A required
:title
field with a minimum of 3 characters and a maximum of 100 characters. - A required
:published_status
field that is eithertrue
orfalse
. - A
:body
string field. - A
:published_on
date field. - An
:author
string field. - An
:is_licenced
field which must always be true.
defmodule Blog do
end
Run the following in your command line from the beta_curriculum folder to track and save your progress in a Git commit.
$ git add .
$ git commit -m "finish ecto changeset section"
Previous | Next |
---|---|
File | Save Game |