rsmml

rsmml

Ecto.Multi - Loop over list of changesets

Hello there, dear community!

I have been dealing the last days with a function that save multiple records at once using Ecto.Multi. At the moment on my code I have 2 functions that successfully works (yeey :tada:) using Multi too.
The logic of this function is to take the Attributes comming after submiting a form. The aim of this form is to allow users to Sign-up for a Sports Club: This form could send:
A - The data to insert 1 new user. (Already working :white_check_mark:).
B - The data to insert 2 new users, a minor user + the “Legal guardian” (Already working :white_check_mark:).
C - (The one I am currently working on) That will insert X amount of users (group of users like 2 parents and 2 children for instance).

Starting from function A.

def create_membership(attrs, club_id, plan_ids) do
    # This will store the new user changeset
    membership_cs = Membership.create_changeset(attrs, plan_ids)

    multi = Multi.insert(Multi.new(), :insert_membership, membership_cs)

    # list of events to record on the activity logs table.
    ~w(register_membership accept_articles join_club)a
    |> Enum.reduce(multi, fn event, multi ->
      Multi.run(multi, {:insert_activity_log, event}, fn repo, %{insert_membership: membership} ->
        membership = Repo.preload(membership, :club)
        Users.update_user(membership.user, %{membership_id: membership.id})
        log_params = %{event: event, actor: :membership, data: %{club_name: membership.club.name}}

        %ActivityLog{}
        |> ActivityLog.create_changeset(membership.club, membership, log_params)
        |> repo.insert()
      end)
    end)
    |> Repo.transaction()
  end

Function B.

This will record 2 new users, a minor user and an adult user. The logic the for the sport clubs is also to save the users on a family group.

def create_membership_with_group(attrs, club_id, plan_ids) do
    membership_cs = Membership.create_changeset_minor(attrs, plan_ids)

    membership_guardian = Membership.create_changeset(attrs, []) #not passing plan_ids since the guardian is not an active member of the club, only pays for the kid.

    multi =
      Multi.insert(Multi.new(), :insert_membership, membership_cs)
      |> Multi.run(:insert_guardian, fn repo, %{insert_membership: membership} ->
        Users.update_user(membership.user, %{membership_id: membership.id})
        membership_number = generate_membership_number(membership.membership_number)

        repo.insert(
          Ecto.Changeset.put_change(
            membership_guardian,
            :membership_number,
            membership_number
          )
        )
      end)
      # After the new 2 memberships are created, we create a new group, and we insert both memberships.
      |> Multi.run(:create_group, fn repo, %{insert_guardian: guardian} ->
        repo.insert(Group.create_changeset(attrs, guardian))
      end)
      |> Multi.run(:update_group, fn repo, %{insert_membership: membership, create_group: group} ->
        repo.update(Group.add_memberships_changeset([membership], group))
      end)

    # List of events to record on the activity-logs table.
    ~w(register_membership accept_articles join_club)a
    |> Enum.reduce(multi, fn event, multi ->
      Multi.run(multi, {:insert_activity_log, event}, fn repo, %{insert_membership: membership} ->
        membership = Repo.preload(membership, :club)
        Users.update_user(membership.user, %{membership_id: membership.id})
        log_params = %{event: event, actor: :membership, data: %{club_name: membership.club.name}}

        %ActivityLog{}
        |> ActivityLog.create_changeset(membership.club, membership, log_params)
        |> repo.insert()
      end)
    end)
    |> Repo.transaction()
  end

Function C.

This will record X new users (minimum 2 as the example above, but it could be up to 12 new users), The logic of the membership group is to have between 1 Parent and 1 children (But in this case the parent is an active members of the club, so it has plans), and 2 Parents and 10 children. The logic the for the sport clubs here is also to save all the new memberships into a family group, as Funciton B.

My attempt:

def create_membership_with_family_group(attrs, club_id, plan_ids) do
    # This creates the changeset for the Payer of the Group (User on the form)
    membership_cs = Membership.create_changeset(attrs, plan_ids)

    # This Returns a list of Changesets for the rest of the members of the group (without the payer).
    membership_fm = []
    membership_fm = for _group_member <- attrs["group_members"] do
      group_member = Membership.create_changeset_family_group_members(attrs, [])
      membership_fm ++ group_member
    end
    
    # The boths variables have the required data, which I checked with the inspector.

    # Here is where my code starts to sink. 
    # Since I have now X amount of memberships, I should do a loop over the list of memberships_fm. 
    # On my logic I should insert the first changeset, membership_cs, and then loop over the rest and insert them too. I am trying to follow the logic from Function B but I am confused on the loop part.
    
    multi_payer = Multi.insert(Multi.new(), :insert_membership, membership_cs) 
    multi =
        membership_fm
        |> Enum.reduce(multi_payer, fn group_member, multi_payer ->
            Multi.insert(Multi.new(), :insert_membership, membership_cs)
            |> Multi.run(:insert_family_group, fn repo, %{insert_membership: membership} ->
              Users.update_user(membership.user, %{membership_id: membership.id})
              membership_number = generate_membership_number(membership.membership_number)

              # Here where I think I should start to loop over membership_fm.
                repo.insert(
                  Ecto.Changeset.put_change(
                    group_member,
                    :membership_number,
                    membership_number
                  )
                )
              end
            end)
            |> Multi.run(:create_group, fn repo, %{insert_membership: payer_group} ->
              repo.insert(Group.create_changeset(attrs, payer_group))
            end)
            |> Multi.run(:update_group, fn repo, %{insert_family_group: memberships, create_group: group} ->
                # Here I insert the list of memberships into the group already created with the payer.
                 repo.update(Group.add_memberships_changeset([memberships], group))
            end)
       end)

    # List of events to record on the activity-logs table.
    ~w(register_membership accept_articles join_club)a
    |> Enum.reduce(multi, fn event, multi ->
      Multi.run(multi, {:insert_activity_log, event}, fn repo, %{insert_membership: membership} ->
        membership = Repo.preload(membership, :club)
        Users.update_user(membership.user, %{membership_id: membership.id})
        log_params = %{event: event, actor: :membership, data: %{club_name: membership.club.name}}

        %ActivityLog{}
        |> ActivityLog.create_changeset(membership.club, membership, log_params)
        |> repo.insert()
      end)
    end)
    |> Repo.transaction()
  end

I hope it’s enough information, if needed I can post more on the thread. I will truly appreciate your time to read and give me a feedback.

Thanks a lot! :smiley:

Most Liked

stefanchrobot

stefanchrobot

Since you seem to be doing only DB operations in the transaction, I would suggest trying doing this with Repo.transaction. In cases where the Multi is not passed between contexts, I found working directly with the transactions easier.

To make this work, I usually follow these rules:

  • Make each function return an ok/error tuple,
  • Use with to code the happy path and have a fallback for the error path,
  • Wrap the whole thing in a transaction.

Something like:

def do_things() do
  Repo.transaction(fn ->
    with {:ok, foo} <- foo(),
         {:ok, bar} <- bar(foo),
         {:ok, baz} <- baz(bar) do
      # The transaction will return {:ok, baz}
      baz
    else
      # The transaction will return {:error, reason}
      {:error, reason} -> Repo.rollback(reason)
    end
  end)
end
dimitarvp

dimitarvp

When I need to iterate on stuff I sometimes use Ecto.Multi — Ecto v3.14.0. In your case you should also notice that you’re not making a unique key per each insert.

Instead of using :insert_membership you should either use a tuple like {:insert_membership, something_unique_here} or just a key with a number contained inside it like String.to_atom("insert_membership_#{key}").

That’s where I’d start.

stefanchrobot

stefanchrobot

The key can be any term, so you can do update_group = {:update_group, index}. This should help you remove the repetitive code.

Last Post!

rsmml

rsmml

Hey guys @dimitarvp @stefanchrobot . Thanks again for the reply! Now the code is way much shorter, and thanks @stefanchrobot for the tip, it helped a lot to get rid of the repetitive code :slight_smile:

Thanks! :smiley: :beers:

Where Next?

Popular in Questions Top

nobody
Hi! In PHP: $_SERVER[‘SERVER_ADDR’] - in Elixir? Searched the docs for ip address and the web, no good results. Thanks!
New
hariharasudhan94
Lets say I have map like this fetching from my database %{"_id" =&gt; #BSON.ObjectId&lt;58eb1a7a9ad169198c3dXXXX&gt;, "email" =&gt; ...
New
SoCreat
i’m a new one to elixir which editor can i use vs code? or atom? Thanks! :smiley:
New
Patoshizzle
After calling mix ecto.create I get this error: 17:00:32.162 [error] GenServer #PID&lt;0.412.0&gt; terminating ** (Postgrex.Error) FATAL...
New
JorisKok
I have a server on AWS, and was running a load test using artillery. When looking at the Phoenix dashboard I see the Ports going to 100% ...
New
Harrisonl
We have an ECS cluster with 4 services, where each task joins a single cluster, via discovery ECS discovery service. Currently when I de...
New
jason.o
In the code below, if the create action is not set to accept “extra_key” as an input, it errors out with a message shown above. Is there ...
New

Other popular topics Top

JakeBecker
TL;DR: I’ve just released an implementation of Microsoft’s IDE-independent Language Server Protocol for Elixir. It adds language support ...
1144 55125 245
New
nsuchy
Hi. I’ve noticed that Windows Powershell has it’s own IEX command and you cannot access Elixir’s IEX due to the conflict. This isn’t a cr...
New
joeerl
Hello again - after a longish gap I’ve decided I really must dig into Elixir and see what’s been happening here - so I have a few questio...
New
chrismccord
Phoenix 1.4.0 released Phoenix 1.4 is out! This release ships with exciting new features, most notably with HTTP2 support, improved deve...
688 31586 112
New
greenz1
I have a phoenix application from which a user can download multiple(5-6) files of size 1MB. I couldn’t find anything related to sending ...
New
shijith.k
I am trying to start a new phoenix project with elixir 1.9, but mix phx.new does not work. It says that ** (Mix) The task "phx.new" could...
New

We're in Beta

About us Mission Statement