Forming a Multi conditionally

Hi!

I have this function that has an Ecto.Multi operation where I include a Multi.insert if the function argument (a map) has a certain field in it.

The way I achieved it was this:

def create_thing(attrs \\ %{}) do
  Multi.new()
  |> Multi.insert(:first, Thing.changeset(%Thing{}, attrs))
  |> has_special_field(attrs)
  |> Repo.transaction()
end

def has_special_field(multi, attrs) do
  if Map.has_key?(attrs, :special_field) do
    multi
    |> Multi.insert(:second, 
                    fn %{ first: %Thing{id: thing_id}} -> 
                      AnotherThing.changeset(
                        %AnotherThing{}, %{thing_id: thing_id, special_field: attrs.special_field}
                      ) 
                    end)
  else
    multi
  end
end

It works, but I can’t help it looks more complicated/ugly than it needs to be.
How would you guys do that?

Thanks!

def has_special_field(multi, %{special_field: field}) do
  Multi.insert(multi, :second, fn %{first: %Thing{id: thing_id}} -> 
    AnotherThing.changeset(%AnotherThing{}, %{thing_id: thing_id, special_field: field})
  end)
end

def has_special_field(multi, _attrs), do: multi
3 Likes

Thanks, that does look neater!

But that’s the way to go with Multi, yeah? There’s no mechanism in Multi itself to conditionally chose what operations to perform?

Be aware about the naming of the functions. has_special_field sounds like a predicate but hides a multi action. It can lead to misreading of the code. The name should explicit this multi. For instance, maybe_insert_multi or insert_multi_if_has_special_field

4 Likes

Hi, thanks for your input!

Noob here though, would you mind elaborating it further? :sweat_smile:
You mean it clashes with some Elixir naming convention?

BTW, the elixir convention is to add a question mark at the end of the predicate name.
But here, it’s more about general best practices in the naming, nothing specifically tied to elixir. The name has to reflect what the function is doing.

1 Like

Yes, I was thinking of Map.has_key?, though it wouldn’t clash because of lack of question mark and the fact it’s a private function (though I didn’t write it as such here), I really like this convention!

Thanks for the clarification, I do think your naming is more descriptive of what the function does!

You can run arbitrary code and logic using Multi.run/3/5 this is useful if for some reason you want the multi operation key to be part of the resulting multi and to check it in other multi operations down the line, e.g. with the has_special_field function

def has_special_field(multi, %{special_field: field}) do
  Multi.insert(multi, :second, fn %{first: %Thing{id: thing_id}} -> 
    AnotherThing.changeset(%AnotherThing{}, %{thing_id: thing_id, special_field: field})
  end)
end

def has_special_field(multi, _attrs), do: Multi.run(multi, :second, fn(_repo, _prev_multis) -> {:ok, nil} end)

This does nothing else except that now the :second operation will be present in the final result either way.
You could also use:

def create_thing(attrs \\ %{}) do
  Multi.new()
  |> Multi.insert(:first, Thing.changeset(%Thing{}, attrs))
  |> Multi.run(:second, fn(transaction_repo, %{first: %Thing{id: thing_id}}) ->
     case Map.has_key?(attrs, :special_field) do
        true -> transaction_repo.insert(AnotherThing.Changeset(.........))
        false -> {:ok, nil}
     end
  end)
  |> Repo.transaction()
end
1 Like

Didn’t need that now but now that you mentioned I feel like it’s just a matter of time I’ll need that! That’s a really good thing to be aware of, thank you!