Proposal: allow Ecto.Multi.insert accessing data from previous steps

1,302 views
Skip to first unread message

Wojtek Mach

unread,
Mar 7, 2016, 1:20:33 PM3/7/16
to elixir-ecto
Hi,
Recently I was rewriting some code to use Multi.

Before: (I left out changesets for brevity)

Repo.transaction(fn ->
  user
= Repo.insert!(%User{username: "alice"})
 
Repo.insert!(%Log{event: "user_created", user_id: user.id})
end)


After:

Ecto.Multi.new
|> Ecto.Multi.insert(:user, %User{username: "alice"})
|> Ecto.Multi.run(:log, fn %{user: user} -> Repo.insert(%Log{event: "user_created", user_id: user.id)) end)
|> Repo.transaction


Is there a better way of doing it, btw?

Since my 2nd operation ends up as an insert I initially expected to be able to use Multi.insert and this is how this could work:

Ecto.Multi.new
|> Ecto.Multi.insert(:user, %User{username: "alice"})
|> Ecto.Multi.insert(:log, fn %{user: user} -> %Log{event: "user_created", user_id: user.id} end)
|> Repo.transaction

I think this use case is fairly common for Multi and I saw it often in early dicussions about it. Do you think what I'm proposing would be useful? Personally I think it would be, because it would be even more obvious what a given step does by just skimming code (an insert/update/delete and nothing else) whereas a `Multi.run` can have arbitrary side effects (technically so does this insert). While I don't think it's documented anywhere as such, I've seen `Multi.run` be used primarly for non-DB operations like doing external service calls etc, and so I think keeping `insert`/`update`/... and `run` for different purposes would have merit. The downsides I can think of right now is obviously having more code and somewhat bigger API surface.

Best,
Wojtek

José Valim

unread,
Mar 7, 2016, 1:23:13 PM3/7/16
to elixi...@googlegroups.com
The second should be done as an association in the first changeset. :)



José Valim
Skype: jv.ptec
Founder and Director of R&D

--
You received this message because you are subscribed to the Google Groups "elixir-ecto" group.
To unsubscribe from this group and stop receiving emails from it, send an email to elixir-ecto...@googlegroups.com.
To view this discussion on the web visit https://groups.google.com/d/msgid/elixir-ecto/ee73e88f-aaa1-46a9-ab1c-089f39834ceb%40googlegroups.com.
For more options, visit https://groups.google.com/d/optout.

Wojtek Mach

unread,
Mar 7, 2016, 1:40:40 PM3/7/16
to elixir-ecto, jose....@plataformatec.com.br

The second should be done as an association in the first changeset. :)

Yeah, I already did that in one particular case and it was indeed better. Using still that example, I don't think it would work for a more general case though when the schemas aren't associated, e.g.:

%Log{event: "package_created", params: %{id: package.id, name: package.name}}

Michał Muskała

unread,
Mar 10, 2016, 5:48:08 PM3/10/16
to elixi...@googlegroups.com
When I was initially thinking about Multi, this was a concern I had as
well. What to do with data that depends on one-another.
I decided not to allow functions in insert/update/delete because of
couple reasons:

* one of the goals of Multi (that I envisioned) was to provide a way
to encode transaction as an inspectable data structure. This has many
benefits - you can defer the execution, pass it around, you could even
destructure in tests without actually running it or running the
database. But in order to do all of it, it has to be as much data and
as little functions as possible. Functions are opaque, you can't look
into them, you need to run them to observe the result.
* a simple (yet controversial) solution to the data dependency on
primary key is to use something that would allow you to generate the
keys client-side. A great solution is UUID. You can generate your
primary key beforehand and insert it where needed yourself.
* there is already a solution for handling dependencies in the form of
nested changesets.
* there is a way to run an arbitrary function

I'm not saying it's a bad idea or that it's something that we
definitely won't do. I'm just trying to explain my reasoning behind
skipping this feature.
Ecto.Multi is something completely new, and I'd say there are yet no
established patterns around it. We're all learning how it can be used
to solve problems.

Regards,
Michał.
> https://groups.google.com/d/msgid/elixir-ecto/a2f40ed3-92a9-4722-a320-4179173e5da3%40googlegroups.com.

Jason Harrelson

unread,
Mar 24, 2016, 5:05:42 PM3/24/16
to elixir-ecto, jose....@plataformatec.com.br
Can anyone explain how one can use nested changesets to insert a parent and 1 or more children?  I have tried it and get an error due to the child's foreign key to the parent being nil.

For instance:

TodoList.changeset(%TodoList{name: "Grocery List", todo_items: [%{description:"Milk"},%{description: "Eggs"}]})
|> Repo.insert!

FYI, I read this article and it did not clarify this exact situation:http://blog.plataformatec.com.br/2015/08/working-with-ecto-associations-and-embeds.

Additionally, I have also been trying to refactor my applications to use Ecto.Multi and have found more situations than not where it would be great to have the data from the prior operations.  Definite up vote from me on this feature request.

Thanks!

José Valim

unread,
Mar 24, 2016, 7:23:01 PM3/24/16
to elixi...@googlegroups.com
If you use Ecto 2.0, this should work:

Repo.insert! %TodoList{
  name: "Grocery List",
  todo_items: [
    %TodoItem{description:"Milk"},
    %TodoItem{description: "Eggs"}
  ]
}



José Valim
Skype: jv.ptec
Founder and Director of R&D

Reply all
Reply to author
Forward
0 new messages