Files
Mitgliederverwaltung/.plans/done/issue-200-n-plus-one-queries-member-list.md
shahondin1624 ee569250ad
Database Portability Tests / Unit Tests (PlatformHelper) (push) Failing after 40s
Database Portability Tests / Integration (mysql) (push) Has been skipped
Database Portability Tests / Integration (postgres) (push) Has been skipped
Database Portability Tests / Integration (sqlite) (push) Has been skipped
Database Portability Tests / Verify no MySQL-specific SQL (push) Successful in 5s
Fix N+1 query problem in MemberService (Closes #200)
- MemberMapper: 8 new *WithRelations() methods that fetch members with
  addresses, phones, and emails in a single query using LEFT JOINs
- MemberMapper: addJoinClauses() and fetchWithRelations() private helpers
  that handle JOIN duplication (one member × multiple sub-entities)
- MemberService: refactored findAll, findByFamily, findByStatus, search,
  findByBirthdayThisMonth, findWithUnpaidFees, findFiltered, fullTextSearch
  to delegate to joined mapper methods
- MemberService: added arrayToMember() and arrayToAddress() helpers so
  buildMatchContext() works with flat-array results from fullTextSearch
- MemberServiceTest: updated all existing tests to mock new method names
  and return flat-array format with nested sub-entities
- MemberServiceTest: added 10 new tests covering joined methods, backward
  compatibility, and correct shape of returned data
- Moved issue-200 plan from open/ to done/
2026-04-28 21:35:42 +02:00

1.8 KiB
Raw Permalink Blame History

Issue #200: N+1 Query Problem in Member List

Problem

The MemberService::findAll() method (and similar methods) loads members, then makes 3 additional queries per member to fetch addresses, phones, and emails:

public function findAll(?int $limit = null, ?int $offset = null): array {
    $members = $this->memberMapper->findAll($limit, $offset);
    return array_map(function (Member $member) {
        $id = $member->getId();
        $addresses = $this->addressMapper->findByMemberId($id);  // +1 query
        $phones = $this->phoneMapper->findByMemberId($id);        // +1 query
        $emails = $this->emailMapper->findByMemberId($id);        // +1 query
        return $this->buildMemberResponse($member, $addresses, $phones, $emails);
    }, $members);
}

For a page of 20 members, this results in 1 + 20×3 = 61 queries.

Impact

  • Slow page loads, especially with pagination
  • Increased database load
  • Poor performance on larger member lists

Solution

Create a new mapper method that uses JOINs to fetch all data in a single query:

SELECT m.*, a.*, p.*, e.*
FROM mv_members m
LEFT JOIN mv_addresses a ON m.id = a.member_id
LEFT JOIN mv_phones p ON m.id = p.member_id
LEFT JOIN mv_emails e ON m.id = e.member_id
WHERE m.deleted_at IS NULL
ORDER BY m.nachname, m.vorname

Then parse the result set in PHP to group related sub-entities.

Tasks

  • Create MemberMapper::findAllWithRelations(?int $limit, ?int $offset): array that returns raw joined data
  • Create a DTO or use an associative array to represent joined results
  • Update MemberService::findAll() to use the new method
  • Apply same pattern to findFiltered(), findByFamily(), search(), fullTextSearch()
  • Benchmark before/after query counts

Labels

  • enhancement
  • backend
  • priority:high
  • performance