There are times when we need to do multiple operations at once on various database tables. For example, when creating a new entity, such as a user
,
we might want to also insert a related property in some other table, such as a table that holds the preferences of a user.
So in this scenario we’d have 2 tables: users
and user_preferences
. The user creation would look as follows:
def create_user(attrs) do
# We'd first create the changesets for the user and the preferences,
# then we can simply do multiple insertions.
# Repo.transaction ensures that, if either operation fails,
# nothing crashes and we receive a descriptive error back.
...
Ecto.Multi.new()
|> Ecto.Multi.insert(:users, user_changeset)
|> Ecto.Multi.insert(:user_preferences, preferences_changeset)
|> Repo.transaction()
end
If a simple insertion is not possible for whatever reason (in this case, we need the freshly created user’s id in order to create the user’s preferences), Ecto.Multi
offers the possibility to run your own set of commands:
def create_user(attrs) do
user_changeset =
%User{}
|> User.changeset(attrs)
Ecto.Multi.new()
|> Ecto.Multi.insert(:users, user_changeset)
|> Ecto.Multi.run(:user_preferences, fn _repo, %{user: user} ->
# building the preferences changeset based off certain attributes
user_preferences_attrs =
%{
:user_id => user.id,
:is_premium_user => attrs["premium"] && true || false,
:email_notifications => attrs["email_notifications_on"] && true || false,
...
}
# calling a creation function defined somewhere else
UserPreferences.create_user_preferences(user, user_preferences_attrs)
end)
|> Repo.transaction()
end
If we were to not have a creation function for the user preferences already defined, we could instead do:
...
|> Ecto.Multi.run(:user_preferences, fn _repo, %{user: user} ->
%UserPreferences{}
|> UserPreferences.changeset(
%{
:user_id => user.id,
:is_premium_user => attrs["premium"] && true || false,
:email_notifications => attrs["email_notifications_on"] && true || false,
...
}
)
|> Repo.insert()
end)
...
The successful return is:
{
:ok,
%{
user: %User{},
user_preferences: %UserPreference{}
}
}
An error on either of the operations would return a structure having as last object a map holding the successful result of the previous operations prior to the failing one:
{:error, :user, %Ecto.Changeset{}, %{}}
{:error, :user_preference, %Ecto.Changeset{}, %{user: %User{}}}
The error is returned right after the failure of an operation, and the operations that follow won’t run in such a case.
Developer. React-Native worshipper. Please don't ask me about tests 😊