performance: improvedd ash querying
All checks were successful
continuous-integration/drone/push Build is passing
All checks were successful
continuous-integration/drone/push Build is passing
This commit is contained in:
parent
2284cd93c4
commit
b584581114
3 changed files with 34 additions and 49 deletions
|
|
@ -329,6 +329,11 @@ end
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
**PR #208:** *Show custom fields per default in member overview* 🔧
|
||||||
|
- added show_in_overview as attribute to custom fields
|
||||||
|
- show custom fields in member overview per default
|
||||||
|
- can be set to false in the settings for the specific custom field
|
||||||
|
|
||||||
## Implementation Decisions
|
## Implementation Decisions
|
||||||
|
|
||||||
### Architecture Patterns
|
### Architecture Patterns
|
||||||
|
|
@ -390,6 +395,7 @@ defmodule Mv.Membership.CustomField do
|
||||||
attribute :value_type, :atom # :string, :integer, :boolean, :date, :email
|
attribute :value_type, :atom # :string, :integer, :boolean, :date, :email
|
||||||
attribute :immutable, :boolean # Can't change after creation
|
attribute :immutable, :boolean # Can't change after creation
|
||||||
attribute :required, :boolean # All members must have this
|
attribute :required, :boolean # All members must have this
|
||||||
|
attribute :show_in_overview, :boolean # "If true, this custom field will be displayed in the member overview table"
|
||||||
end
|
end
|
||||||
|
|
||||||
# CustomFieldValue stores values
|
# CustomFieldValue stores values
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,9 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
"""
|
"""
|
||||||
use MvWeb, :live_view
|
use MvWeb, :live_view
|
||||||
|
|
||||||
|
require Ash.Query
|
||||||
|
import Ash.Expr
|
||||||
|
|
||||||
alias MvWeb.MemberLive.Index.Formatter
|
alias MvWeb.MemberLive.Index.Formatter
|
||||||
|
|
||||||
# Prefix used in sort field names for custom fields (e.g., "custom_field_<id>")
|
# Prefix used in sort field names for custom fields (e.g., "custom_field_<id>")
|
||||||
|
|
@ -40,9 +43,6 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
@impl true
|
@impl true
|
||||||
def mount(_params, _session, socket) do
|
def mount(_params, _session, socket) do
|
||||||
# Load custom fields that should be shown in overview
|
# Load custom fields that should be shown in overview
|
||||||
require Ash.Query
|
|
||||||
import Ash.Expr
|
|
||||||
|
|
||||||
# Note: Using Ash.read! (bang version) - errors will be handled by Phoenix LiveView
|
# Note: Using Ash.read! (bang version) - errors will be handled by Phoenix LiveView
|
||||||
# and result in a 500 error page. This is appropriate for LiveViews where errors
|
# and result in a 500 error page. This is appropriate for LiveViews where errors
|
||||||
# should be visible to the user rather than silently failing.
|
# should be visible to the user rather than silently failing.
|
||||||
|
|
@ -209,8 +209,7 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
""
|
""
|
||||||
|
|
||||||
cfv ->
|
cfv ->
|
||||||
formatted = Formatter.format_custom_field_value(cfv.value, custom_field)
|
Formatter.format_custom_field_value(cfv.value, custom_field)
|
||||||
if formatted == "", do: "", else: formatted
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
}
|
}
|
||||||
|
|
@ -296,17 +295,16 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
#
|
#
|
||||||
# Process:
|
# Process:
|
||||||
# 1. Builds base query with selected fields
|
# 1. Builds base query with selected fields
|
||||||
# 2. Loads custom field values for visible custom fields
|
# 2. Loads custom field values for visible custom fields (filtered at database level)
|
||||||
# 3. Applies search filter if provided
|
# 3. Applies search filter if provided
|
||||||
# 4. Applies sorting (database-level for regular fields, in-memory for custom fields)
|
# 4. Applies sorting (database-level for regular fields, in-memory for custom fields)
|
||||||
# 5. Filters custom field values to only visible ones (reduces memory usage)
|
|
||||||
#
|
#
|
||||||
# Performance Considerations:
|
# Performance Considerations:
|
||||||
|
# - Database-level filtering: Custom field values are filtered directly in the database
|
||||||
|
# using Ash relationship filters, reducing memory usage and improving performance.
|
||||||
# - In-memory sorting: Custom field sorting is done in memory after loading.
|
# - In-memory sorting: Custom field sorting is done in memory after loading.
|
||||||
# This is suitable for small to medium datasets (<1000 members).
|
# This is suitable for small to medium datasets (<1000 members).
|
||||||
# For larger datasets, consider implementing database-level sorting or pagination.
|
# For larger datasets, consider implementing database-level sorting or pagination.
|
||||||
# - Memory filtering: Custom field values are filtered after loading to reduce
|
|
||||||
# memory usage, but all members are still loaded into memory.
|
|
||||||
# - No pagination: All matching members are loaded at once. For large result sets,
|
# - No pagination: All matching members are loaded at once. For large result sets,
|
||||||
# consider implementing pagination (see Issue #165).
|
# consider implementing pagination (see Issue #165).
|
||||||
#
|
#
|
||||||
|
|
@ -329,8 +327,8 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
])
|
])
|
||||||
|
|
||||||
# Load custom field values for visible custom fields
|
# Load custom field values for visible custom fields
|
||||||
custom_field_ids = Enum.map(socket.assigns.custom_fields_visible, & &1.id)
|
custom_field_ids_list = Enum.map(socket.assigns.custom_fields_visible, & &1.id)
|
||||||
query = load_custom_field_values(query, custom_field_ids)
|
query = load_custom_field_values(query, custom_field_ids_list)
|
||||||
|
|
||||||
# Apply the search filter first
|
# Apply the search filter first
|
||||||
query = apply_search_filter(query, search_query)
|
query = apply_search_filter(query, search_query)
|
||||||
|
|
@ -349,13 +347,8 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
# This is appropriate for data loading in LiveViews
|
# This is appropriate for data loading in LiveViews
|
||||||
members = Ash.read!(query)
|
members = Ash.read!(query)
|
||||||
|
|
||||||
# Filter custom field values to only visible ones (reduces memory usage)
|
# Custom field values are already filtered at the database level in load_custom_field_values/2
|
||||||
# Performance: This iterates through all members and their custom_field_values.
|
# No need for in-memory filtering anymore
|
||||||
# For large datasets (>1000 members), this could be optimized by filtering
|
|
||||||
# at the database level, but requires more complex Ash queries.
|
|
||||||
custom_field_ids = MapSet.new(Enum.map(socket.assigns.custom_fields_visible, & &1.id))
|
|
||||||
|
|
||||||
members = filter_member_custom_field_values(members, custom_field_ids)
|
|
||||||
|
|
||||||
# Sort in memory if needed (for custom fields)
|
# Sort in memory if needed (for custom fields)
|
||||||
members =
|
members =
|
||||||
|
|
@ -374,38 +367,28 @@ defmodule MvWeb.MemberLive.Index do
|
||||||
end
|
end
|
||||||
|
|
||||||
# Load custom field values for the given custom field IDs
|
# Load custom field values for the given custom field IDs
|
||||||
|
#
|
||||||
|
# Filters custom field values directly in the database using Ash relationship filters.
|
||||||
|
# This is more efficient than loading all values and filtering in memory.
|
||||||
|
#
|
||||||
|
# Performance: Database-level filtering reduces:
|
||||||
|
# - Memory usage (only visible custom field values are loaded)
|
||||||
|
# - Network transfer (less data from database to application)
|
||||||
|
# - Processing time (no need to iterate through all members and filter)
|
||||||
defp load_custom_field_values(query, []) do
|
defp load_custom_field_values(query, []) do
|
||||||
query
|
query
|
||||||
end
|
end
|
||||||
|
|
||||||
defp load_custom_field_values(query, custom_field_ids) when length(custom_field_ids) > 0 do
|
defp load_custom_field_values(query, custom_field_ids) when length(custom_field_ids) > 0 do
|
||||||
# Load all custom field values with their custom_field relationship
|
# Filter custom field values at the database level using Ash relationship query
|
||||||
# Note: We filter to visible custom fields after loading to reduce memory usage
|
# This ensures only visible custom field values are loaded
|
||||||
# Ash loads relationships efficiently with JOINs, but we only keep visible ones
|
custom_field_values_query =
|
||||||
|
Mv.Membership.CustomFieldValue
|
||||||
|
|> Ash.Query.filter(expr(custom_field_id in ^custom_field_ids))
|
||||||
|
|> Ash.Query.load(custom_field: [:id, :name, :value_type])
|
||||||
|
|
||||||
query
|
query
|
||||||
|> Ash.Query.load(custom_field_values: [custom_field: [:id, :name, :value_type]])
|
|> Ash.Query.load(custom_field_values: custom_field_values_query)
|
||||||
end
|
|
||||||
|
|
||||||
# Filters custom field values to only visible ones for all members
|
|
||||||
defp filter_member_custom_field_values(members, custom_field_ids) do
|
|
||||||
Enum.map(members, fn member ->
|
|
||||||
filter_single_member_custom_field_values(member, custom_field_ids)
|
|
||||||
end)
|
|
||||||
end
|
|
||||||
|
|
||||||
# Filters custom field values for a single member
|
|
||||||
defp filter_single_member_custom_field_values(member, _custom_field_ids)
|
|
||||||
when not is_list(member.custom_field_values) do
|
|
||||||
member
|
|
||||||
end
|
|
||||||
|
|
||||||
defp filter_single_member_custom_field_values(member, custom_field_ids) do
|
|
||||||
filtered_values =
|
|
||||||
Enum.filter(member.custom_field_values, fn cfv ->
|
|
||||||
cfv.custom_field_id in custom_field_ids
|
|
||||||
end)
|
|
||||||
|
|
||||||
%{member | custom_field_values: filtered_values}
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# -------------------------------------------------------------
|
# -------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -45,16 +45,12 @@ defmodule MvWeb.MemberLive.Index.Formatter do
|
||||||
end
|
end
|
||||||
|
|
||||||
# Format value based on type
|
# Format value based on type
|
||||||
defp format_value_by_type(value, :string, _) when is_binary(value) do
|
|
||||||
# Return empty string if value is empty, otherwise return the value
|
|
||||||
if String.trim(value) == "", do: "", else: value
|
|
||||||
end
|
|
||||||
|
|
||||||
defp format_value_by_type(value, :string, _), do: to_string(value)
|
defp format_value_by_type(value, :string, _), do: to_string(value)
|
||||||
|
|
||||||
defp format_value_by_type(value, :integer, _), do: to_string(value)
|
defp format_value_by_type(value, :integer, _), do: to_string(value)
|
||||||
|
|
||||||
defp format_value_by_type(value, :email, _) when is_binary(value) do
|
defp format_value_by_type(value, type, _) when type in [:string, :email] and is_binary(value) do
|
||||||
# Return empty string if value is empty
|
# Return empty string if value is empty
|
||||||
if String.trim(value) == "", do: "", else: value
|
if String.trim(value) == "", do: "", else: value
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue