Published 2019-09-29

Phoenix is a wonderful framework for building performant web applications, and the Phoenix community has created great tutorials that take you from mix all the way to deployment. However, tutorials generally focus on true greenfield work, where you have the luxury of following Phoenix’s conventions at every turn. In this post, I want to talk about introducing Phoenix into a real-world tech stack, where you are likely to leverage one or more existing databases. This will be a short post, because Ecto (and by extension Phoenix) have made the necessary workarounds pretty intuitive and straightforward.


In my recent use case, I had two existing databases to consider. Pretending that my application name is Tacos, the database configuration needs to be updated from a single Tacos.Repo module to two distinct modules:

# lib/tacos/repo.ex
defmodule Tacos.TacoRepo do
  use Ecto.Repo,
    otp_app: :tacos,
    adapter: Ecto.Adapters.Postgres

defmodule Tacos.UserRepo do
  use Ecto.Repo,
    otp_app: :tacos,
    adapter: Ecto.Adapters.Postgres
# config/config.exs
config :tacos,
  ecto_repos: [Tacos.TacoRepo, Tacos.UserRepo]

Also, make the corresponding updates in your environment specific configuration files (config/dev.exs, config/test.exs, etc) so that each repo knows it’s database name, password, etc.


Even though your database already exists, you’ll want migrations in your Phoenix project so that the tables can be used in your test environment. The first option is to leverage mix ecto.dump and mix ecto.load to re-create all tables in one fell swoop. This is an attractive option if you have a large number of tables that would be time consuming to document in individual migration files.

In my case, my application only needed to use a fraction of the existing tables, so dumping/loading all of them felt like overkill and manually migrating them wasn’t much of a burden. Create if not exists is the right tool for this job, since it will create the table in your test environment while not raising an error in production.

def change do
  create_if_not_exists table(:avocados) do
    add :date_picked, :utc_datetime
    # etc...

It is absolutely ok to migrate just the tables you’ll use in your application, and ignore the rest. Going a step further, you can ignore individual columns that you don’t need. It’s nice to work with a slimmed down schema in your Phoenix application if the legacy database tables are bloated with columns you won’t be using.


By default, Ecto creates inserted_at and updated_at columns for tracking insertion metadata. The Ecto timestamps macro accepts options keywords that you can use to override these default column names . If you’re coming from Rails, which is a very common path into Phoenix, your tables will contain created_at instead of inserted_at. Both the inserted_at schema name and inserted_at_source column name can be configured here.

create_if_not_exists table(:avocados) do
  # other fields...
  timestamps(inserted_at: :created_at)

Also, Phoenix’s default type of :naive_datetime can be overridden by passing a type option, but you shouldn’t need to if your database’s ancestry is Rails.


If you need to use something other than id as your primary key, you can set @primary_key as a module attribute:

defmodule Tacos.Ingredients.Tortillas do
  @primary_key {:this_is_the_primary_key, :integer, autogenerate: true }
  schema "TORTILLAS" do
    field :radius_inches, :integer

For some reason, this table was generated by a maniac who named the table’s primary key this_is_the_primary_key. Such is life. If your tables themselves have unusual names, it’s trivial to specify them via the source argument. In the example above, the tortillas table is spelled out in all-caps.

And that’s about it. Having gone through this experience myself the other day, I was very impressed with how well the Phoenix and Ecto teams designed their APIs so that this sort of customization can be painless. If you have any feedback/corrections/suggestions, please hit me up on twitter.

Thanks, Adam