ex_machina icon indicating copy to clipboard operation
ex_machina copied to clipboard

Missing multi-tenancy support

Open joeppeeters opened this issue 6 years ago • 9 comments

Hi,

In our project we implement multi-tenancy by setting prefixes on the Repo (eg. MyApp.Repo.insert(%User{name: 'John'}, prefix: tenant_id)

We'd like to reuse this behaviour in our tests, but ex_machina doesn't seem to support it currently. Would that be something you would consider to add?

An easy way would be to create a factory for a test tenant by configure it in the use statement:

defmodule MyFactory do
  use ExMachina.Ecto, repo: MyRepo, repo_options: [prefix: "test_tenant"]

  ...
end

and then rewriting the ExMachina.EctoStrategy to:

...
def handle_insert(%{__meta__: %{__struct__: Ecto.Schema.Metadata}} = record, %{repo: repo, repo_options}) do
    record
    |> cast
    |> repo.insert!(repo_options)
  end
...

or more flexible at runtime by introducing an insert/3 method.

Is this something worth to consider? Or do you see any other ways to achieve the same behaviour?

joeppeeters avatar Apr 10 '18 16:04 joeppeeters

My team just hit this snag. What options are there with regards to this?

churcho avatar Feb 08 '19 19:02 churcho

Thanks for posting here @churcho, and sorry @joeppeeters for taking so long to respond here! We currently don't have anything that supports multitenancy.

There is also a conversation on https://github.com/thoughtbot/ex_machina/issues/273#issuecomment-367823475 that might be related, since they're also trying to provide extra arguments to Repo.insert!. I think the only option for now is to create a custom strategy that has those options defined in handle_insert. Naturally, that's not ideal because you can't really defined them at runtime like this,

build(:user) |> insert(prefix: "test_tenant")

You would have to define them in each strategy you create,

# in TestTenantEctoStrategy or something like that
def handle_insert(%{__meta__: %{__struct__: Ecto.Schema.Metadata}} = record, %{repo: repo}) do
    record
    |> cast
    |> repo.insert!(prefix: "test_tenant")
  end

Is that something that might work?

germsvel avatar Feb 11 '19 13:02 germsvel

Hi @germsvel I have tried what you suggest with a custom strategy:

defmodule Datastore.TenantEctoStrategy do
  use ExMachina.Strategy, function_name: :insert
...
end

However, this leads to a conflict with the insert function defined within the library Ecto strategy:

== Compilation error in file test/support/factory.ex ==
** (CompileError) test/support/factory.ex:3: def insert/1 conflicts with defaults from insert/2
    test/support/factory.ex:3: (module)

However, it works with another function name.

nicocharlery avatar Apr 09 '19 00:04 nicocharlery

I added something like this to our factory 🤷‍♂

def tenant_insert(factory_name, attrs \\ %{}, tenant) do
    build(factory_name, attrs)
    |> MyApp.Repo.insert!(prefix: tenant)
end

We will see how far this will get us 😅

Edit: With the mentioned release v2.6.0 below this might look like:

def tenant_insert(factory_name, attrs \\ %{}, tenant) do
    insert(factory_name, attrs, prefix: tenant)
end

woolfred avatar Jan 24 '20 16:01 woolfred

The prefix option can also be set on the struct itself with Ecto.put_meta/2

def checkout_factory do
  %Checkout{
    (...)
  }
  |> Ecto.put_meta(prefix: "test")
end

hl avatar Mar 31 '20 08:03 hl

I was looking through this issue once again, and though I still think adding prefix options to insert during runtime could be difficult (given how ExMachina's strategies work), I think the original solution by @joeppeeters in would be a good first step.

It means we'd have to define a new factory per tenant, but maybe that would be good enough for some time? And if you need it per struct, @hl's comment on Ecto.put_meta/2 could be the trick.

What do people think? If it seems good to move with a use ExMachina.Ecto, repo: MyRepo, repo_options: [prefix: "test_tenant"], we can do that.

germsvel avatar Mar 31 '20 14:03 germsvel

I'd very much prefer use ExMachina.Ecto, repo: MyRepo, repo_options: [prefix: "test_tenant"]. I've hit this snag a few times the past years and now again.

mkarnebeek avatar Apr 01 '20 13:04 mkarnebeek

Excellent! Would love a PR for this if someone is interested in doing that. (Or I'll try to get this in at some point)

It's been mentioned before, but just so there's no confusion, the usage would be to specify repo_options in the factory:

defmodule MyTestTenantFactory do
  use ExMachina.Ecto, repo: MyRepo, repo_options: [prefix: "test_tenant"]
end

That would apply globally to all factories defined in MyTestTenantFactory.

Does that sound good? 👍 👎

germsvel avatar Apr 02 '20 12:04 germsvel

I just pushed v2.6.0 of ExMachina which includes passing Repo options to insert/3. I know it's not a way to globally pass the prefix to all factories in a given file, but it might help with unblocking at least some of the work.

germsvel avatar Feb 12 '21 10:02 germsvel