jeffdeville

jeffdeville

Arch Question: Tired of passing this same parameter around for every method call

Situation

I’m working on a multi-tenant library for Ecto. Ecto structs and queries let you set a prefix in the meta area, which is awesome. But it’s getting really verbose to send every query and changeset through a set_prefix() method before forwarding it along to the Repo.

Thoughts / Ideas

  1. This may simply be the elixir/functional way. After all, less magic is good, and not hiding your function inputs is good too. :slight_smile:

  2. Currying - Currying isn’t quite what I want because I’m currying a module, not a function. (Also, I’m not changing the number of parameters required)

  3. Wrapping - I have a repo wrapper that verifies that a prefix was set on relevant Ecto structs. This is easy because I’m just verifying that SOMETHING was set, which means I can build it at compile time with macros. But what I’d really like to be able to do is:

    repo = Repo.prefixify(123)
    user = User.changeset(%User{}, %{name: “joe”})
    repo.insert(user)

and have it sent to the 123 prefix. Automatically. Then later,

loaded_user = repo.get!(User, user.id)

and have it know to pull from prefix 123.

So repo itself is a module.

Agents seem like the closest strategy here. However, it seems like with an agent, you have to keep track of your agent’s pid. Then you call your regular module, just including the pid as an argument. But at this point, all I’ve done is traded passing in a prefix for a pid.

Is there any way to pass this in once, and be done with it? If not, no worries. I just don’t want to let my unfamiliarity with the language limit the easy of use of my library.

Marked As Solved

josevalim

josevalim

Creator of Elixir

One option is to use the process dictionary along side custom functions in the repository. So you would do:

defmodule MyApp.Repo do
  use Ecto.Repo, otp_app: :my_app

  def put_prefix(prefix) do
    Process.put({MyApp.Repo, :prefix}, prefix)
  end

  def tenant_get(query, opts) do
     get(query, Keyword.put_new(opts, :prefix, get_prefix())
  end

  defp get_prefix do
     Process.get({MyApp.Repo, :prefix}) || raise "no Repo prefix set"
  end
end

And then:

MyApp.Repo.tenant_get(User, id)

Keep in mind I am using Ecto 2.1 (currently out as a release candidate) ability to pass the prefix as an option to all Repo operations.

Also Liked

michalmuskala

michalmuskala

I’d say we missed the obvious solution here - anonymous functions.

If we were talking about a single function instead of whole repo - this would be obvious with using partial application, wouldn’t it?

prefixed_insert = &Repo.insert(&1, prefix: "foo")
prefixed_insert.(my_data)

So can we do something similar for a module? We need to do some changes, mainly because now we need to decide at runtime which function to call, fortunately we can use apply/3.

def prefixify(prefix) do
  fn fun, args ->
    [opts | rest] = Enum.reverse(args)
    apply(Repo, fun, Enum.reverse(rest, [Keyword.put_new(opts, :prefix, prefix)])
  end
end

This allows us to call:

prefixed = Repo.prefixify("foo")
prefixed.(:insert, [my_data, []])
prefixed.(:all, [some_query, []])

It’s a bit different than the original, but achieves the goal. Is it worth it and should be done? That’s a completely different question :wink:

jeffdeville

jeffdeville

Nov 2, 2016 Update

So indeed my tests were where I was feeling the most pain.

I worked out a solution in 2 parts for that.

  1. I’m running a test_seeds.exs script at the beginning of all of my specs to do standard setup. That’s reduced general duplication quite a bit, and sped the specs up tremendously as well (9 sec → .8, because the specs were actually creating tenants)
  2. Where I am creating extra setup data, I was unable to use the Ecto strategy for ex_machina, because I couldn’t set the prefix on those structs. So I created a PR for ex_machina (ExMachine PR) that kinda feels like ‘traits’ from factory girl.

When I get back to this project, I’ll look into @josevalim’s insight about Ecto 2.1, and its ability to accept the prefix as a keyword to Repo operations. That is probably the ‘good enough’ solution right there!

Oct 31, 2016 Update

Update for future readers. I realized that the place I was noticing all of this painful duplication was in my specs. But my specs are usually only using a single prefix, and part of the struggle was with ex_machina needing a new (prefix) parameter that hosed its lovely strategy pattern. So the compromise solution I’m going with now is to create a test-only import file that will wrap ex_machina, letting me pass in a tenant that is defaulted to a ‘test’ tenant. And then also wrappers around the Repo that do the same. So far (this is a work in process), I’m not planning to use the wrappers in my production code, because my methods are short, and so I’m comfortable seeing the specification of the tenant. Hopefully, once this is is all complete, I’ll bake it in to the library docs.

That said, if there’s a solution to my original question that I’ve missed, I’d still really like to know!

pba

pba

You could use the pipe operqator |> and instead of:

do

repo = Repo.prefixify(123)
%User{}
|> User.changeset(%{name: "joe"})
|> repo.insert

Also if the module where you to this is User specific you might consider import User
EDIT: Expanded original answer
Regarding the prefix: How about using something like this:

defmodule User do
  defmacro __using__(prefix) do
    quote do
      use Ecto.Schema
      @schema_prefix prefix || Application.get_env(:ex_machina, :prefix, "public")
      #... all the other user stuff
  end
  end
end

and then

defmodule MyPrefixUser do
  use User :my_prefix
end

Where Next?

Popular in Questions Top

sergio
In Ruby, I can go: User.find_by(email: "foobar@email.com").update(email: "hello@email.com") How can I do something similar in Elixir? ...
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
beno
I will often find my self writing things similar to: case some_value do nil -> something() "" -> something() _ -> somethi...
New
marius95
Hello everyone, I try to use an Javascript Event Handler in my root.html.leex file. Therefore I created a function in the app.js file: ...
New
Tee
can someone please explain to me how Enum.reduce works with maps
New
belgoros
I’m not a pro in using Regex and can’t figure out why the following behaviour happens, especially if we take into account the difference ...
New
itssasanka
Hi all, Trying to get some more clarity over utc_datetime and naive_datetime for Ecto: The documentation above suggests that while ...
New
Brian
What is the proper way to load a module from a file in to IEX? In the python world, doing something like this pretty standard: from ....
New
openscript
Hello! Sorry for this astonishing simple question, but I’m really stuck. I try to set up the intellij-elixir plugin, but I don’t know ho...
New
dokuzbir
I want to highlight html closing tags when i click a html tag. That works in .html files but doesnt work for html.eex templates. How can...
New

Other popular topics Top

sen
Hi All, I set a environment variables in dev.exs , like below code. when i start server, how can i set the ${enable} value? thanks. d...
New
danschultzer
None of the current solutions worked well for me, so I went ahead and built a user management system from scratch. This project took far...
548 29603 241
New
electic
Hi, I am new to Elixir. I am trying to use the DateTime component to insert a date into MySQL however the there seems to be no way to fo...
New
shahryarjb
Hello, I have map which I want to convert it to string like this: the map: %{last_name: "tavakkoli", name: "shahryar"} the string I ne...
New
ashish173
I am using Ecto timestamps with postgres, I can see the timestamps() use the :naive_dateime but for my use case I wanted to store the ti...
New
dblack
I’ve got an issue with an app and I’ve no idea of how to troubleshoot it. I’m hoping someone here might have seen something similar. I p...
New
KronicDeth
Elixir plugin for JetBrain’s IntelliJ Platform (including Rubymine) This is a plugin that adds support for Elixir to JetBrains IntelliJ...
289 36352 110
New
komlanvi
Hi everyone, I was playing with phoenix liveView but I run into an issue. I have a form and want to validate each input text when the te...
New
axelson
This post is a wiki (feel free to hit the edit button near the bottom right of this post to add your own changes!) This post collects co...
239 48342 226
New
AstonJ
Seen any cool LiveView demos, sample apps or examples? Please post them here! :003:
New

Latest on Elixir Forum

Elixir Forum

We're in Beta

About us Mission Statement