Home > Back-end >  How do I store a record in two tables with ecto?
How do I store a record in two tables with ecto?

Time:10-18

Let's say I have a schema

schema "posts"
  field ...
  field :post_id (UUID4)
  field :revision_id (UUID4)
end

On commit, I want to insert two copies of this data into two tables:

  • UPSERT table(:posts) on :post_id, with :post_id as primary
  • INSERT table(:revisions) on :revision_id as primary

Here's my code for inserting the two records:

  Multi.new()
  |> Multi.insert(:revisions, post)
  |> Multi.insert(:posts, post, on_conflict: :nothing)
  |> Repo.transaction()

My schema is defined on Post.

Currently it seems to try to insert the record twice on the same table (posts) and ignores the revisions table.

How can I force it to write to two separate tables?

CodePudding user response:

Using Ecto.Multi

Here is a "textbook" example of using Ecto.Multi to create 2 related records: a User record and an Email record (this assumes a 1-to-many relationship between users and emails):

alias Ecto.Multi

Multi.new()
|> Multi.insert(:user, User.changeset(%User{}, user_params) end)
|> Multi.insert(:email, fn %{user: %User{id: user_id}} ->
  Email.changeset(%Email{user_id: user_id}, email_params)
end)
|> Repo.transaction()

For your case, you would need to adapt the models to "posts" and "revisions", and you would need to adapt the query(ies) to have an "upsert" functionality. Make sure you are capturing the necessary data from the first table and passing it to the second so they can be properly associated in the database.

Using Repo.transaction

I find that Repo.transaction/1 is more readable and more flexible (you can even use it while operating against external events such an API calls etc). An outline of using a transaction here would look something like this:

  def save_with_revision(params) do
    Repo.transaction fn ->
      with {:ok, revision} <- insert_revision(params) do
           {:ok, post} <- params |> Map.put(:revision_id, revision.id) |> upsert_post()
      do
        {:ok, post}
      else
        {:error, e} -> Repo.rollback(e)
      end
    end
  end
  
  defp insert_revision(params) do
    # ...
    {:ok, revision}
  end

  defp upsert_post(params) do
    # ...
    {:ok, post}
  end

You would need to implement functions to insert the revision and upsert the post, and I am assuming here that your revisions table is uses an id field as its primary key, but this outline should serve as a useful guide. The important thing is that if either operation fails, execution is routed to Repo.rollback/1: the rows go in together successfully or not at all.

  • Related