POST DIRECTORY
Category software development

I recently migrated a Rails JSONAPI server to Phoenix, and in the process I learned a lot about using Ecto with a database that conforms to Rails naming conventions. It turned out that I was able to share an existing Rails database between the Rails and Phoenix implementations of the server during development, which allowed me to use a phased approach in transitioning to the new Phoenix version.

In his ElixirConf 2016 keynote, Chris McCord announced that Phoenix models will likely be replaced with service modules in Phoenix 1.3. Because of this I’ll stick to describing the parts of Ecto used in Phoenix models while skipping the concept of models in general.

The Ecto library has four core modules that you will need to understand. These are Ecto.Schema, Ecto.Changeset, Ecto.Query, and Ecto.Repo. To get our feet wet and provide a basis for future articles, we’ll focus on schemas here.

The responsibility of Ecto.Schema is to define a struct to hold a table’s record data, and to provide type information to facilitate casting columns and referencing associations. This definition is done using Ecto’s field and association macros. In ActiveRecord terms: Ecto schema fields are like explicit versions of the Ruby magic that turns table columns into model attributes, while Ecto schema associations are very similar to AR associations.

The Ecto documentation is extremely clear and well written, but it took some digging to figure out how Ecto conventions can be over-ridden to work with Rails databases. I wrote this article to share what I’ve learned.

We’ll be using these versions of Rails and Phoenix/Ecto.

Rails 5.0
Phoenix 1.2.1
Ecto 2.1.0-rc.2

NOTE: Ecto stable is currently 2.0.5, but it has a bug in certain has_many through scenarios. This is fixed in the 2.1.0 release candidates. We’ll see how to update this dependency when we set up our new Phoenix application.

Generating the Rails database

We’ll start by generating Rails migrations and models to build an example database. The models have a few basic attributes, but we won’t be using them yet. The models do, however, include examples of all the basic associations supported by ActiveRecord. Here are the relations we will be modeling in terms of Rails associations.

User has_and_belongs_to_many Projects
Project has_and_belongs_to_many Users

Project has_many Lists
List belongs_to Project

List has_many Items
Item belongs_to List

List has_one Status
Status belong_to List

We will also look at writing these through associations.

Project has_many Items through Lists
User has_many Lists through Projects
User has_many Items through Lists
  (a through association based on another through association)
Item has_one Status through List

Assuming we have a new Rails app backed by a PostgreSQL database, we can generate the models and migrations to express these fields and associations.

$ bin/rails g model User email:uniq encrypted_password
$ bin/rails g model Project name slug:uniq
$ bin/rails g migration CreateJoinTableProjectsUsers project user
$ bin/rails g model List name slug project:references
$ bin/rails g model Item description list:references
$ bin/rails g model Status description list:references

Now we need to make a few final adjustments. First, edit the XXX_create_join_table_projects_users.rb migration and uncomment both indexes. Next, change the XXX_create_lists.rb migration to force the List slug to be unique with respect to project_id. We can do this by adding a compound unique index.

add_index :lists, [:project_id, :slug], unique: true

With all this set up we are ready to migrate the database.

$ bin/rake db:migrate

I’ll finish by adding the remaining associations to the new models. You can take a look at the generated migrations, the models including all associations, and the schema.rb file in the sample project at:

https://github.com/HaughtCodeworks/playground-rails/tree/ecto-schemas

Before we start looking at the Ecto schemas we’ll use to access this new database, let’s create some sample data in the Rails console. We just need enough data to test our association definitions later.

user = User.create!(email: "[email protected]")
project = Project.create!(name: "Project", slug: "project", users: [user])
list = List.create!(name: "List", slug: "list", project: project)
Item.create!(description: "Item", list: list)
Status.create!(description: "Status", list: list)

Connecting Ecto to the Rails database

We’ll start by creating a new app named PhoenixApp:

$ mix phoenix.new phoenix_app --no-brunch
$ cd PhoenixApp

As mentioned in the introduction, Ecto 2.0.5 has a bug that has been fixed in the 2.1.0 release candidates. To use the latest rc version of Ecto you’ll need to change your project dependencies and then run mix deps.get to fetch the new versions.

These lines:

defp deps do
  [. . .
   {:phoenix_ecto, "~> 3.0"},
   {:postgrex, ">= 0.0.0"},
   . . .
  ]
end

Become:

defp deps do
  [. . .
   {:ecto, "~> 2.1.0-rc.0"},
   {:phoenix_ecto, "~> 3.1.0-rc.0"},
   {:postgrex, "~> 0.12.0"},
   . . .
  ]
end

Now update config/dev.exs to connect to the Rails development database. Typically this just requires changing the username, password, and database settings.

# Configure your database
config :phoenix_app, PhoenixApp.Repo,
  adapter: Ecto.Adapters.Postgres,
  username: "username",
  password: "password",
  database: "rails_app_development",
  hostname: "localhost",
  pool_size: 10

Finally we can test the connection by starting an iex session. To exit the session type Ctrl-C Ctrl-C.

$ iex -S mix

If iex starts without errors, your connection is configured correctly.

A bad username or password configuration will cause iex to emit errors looking like this:

[error] Postgrex.Protocol (#PID<0.NNN.0>) failed to connect: ** (Postgrex.Error) FATAL (invalid_password): password authentication failed for user "xxx"

A bad database name will cause iex to emit errors looking like this:

[error] Postgrex.Protocol (#PID<0.NNN.0>) failed to connect: ** (Postgrex.Error) FATAL (invalid_catalog_name): database "xxx" does not exist

Generating the Ecto Schemas

Just like in Rails, we’ll use generators to build our Ecto schemas and add the remaining associations by hand. As we generate Phoenix models and Ecto migrations, the mix command output will ask you to run mix ecto.migrate. Since our development database already has a complete database structure built, this will fail with duplicate table errors. For now we’ll just skip running Ecto migrations. For similar reasons we will skip generating a migration for the projects_users join table.

If you want to refer to the final version of the Ecto schemas as we go along, check out the source in web/models at:

https://github.com/HaughtCodeworks/playground-phoenix/tree/ecto-schemas

$ mix phoenix.gen.model User users email:unique encrypted_password
$ mix phoenix.gen.model Project projects name slug:unique
$ mix phoenix.gen.model List lists name slug project_id:references:projects
$ mix phoenix.gen.model Item items description list_id:references:lists
$ mix phoenix.gen.model Status statuses description list_id:references:lists

Let’s test out our new models, start up iex and see if we can load all Users. We won’t get too advanced with our usage of Ecto.Repo, for now it’s enough to understand that Repo.all returns all records, and Repo.one returns a single record. All of our Phoenix modules live under the PhoenixApp namespace, so our Ecto User is PhoenixApp.User and our application Repo is PhoenixApp.Repo. This command fetches all of our application’s Users from our application’s Repo:

$ iex -S mix
iex(1)> PhoenixApp.Repo.all(PhoenixApp.User)
SELECT u0."id", u0."email", u0."encrypted_password", u0."inserted_at", u0."updated_at" FROM "users" AS u0 []
** (Postgrex.Error) ERROR (undefined_column): column u0.inserted_at does not exist
    (ecto) lib/ecto/adapters/sql.ex:415: Ecto.Adapters.SQL.execute_and_cache/7
    (ecto) lib/ecto/repo/queryable.ex:121: Ecto.Repo.Queryable.execute/5
    (ecto) lib/ecto/repo/queryable.ex:35: Ecto.Repo.Queryable.all/4

We’ve hit our first naming convention issue. Ecto uses an inserted_at column for tracking when a record is first inserted into the database, Rails uses created_at. We’ll get that fixed in the next section.

Over-riding the inserted_at column name

Ecto supports over-riding the default timestamps column names both per model schema and globally across all model schemas.

In an Ecto schema definition, timestamps accepts options specifying alternative column names for inserted_at and updated_at. Let’s try configuring this in User.

Change the timestamps definition in web/models/user.ex from:

schema "users" do
  field :login, :string

  timestamps()
end

To:

schema "users" do
  field :login, :string

  timestamps inserted_at: :created_at
end

We can see if it worked in iex. The result is a list of PhoenixApp.User structs. For the rest of the article I’ll be hiding the contents of these structs for clarity, but it’s worth looking them over if you run these commands yourself in a real iex session.

$ iex -S mix
iex(1)> PhoenixApp.Repo.all(PhoenixApp.User)
SELECT u0."id", u0."email", u0."encrypted_password", u0."created_at", u0."updated_at" FROM "users" AS u0 []
[%PhoenixApp.User{__meta__: #Ecto.Schema.Metadata<:loaded, "users">,
  created_at: ~N[2022-10-24 20:42:17.914653], email: "[email protected]",
  encrypted_password: nil, id: 1, updated_at: ~N[2022-10-24 20:42:17.914653]}]

Obviously this configuration will be required by all of our models, so let’s undo the User timestamps change and set the configuration globally instead. The documentation for these global Ecto schema configurations can be found at https://hexdocs.pm/ecto/Ecto.Schema.html#module-schema-attributes.

In web/web.ex add @timestamp_opts to the model function after use Ecto.Schema:

def model do
  quote do
    use Ecto.Schema
    @timestamps_opts inserted_at: :created_at

    import Ecto
    import Ecto.Changeset
    import Ecto.Query
  end
end

We can check our work:

$ iex -S mix
iex(1)> PhoenixApp.Repo.all(PhoenixApp.User)
SELECT u0."id", u0."email", u0."encrypted_password", u0."created_at", u0."updated_at" FROM "users" AS u0 []
[%PhoenixApp.User{ . . . }]

Defining our Ecto schemas

Now we can start defining the fields and associations for our Ecto schemas. We’ll begin by covering the field declarations, then we can add the associations. We’ll cover all of the basic association types and then see how we can re-use those in through associations.

Fields

The generators have done most of the field mapping work for us. Each field declaration consists of a field name, a type, and options. Types can be either primitive or custom, so you are free to add types if your application needs them. Interestingly, both list (array) and map are included in the primitive types.

The field options you’ll most commonly use are default and virtual. The default option is used to provide a static default value for the field when it has not been set. The virtual option flags a field as non-persisting, a common use would be to temporarily hold a plaintext password before encrypting it for writing to the database. You can refer to the Ecto.Schema field documentation for all of the types and options.

Basic associations

We’ll start by looking at defining and accessing the basic associations belongs_to, has_many, many_to_many (has_and_belongs_to_many), and has_one.

belongs_to

Our Ecto model generator commands explicitly named the model table and the foreign keys referring to other tables. By choosing table names and foreign key names that match Rails conventions (which are pretty hard to argue with), we automatically get working generated belongs_to associations in our Ecto schemas.

Let’s test our generated belongs_to definition for Item. To fetch our item, we use Repo.one which is piped to Repo.preload(:list). Repo.preload loads the data for the named association and combines it with the Item returned by Repo.one. We can access the fields of the resulting struct using dot notation.

$ iex -S mix
iex(1)> item = PhoenixApp.Repo.one(PhoenixApp.Item) |> PhoenixApp.Repo.preload(:list)
SELECT i0."id", i0."description", i0."list_id", i0."created_at", i0."updated_at" FROM "items" AS i0 []
SELECT l0."id", l0."name", l0."slug", l0."project_id", l0."created_at", l0."updated_at", l0."id" FROM "lists" AS l0 WHERE (l0."id" = $1) [1]
%PhoenixApp.Item{ . . . }

iex(2)> item.list.name
"List"
has_many

Just like we did for the Rails models, we need to add has_many definitions to our Ecto model schemas by hand. In Project we add a has_many explicitly naming the relationship and specifying the model to use.

schema "projects" do
  . . .
  has_many :lists, PhoenixApp.List
  . . .
end

List has_many Items works the same:

schema "lists" do
  . . .
  has_many :items, PhoenixApp.Item
  . . .
end

And we can test Project has_many Lists out in iex. After we fetch our project we can map over the project.lists list and return the name of each one.

$ iex -S mix
iex(1)> project = PhoenixApp.Repo.one(PhoenixApp.Project) |> PhoenixApp.Repo.preload(:lists)
SELECT p0."id", p0."name", p0."slug", p0."created_at", p0."updated_at" FROM "projects" AS p0 []
SELECT l0."id", l0."name", l0."slug", l0."project_id", l0."created_at", l0."updated_at", l0."project_id" FROM "lists" AS l0 WHERE (l0."project_id" = $1) ORDER BY l0."project_id" [1]
%PhoenixApp.Project{ . . . }

iex(2)> project.lists |> Enum.map(fn l -> l.name end)
["List"]
has_and_belongs_to_many/many_to_many

The Rails has_and_belongs_to_many association is referred to as many_to_many in Ecto. Ecto 2.0 supports many_to_many using either a simple join table (like projects_users in our example) or joining through another Ecto schema definition.

When adding the many_to_many declaration we specify the association name, the association model, and the name of the join table. Here are the many_to_many lines from User and Project.

schema "users" do
  . . .
  many_to_many :projects, PhoenixApp.Project, join_through: "projects_users"
  . . .
end
schema "projects" do
  . . .
  many_to_many :users, PhoenixApp.User, join_through: "projects_users"
  . . .
end

And in iex User many_to_many Projects:

$ iex -S mix
iex(1)> user = PhoenixApp.Repo.one(PhoenixApp.User) |> PhoenixApp.Repo.preload(:projects)
SELECT u0."id", u0."email", u0."encrypted_password", u0."created_at", u0."updated_at" FROM "users" AS u0 []
SELECT p0."id", p0."name", p0."slug", p0."created_at", p0."updated_at", u1."id" FROM "projects" AS p0 INNER JOIN "users" AS u1 ON u1."id" = ANY($1) INNER JOIN "projects_users" AS p2 ON p2."user_id" = u1."id" WHERE (p2."project_id" = p0."id") ORDER BY u1."id" [[1]]
%PhoenixApp.User{ . . . }

iex(2)> user.projects |> Enum.map(fn p -> p.name end)
["Project"]
has_one

As in Rails, Ecto has_one definitions look just like has_many. We can add List has_one Status like this:

schema "lists" do
  . . .
  has_one :status, PhoenixApp.Status
  . . .
end

And it works as expected:

$ iex -S mix
iex(1)> list = PhoenixApp.Repo.one(PhoenixApp.List) |> PhoenixApp.Repo.preload(:status)
SELECT l0."id", l0."name", l0."slug", l0."project_id", l0."created_at", l0."updated_at" FROM "lists" AS l0 []
SELECT s0."id", s0."description", s0."list_id", s0."created_at", s0."updated_at", s0."list_id" FROM "statuses" AS s0 WHERE (s0."list_id" = $1) [1]
%PhoenixApp.List{ . . . }

iex(2)> list.status.description
"Status"

Through associations

Associations can be defined through other associations in Ecto just like in ActiveRecord. Ecto’s through definitions use a list of association names to resolve when fetching the target association.

has_many through

As an example, Project has_many Items through List and User has_many Lists through Project can be expressed as:

schema "projects" do
  . . .
  has_many :items, through: [:lists, :items]
  . . .
end
schema "users" do
  . . .
  has_many :lists, through: [:projects, :lists]
  . . .
end

These through definitions describe the association chain used to fetch the final association and can be arbitrarily deep. You can see them working in iex.

$ iex -S mix
iex(1)> project = PhoenixApp.Repo.one(PhoenixApp.Project) |> PhoenixApp.Repo.preload(:items)
SELECT p0."id", p0."name", p0."slug", p0."created_at", p0."updated_at" FROM "projects" AS p0 []
SELECT l0."id", l0."name", l0."slug", l0."project_id", l0."created_at", l0."updated_at", l0."project_id" FROM "lists" AS l0 WHERE (l0."project_id" = $1) ORDER BY l0."project_id" [1]
SELECT i0."id", i0."description", i0."list_id", i0."created_at", i0."updated_at", i0."list_id" FROM "items" AS i0 WHERE (i0."list_id" = $1) ORDER BY i0."list_id" [1]
%PhoenixApp.Project{ . . . }

iex(2)> project.items |> Enum.map(fn i -> i.description end)
["Item"]

For our last has_many through example, let’s try User has_many Items through Lists. In this case the lists association on User is also a has_many through, and we can chain them together without any problems.

schema "users" do
  . . .
  has_many :items, through: [:lists, :items]
  . . .
end

With our updated version of Ecto we can access this chained through association. One thing to notice is that pre-loading a through relationship like items automatically pre-loads any intermediate associations like projects and lists.

i$ iex -S mix
ex(1)> user = PhoenixApp.Repo.one(PhoenixApp.User) |> PhoenixApp.Repo.preload(:items)
SELECT u0."id", u0."email", u0."encrypted_password", u0."created_at", u0."updated_at" FROM "users" AS u0 []
SELECT p0."id", p0."name", p0."slug", p0."created_at", p0."updated_at", u1."id" FROM "projects" AS p0 INNER JOIN "users" AS u1 ON u1."id" = ANY($1) INNER JOIN "projects_users" AS p2 ON p2."user_id" = u1."id" WHERE (p2."project_id" = p0."id") ORDER BY u1."id" [[1]]
SELECT l0."id", l0."name", l0."slug", l0."project_id", l0."created_at", l0."updated_at", l0."project_id" FROM "lists" AS l0 WHERE (l0."project_id" = $1) ORDER BY l0."project_id" [1]
SELECT i0."id", i0."description", i0."list_id", i0."created_at", i0."updated_at", i0."list_id" FROM "items" AS i0 WHERE (i0."list_id" = $1) ORDER BY i0."list_id" [1]
%PhoenixApp.User{ . . . }

iex(2)> user.items |> Enum.map(fn i -> i.description end)
["Item"]
has_one through

The other through variation we have available is has_one. Let’s add a definition for Item has_one Status through List.

schema "items" do
  . . .
  has_one :status, through: [:list, :status]
  . . .
end

And in iex it just works:

$ iex -S mix
iex(1)> item = PhoenixApp.Repo.one(PhoenixApp.Item) |> PhoenixApp.Repo.preload(:status)
SELECT i0."id", i0."description", i0."list_id", i0."created_at", i0."updated_at" FROM "items" AS i0 []
SELECT l0."id", l0."name", l0."slug", l0."project_id", l0."created_at", l0."updated_at", l0."id" FROM "lists" AS l0 WHERE (l0."id" = $1) [1]
SELECT s0."id", s0."description", s0."list_id", s0."created_at", s0."updated_at", s0."list_id" FROM "statuses" AS s0 WHERE (s0."list_id" = $1) [1]
%PhoenixApp.Item{ . . . }

iex(2)> item.status.description
"Status"

Other Association Types

There are two other varieties of associations that I have skipped, polymorphic associations and self-referential associations. I have not needed to use either of these on a project yet.

The polymorphic association is not really supported by Ecto. From a database theory perspective, polymorphic associations are dangerous because they break enforcement of referential integrity. Ecto prefers that you use a distinct table and foreign key for each “type”, which allows for enforcement of referential integrity constraints.

The other problem with supporting polymorphic associations is that Ecto uses structs to hold record data, but there is no notion of struct inheritance in Elixir. Regardless, there is community interest in supporting some type of polymorphic association in Ecto but any implementation will likely be an extension and not part of Ecto core.

The other association we skipped is self-referential associations, which are used to model tree like relationships. Setting up the association definitions for has_many :children and belongs_to :parent should be straight forward, and functional languages make it easy to use recursion to fetch data to any depth. This may be an interesting topic to explore in the future.

Next Steps

So far our use of Ecto has been straightforward, we were able to create Ecto schemas and define fields and associations compatible with Rails database conventions. At this point we have the read side of interacting with a Rails database covered.

Obviously reading records out of the database is only a small part of the story. If we want to replicate the behavior of ActiveRecord models, we’re still missing a lot of pieces. We need to handle record inserts and updates with validation and error reporting. We need callback behaviors that generate slugs or encrypt passwords. We’ll want to catch and handle violations of database constraints. Most of these behaviors will be implemented in the form of Ecto changesets.

To really use our new Ecto schemas in an application, we also need to understand Ecto’s query interface. We’ll want to join on our schema associations and access those joined tables in composed queries. We’ll want to write Ecto migrations that can duplicate the Rails database structure. And at some point we may want to use migrations to move some of our Rails naming conventions toward the Ecto ones.

That’s a lot to cover, and Ecto is definitely a different beast than Rails ActiveRecord. Fortunately our familiarity with ActiveRecord allows us to draw parallels and emphasize the differences between the two. Before long you’ll be just as good at thinking in Ecto as you are at working with ActiveRecord.