multi.ex (29374B)
1 defmodule Ecto.Multi do 2 @moduledoc """ 3 `Ecto.Multi` is a data structure for grouping multiple Repo operations. 4 5 `Ecto.Multi` makes it possible to pack operations that should be 6 performed in a single database transaction and gives a way to introspect 7 the queued operations without actually performing them. Each operation 8 is given a name that is unique and will identify its result in case of 9 success or failure. 10 11 If a multi is valid (i.e. all the changesets in it are valid), 12 all operations will be executed in the order they were added. 13 14 The `Ecto.Multi` structure should be considered opaque. You can use 15 `%Ecto.Multi{}` to pattern match the type, but accessing fields or 16 directly modifying them is not advised. 17 18 `Ecto.Multi.to_list/1` returns a canonical representation of the 19 structure that can be used for introspection. 20 21 ## Changesets 22 23 If multi contains operations that accept changesets (like `insert/4`, 24 `update/4` or `delete/4`) they will be checked before starting the 25 transaction. If any changeset has errors, the transaction won't even 26 be started and the error will be immediately returned. 27 28 Note: `insert/4`, `update/4`, `insert_or_update/4`, and `delete/4` 29 variants that accept a function are not performing such checks since 30 the functions are executed after the transaction has started. 31 32 ## Run 33 34 Multi allows you to run arbitrary functions as part of your transaction 35 via `run/3` and `run/5`. This is especially useful when an operation 36 depends on the value of a previous operation. For this reason, the 37 function given as a callback to `run/3` and `run/5` will receive the repo 38 as the first argument, and all changes performed by the multi so far as a 39 map for the second argument. 40 41 The function given to `run` must return `{:ok, value}` or `{:error, value}` 42 as its result. Returning an error will abort any further operations 43 and make the whole multi fail. 44 45 ## Example 46 47 Let's look at an example definition and usage. The use case we'll be 48 looking into is resetting a password. We need to update the account 49 with proper information, log the request and remove all current sessions: 50 51 defmodule PasswordManager do 52 alias Ecto.Multi 53 54 def reset(account, params) do 55 Multi.new() 56 |> Multi.update(:account, Account.password_reset_changeset(account, params)) 57 |> Multi.insert(:log, Log.password_reset_changeset(account, params)) 58 |> Multi.delete_all(:sessions, Ecto.assoc(account, :sessions)) 59 end 60 end 61 62 We can later execute it in the integration layer using Repo: 63 64 Repo.transaction(PasswordManager.reset(account, params)) 65 66 By pattern matching on the result we can differentiate different conditions: 67 68 case result do 69 {:ok, %{account: account, log: log, sessions: sessions}} -> 70 # Operation was successful, we can access results (exactly the same 71 # we would get from running corresponding Repo functions) under keys 72 # we used for naming the operations. 73 {:error, failed_operation, failed_value, changes_so_far} -> 74 # One of the operations failed. We can access the operation's failure 75 # value (like changeset for operations on changesets) to prepare a 76 # proper response. We also get access to the results of any operations 77 # that succeeded before the indicated operation failed. However, any 78 # successful operations would have been rolled back. 79 end 80 81 We can also easily unit test our transaction without actually running it. 82 Since changesets can use in-memory-data, we can use an account that is 83 constructed in memory as well (without persisting it to the database): 84 85 test "dry run password reset" do 86 account = %Account{password: "letmein"} 87 multi = PasswordManager.reset(account, params) 88 89 assert [ 90 {:account, {:update, account_changeset, []}}, 91 {:log, {:insert, log_changeset, []}}, 92 {:sessions, {:delete_all, query, []}} 93 ] = Ecto.Multi.to_list(multi) 94 95 # We can introspect changesets and query to see if everything 96 # is as expected, for example: 97 assert account_changeset.valid? 98 assert log_changeset.valid? 99 assert inspect(query) == "#Ecto.Query<from a in Session>" 100 end 101 102 The name of each operation does not have to be an atom. This can be particularly 103 useful when you wish to update a collection of changesets at once, and track their 104 errors individually: 105 106 accounts = [%Account{id: 1}, %Account{id: 2}] 107 108 Enum.reduce(accounts, Multi.new(), fn account, multi -> 109 Multi.update( 110 multi, 111 {:account, account.id}, 112 Account.password_reset_changeset(account, params) 113 ) 114 end) 115 """ 116 117 alias __MODULE__ 118 alias Ecto.Changeset 119 120 defstruct operations: [], names: MapSet.new() 121 122 @type changes :: map 123 @type run :: ((Ecto.Repo.t, changes) -> {:ok | :error, any}) | {module, atom, [any]} 124 @type fun(result) :: (changes -> result) 125 @type merge :: (changes -> t) | {module, atom, [any]} 126 @typep schema_or_source :: binary | {binary, module} | module 127 @typep operation :: {:changeset, Changeset.t, Keyword.t} | 128 {:run, run} | 129 {:put, any} | 130 {:inspect, Keyword.t} | 131 {:merge, merge} | 132 {:update_all, Ecto.Query.t, Keyword.t} | 133 {:delete_all, Ecto.Query.t, Keyword.t} | 134 {:insert_all, schema_or_source, [map | Keyword.t], Keyword.t} 135 @typep operations :: [{name, operation}] 136 @typep names :: MapSet.t 137 @type name :: any 138 @type t :: %__MODULE__{operations: operations, names: names} 139 140 @doc """ 141 Returns an empty `Ecto.Multi` struct. 142 143 ## Example 144 145 iex> Ecto.Multi.new() |> Ecto.Multi.to_list() 146 [] 147 148 """ 149 @spec new :: t 150 def new do 151 %Multi{} 152 end 153 154 @doc """ 155 Appends the second multi to the first one. 156 157 All names must be unique between both structures. 158 159 ## Example 160 161 iex> lhs = Ecto.Multi.new() |> Ecto.Multi.run(:left, fn _, changes -> {:ok, changes} end) 162 iex> rhs = Ecto.Multi.new() |> Ecto.Multi.run(:right, fn _, changes -> {:error, changes} end) 163 iex> Ecto.Multi.append(lhs, rhs) |> Ecto.Multi.to_list |> Keyword.keys 164 [:left, :right] 165 166 """ 167 @spec append(t, t) :: t 168 def append(lhs, rhs) do 169 merge_structs(lhs, rhs, &(&2 ++ &1)) 170 end 171 172 @doc """ 173 Prepends the second multi to the first one. 174 175 All names must be unique between both structures. 176 177 ## Example 178 179 iex> lhs = Ecto.Multi.new() |> Ecto.Multi.run(:left, fn _, changes -> {:ok, changes} end) 180 iex> rhs = Ecto.Multi.new() |> Ecto.Multi.run(:right, fn _, changes -> {:error, changes} end) 181 iex> Ecto.Multi.prepend(lhs, rhs) |> Ecto.Multi.to_list |> Keyword.keys 182 [:right, :left] 183 184 """ 185 @spec prepend(t, t) :: t 186 def prepend(lhs, rhs) do 187 merge_structs(lhs, rhs, &(&1 ++ &2)) 188 end 189 190 defp merge_structs(%Multi{} = lhs, %Multi{} = rhs, joiner) do 191 %{names: lhs_names, operations: lhs_ops} = lhs 192 %{names: rhs_names, operations: rhs_ops} = rhs 193 case MapSet.intersection(lhs_names, rhs_names) |> MapSet.to_list do 194 [] -> 195 %Multi{names: MapSet.union(lhs_names, rhs_names), 196 operations: joiner.(lhs_ops, rhs_ops)} 197 common -> 198 raise ArgumentError, """ 199 error when merging the following Ecto.Multi structs: 200 201 #{Kernel.inspect lhs} 202 203 #{Kernel.inspect rhs} 204 205 both declared operations: #{Kernel.inspect common} 206 """ 207 end 208 end 209 210 @doc """ 211 Merges a multi returned dynamically by an anonymous function. 212 213 This function is useful when the multi to be merged requires information 214 from the original multi. Hence the second argument is an anonymous function 215 that receives the multi changes so far. The anonymous function must return 216 another multi. 217 218 If you would prefer to simply merge two multis together, see `append/2` or 219 `prepend/2`. 220 221 Duplicated operations are not allowed. 222 223 ## Example 224 225 multi = 226 Ecto.Multi.new() 227 |> Ecto.Multi.insert(:post, %Post{title: "first"}) 228 229 multi 230 |> Ecto.Multi.merge(fn %{post: post} -> 231 Ecto.Multi.new() 232 |> Ecto.Multi.insert(:comment, Ecto.build_assoc(post, :comments)) 233 end) 234 |> MyApp.Repo.transaction() 235 """ 236 @spec merge(t, (changes -> t)) :: t 237 def merge(%Multi{} = multi, merge) when is_function(merge, 1) do 238 Map.update!(multi, :operations, &[{:merge, {:merge, merge}} | &1]) 239 end 240 241 @doc """ 242 Merges a multi returned dynamically by calling `module` and `function` with `args`. 243 244 Similar to `merge/2`, but allows to pass module name, function and arguments. 245 The function should return an `Ecto.Multi`, and receives changes so far 246 as the first argument (prepended to those passed in the call to the function). 247 248 Duplicated operations are not allowed. 249 """ 250 @spec merge(t, module, function, args) :: t when function: atom, args: [any] 251 def merge(%Multi{} = multi, mod, fun, args) 252 when is_atom(mod) and is_atom(fun) and is_list(args) do 253 Map.update!(multi, :operations, &[{:merge, {:merge, {mod, fun, args}}} | &1]) 254 end 255 256 @doc """ 257 Adds an insert operation to the multi. 258 259 Accepts the same arguments and options as `c:Ecto.Repo.insert/2` does. 260 261 ## Example 262 263 Ecto.Multi.new() 264 |> Ecto.Multi.insert(:insert, %Post{title: "first"}) 265 |> MyApp.Repo.transaction() 266 267 Ecto.Multi.new() 268 |> Ecto.Multi.insert(:post, %Post{title: "first"}) 269 |> Ecto.Multi.insert(:comment, fn %{post: post} -> 270 Ecto.build_assoc(post, :comments) 271 end) 272 |> MyApp.Repo.transaction() 273 274 """ 275 @spec insert(t, name, Changeset.t | Ecto.Schema.t | fun(Changeset.t | Ecto.Schema.t), Keyword.t) :: t 276 def insert(multi, name, changeset_or_struct_or_fun, opts \\ []) 277 278 def insert(multi, name, %Changeset{} = changeset, opts) do 279 add_changeset(multi, :insert, name, changeset, opts) 280 end 281 282 def insert(multi, name, %_{} = struct, opts) do 283 insert(multi, name, Changeset.change(struct), opts) 284 end 285 286 def insert(multi, name, fun, opts) when is_function(fun, 1) do 287 run(multi, name, operation_fun({:insert, fun}, opts)) 288 end 289 290 @doc """ 291 Adds an update operation to the multi. 292 293 Accepts the same arguments and options as `c:Ecto.Repo.update/2` does. 294 295 ## Example 296 297 post = MyApp.Repo.get!(Post, 1) 298 changeset = Ecto.Changeset.change(post, title: "New title") 299 Ecto.Multi.new() 300 |> Ecto.Multi.update(:update, changeset) 301 |> MyApp.Repo.transaction() 302 303 Ecto.Multi.new() 304 |> Ecto.Multi.insert(:post, %Post{title: "first"}) 305 |> Ecto.Multi.update(:fun, fn %{post: post} -> 306 Ecto.Changeset.change(post, title: "New title") 307 end) 308 |> MyApp.Repo.transaction() 309 310 """ 311 @spec update(t, name, Changeset.t | fun(Changeset.t), Keyword.t) :: t 312 def update(multi, name, changeset_or_fun, opts \\ []) 313 314 def update(multi, name, %Changeset{} = changeset, opts) do 315 add_changeset(multi, :update, name, changeset, opts) 316 end 317 318 def update(multi, name, fun, opts) when is_function(fun, 1) do 319 run(multi, name, operation_fun({:update, fun}, opts)) 320 end 321 322 @doc """ 323 Inserts or updates a changeset depending on whether the changeset was persisted or not. 324 325 Accepts the same arguments and options as `c:Ecto.Repo.insert_or_update/2` does. 326 327 ## Example 328 329 changeset = Post.changeset(%Post{}, %{title: "New title"}) 330 Ecto.Multi.new() 331 |> Ecto.Multi.insert_or_update(:insert_or_update, changeset) 332 |> MyApp.Repo.transaction() 333 334 Ecto.Multi.new() 335 |> Ecto.Multi.run(:post, fn repo, _changes -> 336 {:ok, repo.get(Post, 1) || %Post{}} 337 end) 338 |> Ecto.Multi.insert_or_update(:update, fn %{post: post} -> 339 Ecto.Changeset.change(post, title: "New title") 340 end) 341 |> MyApp.Repo.transaction() 342 343 """ 344 @spec insert_or_update(t, name, Changeset.t | fun(Changeset.t), Keyword.t) :: t 345 def insert_or_update(multi, name, changeset_or_fun, opts \\ []) 346 347 def insert_or_update(multi, name, %Changeset{data: %{__meta__: %{state: :loaded}}} = changeset, opts) do 348 add_changeset(multi, :update, name, changeset, opts) 349 end 350 351 def insert_or_update(multi, name, %Changeset{} = changeset, opts) do 352 add_changeset(multi, :insert, name, changeset, opts) 353 end 354 355 def insert_or_update(multi, name, fun, opts) when is_function(fun, 1) do 356 run(multi, name, operation_fun({:insert_or_update, fun}, opts)) 357 end 358 359 @doc """ 360 Adds a delete operation to the multi. 361 362 Accepts the same arguments and options as `c:Ecto.Repo.delete/2` does. 363 364 ## Example 365 366 post = MyApp.Repo.get!(Post, 1) 367 Ecto.Multi.new() 368 |> Ecto.Multi.delete(:delete, post) 369 |> MyApp.Repo.transaction() 370 371 Ecto.Multi.new() 372 |> Ecto.Multi.run(:post, fn repo, _changes -> 373 case repo.get(Post, 1) do 374 nil -> {:error, :not_found} 375 post -> {:ok, post} 376 end 377 end) 378 |> Ecto.Multi.delete(:delete, fn %{post: post} -> 379 # Others validations 380 post 381 end) 382 |> MyApp.Repo.transaction() 383 384 """ 385 @spec delete(t, name, Changeset.t | Ecto.Schema.t | fun(Changeset.t | Ecto.Schema.t), Keyword.t) :: t 386 def delete(multi, name, changeset_or_struct_fun, opts \\ []) 387 388 def delete(multi, name, %Changeset{} = changeset, opts) do 389 add_changeset(multi, :delete, name, changeset, opts) 390 end 391 392 def delete(multi, name, %_{} = struct, opts) do 393 delete(multi, name, Changeset.change(struct), opts) 394 end 395 396 def delete(multi, name, fun, opts) when is_function(fun, 1) do 397 run(multi, name, operation_fun({:delete, fun}, opts)) 398 end 399 400 @doc """ 401 Runs a query expecting one result and stores it in the multi. 402 403 Accepts the same arguments and options as `c:Ecto.Repo.one/2`. 404 405 ## Example 406 407 Ecto.Multi.new() 408 |> Ecto.Multi.one(:post, Post) 409 |> Ecto.Multi.one(:author, fn %{post: post} -> 410 from(a in Author, where: a.id == ^post.author_id) 411 end) 412 |> MyApp.Repo.transaction() 413 """ 414 @spec one( 415 t, 416 name, 417 queryable :: Ecto.Queryable.t | (any -> Ecto.Queryable.t), 418 opts :: Keyword.t 419 ) :: t 420 def one(multi, name, queryable_or_fun, opts \\ []) 421 422 def one(multi, name, fun, opts) when is_function(fun, 1) do 423 run(multi, name, operation_fun({:one, fun}, opts)) 424 end 425 426 def one(multi, name, queryable, opts) do 427 run(multi, name, operation_fun({:one, fn _ -> queryable end}, opts)) 428 end 429 430 @doc """ 431 Runs a query and stores all entries in the multi. 432 433 Accepts the same arguments and options as `c:Ecto.Repo.all/2` does. 434 435 ## Example 436 437 Ecto.Multi.new() 438 |> Ecto.Multi.all(:all, Post) 439 |> MyApp.Repo.transaction() 440 441 Ecto.Multi.new() 442 |> Ecto.Multi.all(:all, fn _changes -> Post end) 443 |> MyApp.Repo.transaction() 444 """ 445 @spec all( 446 t, 447 name, 448 queryable :: Ecto.Queryable.t | (any -> Ecto.Queryable.t), 449 opts :: Keyword.t 450 ) :: t 451 def all(multi, name, queryable_or_fun, opts \\ []) 452 453 def all(multi, name, fun, opts) when is_function(fun, 1) do 454 run(multi, name, operation_fun({:all, fun}, opts)) 455 end 456 457 def all(multi, name, queryable, opts) do 458 run(multi, name, operation_fun({:all, fn _ -> queryable end}, opts)) 459 end 460 461 defp add_changeset(multi, action, name, changeset, opts) when is_list(opts) do 462 add_operation(multi, name, {:changeset, put_action(changeset, action), opts}) 463 end 464 465 defp put_action(%{action: nil} = changeset, action) do 466 %{changeset | action: action} 467 end 468 469 defp put_action(%{action: action} = changeset, action) do 470 changeset 471 end 472 473 defp put_action(%{action: original}, action) do 474 raise ArgumentError, "you provided a changeset with an action already set " <> 475 "to #{Kernel.inspect original} when trying to #{action} it" 476 end 477 478 @doc """ 479 Causes the multi to fail with the given value. 480 481 Running the multi in a transaction will execute 482 no previous steps and returns the value of the first 483 error added. 484 """ 485 @spec error(t, name, error :: term) :: t 486 def error(multi, name, value) do 487 add_operation(multi, name, {:error, value}) 488 end 489 490 @doc """ 491 Adds a function to run as part of the multi. 492 493 The function should return either `{:ok, value}` or `{:error, value}`, 494 and receives the repo as the first argument, and the changes so far 495 as the second argument. 496 497 ## Example 498 499 Ecto.Multi.run(multi, :write, fn _repo, %{image: image} -> 500 with :ok <- File.write(image.name, image.contents) do 501 {:ok, nil} 502 end 503 end) 504 """ 505 @spec run(t, name, run) :: t 506 def run(multi, name, run) when is_function(run, 2) do 507 add_operation(multi, name, {:run, run}) 508 end 509 510 @doc """ 511 Adds a function to run as part of the multi. 512 513 Similar to `run/3`, but allows to pass module name, function and arguments. 514 The function should return either `{:ok, value}` or `{:error, value}`, and 515 receives the repo as the first argument, and the changes so far as the 516 second argument (prepended to those passed in the call to the function). 517 """ 518 @spec run(t, name, module, function, args) :: t when function: atom, args: [any] 519 def run(multi, name, mod, fun, args) 520 when is_atom(mod) and is_atom(fun) and is_list(args) do 521 add_operation(multi, name, {:run, {mod, fun, args}}) 522 end 523 524 @doc """ 525 Adds an insert_all operation to the multi. 526 527 Accepts the same arguments and options as `c:Ecto.Repo.insert_all/3` does. 528 529 ## Example 530 531 posts = [%{title: "My first post"}, %{title: "My second post"}] 532 Ecto.Multi.new() 533 |> Ecto.Multi.insert_all(:insert_all, Post, posts) 534 |> MyApp.Repo.transaction() 535 536 Ecto.Multi.new() 537 |> Ecto.Multi.run(:post, fn repo, _changes -> 538 case repo.get(Post, 1) do 539 nil -> {:error, :not_found} 540 post -> {:ok, post} 541 end 542 end) 543 |> Ecto.Multi.insert_all(:insert_all, Comment, fn %{post: post} -> 544 # Others validations 545 546 entries 547 |> Enum.map(fn comment -> 548 Map.put(comment, :post_id, post.id) 549 end) 550 end) 551 |> MyApp.Repo.transaction() 552 553 """ 554 @spec insert_all( 555 t, 556 name, 557 schema_or_source, 558 entries_or_query_or_fun :: [map | Keyword.t()] | fun([map | Keyword.t()]) | Ecto.Query.t(), 559 Keyword.t() 560 ) :: t 561 def insert_all(multi, name, schema_or_source, entries_or_query_or_fun, opts \\ []) 562 563 def insert_all(multi, name, schema_or_source, entries_fun, opts) 564 when is_function(entries_fun, 1) and is_list(opts) do 565 run(multi, name, operation_fun({:insert_all, schema_or_source, entries_fun}, opts)) 566 end 567 568 def insert_all(multi, name, schema_or_source, entries_or_query, opts) when is_list(opts) do 569 add_operation(multi, name, {:insert_all, schema_or_source, entries_or_query, opts}) 570 end 571 572 @doc """ 573 Adds an update_all operation to the multi. 574 575 Accepts the same arguments and options as `c:Ecto.Repo.update_all/3` does. 576 577 ## Example 578 579 Ecto.Multi.new() 580 |> Ecto.Multi.update_all(:update_all, Post, set: [title: "New title"]) 581 |> MyApp.Repo.transaction() 582 583 Ecto.Multi.new() 584 |> Ecto.Multi.run(:post, fn repo, _changes -> 585 case repo.get(Post, 1) do 586 nil -> {:error, :not_found} 587 post -> {:ok, post} 588 end 589 end) 590 |> Ecto.Multi.update_all(:update_all, fn %{post: post} -> 591 # Others validations 592 from(c in Comment, where: c.post_id == ^post.id, update: [set: [title: "New title"]]) 593 end, []) 594 |> MyApp.Repo.transaction() 595 596 """ 597 @spec update_all(t, name, Ecto.Queryable.t | fun(Ecto.Queryable.t), Keyword.t, Keyword.t) :: t 598 def update_all(multi, name, queryable_or_fun, updates, opts \\ []) 599 600 def update_all(multi, name, queryable_fun, updates, opts) when is_function(queryable_fun, 1) and is_list(opts) do 601 run(multi, name, operation_fun({:update_all, queryable_fun, updates}, opts)) 602 end 603 604 def update_all(multi, name, queryable, updates, opts) when is_list(opts) do 605 query = Ecto.Queryable.to_query(queryable) 606 add_operation(multi, name, {:update_all, query, updates, opts}) 607 end 608 609 @doc """ 610 Adds a delete_all operation to the multi. 611 612 Accepts the same arguments and options as `c:Ecto.Repo.delete_all/2` does. 613 614 ## Example 615 616 queryable = from(p in Post, where: p.id < 5) 617 Ecto.Multi.new() 618 |> Ecto.Multi.delete_all(:delete_all, queryable) 619 |> MyApp.Repo.transaction() 620 621 Ecto.Multi.new() 622 |> Ecto.Multi.run(:post, fn repo, _changes -> 623 case repo.get(Post, 1) do 624 nil -> {:error, :not_found} 625 post -> {:ok, post} 626 end 627 end) 628 |> Ecto.Multi.delete_all(:delete_all, fn %{post: post} -> 629 # Others validations 630 from(c in Comment, where: c.post_id == ^post.id) 631 end) 632 |> MyApp.Repo.transaction() 633 634 """ 635 @spec delete_all(t, name, Ecto.Queryable.t | fun(Ecto.Queryable.t), Keyword.t) :: t 636 def delete_all(multi, name, queryable_or_fun, opts \\ []) 637 638 def delete_all(multi, name, fun, opts) when is_function(fun, 1) and is_list(opts) do 639 run(multi, name, operation_fun({:delete_all, fun}, opts)) 640 end 641 642 def delete_all(multi, name, queryable, opts) when is_list(opts) do 643 query = Ecto.Queryable.to_query(queryable) 644 add_operation(multi, name, {:delete_all, query, opts}) 645 end 646 647 defp add_operation(%Multi{} = multi, name, operation) do 648 %{operations: operations, names: names} = multi 649 if MapSet.member?(names, name) do 650 raise "#{Kernel.inspect name} is already a member of the Ecto.Multi: \n#{Kernel.inspect multi}" 651 else 652 %{multi | operations: [{name, operation} | operations], 653 names: MapSet.put(names, name)} 654 end 655 end 656 657 @doc """ 658 Returns the list of operations stored in `multi`. 659 660 Always use this function when you need to access the operations you 661 have defined in `Ecto.Multi`. Inspecting the `Ecto.Multi` struct internals 662 directly is discouraged. 663 """ 664 @spec to_list(t) :: [{name, term}] 665 def to_list(%Multi{operations: operations}) do 666 operations 667 |> Enum.reverse 668 |> Enum.map(&format_operation/1) 669 end 670 671 defp format_operation({name, {:changeset, changeset, opts}}), 672 do: {name, {changeset.action, changeset, opts}} 673 defp format_operation(other), 674 do: other 675 676 @doc """ 677 Adds a value to the changes so far under the given name. 678 679 The given `value` is added to the multi before the transaction starts. 680 If you would like to run arbitrary functions as part of your transaction, 681 see `run/3` or `run/5`. 682 683 ## Example 684 685 Imagine there is an existing company schema that you retrieved from 686 the database. You can insert it as a change in the multi using `put/3`: 687 688 Ecto.Multi.new() 689 |> Ecto.Multi.put(:company, company) 690 |> Ecto.Multi.insert(:user, fn changes -> User.changeset(changes.company) end) 691 |> Ecto.Multi.insert(:person, fn changes -> Person.changeset(changes.user, changes.company) end) 692 |> MyApp.Repo.transaction() 693 694 In the example above there isn't a large benefit in putting the 695 `company` in the multi, because you could also access the 696 `company` variable directly inside the anonymous function. 697 698 However, the benefit of `put/3` is when composing `Ecto.Multi`s. 699 If the insert operations above were defined in another module, 700 you could use `put(:company, company)` to inject changes that 701 will be accessed by other functions down the chain, removing 702 the need to pass both `multi` and `company` values around. 703 """ 704 @spec put(t, name, any) :: t 705 def put(multi, name, value) do 706 add_operation(multi, name, {:put, value}) 707 end 708 709 @doc """ 710 Inspects results from a Multi 711 712 By default, the name is shown as a label to the inspect, custom labels are 713 supported through the `IO.inspect/2` `label` option. 714 715 ## Options 716 717 All options for IO.inspect/2 are supported, it also support the following ones: 718 719 * `:only` - A field or a list of fields to inspect, will print the entire 720 map by default. 721 722 ## Examples 723 724 Ecto.Multi.new() 725 |> Ecto.Multi.insert(:person_a, changeset) 726 |> Ecto.Multi.insert(:person_b, changeset) 727 |> Ecto.Multi.inspect() 728 |> MyApp.Repo.transaction() 729 730 Prints: 731 %{person_a: %Person{...}, person_b: %Person{...}} 732 733 We can use the `:only` option to limit which fields will be printed: 734 735 Ecto.Multi.new() 736 |> Ecto.Multi.insert(:person_a, changeset) 737 |> Ecto.Multi.insert(:person_b, changeset) 738 |> Ecto.Multi.inspect(only: :person_a) 739 |> MyApp.Repo.transaction() 740 741 Prints: 742 %{person_a: %Person{...}} 743 744 """ 745 @spec inspect(t, Keyword.t) :: t 746 def inspect(multi, opts \\ []) do 747 Map.update!(multi, :operations, &[{:inspect, {:inspect, opts}} | &1]) 748 end 749 750 @doc false 751 @spec __apply__(t, Ecto.Repo.t, fun, (term -> no_return)) :: {:ok, term} | {:error, term} 752 def __apply__(%Multi{} = multi, repo, wrap, return) do 753 operations = Enum.reverse(multi.operations) 754 755 with {:ok, operations} <- check_operations_valid(operations) do 756 apply_operations(operations, multi.names, repo, wrap, return) 757 end 758 end 759 760 defp check_operations_valid(operations) do 761 Enum.find_value(operations, &invalid_operation/1) || {:ok, operations} 762 end 763 764 defp invalid_operation({name, {:changeset, %{valid?: false} = changeset, _}}), 765 do: {:error, {name, changeset, %{}}} 766 defp invalid_operation({name, {:error, value}}), 767 do: {:error, {name, value, %{}}} 768 defp invalid_operation(_operation), 769 do: nil 770 771 defp apply_operations([], _names, _repo, _wrap, _return), do: {:ok, %{}} 772 defp apply_operations(operations, names, repo, wrap, return) do 773 wrap.(fn -> 774 operations 775 |> Enum.reduce({%{}, names}, &apply_operation(&1, repo, wrap, return, &2)) 776 |> elem(0) 777 end) 778 end 779 780 defp apply_operation({_, {:merge, merge}}, repo, wrap, return, {acc, names}) do 781 case __apply__(apply_merge_fun(merge, acc), repo, wrap, return) do 782 {:ok, value} -> 783 merge_results(acc, value, names) 784 {:error, {name, value, nested_acc}} -> 785 {acc, _names} = merge_results(acc, nested_acc, names) 786 return.({name, value, acc}) 787 end 788 end 789 790 defp apply_operation({_name, {:inspect, opts}}, _repo, _wrap_, _return, {acc, names}) do 791 if opts[:only] do 792 acc |> Map.take(List.wrap(opts[:only])) |> IO.inspect(opts) 793 else 794 IO.inspect(acc, opts) 795 end 796 797 {acc, names} 798 end 799 800 defp apply_operation({name, operation}, repo, wrap, return, {acc, names}) do 801 case apply_operation(operation, acc, {wrap, return}, repo) do 802 {:ok, value} -> 803 {Map.put(acc, name, value), names} 804 {:error, value} -> 805 return.({name, value, acc}) 806 other -> 807 raise "expected Ecto.Multi callback named `#{Kernel.inspect name}` to return either {:ok, value} or {:error, value}, got: #{Kernel.inspect other}" 808 end 809 end 810 811 defp apply_operation({:changeset, changeset, opts}, _acc, _apply_args, repo), 812 do: apply(repo, changeset.action, [changeset, opts]) 813 defp apply_operation({:run, run}, acc, _apply_args, repo), 814 do: apply_run_fun(run, repo, acc) 815 defp apply_operation({:error, value}, _acc, _apply_args, _repo), 816 do: {:error, value} 817 defp apply_operation({:insert_all, source, entries, opts}, _acc, _apply_args, repo), 818 do: {:ok, repo.insert_all(source, entries, opts)} 819 defp apply_operation({:update_all, query, updates, opts}, _acc, _apply_args, repo), 820 do: {:ok, repo.update_all(query, updates, opts)} 821 defp apply_operation({:delete_all, query, opts}, _acc, _apply_args, repo), 822 do: {:ok, repo.delete_all(query, opts)} 823 defp apply_operation({:put, value}, _acc, _apply_args, _repo), 824 do: {:ok, value} 825 826 defp apply_merge_fun({mod, fun, args}, acc), do: apply(mod, fun, [acc | args]) 827 defp apply_merge_fun(fun, acc), do: apply(fun, [acc]) 828 829 defp apply_run_fun({mod, fun, args}, repo, acc), do: apply(mod, fun, [repo, acc | args]) 830 defp apply_run_fun(fun, repo, acc), do: apply(fun, [repo, acc]) 831 832 defp merge_results(changes, new_changes, names) do 833 new_names = new_changes |> Map.keys |> MapSet.new() 834 case MapSet.intersection(names, new_names) |> MapSet.to_list do 835 [] -> 836 {Map.merge(changes, new_changes), MapSet.union(names, new_names)} 837 common -> 838 raise "cannot merge multi, the following operations were found in " <> 839 "both Ecto.Multi: #{Kernel.inspect common}" 840 end 841 end 842 843 defp operation_fun({:update_all, queryable_fun, updates}, opts) do 844 fn repo, changes -> 845 {:ok, repo.update_all(queryable_fun.(changes), updates, opts)} 846 end 847 end 848 849 defp operation_fun({:insert_all, schema_or_source, entries_fun}, opts) do 850 fn repo, changes -> 851 {:ok, repo.insert_all(schema_or_source, entries_fun.(changes), opts)} 852 end 853 end 854 855 defp operation_fun({:delete_all, fun}, opts) do 856 fn repo, changes -> 857 {:ok, repo.delete_all(fun.(changes), opts)} 858 end 859 end 860 861 defp operation_fun({:one, fun}, opts) do 862 fn repo, changes -> 863 {:ok, repo.one(fun.(changes), opts)} 864 end 865 end 866 867 defp operation_fun({:all, fun}, opts) do 868 fn repo, changes -> 869 {:ok, repo.all(fun.(changes), opts)} 870 end 871 end 872 873 defp operation_fun({operation, fun}, opts) do 874 fn repo, changes -> 875 apply(repo, operation, [fun.(changes), opts]) 876 end 877 end 878 end