Skip to content

Latest commit

 

History

History
343 lines (256 loc) · 10.1 KB

ecto_changeset.livemd

File metadata and controls

343 lines (256 loc) · 10.1 KB

Ecto

Mix.install([
  {:youtube, github: "brooklinjazz/youtube"},
  {:hidden_cell, github: "brooklinjazz/hidden_cell"},
  {:tested_cell, github: "brooklinjazz/tested_cell"},
  {:utils, path: "#{__DIR__}/../utils"}
])

Navigation

Return Home Report An Issue

Setup

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

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.

Changesets

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
Loading

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 to cast/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
Loading

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
Loading

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

Your Turn

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.

Schemaless Changesets

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

Your Turn

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 either true or false.
  • 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

Commit Your Progress

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"

Up Next

Previous Next
File Save Game