rsmml

rsmml

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:

Showing Posts 1 to 6

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

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
rsmml

rsmml OP

Thanks @stefanchrobot and @dimitarvp Both answers helped me to go through a solution. Indeed the String.to_atom("insert_membership_#{key}") was a key part of it.

My solution:

def create_membership_with_family_group(attrs, club_id, plan_ids) do
    membership_cs =
      attrs
      |> Membership.create_changeset(plan_ids)

    membership_fm = []
    membership_fm = for _group_member <- attrs["group_members"] do
      group_member =
        attrs
        |> handle_payer(club_id)

      membership_fm ++ group_member
    end

    membership_fm_updated = handle_family_member(membership_fm)
    membership_fm_updated_cs = []
    membership_fm_updated_cs = for group_member <- membership_fm_updated do
      group_member =
        group_member
        |> Membership.create_changeset_family_group_members([])

        membership_fm_updated_cs ++ group_member
    end

    membership_fm = membership_fm_updated_cs |> Enum.with_index

    multi_payer = Multi.insert(Multi.new(), :insert_membership, membership_cs)
    multi =
      membership_fm
      |> Enum.reduce(multi_payer, fn {group_member, index}, acc ->
        Multi.run(acc, String.to_atom("insert_family_group_#{index}"), fn repo, %{insert_membership: membership} ->
          Users.update_user(membership.user, %{membership_id: membership.id})
          membership_number = generate_membership_number_fm(membership.membership_number, index)

          repo.insert(
            Ecto.Changeset.put_change(
              group_member,
              :membership_number,
              membership_number
            )
          )
        end)
      end)
# Here I realized that I was creating a group fo each member of the team, when I needed to create 1 group and add all the members of the team inside that group :facepalm:
    multi =
      multi
      |> Multi.run(:create_group, fn repo, %{insert_membership: payer_group} ->
        repo.insert(Group.create_changeset(attrs, payer_group))
      end)

    multi =
      membership_fm
      |> Enum.reduce(multi, fn {_group_member, index}, acc ->
        update_group = String.to_atom("update_group_#{index}")

        cond do
          update_group == :update_group_0 ->
            Multi.run(acc, update_group, fn repo, %{insert_family_group_0: membership, create_group: group} ->
              repo.update(Group.add_memberships_changeset([membership], group))
            end)

          update_group == :update_group_1 ->
            Multi.run(acc, update_group, fn repo, %{insert_family_group_1: membership, create_group: group} ->
              repo.update(Group.add_memberships_changeset([membership], group))
            end)

          update_group == :update_group_2 ->
            Multi.run(acc, update_group, fn repo, %{insert_family_group_2: membership, create_group: group} ->
              repo.update(Group.add_memberships_changeset([membership], group))
            end)

          update_group == :update_group_3 ->
            Multi.run(acc, update_group, fn repo, %{insert_family_group_3: membership, create_group: group} ->
              repo.update(Group.add_memberships_changeset([membership], group))
            end)

          update_group == :update_group_4 ->
            Multi.run(acc, update_group, fn repo, %{insert_family_group_4: membership, create_group: group} ->
              repo.update(Group.add_memberships_changeset([membership], group))
            end)

          update_group == :update_group_5 ->
            Multi.run(acc, update_group, fn repo, %{insert_family_group_5: membership, create_group: group} ->
              repo.update(Group.add_memberships_changeset([membership], group))
            end)

          update_group == :update_group_6 ->
            Multi.run(acc, update_group, fn repo, %{insert_family_group_6: membership, create_group: group} ->
              repo.update(Group.add_memberships_changeset([membership], group))
            end)

          update_group == :update_group_7 ->
            Multi.run(acc, update_group, fn repo, %{insert_family_group_7: membership, create_group: group} ->
              repo.update(Group.add_memberships_changeset([membership], group))
            end)

          update_group == :update_group_8 ->
            Multi.run(acc, update_group, fn repo, %{insert_family_group_8: membership, create_group: group} ->
              repo.update(Group.add_memberships_changeset([membership], group))
            end)

          update_group == :update_group_9 ->
            Multi.run(acc, update_group, fn repo, %{insert_family_group_9: membership, create_group: group} ->
              repo.update(Group.add_memberships_changeset([membership], group))
            end)

          update_group == :update_group_10 ->
            Multi.run(acc, update_group, fn repo, %{insert_family_group_10: membership, create_group: group} ->
              repo.update(Group.add_memberships_changeset([membership], group))
            end)
        end
      end)

    ~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

At the moment of updating the created group, I have to pattern match with the key I used with String.to_atom("insert_family_group_#{index}"). I couldn’t find a way to make the key “dynamic” so that I pattern match like %{insert_family_group_“#{index}”: membership, …} for instance
I tried doing something like

map = %{"insert_family_group_#{index}" => membership , "create_group": group}
map = for {key, val} <- map, into: %{}, do: {String.to_atom(key), val}
iex> %{insert_family_group_0: membership, create_group: group}

but membership and group are not defined. Is there any way to solve that?

Thanks for the replies, and sorry for the late reply :beers:

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 OP

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:

— All posts loaded —

Where Next? Top

Trending in Questions Top

Blokh
Hey guys, I’ve got a huge CSV ( around 10 GB ) that needs to be processed hourly Do you guys have any suggestions what is the best prac...
New
RSP87
I’m working on a project that simulates the bumbl example in the programming phoenix book. It acts almost like an email client. We have a...
New
kszambelanczyk
Hello! Could someone please give me a help/sample code, how to delete a file from s3 using waffle/waffle_ecto from Phoenix app. I creat...
New
RemyXRenard
I’m seeing that a list inside a Kino.DataTable will be interpreted as a charlist, even if the Kino.configure() is set to charlists: :as_l...
New
velrest
So my question is quite simple and i have found no conclusive answer on forum, google or AI. Should we use :erlang.float for Integer to ...
New
samoloth
Hi, I’ve just set up an application with ash_authentication. There is only magic link strategy for now, so there is no confirmation add o...
New
FlyingNoodle
If a change or preparation module uses Ash.Changeset.get_argument/2 or Ash.Query.get_argument/2 (or any of the other get_argument functio...
New

Other Trending Topics Top

mudasobwa
I am happy to introduce the very α version of the new programming language compiled to BEAM. Welcome Cure. It has literally three kille...
New
garrison
Hobbes is a low-level distributed database for the Elixir programming language. Hobbes provides a simple, safe, and scalable storage lay...
New
marciok
Hi there! We created Gust: A task orchestrator inspired by Airflow. For those who have never heard about Aiflow, it’s a Python-based wor...
New
jimsynz
Beam Bots (or just BB for short) is a framework for building fault-tolerant robotics applications in Elixir using familiar OTP patterns. ...
New
Damirados
Hello everyone. After busy few months I am happy to announce v0.1.0 of Emerge &amp; Solve. They are GUI (Emerge) and State management (S...
New
netoum
Corex is an accessible, unstyled UI component library for Phoenix that integrates Zag.js state machines using Vanilla JavaScript and Live...
New

We're in Beta

About us Mission Statement

Options

Thread Display Mode




Thread Preview

Skip Thread Previews