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.
Written by Simon Ström as a way to remember. It's a dev log of thinks I want to remember.