How do you approach Ecto.Multi composition?

I’m finding myself working with Ecto.Multi more and more recently, and I’m trying to understand what are the best practices to work with them.

My question for this topic is: what is the suggested approach to factor out a common Multi operation that you want to execute in multiple places?

The approaches I’m aware of are:

Creating a function that accepts a Multi and returns a Multi

Example:

def create_foo(attrs) do
  Multi.new()
  |> Multi.insert(:foo, Foo.changeset(%Foo{}, attrs))
  |> associate_foo_with_bars()
  |> ...
end

def update_foo(foo, attrs) do
  Multi.new()
  |> Multi.insert(:foo, Foo.changeset(foo, attrs))
  |> associate_foo_with_bars()
  |> ...
end

defp associate_foo_with_bars(multi) do
  multi
  |> Multi.update_all(...)
end

Extracting the function you pass as argument to Multi.whatever

Example:

def create_foo(attrs) do
  Multi.new()
  |> Multi.insert(:foo, Foo.changeset(%Foo{}, attrs))
  |> Multi.update_all(:associate, &associate_foo_with_bars/1, [])
  |> ...
end

def update_foo(foo, attrs) do
  Multi.new()
  |> Multi.insert(:foo, Foo.changeset(foo, attrs))
  |> Multi.update_all(:associate, &associate_foo_with_bars/1, [])
  |> ...
end

defp associate_foo_with_bars(%{foo: foo}) do
   from b in Bars,
     ...
end

Using Multi.merge

def create_foo(attrs) do
  Multi.new()
  |> Multi.insert(:foo, Foo.changeset(%Foo{}, attrs))
  |> Multi.merge(&associate_foo_with_bars/1)
  |> ...
end

def update_foo(foo, attrs) do
  Multi.new()
  |> Multi.insert(:foo, Foo.changeset(foo, attrs))
  |> Multi.merge(&associate_foo_with_bars/1)
  |> ...
end

defp associate_foo_with_bars(%{foo: foo}) do
  Multi.new()
  |> Multi.update_all(:associate, ...
    from b in Bars,
      ...
  end)
end

I’m interested in the pros and cons of the approaches (and possibly to other approaches), and what approach you think works best for specific situations.

Moreover, I’d like to know if you consider the name of the operations part of the API to compose your Multis.
I’ll expand on what I mean: in examples 2 and 3 above, my function accepts changes, and I assert that there’s a :foo key that contains the Foo I need to use in the next operation.
Another approach could be pattern match on :foo in the top Multi and just pass the Foo struct to the functions, that would not be coupled to the operation name.
Do you accept the whole changes map in your Multi composition function or the single result of an operation?


Please note the aim of threads like this is for you to explain things in your own words. Please see this thread for details :slight_smile: (videos/links to further reading are also welcome though!)

6 Likes

I’m also interested in this topic!

1 Like