From file system to ecto

Published December 11, 2018 by Toran Billups

In part 8 of the url shortener adventure we migrate away from the file system and learn how to use Ecto and Postgres.

Part 8: Ecto

Today we write each shortened url to the file system and this helps us restore application state between restarts. While that solved the problem originally, most real Elixir applications store data in a database such as Postgres or MySQL.

We begin by adding a few dependencies to the mix.exs file that explicitly pull in ecto_sql, postgrex and phoenix_ecto.

    defmodule Example.MixProject do
      use Mix.Project
    
      defp deps do
        [
          ...
          {:phoenix_ecto, "~> 4.0"},
          {:ecto_sql, "~> 3.0"},
          {:postgrex, ">= 0.0.0"}
        ]
      end
    
    end
  

Install these new dependencies using mix on the command line.

    mix deps.get
  

Next add a new file named repo.ex that will tell Ecto about the adapter we are using for this project.

    defmodule Example.Repo do
      use Ecto.Repo,
        otp_app: :example,
        adapter: Ecto.Adapters.Postgres
    end
  

Before we can use this Repo we need to update the application.ex file. Add an entry for `Example.Repo` to the child process list.

    defmodule Example.Application do
      use Application
    
      def start(_type, _args) do
        import Supervisor.Spec
    
        children = [
          supervisor(Example.Repo, []),
          supervisor(ExampleWeb.Endpoint, []),
          ...
        ]
    
        opts = [strategy: :one_for_one, name: Example.Supervisor]
        Supervisor.start_link(children, opts)
      end
    end
  

Now we can add a schema that defines the link data structure we plan to store in the database. Add the file link.ex and include 2 fields along with a simple timestamp to capture `inserted_at` along with `updated_at` datetimes.

    defmodule Example.Link do
      use Ecto.Schema
    
      schema "links" do
        field :hash, :string
        field :url, :string
    
        timestamps()
      end
    end
  

Next we need to add some configuration to wire up our Ecto repository. Open the config.exs file and add a line to point `ecto_repos` at the Repo we created earlier.

    use Mix.Config
    
    config :example,
      ecto_repos: [Example.Repo]
    ...
  

Now in the dev.exs file we need to configure the username, password, database, and hostname to connect with Postgres.

    # Configure your database
    config :example, Example.Repo,
      username: "postgres",
      password: "postgres",
      database: "example_dev",
      hostname: "localhost",
      pool_size: 10
  

We now have enough code in place to run a few ecto commands with mix to see if we can create the database we've configured.

note: this step does assume you've got postgres installed and running
    mix ecto.create
    mix ecto.gen.migration create_links
  

Next open the create_links.exs migration file and fill out the `change` function to create our table with hash and url fields.

    defmodule Example.Repo.Migrations.CreateLinks do
      use Ecto.Migration
    
      def change do
        create table(:links) do
          add :hash, :string, primary_key: true
          add :url, :string
    
          timestamps()
        end
    
        create unique_index(:links, [:url])
      end
    end
  

Now run that migration using mix from the command line.

    mix ecto.migrate
  

Later we will run the tests to confirm this migration created the tables but a quick spot check with the `psql` command line tool will help us feel warm and fuzzy.

    psql -h localhost -d example_dev -U postgres
  

If this connects run the command `\dt` to see what tables were created. You should see the `links` table we created in the migration above.

Now that we have a schema and database connectivity we need to write a module that will let us query with Ecto. Add a new file to the project named links.ex that will serve as the interface between Elixir and Postgres.

    defmodule Example.Links do
      import Ecto.Query, warn: false
    
      alias Example.Repo
      alias Example.Link
    
      def all do
        Repo.all(Link)
      end
    
      def create_link(attrs \\ %{}) do
        Link
        |> struct(attrs)
        |> Repo.insert()
      end
    end
  

The module in our application that persists shortened links today is cache.ex. We've isolated the file system enough that this swap should be painless. First update `handle_call` to use the new `Links.all` function. We pull out the hash and url values and throw them into a Map. Next update `handle_cast` to use the `Links.create_link` function. We pass the hash and url arguments down using a Map that will later be transformed into a struct before Ecto does the insert.

    defmodule Example.Cache do
      use GenServer
    
      alias Example.Links
    
      ...
    
      @impl GenServer
      def handle_call({:all}, _timeout, _state) do
        state = for %Example.Link{hash: key, url: value} <- Links.all(), into: %{}, do: {key, value}
        {:reply, state, state}
      end
    
      @impl GenServer
      def handle_cast({:put, key, value}, state) do
        Links.create_link(%{hash: key, url: value})
        {:noreply, Map.put(state, key, value)}
      end
    end
  

To test this functionality we first need to swap out the file system helpers I wrote previously for the `DataCase` helper that ships with Phoenix.

    defmodule CacheTest do
      use ExampleWeb.DataCase, async: false
    
      test "all returns state and put updates it" do
        {:ok, _} = GenServer.start_link(Example.Cache, :ok)
    
        assert Example.Cache.all(:cache) === %{}
    
        Example.Cache.put(:cache, "x", "google.com")
    
        assert Example.Cache.all(:cache) === %{"x" => "google.com"}
      end
    end
  

Update the test helper itself to include the Sandbox adapter.

    ExUnit.start()
    Ecto.Adapters.SQL.Sandbox.mode(Example.Repo, :manual)
  

Update the test.exs file to configure the username, password, database, and hostname to connect with Postgres.

    # Configure your database
    config :example, Example.Repo,
      username: "postgres",
      password: "postgres",
      database: "example_test",
      hostname: "localhost",
      pool: Ecto.Adapters.SQL.Sandbox
  

Update the mix.exs file to include the aliases that setup and teardown our database between tests.

    defmodule Example.MixProject do
      use Mix.Project
    
      def project do
        [
          app: :example,
          ...
          aliases: aliases(),
          deps: deps()
        ]
      end
    
      defp aliases do
        [
          "ecto.setup": ["ecto.create", "ecto.migrate", "run priv/repo/seeds.exs"],
          "ecto.reset": ["ecto.drop", "ecto.setup"],
          test: ["ecto.create --quiet", "ecto.migrate", "test"]
        ]
      end
    end
  

Now you can run the cache test from the command line with mix to see it work end to end.

    mix test test/cache_test.exs
  

You can track my progress on github commit by commit. If you just want the code for this post checkout this commit.


Buy Me a Coffee

Twitter / Github / Email