Condo is a multi-tenant adapter for Ecto. You can use this to create new schemata in PostgreSQL. It's main advantages are:
- Compile-time migrations
- Prioritize
Ecto.Repo
functions for modifying and reading data
If available in Hex, the package can be installed
by adding condo
to your list of dependencies in mix.exs
:
def deps do
[
{:condo, "~> 0.1.0"}
]
end
Documentation can be generated with ExDoc and published on HexDocs. Once published, the docs can be found at https://hexdocs.pm/condo.
To set Condo up, you'll need to decide on two things.
- What prefix would you like for your tenants?
- Where would you like to store your migrations?
I recommend setting your migration prefix to a description of your data instead
of simply calling it tenant_
as is the default. For some that's store_
(SaaS) and for others that's region_
(sharding by and for geolocation.) You
can do this by:
config :condo,
prefix: "tenant_"
Next up is simply supplying a place where you would like your migrations written
to. Condo will figure out the path based on the module name supplied. If you're
managing tenants by App.Company
then you would ideally want the migration
module namespace to be App.Company.Migrations
. You can set this as so:
config :condo,
migration_namespace: "App.Company.Migrations"
And Condo will now store the migrations in the lib/app/company/migrations
folder. Magic!
First up is the initial migration, you create one as so:
mix condo.gen.migration create_products
This will create a template that should look as so:
defmodule BlitzPG.LeagueMatches.Migrations.CreateProducts do
use Ecto.Migration
def version, do: 20191122003859
def change do
end
end
You'll notice that unlike Ecto's migration generator, Condo adds a version()
function. Since Condo's migrations are compile-time and not run-time, we don't
have access to reading the timestamp off of the file name. However, Condo does
prefix all of the file names with the timestamp since this does help
significantly with telling the order of the migrations and how they're run. The
same result is created with exposing the timestamp in the module as a function
instead of in the filename.
As you would think, Condo's migration commands are not that different from Ecto's and even accept similar argument for feature parity.
mix condo.migrate
mix condo.migrate -r App.ObscureRepo
mix condo.rollback
mix condo.rollback -r App.ObscureRepo
Condo doesn't just aim to give you easier migration handling with non-public postgres schemas, but also to make sure your application code better manages it.
Although it's quite simple to do it yourself you can get a schema prefix made for you as so:
# DIY
Repo.all(Product, prefix: "store_#{store.id}")
# Condo Method with a struct supplied
Repo.all(Product, prefix: Condo.prefix(store))
# Or pass in a binary, atom, or integer without the struct needed
Repo.all(Product, prefix: Condo.prefix(store.id))
Repo.all(Product, prefix: Condo.prefix(store.uuid))
Repo.all(Product, prefix: Condo.prefix(:north_america))
# Get back to the public schema just-in-case
Condo.prefix(:public)
# => "public"
# Set the prefix on an Ecto.Queryable.t()
import Ecto.Query
User |> where([u], u.email == ^email) |> Condo.prefix(:foo) |> Repo.one()
Just like apartmentex, Condo supports CRUD functions in parity to Ecto.Repo
.
To achieve this, we just do some meta-programming and keep it simple. The main
different is that we add a second parameter just before the final options to
specify a region. This will also run a function to figure out which Repo
to
use if you're sharding tenants over multiple instances.
Condo.Repo.all(Game, :na1) == Repo.NA1.all(Game, prefix: Condo.prefix(:na1))
Condo.Repo.all(Game, :sa1) == Repo.SA1.all(Game, prefix: Condo.prefix(:sa1))
Schema
module to cache migration SQL for creating and running new migrations- Setup tests