Hello again, and welcome to ElixirSips Episode 025: Ecto, Part 2 - Dwitter. In today's episode, we're going to continue our exploration of Ecto by modifying our existing Dwitter webapp we built in Dynamo to use Ecto rather than Amnesia for its persistence layer.

I've tagged this version at https://github.com/knewter/dwitter/tree/episode9 - feel free to clone it there and work through it with me.

Project

Setup

Go ahead and fetch the repo:

git clone https://github.com/knewter/dwitter
cd dwitter
git checkout episode9

Now the first thing we're going to do is rip out the Amnesia dependency. Open up mix.exs and change the deps to the following:

  defp deps do
    [
      { :cowboy, github: "extend/cowboy" },
      { :dynamo, "0.1.0-dev", github: "elixir-lang/dynamo" },
      { :postgrex, github: "ericmj/postgrex" },
      { :ecto, github: "elixir-lang/ecto" }
    ]
  end

Now we need to get the dependencies. Since this is based on an older episode, it'll be locked to older versions of some dependencies. Since we changed a good bit, I'm just going to remove the mix.lock file and get the deps fresh:

rm mix.lock
mix deps.get

Now there's still code in the repo referencing Amnesia, so let's start out by ripping that out. First, open up lib/dwitter.ex and rip out all of the Amnesia stuff, leaving you with the following:

defmodule Dwitter do
  use Application.Behaviour

  @doc """
  The application callback used to start this
  application and its Dynamos.
  """
  def start(_type, _args) do
    Dwitter.Supervisor.start_link
  end
end

Next, open up lib/dwitter/supervisor.ex and make it look like the following:

defmodule Dwitter.Supervisor do
  use Supervisor.Behaviour

  def start_link do
    :supervisor.start_link(__MODULE__, [])
  end

  def init([]) do
    children = [
      worker(Dwitter.Dynamo, []),
      worker(Dwitter.Repo, [])
    ]

    # See http://elixir-lang.org/docs/stable/Supervisor.Behaviour.html
    # for other strategies and supported options
    supervise(children, strategy: :one_for_one)
  end
end

Next, git rm lib/dwitter/database.ex test/amnesia_test.exs

Finally, open up web/routers/application_router.ex and gut it to look like the following:

defmodule ApplicationRouter do
  use Dynamo.Router

  prepare do
    # Pick which parts of the request you want to fetch
    # You can comment the line below if you don't need
    # any of them or move them to a forwarded router
    conn = conn.fetch([:cookies, :params])
    conn = conn.assign(:title, "Welcome to Dwitter!")
  end

  # It is common to break your Dynamo into many
  # routers, forwarding the requests between them:
  # forward "/posts", to: PostsRouter

  post "/post" do
    # Store a new dweet based on conn.params[:content] and assign to dweet
    dweet = {}
    conn = conn.assign(:dweet, dweet)
    render conn, "post_complete.html"
  end

  get "/" do
    # Assign recent_dweets to the last 10 dweets
    recent_dweets = []
    conn = conn.assign(:recent_dweets, recent_dweets)
    render conn, "index.html"
  end
end

This has undone most of our application, but we will now set out to rebuild it using Ecto :) NOTE: If I'd built acceptance tests this would be a lot more legitimate.

Add Ecto Repo and Models

Now, we're going to add an Ecto Repo and Models to this project. I'm going to run through this quickly, because we covered it in the last episode.

First, open up lib/dwitter/repo.ex and add the following:

defmodule Dwitter.Repo do
  use Ecto.Repo, adapter: Ecto.Adapters.Postgres

  def url do
    "ecto://postgres:postgres@localhost/dwitter"
  end

  def priv do
    app_dir(:dwitter, "priv/repo")
  end
end

We'll go ahead and create the database:

$ sudo su postgres
$ psql
postgres# CREATE DATABASE dwitter ENCODING='UTF8' LC_COLLATE='en_US.UTF-8' LC_CTYPE='en_US.UTF-8';

Next, we'll make the Dweet entity that mimics our existing Amnesia-based Dweet, for now. Open up lib/dwitter/dweet.ex and add the following:

defmodule Dwitter.Dweet do
  use Ecto.Model

  queryable "dweets" do
    field :content, :string
    field :author,  :string
  end
end

Next, make a migration:

mix compile
mix ecto.gen.migration Dwitter.Repo create_dweets

Now, this generated a migration in a spot I didn't expect, and I've opened an issue to figure out what the proper behaviour should be. Seems to be caused by a bit of a fight between Dynamo and Ecto. Anyway, just a heads up.

For now, we'll just roll with it. Open up tmp/dev/dwitter/priv/repo/migrations/20131106135013_create_dweets.exs and make it look like the following:

defmodule Dwitter.Repo.Migrations.CreateDweets do
  use Ecto.Migration

  def up do
    "CREATE TABLE dweets(id serial primary key, content varchar(140), author varchar(50))"
  end

  def down do
    "DROP TABLE dweets"
  end
end

Next, run the migrations with mix ecto.migrate Dwitter.Repo.

Because I'm not sure what the best strategy is yet for test databases with Ecto, I'm just going to cowboy code this and not do it Test-Driven. Please understand: this makes me extremely sad and I do not advocate it for Actual Development Projects.

Moving on - let's see what an Ecto-backed Dynamo Router would look like. Go ahead and start the server with mix server and visit the app in the browser, and you should see our exquisitely designed interface.

Now let's make posting a dweet store it in the database. Open up web/routers/application_router.ex and modify the '/post' route:

  post "/post" do
    # Store a new dweet based on conn.params[:content] and assign to dweet
    dweet = Dwitter.Dweet.new(content: conn.params[:content], author: "elixirsips")
    Dwitter.Repo.create(dweet)
    conn = conn.assign(:dweet, dweet)
    render conn, "post_complete.html"
  end

Go ahead and submit the form, and you should see that your Dweet was posted successfully. However, once we visit the index page, we won't be able to see any dweets because we've hardcoded recent_dweets to be an empty list. Open up the application_router.ex again and modify the '/' route:

  get "/" do
    # Assign recent_dweets to the last 10 dweets
    query = from d in Dwitter.Dweet, order_by: [desc: d.id], limit: 10, select: d
    recent_dweets = Dwitter.Repo.all(query)
    conn = conn.assign(:recent_dweets, recent_dweets)
    render conn, "index.html"
  end

Here we're using the Ecto.Query interface, which means we need our router to use Ecto.Query - go ahead and add that at the top of the router. Now visit the root route again, and we'll see our recent Dweets.

Summary

That's it! We've successfully replaced Amnesia with Ecto for our persistence layer, and it resulted in less code. Once again, in the interest of time, we didn't really do any testing. In the next episode, we'll explore adding an Author entity into our application, rather than using a string to represent our author - this will allow things like profile pages, viewing dweets by a given author, etc. We'll implement this by driving it with acceptance tests. See you soon!

Josh Adams

I've been building web-based software for businesses for over 18 years. In the last four years I realized that functional programming was in fact amazing, and have been pretty eager since then to help people build software better.

  1. Comments for Ecto, Part 2 - Dwitter

You must login to comment

You May Also Like