New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Introducing Ecto.Multi #1114
Comments
That is awesome! 👍 Is there a way for My use case is that almost all cases where we use transactions right now we start by first checking if the user a) has access b) has the last state ( Maybe allowing |
@MSch theoretically you should be able to just call Repo.transaction with the new multi inside the |
👍 This sounds fantastic. It has been great watching the conversation around this unfold. |
Feel free to ignore this comment but I'm wondering if this is an |
Thanks @josevalim this is exactly what I was looking for |
This kind of functionality is often called "session". Maybe that could be a better name? |
@solnic wouldn't it be called a |
The architectural pattern is Unit of Work if… we still care about Fowler’s stuff http://martinfowler.com/eaaCatalog/unitOfWork.html
|
Hmm. Not sure if the intention of this feature is to provide a mechanism like UoW with dependency resolution for operations where they depend on each other and making sure that execution order is correct? From the examples Jose gave it looks like an explicit way of setting up a pipeline of operations for later execution. That's why I thought about a "Session", since that's what you do. You start a persistence session where one or more operations will be executed, possibly within a transaction, but the session does not imply a transaction as that can be db-specific. |
I find the name "Session" confusing, there are too many things called Session. Calling it Ecto.Session would mean almost every time the concept is introduced it will be followed up by a disclaimer like "not your browser's session". I am mostly ok with "Unit of Work" but it can introduce the mismatch @solnic has presented and that sometimes happens with Repo. If someone takes the definition to its heart, it will be confusing because it isn't a direct match. My preferences so far are Thanks everyone for the feedback so far! |
I like OperationSet more than multi... but I also don't want to be typing On Tue, Dec 8, 2015 at 5:41 AM, José Valim notifications@github.com wrote:
|
I like One of the features I initially imagined for something like this was a way to simulate |
@jeregrine @josevalim I will do some testing later today with PostgreSQL. As far as naming, I'm perfectly fine with |
Oh, I forgot about one thing. If we decide to go with |
As far as functionality goes, I'm not sure why |
@solnic in a conversation with @michalmuskala I do remember we had a use case for running custom code but I can't recall it on top of my mind. Do you have any concern regarding Multi.run? |
@josevalim it feels like out of scope of Ecto's functionality. Why would you want to run arbitrary functions using Ecto's interface? I think the result of executing Ecto's operations should be enough to decide what to do next. |
I think the use case for The difference between running a function before call to |
@solnic I see. In the starting example: Ecto.Multi.new
|> Ecto.Multi.update(:user, user_changeset)
|> Ecto.Multi.insert(:log, log_changeset)
|> Ecto.Multi.run(:charge_credit_card, fn changes_so_far -> {:ok, _} | {:error _} end) The idea is that you may need some previous information, like the |
The main driver for the |
@chrismccord If I understand you right - the I guess I'm getting confused as to how this is better than using what's already there. Then again my mind could be locked in Postgres land. Mongo wouldn't support this. |
Yes, calls out to some payment processor for this example and if it fails or raises then it rolls back the whole thing. The issue with manual rollbacks or raises is you have to compose that yourself and it has turned into a nested series of repo calls and case statements, where all I want is to continue on success or rollback the whole sequence on failure. @robconery just so I'm not missing something, can you turn the following code sample into existing code that raises/rollbacks, so we can compare to what's already there?: opts =
Multi.new
|> Multi.insert(:order, order_changeset)
|> Multi.run(:products, %{order: order} -> decrement_stock_counts(order) end)
|> Multi.update(:user, %{user | last_order_placed_at: DateTime.utc()})
|> Multi.insert(:payment, payment)
|> Multi.run(:charge_card, fn %{payment: %{trans_id: id} -> {:ok, ...} end)
case Repo.transaction(opts) do
{:ok, %{user: user, charge_card: log}} ->
...
{:error, :charge_card, %{charge_card: _errors}} ->
conn
|> put_flash("There was an error processing your payment")
|> render("checkout.html")
{:error, key, %{...}} ->
...
end |
I agree with @chrismccord. I am already running into the issue where I ask "what if this query is dependent on the results of another query and upon execution it fails? How do I revert the database back to a state before the previous query?" I've been writing transactions in SQL to overcome this pitfall and |
|
@chrismccord Sorry I'm confused - probably as my question is a bit muddled. Let's try again... If I run I was thinking something like this, with the current codebase: case Repo.transaction fn ->
charge = Stripe.charge_card(...)
user = User.insert!(%User{email: "..."})
log = Logs.insert!(%Log{user_id: user.id, entry: "Logged"})
end If the call to This can be a problem, of course, if you call out to another app - but I'm just talking about the DB at this point. Nothing manual needs to happen here. Given this, I guess I'm unclear on what |
@robconery Multi is doing what we have discussed on #1009. Your code is perfectly fine when you don't expect things to fail but, if you assume things can fail along the way because of data or constraints, then you need to wrap each operation in multiple case blocks with explicit rollbacks. Something like this: Repo.transaction fn ->
case User.insert(%User{email: "..."}) do
{:ok, user} ->
case Log.insert(%Log{...}) do
{:ok, log} ->
Stripe.charge(...)
%{user: user, log: log}
{:error, log_changeset} ->
Repo.rollback(%{log_changeset: log_changeset})
end
{:error, user_changeset} ->
Repo.rollback(%{user_changeset: user_changeset})
end
end In other words, multi is executing every step one by one and returning exactly which step is failing, accumulating the data along the way. PS: Mongo can support Multi but, due to lack of transactions, it should not support Ecto.Multi that have |
@josevalim Ah - the rollback needs to be explicit! For some reason I thought I saw in your source that you caught the error and let it bubble. That's what we do with Moebius: Then in the The exact error is returned with the It seems odd to me that you would need to |
+1 for |
Would we be able to use |
No. I believe it simply can't work with the same semantics on Mongo (or maybe it can now that it ships with PG :P). It is ultimately a question mongo_ecto should answer though. |
I like Overall I like the idea a lot. Can't wait to give it a try |
I believe it can work. It obviously won't give transactional guarantees, but it will allow you to batch multiple operations together. |
This flow seems to fit the new Just seems as if the code example is trying to reinvent the wheel when it comes to a transactional pipeline (which |
Also, from the way this is handled it seems like it's used almost exactly like Streams are used. Perhaps Ecto.Stream is a fitting name that will align to Elixir's brand as well. |
@mgwidmann could you clarify? this is definitely not a stream. :) it is not a collection of data of any sorts. Maybe run gives an idea of a callback but even the arguments and expect results does not map to something like a stream. |
Was just thinking its built up as a data structure like a stream is and then executed with Was just a naming thought :) |
Well, working with data structures is common throughout Elixir, not specific to streams. And it is not executed by calling |
How about Ecto.Procedure? Agree that we shouldn't bikeshed on the name too much though. |
Two more suggestions: Ecto.OperationChain or, more simply, Ecto.Chain I presume Multi came from Redis (or another tool, or coincidence). I’ve never much liked it in Redis. I like OperationSet, which I agree could imply a lack of ordering. Since OrderedOperationSet is too verbose (and OperationPoset too obscure and ugly) instead I suggest Chain and OperationChain, both of which imply order. That said, I love terse code, so I could learn to live with Multi, but perhaps not love it. |
I think we need a sense of humor on this:
Or maybe...
Or...
|
I know! How about |
@robconery nice. How about Actually, sorry to have initiated the naming controversy on this thread. 😞 I ❤️ |
Just I quick question, can new Repo.transaction fn ->
case User.insert(%User{email: "..."}) do
{:ok, user} ->
case Log.insert(%Log{...}) do
{:ok, log} ->
Stripe.charge(...)
%{user: user, log: log}
{:error, log_changeset} ->
Repo.rollback(%{log_changeset: log_changeset})
end
{:error, user_changeset} ->
Repo.rollback(%{user_changeset: user_changeset})
end
end If yes, and if I understand correctly the main advantage of using |
No, the biggest win for me is ability to pass the whole operation around as a data structure. This means you could create it in one place (having the functions pure, and extremely easy to test), and defer actually running everything until the controller. This allows you to separate business logic - manipulating data, from impure operations such as interfacing with database. |
Oh, you're right. Didn't think about that, but on the other hand, I think if I'm trying to do many things inside one transaction, probably best place to create that operation is controller, anyway. |
Ok, couple of new thoughts/questions: user_changeset = User.changeset(%User{}, params)
log_changeset = Log.changeset(%Log{}, event: "user updated")
multi =
Ecto.Multi.new
|> Ecto.Multi.update(:user, user_changeset)
|> Ecto.Multi.insert(:log, log_changeset)
|> Ecto.Multi.run(:charge_credit_card, fn changes_so_far -> {:ok, _} | {:error _} end)
case Repo.transaction(multi) do
{:ok, %{user: user, log: log}} ->
...
{:error, key_that_errored, %{user: user_changeset}} ->
...
end In this example, can I get reference to user updated in Second question, currently I have this snippet of code in my app: case Repo.transaction fn ->
location_params = game_params["location"] || %{}
case Repo.insert Location.create_changeset(location_params) do
{:ok, location} ->
case Repo.insert Game.create_changeset(game_params, location.id) do
{:ok, game} ->
case Repo.insert Attendance.host_changeset(game.id, conn.assigns.current_user.id) do
{:ok, _attendance} ->
case Repo.update Game.players_count_changeset(game, :inc) do
{:ok, game} -> %{game | location: location}
{:error, changeset} -> Repo.rollback(changeset)
end
{:error, changeset} -> Repo.rollback(changeset)
end
{:error, changeset} -> Repo.rollback(changeset)
end
{:error, changeset} -> Repo.rollback(changeset)
end
end do
{:ok, game} -> conn |> render("show.json", game: game)
{:error, changeset} -> conn |> error(changeset)
end That |
If they are associations, you should use the association support that exists in changesets today. Other than that, you can always use
I think Ecto.Multi could do that automatically for the first entry, yes. |
For example, here: case Repo.insert Location.create_changeset(location_params) do
{:ok, location} ->
case Repo.insert Game.create_changeset(game_params, location.id) do
{:ok, game} ->
# some code
{:error, changeset} -> Repo.rollback(changeset)
end
{:error, changeset} -> Repo.rollback(changeset)
end
Not sure what you mean by first entry, but I want to validate changeset of second operation. Actually it'd be great if |
We are going to support belongs_to in 2.0, which is the same version Milti will land but you could always flip them. There is nothing stopping you from adding the location association to the game changeset and inserting the game changeset. :) |
This is exactly what |
@michalmuskala wow, that's great. I wasn't sure yet how much |
A hearty +1. |
Ecto.Multi is a data structure that stores multiple operations to run inside a transaction. It will also help clean up many cases of
Repo.transaction
as well as help reduce the use of model callbacks.Let's see an example:
All operations in
Ecto.Multi
are tagged and stored in the order they are defined. Duplicated keys should raise. Once built, the underlyingEcto.Multi
struct can be given toRepo.transaction
which will return{:ok, map_of_structs}
or{:error, key_that_errored, map_of_changesets}
wherekey_that_errored
is the first key that failed the operation.API
Ecto.Multi.new()
- returns a newEcto.Multi
structEcto.Multi.insert(multi, key, changeset_or_struct, opts)
- stores an insert to be performed on transactionEcto.Multi.update(multi, key, changeset, opts)
- stores an update to be performed on transactionEcto.Multi.delete(multi, key, changeset_or_struct, opts)
- stores a delete to be performed on transactionEcto.Multi.run(multi, key, fun)
andEcto.Multi.run(multi, key, mod, fun, args)
- stores a function that will be executed if the previous operations succeed. The function will receive a map with the result of all the previous operations (which must have succeeded up to this point). Returns{:ok, value}
or{:error, value}
Ecto.Multi.all|update_all|delete_all(query, opts)
- query based operationsEcto.Multi.to_list(multi)
- converts it to a list in the format[user: {:update, args}, log: {:insert, args}, ...]
Ecto.Multi.prepend|append(multi, other_multi)
- prepends or appends the operations in other multiEcto.Repo.transaction/2
will be changed to acceptEcto.Multi
structs with the semantics specified above. It is worth pointing out that, althoughinsert
anddelete
accept "structs", it always return changesets in{:error, key_that_errored, map_of_changesets}
. The only exceptions are therun/3
andrun/5
functions which will return thevalue
in{:ok, value}
and{:error, value}
, or:skipped
if the operation failed before the function was executed.Implementation
I was thinking Ecto.Multi could be a struct with a list, that keeps the reverse ordering of operations, and a map, which keeps the key-value mapping. It is better than a keyword list because every time we add a new operation we want to check the key is new and that would be faster against a map.
FAQ
A: Why
Ecto.Multi
and not something likeEcto.Transaction
?Because
Repo.transaction/2
is potentially only one of the many functions that may use it. For example, Mongo does not provide transaction support but supports performing multiple operations at once.Acknowledgements
/cc @solnic @MSch @chrismccord @jeregrine
The text was updated successfully, but these errors were encountered: