Powered by AppSignal & Oban Pro
Would you like to see your link here? Contact us
Notesclub

Phoenix And Ecto

deprecated_phoenix_journal_project.livemd

Phoenix And Ecto

Mix.install([
  {:jason, "~> 1.4"},
  {:kino, "~> 0.9", override: true},
  {:youtube, github: "brooklinjazz/youtube"},
  {:hidden_cell, github: "brooklinjazz/hidden_cell"}
])

Navigation

Home Report An Issue Relational Database Management SystemsSQL Drills

Review Questions

Upon completing this lesson, a student should be able to answer the following questions.

  • How do we generate a resource in a Phoenix application?
  • What is the router, and how do we define routes in a Phoenix application?

Overview

Ecto provides a standard API layer for communicating with the database of an Elixir application.

By default, Ecto uses a PostgreSQL Database. Ensure you already have PostgreSQL installed on your computer from the Relational Database Management Systems lesson.

Ecto splits into four main modules.

  • Ecto.Repo handles all communication between the application and the database. Ecto.Repo reads and writes from the underlying PostgreSQL database.
  • Ecto.Query built queries to retrieve and manipulate data with the Ecto.Repo repository.
  • Ecto.Schema maps the application struct data representation to the underlying PostgreSQL database representation.
  • Ecto.Changeset creates changesets for validating and applying constraints to structs.

Phoenix uses Ecto to handle the data layer of a web application.

Create A Journal Project

We’re going to build a journal application to learn how to use Phoenix with Ecto.

You can reference the DockYard Academy Journal project example if you want to see the completed project.

Our journal will store journal entries that have a title and content.

classDiagram
  class Entry {
    title: :string
    content: :text 
  }

We will be able to create, read, update, and delete journal entries.

The four CRUD actions (create, read, update, and delete) form the basis of most applications.

First, create a new journal project.

$ mix phx.new journal

Install project dependencies from the project folder.

$ mix deps.get

Create and migrate the database from the project folder.

$ mix ecto.setup

Next, we can use generators provided by phoenix to create the controllers, views, and context for the journal Entries.

$ mix phx.gen.html Entries Entry entries title:string content:text

The mix phx.gen.html command accepts several arguments.

  • The context: Entries.
  • The schema module: Entry.
  • The table name and an optional list of attributes and their types: entries title:string content:text

This command creates several modules and files.

  • The JournalWeb.EntryController controller module.
  • The JournalWeb.EntryView view module.
  • The Journal.Entries context file.
  • Templates for the entry CRUD actions.
  • A migration file that creates the entries table in PostgreSQL.
  • Several test files.

We can see the exact files created in the output.

* creating lib/journal_web/controllers/entry_controller.ex
* creating lib/journal_web/templates/entry/edit.html.heex
* creating lib/journal_web/templates/entry/form.html.heex
* creating lib/journal_web/templates/entry/index.html.heex
* creating lib/journal_web/templates/entry/new.html.heex
* creating lib/journal_web/templates/entry/show.html.heex
* creating lib/journal_web/views/entry_view.ex
* creating test/journal_web/controllers/entry_controller_test.exs
* creating lib/journal/entries/entry.ex
* creating priv/repo/migrations/20220710205852_create_entries.exs
* creating lib/journal/entries.ex
* injecting lib/journal/entries.ex
* creating test/journal/entries_test.exs
* injecting test/journal/entries_test.exs
* creating test/support/fixtures/entries_fixtures.ex
* injecting test/support/fixtures/entries_fixtures.ex

Phoenix Pipeline

In the Phoenix web framework, HTTP requests flow through a pipeline of components before being processed and returned to the client.

The first component in the pipeline is the Endpoint, which is responsible for receiving the incoming HTTP request and passing it on to the Router. The Router uses the request’s path and method to determine which controller should handle the request.

Once the appropriate controller has been identified, the request is passed to the controller, which is responsible for handling the request. The controller may perform various tasks, such as querying a database or calling other services, in order to generate a response to the request.

The controller delegates to the context, which provides access to the business logic of our application. The controller might use the context to read or write data to the database as part of handling the request.

Next, controller delegates to the view, which is responsible for rendering the response for our request. Typically, the view uses data provided by the controller to populate the template, and returns the resulting HTML to the client.

Finally, the response is returned to the client, completing the flow of the HTTP request through the Phoenix pipeline.

Overall, the Phoenix pipeline is designed to provide a modular, flexible way to process HTTP requests and generate responses in a web application. The pipeline allows developers to customize and extend the behavior of their application at each stage of the request-response cycle.

Here’s a diagram with an overview of that process.

Now that we’re adding Ecto, we’re expanding the layers for the Model portion of the diagram.

The context is the entry point to the business logic of our application. Each context module provides access to various resources that may be needed by the controller to handle an HTTP request, for example our mix phx.gen.html command generated an Entries context in our Journal application.

To better understand the context, we’ll provide a high-level overview of how data flows through our database from the context.

Writing Data

When writing data to the database, our context will typically build up a Changeset using the Schema module for a resource.

flowchart
  subgraph Context
    attrs --> Schema --> Changeset --validated data--> Repo --SQL--> DB[Write To Database]
  end

Here’s an example of this flow using the Entries.create_entry/2 context function in entries.ex from our Journal application.

  def create_entry(attrs \\ %{}) do
    # Create Changeset 
    %Entry{}
    |> Entry.changeset(attrs)
    # Provide Changeset to Repo to generate the SQL to insert an entry into the database
    |> Repo.insert()
  end

Reading Data

When reading data from the database, we use the Ecto.Query module to build up a query, then pass the query to the Repo to retrieve data from the database.

flowchart
  subgraph Context
    attrs --> Schema --> Ecto.Query --> Repo
  end

Here’s an example using the Entries.list_entries/0 function from our Journal application.

def list_entries do
  Repo.all(Entry)
end

It may not immediately be clear what the Ecto.Query portion is, because Repo understands we want to retrieve all data from the entries table when we pass it the Entry schema.

This is shorthand for the following query which retrieves all journal entries from the entries table using the Ecto.Query.from/2 macro.

def list_entries do
  from(entry in Entry)
  |> Repo.all()
end

Ecto.Query is smart enough to understand to use the "entries" table from the Entry schema and transform the data into an Entry struct.

For demonstration purposes, we can use the table name and perform this transformation manually.

def list_entries do
  from(entry in "entries",
    select: %Entry{
      id: entry.id,
      title: entry.title,
      content: entry.content,
      inserted_at: entry.inserted_at,
      updated_at: entry.updated_at
    }
  )
  |> Repo.all()
end

Clearly this is more verbose than using the schema, so we’ll revert back to using the Entry schema.

def list_entries do
  Repo.all(Entry)
end

Here’s a visual diagram of all the components in our Phoenix application and how they connect.

flowchart
  direction BT
  subgraph app
    direction LR
    Context --> Database
    subgraph Database
      subgraph Read
        Ecto.Schema --> Ecto.Query --> Ecto.Repo
      end
      subgraph Write
        direction TB
        ES1[Ecto.Schema] --> Ecto.Changeset --> ER1[Ecto.Repo]
      end
      Ecto.Migration
    end
  end
  subgraph app_web
    direction LR
    Endpoint --> Router --> Controller --> View --> Template
  end
  app_web --Controller Delegates To--> app

Router

We need to add a route for entries. We’ll replace the page controller and use the "/" route with the resources/4 macro.

scope "/", JournalWeb do
  pipe_through :browser

  resources "/", EntryController
end

The resources/4 macro creates a standard matrix of HTTP actions. The resources/4 macro is for convenience, so we don’t need to define individual routes. For example, we can replace resources/4 with the following and the application will still have the same behavior.

scope "/", JournalWeb do
  pipe_through :browser

  get "/", EntryController, :index
  get "/:id/edit", EntryController, :edit
  get "/new", EntryController, :new
  get "/:id", EntryController, :show
  post "/", EntryController, :create
  patch "/:id", EntryController, :update
  put "/:id", EntryController, :update
  delete "/:id", EntryController, :delete
end

See Phoenix.Router.resources/4 for more configuration options.

The main HTTP actions are GET, POST, PATCH, PUT, and DELETE. They generally refer to different intentions for manipulating a resource, such as our journal entries.

  • GET: retrieve a resource.
  • POST: submit a resource.
  • PATCH: partially modify a resource.
  • PUT: completely replace a resource.
  • DELETE: delete a resource.

We can see the generated routes by running the following command.

$ mix phx.routes

Observe the following in the output.

entries_path  GET     /                                     JournalWeb.EntryController :index
entries_path  GET     /:id/edit                             JournalWeb.EntryController :edit
entries_path  GET     /new                                  JournalWeb.EntryController :new
entries_path  GET     /:id                                  JournalWeb.EntryController :show
entries_path  POST    /                                     JournalWeb.EntryController :create
entries_path  PATCH   /:id                                  JournalWeb.EntryController :update
              PUT     /:id                                  JournalWeb.EntryController :update
entries_path  DELETE  /:id                                  JournalWeb.EntryController :delete

These routes correspond to our controller actions.

Controllers

When the client triggers one of these HTTP actions, the server executes the corresponding action in the JournalWeb.EntryController, either :index, :edit, :new, :show, :create, :update, or :delete.

We can see these functions in lib/journal_web/controllers/entry_controller.ex.

defmodule JournalWeb.EntryController do
  use JournalWeb, :controller

  alias Journal.Entries
  alias Journal.Entries.Entry

  def index(conn, _params) do
    entries = Entries.list_entries()
    render(conn, "index.html", entries: entries)
  end

  def new(conn, _params) do
    changeset = Entries.change_entry(%Entry{})
    render(conn, "new.html", changeset: changeset)
  end

  def create(conn, %{"entry" => entry_params}) do
    case Entries.create_entry(entry_params) do
      {:ok, entry} ->
        conn
        |> put_flash(:info, "Entry created successfully.")
        |> redirect(to: Routes.entry_path(conn, :show, entry))

      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, "new.html", changeset: changeset)
    end
  end

  def show(conn, %{"id" => id}) do
    entry = Entries.get_entry!(id)
    render(conn, "show.html", entry: entry)
  end

  def edit(conn, %{"id" => id}) do
    entry = Entries.get_entry!(id)
    changeset = Entries.change_entry(entry)
    render(conn, "edit.html", entry: entry, changeset: changeset)
  end

  def update(conn, %{"id" => id, "entry" => entry_params}) do
    entry = Entries.get_entry!(id)

    case Entries.update_entry(entry, entry_params) do
      {:ok, entry} ->
        conn
        |> put_flash(:info, "Entry updated successfully.")
        |> redirect(to: Routes.entry_path(conn, :show, entry))

      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, "edit.html", entry: entry, changeset: changeset)
    end
  end

  def delete(conn, %{"id" => id}) do
    entry = Entries.get_entry!(id)
    {:ok, _entry} = Entries.delete_entry(entry)

    conn
    |> put_flash(:info, "Entry deleted successfully.")
    |> redirect(to: Routes.entry_path(conn, :index))
  end
end

Many of our controller actions delegate to functions defined in the context.

Context

Our EntryController uses the Entries context. Contexts are modules that group related functionality. In this case, the Entries context contains functions for working with entries in the database.

Here’s our Entries module.

defmodule Journal.Entries do
  @moduledoc """
  The Entries context.
  """

  import Ecto.Query, warn: false
  alias Journal.Repo

  alias Journal.Entries.Entry

  @doc """
  Returns the list of entries.

  ## Examples

      iex> list_entries()
      [%Entry{}, ...]

  """
  def list_entries do
    Repo.all(Entry)
  end

  @doc """
  Gets a single entry.

  Raises `Ecto.NoResultsError` if the Entry does not exist.

  ## Examples

      iex> get_entry!(123)
      %Entry{}

      iex> get_entry!(456)
      ** (Ecto.NoResultsError)

  """
  def get_entry!(id), do: Repo.get!(Entry, id)

  @doc """
  Creates a entry.

  ## Examples

      iex> create_entry(%{field: value})
      {:ok, %Entry{}}

      iex> create_entry(%{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def create_entry(attrs \\ %{}) do
    %Entry{}
    |> Entry.changeset(attrs)
    |> Repo.insert()
  end

  @doc """
  Updates a entry.

  ## Examples

      iex> update_entry(entry, %{field: new_value})
      {:ok, %Entry{}}

      iex> update_entry(entry, %{field: bad_value})
      {:error, %Ecto.Changeset{}}

  """
  def update_entry(%Entry{} = entry, attrs) do
    entry
    |> Entry.changeset(attrs)
    |> Repo.update()
  end

  @doc """
  Deletes a entry.

  ## Examples

      iex> delete_entry(entry)
      {:ok, %Entry{}}

      iex> delete_entry(entry)
      {:error, %Ecto.Changeset{}}

  """
  def delete_entry(%Entry{} = entry) do
    Repo.delete(entry)
  end

  @doc """
  Returns an `%Ecto.Changeset{}` for tracking entry changes.

  ## Examples

      iex> change_entry(entry)
      %Ecto.Changeset{data: %Entry{}}

  """
  def change_entry(%Entry{} = entry, attrs \\ %{}) do
    Entry.changeset(entry, attrs)
  end
end

Schema

The Entries context exposes functions for CRUD (create, read, update, delete) actions. These functions work with an Entry schema and the Repo module.

An Ecto.Schema maps Elixir external data into Elixir structs. In this case, the Entry schema maps data from the entries table into an Entry struct.

Here’s our Entry schema.

defmodule Journal.Entries.Entry do
  use Ecto.Schema
  import Ecto.Changeset

  schema "entries" do
    field :content, :string
    field :title, :string

    timestamps()
  end

  @doc false
  def changeset(entry, attrs) do
    entry
    |> cast(attrs, [:title, :content])
    |> validate_required([:title, :content])
  end
end

The Ecto.schema/2 macro plays the same roll that @types does for schemaless changesets, which we learned about in the Ecto Changesets material.

Each field/3 macro defines the fields of our schema and their type. You can find the full list of valid field types at Ecto.Schema: Types and casting.

We can use the Entry.changeset/2 function to validate data before we attempt to insert it into the database. For example, in the Entires context, the Ecto.Repo.insert/2 takes in the changeset returned from Entry.changeset/2.

  def create_entry(attrs \\ %{}) do
    %Entry{}
    |> Entry.changeset(attrs)
    |> Repo.insert()
  end

Repo

The Ecto.Repo module defines functions for working with the database.

In our Entries context, we see some common functions:

These cover the full range of CRUD (create, read, update, delete) actions.

Migrations

Earlier, the mix phx.gen.html command generated an Ecto migration file priv/repo/migrations/create_entries.ex which creates the entries table in our PostgreSQL database.

defmodule Journal.Repo.Migrations.CreateEntries do
  use Ecto.Migration

  def change do
    create table(:entries) do
      add :title, :string
      add :content, :text

      timestamps()
    end
  end
end

> The actual file will have a timestamp in its name. i.e. 20220710205852_create_entries.

:string is a simple string limited to 255 characters, and :text is a string with a larger character limit.

See the Attributes documentation for a complete list of attribute types.

Run the following command to run all migration files in the priv/repo/migrations/ folder.

$ mix ecto.migrate
The database for Journal.Repo has been created

14:13:55.631 [info]  == Running 20220710205852 Journal.Repo.Migrations.CreateEntries.change/0 forward

14:13:55.633 [info]  create table entries

14:13:55.642 [info]  == Migrated 20220710205852 in 0.0s

GET :index

To better understand our journal project we’re going to walk through all of the HTTP requests and how they flow through our application.

Ensure you have started the server, and visit http://localhost:4000.

$ mix phx.server

You’ll see the following HTML web page.

When we visit http://localhost:4000, the browser sends an HTTP GET request to the "/" route of the application.

This triggers the EntryController.index/2 function in lib/journal_web/controllers/entry_controller.ex.

def index(conn, _params) do
  entries = Entries.list_entries()
  render(conn, "index.html", entries: entries)
end

Which retrieves a list of entries (currently an empty list []) from the Entries context in lib/journal/entries.ex.

def list_entries do
  Repo.all(Entry)
end

The EntryController.index/2 function then calls the render/3 macro which delegates to the EntryView module in lib/journal_web/views/journal_view.ex.

defmodule JournalWeb.EntryView do
  use JournalWeb, :view
end

Which uses the lib/journal_web/templates/entry/index.html.heex template file to build the HTML web page. This web page includes a link that navigates us to the "/new" route.

<span><%= link "New Entry", to: Routes.entry_path(@conn, :new) %></span>

See Phoenix.HTML.Link for more on the link/2 function.

GET :new

Click the "New Entry" link from the browser to navigate to http://localhost:4000/new.

By clicking the link, the browser sends an HTTP GET request to the "/new" route, which triggers the EntryController.new/2 action.

def new(conn, _params) do
  changeset = Entries.change_entry(%Entry{})
  render(conn, "new.html", changeset: changeset)
end

The Entries.change_entry/1 function retrieves the Ecto Changeset for the Entry schema. A form later uses this changeset to display form data validation errors.

# Lib/journal/entries.ex
def change_entry(%Entry{} = entry, attrs \\ %{}) do
  Entry.changeset(entry, attrs)
end

# Lib/journal/entries/entry.ex
def changeset(entry, attrs) do
  entry
  |> cast(attrs, [:title, :content])
  |> validate_required([:title, :content])
end

The render/3 macro delegates to the EntryView module and renders the lib/journal_web/templates/entry/new.html.heex template file.

<h1>New Entry</h1>

<%= render "form.html", Map.put(assigns, :action, Routes.entry_path(@conn, :create)) %>

<span><%= link "Back", to: Routes.entry_path(@conn, :index) %></span>

Which uses the render/3 macro to render the lib/journal_web/templates/entry/form.html.heex template file.

<.form let={f} for={@changeset} action={@action}>
  <%= if @changeset.action do %>
    
      <p>Oops, something went wrong! Please check the errors below.</p>
    
  <% end %>

  <%= label f, :title %>
  <%= text_input f, :title %>
  <%= error_tag f, :title %>

  <%= label f, :content %>
  <%= textarea f, :content %>
  <%= error_tag f, :content %>

  
    <%= submit "Save" %>
  

This form uses the changeset from the Entry schema to display validation errors. Click "Submit" without filling in the form, and you’ll see the following errors.

When we submit the form, it sends an HTTP POST request to the "/" route with the content of the form as parameters.

For more on forms, see the Phoenix.HTML.Form documentation.

POST :create

While still on http://localhost:4000/new, enter a title and some content into the form and click the submit button.

This sends a POST request to "/" which triggers the EntryController.create/2 function.

def create(conn, %{"entry" => entry_params}) do
  case Entries.create_entry(entry_params) do
    {:ok, entry} ->
      conn
      |> put_flash(:info, "Entry created successfully.")
      |> redirect(to: Routes.entry_path(conn, :show, entry))

    {:error, %Ecto.Changeset{} = changeset} ->
      render(conn, "new.html", changeset: changeset)
  end
end

entry_params is a map with the form content such as %{"title" => "My Title", "content" => "My Content"}.

If we successfully create an entry, we flash a success message using the put_flash/3 macro and redirect the user to the show page.

Otherwise, we re-render the new.html.heex template with an updated changeset that contains the form validation errors.

GET :show

After successfully creating a journal entry, the controller redirects us to http://localhost:4000/1.

This is the "/:id" route which triggers the EntryController.show/2 function.

def show(conn, %{"id" => id}) do
  entry = Entries.get_entry!(id)
  render(conn, "show.html", entry: entry)
end

The show/2 function retrieves a single entry by its :id in the database, then uses the render/3 function to render the lib/journal_web/templates/entry/show.html.heex template with the entry data.

<h1>Show Entry</h1>

<ul>

  <li>
    <strong>Title:</strong>
    <%= @entry.title %>
  </li>

  <li>
    <strong>Content:</strong>
    <%= @entry.content %>
  </li>

</ul>

<span><%= link "Edit", to: Routes.entry_path(@conn, :edit, @entry) %></span> |
<span><%= link "Back", to: Routes.entry_path(@conn, :index) %></span>

GET :edit

We can edit existing journal entries. Click on the "Edit" button to be taken to http://localhost:4000/1/edit. This is the ":id/edit" route which triggers the EntryController.edit/2 function.

def edit(conn, %{"id" => id}) do
  entry = Entries.get_entry!(id)
  changeset = Entries.change_entry(entry)
  render(conn, "edit.html", entry: entry, changeset: changeset)
end

The EntryController.edit/2 function retrieves an entry by its id and the Entry changeset.

It then calls the render/3 function to render the lib/journal_web/templates/entry/edit.html.heex template.

<h1>Edit Entry</h1>

<%= render "form.html", Map.put(assigns, :action, Routes.entry_path(@conn, :update, @entry)) %>

<span><%= link "Back", to: Routes.entry_path(@conn, :index) %></span>

The lib/journal_web/templates/entry/edit.html.heex template renders the same form we saw in lib/journal_web/templates/entry/new.html.heex but with a different action.

PUT :update

This time, when we submit the form on http://localhost:4000/1/edit it sends a PUT request to the "/:id" route, which triggers the EntryController.update/2 function.

def update(conn, %{"id" => id, "entry" => entry_params}) do
  entry = Entries.get_entry!(id)

  case Entries.update_entry(entry, entry_params) do
    {:ok, entry} ->
      conn
      |> put_flash(:info, "Entry updated successfully.")
      |> redirect(to: Routes.entry_path(conn, :show, entry))

    {:error, %Ecto.Changeset{} = changeset} ->
      render(conn, "edit.html", entry: entry, changeset: changeset)
  end
end

If we successfully update the entry, we flash a success message and redirect back to the show page.

Otherwise we display any errors in the form.

DELETE :delete

If we navigate back to http://localhost:4000/ we’ll see a list of our entries.

The template in lib/journal_web/templates/entry/index.html.heex builds this web page.

<h1>Listing Entries</h1>

<table>
  <thead>
    <tr>
      <th>Title</th>
      <th>Content</th>

      <th></th>
    </tr>
  </thead>
  <tbody>
<%= for entry <- @entries do %>
    <tr>
      <td><%= entry.title %></td>
      <td><%= entry.content %></td>

      <td>
        <span><%= link "Show", to: Routes.entry_path(@conn, :show, entry) %></span>
        <span><%= link "Edit", to: Routes.entry_path(@conn, :edit, entry) %></span>
        <span><%= link "Delete", to: Routes.entry_path(@conn, :delete, entry), method: :delete, data: [confirm: "Are you sure?"] %></span>
      </td>
    </tr>
<% end %>
  </tbody>
</table>

<span><%= link "New Entry", to: Routes.entry_path(@conn, :new) %></span>

Each entry has a "Delete" link which triggers an HTTP DELETE request to the "/:id" route. This calls the EntryController.delete/2 function with the entry id.

def delete(conn, %{"id" => id}) do
  entry = Entries.get_entry!(id)
  {:ok, _entry} = Entries.delete_entry(entry)

  conn
  |> put_flash(:info, "Entry deleted successfully.")
  |> redirect(to: Routes.entry_path(conn, :index))
end

The EntryController.delete/2 function deletes the entry and redirects the user back to the "/" route.

Click the "Delete" link in the browser and see it deleted from the page.

Cleanup And Push To GitHub

Since we removed the main page from the project, we no longer need the PageController.

Remove the following files/folders:

  • test/journal/controllers/page_controller.ex
  • test/journal_web/controllers/page_controller_test.ex
  • test/journal/templates/page

Ensure that all tests pass.

mix test

Now stage and commit your changes from the journal project folder.

git add .
git commit -m "finish journal project"

Now you can create a journal repository on GitHub and push your project. Follow the instructions on GitHub to connect your local project to the remote repository.

Further Reading

Consider the following resource(s) to deepen your understanding of the topic.

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 Phoenix And Ecto 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 Report An Issue Relational Database Management SystemsSQL Drills