zamith
Hi,
I’m implementing a system that implements multitenancy via multiple databases. For business reasons I cannot do away with prefixes, which I’d prefer. In any case, I was able to make it work by using put_dynamic_repo, but am concerned it might be a bit too “magic”.
I’m doing this with a Phoenix app, so I set the repo on the request process, any sub process will have to set the repo again, which is easy to forget.
I’ve been pointed out to an alternative that is to wrap the Ecto API and require a pid for every call, which seems more obvious but both more labor intensive as well as brittle, since any change in the API breaks the code.
I’m very torn in terms of what’s the best option (and maybe there’s even another that I’m missing). Any advice would be great.
Here’s what I’m using to manage the repos, note that the tenants are agencies and I’m using a GenServer to cache the dynamic repos that were created. Also have a function to create a new database and run the migrations if needed.
defmodule MyApp.RepoManager do
use GenServer
alias MyApp.{
Application,
Repo
}
def start_link(settings) when is_list(settings) do
GenServer.start_link(__MODULE__, settings, name: __MODULE__)
end
def set_agency_repo(agency, ensure_exists \\ false) do
if ensure_exists do
ensure_repo_exists(agency)
end
repo_pid = GenServer.call(__MODULE__, {:get_dynamic_repo, agency})
Repo.put_dynamic_repo(repo_pid)
{:ok, repo_pid}
end
def unset_agency_repo do
repo_pid = Repo.put_dynamic_repo(Repo)
{:ok, repo_pid}
end
def init(_opts) do
{:ok, %{repos: %{}}}
end
def handle_call({:get_dynamic_repo, agency}, _from, state) do
case state.repos[get_database_name(agency)] do
nil ->
{:ok, repo_pid} = Repo.start_link(get_connection_options(agency))
{:reply, repo_pid,
%{state | repos: Map.put_new(state.repos, get_database_name(agency), repo_pid)}}
repo_pid ->
{:reply, repo_pid, state}
end
end
defp get_database_name(agency) do
"my_app_#{agency.slug}"
end
defp get_connection_options(agency) do
[
name: nil,
pool_size: 2,
database: get_database_name(agency)
] ++ Application.db_config()
end
defp ensure_repo_exists(agency) do
options = get_connection_options(agency)
Repo.__adapter__().storage_up(options)
{:ok, repo_pid} = Repo.start_link(options)
Repo.put_dynamic_repo(repo_pid)
Ecto.Migrator.run(Repo, :up, all: true, dynamic_repo: repo_pid)
Repo.stop(1000)
Repo.put_dynamic_repo(Repo)
end
# TODO: Manage pool size better
# TODO: Support removing repos
end
Trending in Questions
Other Trending Topics
Categories:
Sub Categories:
Forums
Popular Tags
- #ecto
- #liveview
- #troubleshooting
- #learning-elixir
- #library
- #deployment
- #erlang
- #testing
- #genserver
- #mix
- #absinthe
- #remote-other
- #otp
- #plug
- #how-to-question
- #macros
- #postgres
- #elixirconf
- #channels
- #exunit
- #discussion
- #code-sync
- #podcasts
- #javascript
- #onsite
- #dialyzer
- #docker
- #authentication
- #ai
- #umbrella
- #full-time-contract
- #podcasts-by-brainlid
- #ecto-query
- #blog-post
- #elixirconf-us
- #elixir-ls
- #phoenix_html
- #iex
- #graphql
- #genstage
- #websockets
- #supervisor
- #advent-of-code
- #distillery
- #processes
- #elixirconf-eu
- #api
- #forms
- #security
- #metaprogramming










Showing Posts 1 to 10- Show Best Posts
- Show All (oldest first)
- Show All (newest first)
LostKobrakai
If you immitate the
Ecto.Repobehaviour, just with a pid as first parameter everywhere needed this would be just as brittle as theMyApp.Repoapi. Not sure why you’d feel one is more brittle than the other.zamith
What I meant was that if the Ecto internal API changes (Ecto.Adapter.Schema and the sorts), it would break my app, which is different from the public API.
But is that the approach you would recommend? What issues are there with using
put_dynamic_repothat I’m missing?LostKobrakai
Why would you need to depend on ecto internal api? The public api of ecto doesn’t change without a major version bump and your wrapper could just manage the “dynamic repo” without making callers be concerned with it.
zamith
Ok, let me share some code to ensure we’re talking about the same thing:
Something like this. is Ecto.Repo.Schema considered public? Also, it means that every call to Ecto needs to use this wrapper.
Regardless of the brittleness or not, is this the preferred approach for multitenancy with multiple databases?
LostKobrakai
Nope
https://github.com/elixir-ecto/ecto/blob/5ec093becad402903774aa5c28e17fdfff9d7f2a/lib/ecto/repo/schema.ex#L4
Therefore you should rather make it so that your wrapper sets a dynamic repo on each call, like this:
Dynamic repo support was added for the usecase of multiple dynamically setup repos, so yes.
kip
May I clarify what constitutes a database in this context? For example, a Postgres server can host multiple databases and therefore only one Repo would be necessary. But perhaps you are referring to different servers (rather than databases) which would require multiple Repos.
zamith
Ah, yes. It might need to run in different servers, yes.
zamith
Ah, I see. Why do you think this is best that
put_dynamic_repoat the beginning of the request, which would affect all Ecto calls?LostKobrakai
Because this will leave the current dynamic repo for processes intact. You could skip it if you’re not using Ecto directly anymore. Otherwise afaik this is all you can do using public api.
sasajuric
My clients are currently doing the same things. I did an initial sketch of this, and our current draft is to use a small wrapper around
put_dynamic_repoat the top-level context functions. Basically each top-level context function would have to do something likeTenant.with(tenant_id, fn -> do_something end). Given their usage patterns, perhaps we end up setting a global default from some phoenix plug or absinthe middleware, b/c a single request handler doesn’t have to deal with multiple tenants, so there should be no risk in working with a wrong db.As an aside, there was a small bug in EctoSandbox which didn’t honor the dynamic_repo setting. The fix has been merged and I think it will be included in the upcoming Ecto 3.5.0.