Create and insert two associated models at the same time

I have a User model

  schema "users" do
    field :email, :string
    field :username, :string
    field :password, :string, virtual: true
    # .. some more fields
    has_one :user_profiles, UserProfile, on_delete: :delete_all
    timestamps()
  end

  def registration_changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:email, :password, :username])
    |> validate_required([:email, :password, :username])
     # .. some more functions
    |> put_assoc(:user_profiles, UserProfile.changeset(%UserProfile{}, params), required: true)

  end

and a UserProfile model

  schema "user_profiles" do
    field :first_name, :string, default: ""
    field :last_name, :string, default: ""
    # ... some more fields
    belongs_to :users, User

    timestamps()
  end

def changeset(struct, params \\ %{}) do
    struct
    |> cast(params, [:first_name, :last_name,
                    # ... rest of the fields
                    ])
    # ... some validators etc
  end

In the registration_changeset/2 function, the params parametter, is a map that has all the fields. So at the end I do get a valid changeset that has the UserProfile changeset as an association. So far the User and UserProfile are not created yet in the database.

When I try

Repo.insert changeset  

I get an exception

** (Postgrex.Error) ERROR (not_null_violation): null value in column “user_id” violates not-null constraint

table: user_profiles
column: user_id

So it seems that Ecto first inserts the User in the database then proceeds to insert the UserProfile. But it does not pass the id of the newly created model to the belongs_to :users, User field.

How can it be that it creates both the user and the profile in one insert?

2 Likes

Reverse your logic. Start with the profile, and put_assoc the user record. The belongs_to will then trigger the save on the User record, returning a user ID that can then be used on the profile record.

Alternatively, store the profile ID on the User, and have it “belongs_to” a User Profile. Then the profile will save first and then the User.

1 Like

Thank you very much. This worked.

You can also check out Ecto.Multi to see if that works for you

3 Likes

This looks very interesting. I will definitely check it out

I like it a lot, it allows me to have a clean User and UserProfile model and that will benefit you a lot when your associations grow.

I’ve also offloaded this type of creation to a service module in web/services/user_service.ex to have an even cleaner setup.

Example:

MyApp.UserService.create_user(email, ...)
2 Likes

Can you please show any simple example for this use case? The module example shows unrelated actions.
How could this work for creating a user object and then use it’s id to use it in the profile relation?

Also in the case that one of the transactions fails, does it roll back all the changes or does it leave the already written records in the database?

1 Like

If using Multi then it is run in a transaction, so if it fails then everything is rolled back.

But in Multi you’d first put in the profile, then in a callback on that insert use the ID it saved to put in the user.

1 Like

Thanks. But now I have two kind of embarrassing questions. I am already searching Github and Google for explanations and use examples but still don’t understand.

  • What is a callback in this case? From the docs example in the line

    |> Multi.update(:account, Account.password_reset_changeset(account, params))
    the callback is the Account.password_reset_changeset/2 function?

Edit: I think the callback part is the function added with the run/3 and run/5 functions. Is this right?

  • What is the name parameter in the functions? I see it is represented with atoms but I don’t understand what it means. My guess is that for functions like insert/4, delete/4 is a model schema while for others it is a function to be run. Is this correct?

You should get this ebook http://pages.plataformatec.com.br/ebook-whats-new-in-ecto-2-0 and read https://github.com/elixir-ecto/ecto/issues/1114

1 Like

From my current project

https://gist.github.com/hl/503aade8f5f454aae7ee1d9f0c983352

1 Like

Thank you. This was exactly what I needed.

That uses Repo.insert inside a Multi.run though, which makes it not able to be looked at via Multi.to_list. We really need options added to Multi.insert/update/etc... that lets you assign information to a changeset based on values in the overall multi state at that time, maybe something like (following your gist):

Multi.new
|> Multi.insert(:shop, Shop.changeset(%Shop{}, params))
|> Multi.insert(:shop_user, %ShopUser{}, changeset: &ShopUser.changeset/2, change: [
  shop_id: {:shop, :id}, user_id: {nil, shop.id}
])
|> Repo.transaction()

WhichShopUserWhere the :changeset opt passed in to insert(/update/blah) defines the changeset function to call (possible defaulting to the %ShopUser.__struct__.changeset function or so?) and the :change opt is passed a keyword list where the keys become the keys of the map passed to the changeset functionand the values are tuples where {id, lookup} id in this case would be a name in the state that is passed in up to this point (:shop in this case since that is the name used to insert the %Shop{}) and the value being an atom to lookup, except when name is nil then the value itself is directly used (like in the above {nil, shop.id} part to put the shop.id directly in).

That way you’d still be able to introspect an Ecto.Multi.

Plus could make a new function like Ecto.insert_changeset as a macro to give a better dsel to it or something.

2 Likes

Thanks for taking a look at it. I agree, options would be better. Using run was the only way I found to make it work.

1 Like

There’s Multi.merge that was supposed to cover that use case, but it’s a bit clunky in use, unfortunately.

Multi.new
|> Multi.insert(:account, Account.changeset(%Account{}, account_params))
|> Multi.update(:show, Show.changeset(show, show_params))
|> Multi.merge(fn changes ->
  Multi.new |> Multi.insert(:log, Log.record(changes))
end)

Multi.merge/2 allows you to return any multi with any operation created based on the “changes so far” it receives as an argument.

Ah, I was wondering its use-case, thought it was just like a generic Multi.prepend/append or so. Ecto.Multi needs a lot more docs and examples. ^.^

I have a lot of code like this:

def insert(params, opts \\ []) when is_map(params) and is_list(opts) do
  name = opts[:name] || :notification
  broadcast_name = opts[:broadcast] || :broadcast
  changeset = MyServer.Notification.changeset(new(), params)

  Multi.new
  |> Multi.insert(name, changeset)
end


def assign_uid(%Multi{} = multi, uid, opts \\ []) when is_integer(uid) and is_list(opts) do
  name = opts[:name] || :notification_user
  broadcast_name = opts[:broadcast] || :broadcast
  notification_name = opts[:notification_name] || :notification

  multi
  |> Multi.run(name, fn state ->
    notification = state[notification_name]
    MyServer.Notification.User.changeset(%MyServer.Notification.User{}, %{uid: uid, notification_id: notification.id})
    |> MyServer.Repo.insert()
  end)
  |> Multi.run(broadcast_name, __MODULE__, :broadcast_notifications_of_uid, ["direct", uid])
  # TODO:  Need to run broadcast_notifications_of_uid outside of the transaction, spawn perhaps?
  # Just need it to be broadcast only when the commit is successful, but whoever calls this may not
  # be handling DB transactions so cannot trust that it will always be done everywhere...
end

Er, I need to fix that TODO sometime… ^.^

A usual use-case of this would be something like:

Notifications.insert(%{title: "Blah", ...}, name: :notif_message)
|> Notifications.assign_uid(person1, name: notif_uid1, notification_name: :notif_message, broadcast_name: :notif_bc1)
|> Notifications.assign_uid(person2, name: notif_uid2, notification_name: :notif_message, broadcast_name: :notif_bc2)
|> Repo.transaction()

Or where the Multi gets handed up and up and up the chain combined with others via Multi.prepend/append, combined with other things until eventually Repo.transaction is called on it.

A side note, I would love to have a Multi.run_after/# that takes the same as Multi.run/# but is only run after everything else possible is run and their return is returned verbatum, no ability to cancel and rollback the operations. I really want to be able to attach, say, a notification broadcast to when I put a notification in the database, but only have it broadcast if the whole transaction went through. This is especially important when the notification itself is only added because it is notifying about something else that was added in the same transaction and that thing does not know anything about notifications, only that it asked for the thing it cares about to be added, so it does not know that it should broadcast notification updates or even to whom nor should it care. Would be even nicer if the after_run was run after the transaction was complete instead of inside of it, but that does not really matter as it can be worked around anyway. :slight_smile:

EDIT: Sadly, Ecto.Multi.merge/2 still obfuscates the operations, thus making Ecto.Multi.to_list fairly useless on it…

For a while there was a Multi.insert/update/delete version on Ecto master that accepted a function with changes so far, but someone removed it :wink: The funny thing is, from my experience in a lot of cases the clunkiness of Ecto.merge forced me to find an alternative solution that was as good or even better, e.g.:

  1. using assocs
  2. using UUIDs and generating them before DB transaction even begins

In both cases we stay out of Multi.run, which makes Multi.to_list useful to inspect changes. But yeah, once in a while it would still be nice to have something like Multi.insert(:b, fn %{a: a} -> ...end)

Not possible in my situation, these are arbitrary links made by other modules, the Notification module knows nothing about them and as such nor does its schema.

Also ditto, the other modules react to the notifications being created, they just need to make a link.

Another aside, would be nice to have a Multi.run_on_failure/# for if a transaction fails too. ^.^

@voger, which Ecto version are you using? You no longer need to reverse since Ecto 2.0 as Ecto takes care of it for you. If using Ecto 1.0 then it is necessary as @dotdotdotPaul explained.

@josevalim I am using Ecto 2.0.5