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.7.1. 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.

dimitarvp

dimitarvp

That’s a super long function. Break it apart a little bit. :slight_smile:

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

srinivasu
How to handle excepions in elixir? Suppose i have A, B, C ,D, E modules. and each module has get() function. A.get() method will call th...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
quazar
How to set Jason to encode all fields in ecto schema, I don’t care about security and implementing only is taking long list of attributes...
New
Jim
As a follow up to my earlier question: I have the code compiling and running but not getting a successful login from the rest server. ...
New
sacepums
Hey guys. I'm new to elixir and im really stocked about it. But I ran into a bit of problem - I need to convert a date sting, for examp...
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
lastday4you
I wanted to check elixir version in phoenix because i found that my elixir is 1.5 but when i use Enum.chunk_by it said the function is un...
New
fayddelight
I tried installing elixir 1.11.2 erlang 23.3.4 via asdf in my zsh shell. Enabled the versions locally and globally. When I list them ...
New
stefanluptak
Hello everybody, usually, I use a 29" ultra-wide monitor for VSCode which can easily accomodate explorer (files panel) + file with code ...
New
LegitStack
I’m trying to make a websocket server in Phoenix or raw Elixir. I heard about gun, I think I could use cowboy, but since I’m not that sma...
New

Other popular topics Top

dotdotdotPaul
Okay, I'm having a heck of a time trying to figure out how to best handle the validation of belongs_to associations in Ecto. I'm sure I'...
New
gshaw
What is the idiomatic way of matching for not nil in Elixir? E.g., First way: defp halt_if_not_signed_in(conn, signed_in_account) when...
New
jerry
Good day to you all. I have been struggling to get a query involving like and ilike to work. Can anyone assist me on this, please? pro...
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
fireproofsocks
I’m working on defining a simple Ecto schema for a table (in PostGres), but I don’t see where I can define a column as NOT NULL. Conside...
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
vertexbuffer
Hello, can anybody help here..? I have a list of players and I what to delete an element, but every for loop the list is reverting to ori...
New
stefanchrobot
What’s the safe way to decode a JSON string into a struct? I want to avoid calling String.to_atom. Jason.decode can give me a map with st...
New
WestKeys
Currently suffering from paralysis by [HTTP client] analysis. This is rather unusual in Elixirland as there tends to be consensus on the ...
New
romenigld
I am trying to run a deploy with docker and I successfully runned with this command: docker build -t romenigld/blog-prod . but when I t...
New

We're in Beta

About us Mission Statement