Tinkering with Code.

Working Ecto embeds in Phoenix

May 15, 2016

I was playing around with a small phoenix app and wanted to embed some data in the model instead of of having an additional table.

I want one model called Profile. A profile should have some fields, and embed address.

mix phoenix.new {AppName}
cd {AppName}
mix test

That is your basic app. Now we need our profile, so let us generate it.

mix phoenix.gen.controller Profile profiles name:string email:string

This will give us a controller and a model, and some basic tests for it.

No we need to add our embedded model address. It will be stored in a jsonb column on the profile model.

In the end we want to be able to see the address from the show action on the ProfileController. So we modify the ProfileController#show to reflect the end result we want.

test "shows chosen resource", %{conn: conn} do
    profile = Repo.insert! %Profile{}
    changeset = Ecto.Changeset.change(profile)

    changeset = Ecto.Changeset.put_embed(changeset, :address,
      %Resume.Address{
        street: "StreetName",
        city: "TheCityOfMyDreams",
        zip: "12345",
        country: "MyCountry"
      }
    )
    Repo.update!(changeset)

    conn = get conn, profile_path(conn, :show, profile)
    assert json_response(conn, 200)["data"] == %{"id" => profile.id,
      "name" => profile.name,
      "email" => profile.email,
      "address" => %{
        "city" => "TheCityOfMyDreams",
        "country" => "MyCountry",
        "street" => "StreetName",
        "zip" => "12345",
      }
    }
  end

To set an address to our profile we use Ecto.Changeset.put_embed/3.

The controller action does the correct thing; it fetches the profile and renders it. However, the rendering function does not by default include the address. To add it modify the profile view file.

  def render("profile.json", %{profile: profile}) do
    %{
      id: profile.id,
      name: profile.name,
      email: profile.email,
      address: render_one(profile.address, MyApp.AddressView, "address.json")
    }
  end

We call MyAppAddressView and asks it to render an address. So we need to implement it. Create web/view/address_view.ex with the following.

defmodule MyApp.AddressView do
  use Resume.Web, :view

  def render("address.json", %{address: address}) do
    %{
      street: address.street,
      zip: address.zip,
      city: address.city,
      country: address.country
    }
  end
end

Cool!

Easy and explicit. Here we assume that profile.address is a valid thing. It should be embedded in the model.

The tests for it would be something like this:

 test "has embedded address" do
    changeset = Profile.changeset(%Profile{}, @valid_attrs)
    changeset = Ecto.Changeset.put_embed(changeset, :address,
      %Resume.Address{ street: "StreetName" }
    )

    assert changeset.changes.address.changes.street == "StreetName"
  end

It is a bit to explicit to check that the street name is correct, but it does the trick. Refactoring is your friend.

Now let us add address to profiles, and make this test pass.

  schema "profiles" do
    field :name, :string
    field :email, :string
    embeds_one :address, MyApp.Address

    timestamps
  end

We state that we want profiles to have ONE address, and we define an address to be MyApp.Address. Create a new model for it in web/model/address.ex

defmodule Resume.Address do
  use Ecto.Schema

  embedded_schema do
    field :street, :string
    field :zip, :string
    field :city, :string
    field :country, :string

    timestamps
  end
end

Nothing fancy. It just defines a schema. But for this to work, profiles needs a place to store it. We will add a address column in the profile table. It should be of type jsonb. We do that by setting it to a map.

Create a new migration

mix ecto.gen.migration AddAddressToProfile

Open the new file and add the migration

defmodule MyApp.Repo.Migrations.AddAddressToProfile do
  use Ecto.Migration

  def change do
    alter table(:profiles) do
      add :address, :map
    end
  end
end

We use the type map.

mix ecto.create
mix ecto.migrate
mix test

It should all be green and pretty.


Simon Ström

Written by Simon Ström as a way to remember. It's a dev log of thinks I want to remember.