Hello!
Just want to share my experience with you.
Maybe it will save some hours for somebody.
I have 2 applications in umbrella application:
- Secutity
- Store
Application Security can be used in other projects.
Each application has its own repository, configured to the same database (Store is sharing Security functional):
repo_transaction_umbrella/config/dev.exs:
use Mix.Config
config :security, Security.Repo,
adapter: Ecto.Adapters.Postgres,
username: "postgres",
password: "postgres",
database: "repo_transaction_umbrella_dev",
hostname: "localhost",
pool_size: 10
config :store, Store.Repo,
adapter: Ecto.Adapters.Postgres,
username: "postgres",
password: "postgres",
database: "repo_transaction_umbrella_dev",
hostname: "localhost",
pool_size: 10
repo_transaction_umbrella/apps/security/lib/security/contexts/user.ex:
defmodule Security.User do
alias Security.{Repo, UserSchema}
def add(params) do
changeset = UserSchema.build_changeset(%UserSchema{}, params)
Repo.insert(changeset)
end
end
repo_transaction_umbrella/apps/store/lib/store/contexts/profile.ex:
defmodule Store.Profile do
alias Store.{Repo, ProfileSchema}
def add(params) do
changeset = ProfileSchema.build_changeset(%ProfileSchema{}, params)
Repo.insert(changeset)
end
end
repo_transaction_umbrella/apps/store/lib/store/contexts/customer.ex:
defmodule Store.Customer do
alias Ecto.Multi
alias Store.{Repo, Profile}
alias Security.User
def add(params) when is_map(params) do
multi =
Multi.new
|> Multi.run(:user, fn _ -> User.add(params) end)
|> Multi.run(:profile, fn %{user: user} ->
params
|> Map.put_new("user_id", user.id)
|> Profile.add()
end)
case Repo.transaction(multi) do
{:ok, result} -> {:ok, result}
{:error, :user, changeset, %{}} -> {:error, :user, changeset}
{:error, :profile, changeset, %{}} -> {:error, :profile, changeset}
end
end
end
I try to add new customer with Store.Customer.add/1 and incorrect parameters (name is empty):
iex(1)> params = %{"username" => "user_1", "name" => ""}
%{"name" => "", "username" => "user_1"}
iex(2)> alias Store.Customer
Store.Customer
iex(3)> Customer.add(params)
19:16:00.727 [debug] QUERY OK db=0.3ms
begin []
19:16:00.765 [debug] QUERY OK db=3.6ms
INSERT INTO "security"."users" ("username","inserted_at","updated_at") VALUES ($1,$2,$3) RETURNING "id" ["user_1", {{2017, 1, 27}, {16, 16, 0, 745833}}, {{2017, 1, 27}, {16, 16, 0, 753016}}]
19:16:00.768 [debug] QUERY OK db=0.4ms
rollback []
{:error, :profile,
#Ecto.Changeset<action: :insert, changes: %{user_id: 110},
errors: [name: {"can't be blank", [validation: :required]}],
data: #Store.ProfileSchema<>, valid?: false>}
Looks ok - transaction started, insert ok, then rollback (because second insert failed).
But, in fact, ROLLBACK did nothing with this INSERT.
Reason - separate Repo configuration, separate sessions in PostgreSQL.
Log statement from postgresql.conf helped to figure it out:
repo_transaction_umbrella_dev_(postgres)_[BEGIN]_{00000}_14/6LOG: execute POSTGREX_BEGIN: BEGIN
repo_transaction_umbrella_dev_(postgres)_[INSERT]_{00000}_30/6LOG: execute <unnamed>: INSERT INTO "security"."users" ("username","inserted_at","updated_at") VALUES ($1,$2,$3) RETURNING "id"
repo_transaction_umbrella_dev_(postgres)_[INSERT]_{00000}_30/6DETAIL: parameters: $1 = 'user_1', $2 = '2017-01-27 15:56:00.280901', $3 = '2017-01-27 15:56:00.287823'
repo_transaction_umbrella_dev_(postgres)_[ROLLBACK]_{00000}_14/6LOG: execute POSTGREX_ROLLBACK: ROLLBACK
We started transaction with BEGIN in session 14, then did INSERT in session 30, then ROLLBACK in session 14.
Because Store.Customer.add/1 executes Security.User.add/1 (Security.Repo) and Store.Profile.add/1 (Store.Repo) in transaction from Store.Repo.
The solution is simple - pass the same Repo as you are using for Repo.transaction to functions as parameter if there are functions with different repo configurations in your transaction:
repo_transaction_umbrella/apps/security/lib/security/contexts/user.ex:
defmodule Security.User do
alias Security.{Repo, UserSchema}
def add(params) do
changeset = UserSchema.build_changeset(%UserSchema{}, params)
Repo.insert(changeset)
end
def add(params, repo) do
changeset = UserSchema.build_changeset(%UserSchema{}, params)
repo.insert(changeset)
end
end
repo_transaction_umbrella/apps/store/lib/store/contexts/profile.ex:
defmodule Store.Profile do
alias Store.{Repo, ProfileSchema}
def add(params) do
changeset = ProfileSchema.build_changeset(%ProfileSchema{}, params)
Repo.insert(changeset)
end
def add(params, repo) do
changeset = ProfileSchema.build_changeset(%ProfileSchema{}, params)
repo.insert(changeset)
end
end
repo_transaction_umbrella/apps/store/lib/store/contexts/customer.ex:
defmodule Store.Customer do
alias Ecto.Multi
alias Store.{Repo, Profile}
alias Security.User
def add(params) when is_map(params) do
multi =
Multi.new
|> Multi.run(:user, fn _ -> User.add(params, Repo) end)
|> Multi.run(:profile, fn %{user: user} ->
params
|> Map.put_new("user_id", user.id)
|> Profile.add(Repo)
end)
case Repo.transaction(multi) do
{:ok, result} -> {:ok, result}
{:error, :user, changeset, %{}} -> {:error, :user, changeset}
{:error, :profile, changeset, %{}} -> {:error, :profile, changeset}
end
end
end
I have loaded this test umbrella application to GitHub.