Multi-tenancy with Ecto, Part 1

With Ecto 2.0, there is support for Postgres schema or multiple databases for MySQL. What I am trying to do here is to use Postgres schema to achieve multi-tenancy, and of course using Ecto.

Disclaimer: This is mostly for my personal notes as I try to understand Ecto/Elixir better. There is also library called Apartmentex that you could use to do multi-tenancy.

This assumes the following model in web/models/guard.ex

defmodule Tenancy.Guard do
  use Tenancy.Web, :model

  schema "guards" do
    field :name, :string
    field :position, :string

    timestamps()
  end

  @doc """
  Builds a changeset based on the `struct` and `params`.
  """
  def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:name, :position])
    |> validate_required([:name, :position])
  end
end

Setup

You would also need to create your own schema by issuing CREATE SCHEMA "tenant_1"; from psql shell. I don't think Ecto has a built-in function to actually create a schema for you.

Then, run migration for your tenant by running mix ecto.migrate --prefix "tenant_1"

Fire up iex console by running iex -S mix phoenix.server.

First

These are the things that I do to actually insert/update records into a correct schema. Also, there are few things that are still confusing to me.

When defining a model, you could set module attribute @schema_prefix to tell Ecto that this table should be in postgres schema as defined in @schema_prefix. But for multi-tenancy purpose, @schema_prefix needs to be set dynamically - usually from subdomain i.e. tenant-1.example.com should use postgres schema for tenant-1. However, elixir module attribute is resolved at compile time, so you can't change that dynamically during run time.

Then, I found out about Ecto.put_meta/2 after looking through Ecto source code.

iex(13)> h Ecto.put_meta

def put_meta(struct, opts)                           

Returns a new struct with updated metadata.

It is possible to set:

  • :source - changes the struct query source
  • :prefix - changes the struct query prefix
  • :context - changes the struct meta context
  • :state - changes the struct state

I have no idea what :source is for.

Getting meta

Initially I thought I could update the changeset's __meta__, but Ecto.Changeset does not have __meta__ key. Ecto.Schema does!

iex> changeset = Guard.changeset(%Guard{name: "Abu", position: "Guard"}, %{})       
#Ecto.Changeset<action: nil, changes: %{}, errors: [], data: #Guard<>,
 valid?: true>

What we are interested in updating is in changset.data.

iex> guard_data = Ecto.put_meta(changeset.data, prefix: "tenant_1")
%Tenancy.Guard{__meta__: #Ecto.Schema.Metadata<:built, "tenant_1", "guards">,
 id: nil, inserted_at: nil, name: "Abu", position: "Guard", updated_at: nil}

iex> changeset = %{changeset | data: guard_data}
#Ecto.Changeset<action: nil, changes: %{}, errors: [], data: #Tenancy.Guard<>,
 valid?: true>

iex> changeset.data
%Tenancy.Guard{__meta__: #Ecto.Schema.Metadata<:built, "tenant_1", "guards">,
 id: nil, inserted_at: nil, name: "Abu", position: "Guard", updated_at: nil}

iex> {:ok, guard} = Repo.insert(changeset)
{:ok,
 %Tenancy.Guard{__meta__: #Ecto.Schema.Metadata<:loaded, "tenant_1", "guards">,
  id: 1, inserted_at: #Ecto.DateTime<2016-11-14 03:26:28>, name: "Abu",
  position: "Guard", updated_at: #Ecto.DateTime<2016-11-14 03:26:28>}}

So, what I did was extract out the changeset.data because changeset.data is Ecto.Schema. Then update __meta__ to include :prefix option and create a new changeset. Then you could just insert the changeset and it will be added to the correct postgres schema.

You could also do it more succinctly in Elixir, something like this:

model = %Guard{}
|> Ecto.put_meta(prefix: "tenant_1")

Guard.changeset(model, %{name: "Abu", position: "Guard"})
|> Repo.insert

Future

From ecto's repo, this issue https://github.com/elixir-ecto/ecto/issues/1675 seems to standardize the Ecto.Repo API to take option for :prefix, which is nice.

Then you could actually just do something like,

iex> {:ok, guard} = Repo.insert(changeset_without_meta_prefix, prefix: "tenant_1")
{:ok,
 %Guard{__meta__: #Ecto.Schema.Metadata<:loaded, "guards">, id: 1,
  inserted_at: #Ecto.DateTime<2016-11-14 03:10:54>, name: "Abu",
  position: "Guard", updated_at: #Ecto.DateTime<2016-11-14 03:10:54>}}

Then the record will be added to the correct table in tenant_1 schema. But of course, the above doesn't work because this feature is only in master branch at this point in time.

Part 2

What I want to do next is automatically issue query for creating postgres schema when new tenant is created. It also should delete the schema if tenant is deleted. Then, create a mix task to handle migrations.