{"action":"created","issue":{"url":"https://api.github.com/repos/elixir-lang/ecto/issues/1114","labels_url":"https://api.github.com/repos/elixir-lang/ecto/issues/1114/labels{/name}","comments_url":"https://api.github.com/repos/elixir-lang/ecto/issues/1114/comments","events_url":"https://api.github.com/repos/elixir-lang/ecto/issues/1114/events","html_url":"https://github.com/elixir-lang/ecto/issues/1114","id":119835804,"number":1114,"title":"Introducing Ecto.Multi","user":{"login":"josevalim","id":9582,"avatar_url":"https://avatars.githubusercontent.com/u/9582?v=3","gravatar_id":"","url":"https://api.github.com/users/josevalim","html_url":"https://github.com/josevalim","followers_url":"https://api.github.com/users/josevalim/followers","following_url":"https://api.github.com/users/josevalim/following{/other_user}","gists_url":"https://api.github.com/users/josevalim/gists{/gist_id}","starred_url":"https://api.github.com/users/josevalim/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/josevalim/subscriptions","organizations_url":"https://api.github.com/users/josevalim/orgs","repos_url":"https://api.github.com/users/josevalim/repos","events_url":"https://api.github.com/users/josevalim/events{/privacy}","received_events_url":"https://api.github.com/users/josevalim/received_events","type":"User","site_admin":false},"labels":[{"url":"https://api.github.com/repos/elixir-lang/ecto/labels/Kind:Feature","name":"Kind:Feature","color":"009800"},{"url":"https://api.github.com/repos/elixir-lang/ecto/labels/Level:Advanced","name":"Level:Advanced","color":"f7c6c7"}],"state":"open","locked":false,"assignee":null,"milestone":{"url":"https://api.github.com/repos/elixir-lang/ecto/milestones/3","html_url":"https://github.com/elixir-lang/ecto/milestones/v1.1","labels_url":"https://api.github.com/repos/elixir-lang/ecto/milestones/3/labels","id":1433382,"number":3,"title":"v1.1","description":"","creator":{"login":"josevalim","id":9582,"avatar_url":"https://avatars.githubusercontent.com/u/9582?v=3","gravatar_id":"","url":"https://api.github.com/users/josevalim","html_url":"https://github.com/josevalim","followers_url":"https://api.github.com/users/josevalim/followers","following_url":"https://api.github.com/users/josevalim/following{/other_user}","gists_url":"https://api.github.com/users/josevalim/gists{/gist_id}","starred_url":"https://api.github.com/users/josevalim/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/josevalim/subscriptions","organizations_url":"https://api.github.com/users/josevalim/orgs","repos_url":"https://api.github.com/users/josevalim/repos","events_url":"https://api.github.com/users/josevalim/events{/privacy}","received_events_url":"https://api.github.com/users/josevalim/received_events","type":"User","site_admin":false},"open_issues":4,"closed_issues":4,"state":"open","created_at":"2015-11-28T15:49:47Z","updated_at":"2015-12-03T13:00:03Z","due_on":"2015-12-12T23:00:00Z","closed_at":null},"comments":18,"created_at":"2015-12-01T23:53:05Z","updated_at":"2015-12-08T19:09:20Z","closed_at":null,"body":"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.\r\n\r\nLet's see an example:\r\n\r\n```elixir\r\nuser_changeset = User.changeset(%User{}, params)\r\nlog_changeset = Log.changeset(%Log{}, event: \"user updated\")\r\n\r\nmulti =\r\n  Ecto.Multi.new\r\n  |> Ecto.Multi.update(:user, user_changeset)\r\n  |> Ecto.Multi.insert(:log, log_changeset)\r\n  |> Ecto.Multi.run(:charge_credit_card, fn changes_so_far -> {:ok, _} | {:error _} end)\r\n\r\ncase Repo.transaction(multi) do\r\n  {:ok, %{user: user, log: log}} ->\r\n    ...\r\n  {:error, key_that_errored, %{user: user_changeset}} ->\r\n    ...\r\nend\r\n```\r\n\r\nAll operations in `Ecto.Multi` are tagged and stored in the order they are defined. Duplicated keys should raise. Once built, the underlying `Ecto.Multi` struct can be given to `Repo.transaction` which will return `{:ok, map_of_structs}` or `{:error, key_that_errored, map_of_changesets}` where `key_that_errored` is the first key that failed the operation.\r\n\r\n### API\r\n\r\n* `Ecto.Multi.new()` - returns a new `Ecto.Multi` struct\r\n* `Ecto.Multi.insert(multi, key, changeset_or_struct, opts)` - stores an insert to be performed on transaction\r\n* `Ecto.Multi.update(multi, key, changeset, opts)` - stores an update to be performed on transaction\r\n* `Ecto.Multi.delete(multi, key, changeset_or_struct, opts)` - stores a delete to be performed on transaction\r\n* `Ecto.Multi.run(multi, key, fun)` and `Ecto.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}`\r\n* `Ecto.Multi.all|update_all|delete_all(query, opts)` - query based operations\r\n* `Ecto.Multi.to_list(multi)` - converts it to a list in the format `[user: {:update, args}, log: {:insert, args}, ...]`\r\n* `Ecto.Multi.prepend|append(multi, other_multi)` - prepends or appends the operations in other multi\r\n\r\n`Ecto.Repo.transaction/2` will be changed to accept `Ecto.Multi` structs with the semantics specified above. It is worth pointing out that, although `insert` and `delete` accept \"structs\", it always return changesets in `{:error, key_that_errored, map_of_changesets}`. The only exceptions are the `run/3` and `run/5` functions which will return the `value` in `{:ok, value}` and `{:error, value}`, or `:skipped` if the operation failed before the function was executed.\r\n\r\n### Implementation\r\n\r\nI 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.\r\n\r\n### FAQ\r\n\r\n##### A: Why `Ecto.Multi` and not something like `Ecto.Transaction`?\r\n\r\nBecause `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.\r\n\r\n### Acknowledgements\r\n\r\n* @robconery for #1009\r\n* @michalmuskala for https://github.com/michalmuskala/transchange and for pushing towards the idea\r\n\r\n/cc @solnic @msch @chrismccord @jeregrine "},"comment":{"url":"https://api.github.com/repos/elixir-lang/ecto/issues/comments/162984686","html_url":"https://github.com/elixir-lang/ecto/issues/1114#issuecomment-162984686","issue_url":"https://api.github.com/repos/elixir-lang/ecto/issues/1114","id":162984686,"user":{"login":"michalmuskala","id":477062,"avatar_url":"https://avatars.githubusercontent.com/u/477062?v=3","gravatar_id":"","url":"https://api.github.com/users/michalmuskala","html_url":"https://github.com/michalmuskala","followers_url":"https://api.github.com/users/michalmuskala/followers","following_url":"https://api.github.com/users/michalmuskala/following{/other_user}","gists_url":"https://api.github.com/users/michalmuskala/gists{/gist_id}","starred_url":"https://api.github.com/users/michalmuskala/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/michalmuskala/subscriptions","organizations_url":"https://api.github.com/users/michalmuskala/orgs","repos_url":"https://api.github.com/users/michalmuskala/repos","events_url":"https://api.github.com/users/michalmuskala/events{/privacy}","received_events_url":"https://api.github.com/users/michalmuskala/received_events","type":"User","site_admin":false},"created_at":"2015-12-08T19:09:20Z","updated_at":"2015-12-08T19:09:20Z","body":"I think the use case for `run` was coming from @chrismccord. If I recall correctly it was basically about deciding based on some external state to either continue or cancel the transaction.\r\n\r\nThe difference between running a function before call to `Repo.transaction` or after it, and via `Multi.run` would be that the later would be running inside the transaction."}}