Optimize member count queries to avoid N+1 problem
Load all member counts in a single query during mount. Counts are stored in assigns as a map and retrieved without additional queries.
This commit is contained in:
parent
46af6bbbed
commit
18766df224
1 changed files with 36 additions and 12 deletions
|
|
@ -18,15 +18,20 @@ defmodule MvWeb.MembershipFeeTypeLive.Index do
|
||||||
|
|
||||||
alias Mv.MembershipFees
|
alias Mv.MembershipFees
|
||||||
alias Mv.MembershipFees.MembershipFeeType
|
alias Mv.MembershipFees.MembershipFeeType
|
||||||
|
alias Mv.Membership
|
||||||
alias Mv.Membership.Member
|
alias Mv.Membership.Member
|
||||||
alias MvWeb.Helpers.MembershipFeeHelpers
|
alias MvWeb.Helpers.MembershipFeeHelpers
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
|
fee_types = load_membership_fee_types()
|
||||||
|
member_counts = load_member_counts(fee_types)
|
||||||
|
|
||||||
{:ok,
|
{:ok,
|
||||||
socket
|
socket
|
||||||
|> assign(:page_title, gettext("Membership Fee Types"))
|
|> assign(:page_title, gettext("Membership Fee Types"))
|
||||||
|> assign(:membership_fee_types, load_membership_fee_types())}
|
|> assign(:membership_fee_types, fee_types)
|
||||||
|
|> assign(:member_counts, member_counts)}
|
||||||
end
|
end
|
||||||
|
|
||||||
@impl true
|
@impl true
|
||||||
|
|
@ -66,7 +71,7 @@ defmodule MvWeb.MembershipFeeTypeLive.Index do
|
||||||
</:col>
|
</:col>
|
||||||
|
|
||||||
<:col :let={mft} label={gettext("Members")}>
|
<:col :let={mft} label={gettext("Members")}>
|
||||||
<span class="badge badge-ghost">{get_member_count(mft)}</span>
|
<span class="badge badge-ghost">{get_member_count(mft, @member_counts)}</span>
|
||||||
</:col>
|
</:col>
|
||||||
|
|
||||||
<:action :let={mft}>
|
<:action :let={mft}>
|
||||||
|
|
@ -82,18 +87,20 @@ defmodule MvWeb.MembershipFeeTypeLive.Index do
|
||||||
data-confirm={gettext("Are you sure?")}
|
data-confirm={gettext("Are you sure?")}
|
||||||
class={[
|
class={[
|
||||||
"btn btn-ghost btn-xs",
|
"btn btn-ghost btn-xs",
|
||||||
if(get_member_count(mft) > 0,
|
if(get_member_count(mft, @member_counts) > 0,
|
||||||
do: "text-error opacity-50 cursor-not-allowed",
|
do: "text-error opacity-50 cursor-not-allowed",
|
||||||
else: "text-error"
|
else: "text-error"
|
||||||
)
|
)
|
||||||
]}
|
]}
|
||||||
title={
|
title={
|
||||||
if get_member_count(mft) > 0,
|
if get_member_count(mft, @member_counts) > 0,
|
||||||
do:
|
do:
|
||||||
gettext("Cannot delete - %{count} member(s) assigned", count: get_member_count(mft)),
|
gettext("Cannot delete - %{count} member(s) assigned",
|
||||||
|
count: get_member_count(mft, @member_counts)
|
||||||
|
),
|
||||||
else: gettext("Delete")
|
else: gettext("Delete")
|
||||||
}
|
}
|
||||||
disabled={get_member_count(mft) > 0}
|
disabled={get_member_count(mft, @member_counts) > 0}
|
||||||
>
|
>
|
||||||
<.icon name="hero-trash" class="size-4" />
|
<.icon name="hero-trash" class="size-4" />
|
||||||
</button>
|
</button>
|
||||||
|
|
@ -112,10 +119,12 @@ defmodule MvWeb.MembershipFeeTypeLive.Index do
|
||||||
case Ash.destroy(fee_type, domain: MembershipFees) do
|
case Ash.destroy(fee_type, domain: MembershipFees) do
|
||||||
:ok ->
|
:ok ->
|
||||||
updated_types = Enum.reject(socket.assigns.membership_fee_types, &(&1.id == id))
|
updated_types = Enum.reject(socket.assigns.membership_fee_types, &(&1.id == id))
|
||||||
|
updated_counts = Map.delete(socket.assigns.member_counts, id)
|
||||||
|
|
||||||
{:noreply,
|
{:noreply,
|
||||||
socket
|
socket
|
||||||
|> assign(:membership_fee_types, updated_types)
|
|> assign(:membership_fee_types, updated_types)
|
||||||
|
|> assign(:member_counts, updated_counts)
|
||||||
|> put_flash(:info, gettext("Membership fee type deleted"))}
|
|> put_flash(:info, gettext("Membership fee type deleted"))}
|
||||||
|
|
||||||
{:error, error} ->
|
{:error, error} ->
|
||||||
|
|
@ -131,12 +140,27 @@ defmodule MvWeb.MembershipFeeTypeLive.Index do
|
||||||
|> Ash.read!(domain: MembershipFees)
|
|> Ash.read!(domain: MembershipFees)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp get_member_count(fee_type) do
|
# Loads all member counts for fee types in a single query to avoid N+1 queries
|
||||||
# Count members with this fee type
|
defp load_member_counts(fee_types) do
|
||||||
case Ash.count(Member |> Ash.Query.filter(membership_fee_type_id == ^fee_type.id)) do
|
fee_type_ids = Enum.map(fee_types, & &1.id)
|
||||||
{:ok, count} -> count
|
|
||||||
_ -> 0
|
# Load all members with membership_fee_type_id in a single query
|
||||||
|
members =
|
||||||
|
Member
|
||||||
|
|> Ash.Query.filter(membership_fee_type_id in ^fee_type_ids)
|
||||||
|
|> Ash.Query.select([:membership_fee_type_id])
|
||||||
|
|> Ash.read!(domain: Membership)
|
||||||
|
|
||||||
|
# Group by membership_fee_type_id and count
|
||||||
|
members
|
||||||
|
|> Enum.group_by(& &1.membership_fee_type_id)
|
||||||
|
|> Enum.map(fn {fee_type_id, members_list} -> {fee_type_id, length(members_list)} end)
|
||||||
|
|> Map.new()
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# Gets member count from preloaded assigns map
|
||||||
|
defp get_member_count(fee_type, member_counts) do
|
||||||
|
Map.get(member_counts, fee_type.id, 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
defp format_error(%Ash.Error.Invalid{} = error) do
|
defp format_error(%Ash.Error.Invalid{} = error) do
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue