Fix N+1 user queries in league loader and settings loader

Add findUsersByClerkIds() batch function to the user model and replace two
separate Promise.all+findUserByClerkId loops (one for owners, one for
commissioners) with a single inArray query in both $leagueId.server.ts and
$leagueId.settings.tsx. The merged query covers both owner and commissioner
IDs in one round-trip.

https://claude.ai/code/session_01VAkeDDVZMYS1DweQnUrRnH
This commit is contained in:
Claude 2026-03-18 23:30:31 +00:00
parent 3ba9d15f9e
commit 1b5016ab2a
No known key found for this signature in database
3 changed files with 47 additions and 54 deletions

View file

@ -1,4 +1,4 @@
import { eq } from "drizzle-orm"; import { eq, inArray } from "drizzle-orm";
import { database } from "~/database/context"; import { database } from "~/database/context";
import * as schema from "~/database/schema"; import * as schema from "~/database/schema";
@ -38,6 +38,15 @@ export async function findUserByClerkId(
}); });
} }
export async function findUsersByClerkIds(clerkIds: string[]): Promise<User[]> {
if (clerkIds.length === 0) return [];
const db = database();
return await db
.select()
.from(schema.users)
.where(inArray(schema.users.clerkId, clerkIds));
}
export async function updateUser( export async function updateUser(
id: string, id: string,
data: Partial<NewUser> data: Partial<NewUser>

View file

@ -7,7 +7,7 @@ import {
findCommissionersByLeagueId, findCommissionersByLeagueId,
isUserLeagueMember, isUserLeagueMember,
isCommissioner, isCommissioner,
findUserByClerkId, findUsersByClerkIds,
getUserDisplayName, getUserDisplayName,
findDraftSlotsBySeasonId, findDraftSlotsBySeasonId,
} from "~/models"; } from "~/models";
@ -75,39 +75,25 @@ export async function loader(args: Route.LoaderArgs) {
? await getSeasonStandings(season.id) ? await getSeasonStandings(season.id)
: []; : [];
// Fetch user data for team owners // Batch-fetch all users needed for owner and commissioner maps in one query
const ownerIds = teams const ownerIds = [...new Set(teams.map((t) => t.ownerId).filter((id): id is string => id !== null))];
.map((t) => t.ownerId) const commissionerIds = commissioners.map((c) => c.userId);
.filter((id): id is string => id !== null); const allUserIds = [...new Set([...ownerIds, ...commissionerIds])];
const uniqueOwnerIds = [...new Set(ownerIds)]; const userRows = await findUsersByClerkIds(allUserIds);
const owners = await Promise.all( const userByClerkId = new Map(userRows.map((u) => [u.clerkId, u]));
uniqueOwnerIds.map(async (ownerId) => {
const user = await findUserByClerkId(ownerId);
return user
? { clerkId: ownerId, name: getUserDisplayName(user) }
: null;
})
);
const ownerMap = new Map( const ownerMap = new Map(
owners ownerIds
.filter((o): o is NonNullable<typeof o> => o !== null) .map((id) => [id, userByClerkId.get(id)] as const)
.map((o) => [o.clerkId, o.name]) .filter((entry): entry is [string, NonNullable<typeof entry[1]>] => entry[1] != null)
.map(([id, user]) => [id, getUserDisplayName(user)])
); );
// Fetch user data for commissioners
const commissionerIds = commissioners.map((c) => c.userId);
const commissionerUsers = await Promise.all(
commissionerIds.map(async (commissionerId) => {
const user = await findUserByClerkId(commissionerId);
return user
? { clerkId: commissionerId, name: getUserDisplayName(user) }
: null;
})
);
const commissionerMap = new Map( const commissionerMap = new Map(
commissionerUsers commissionerIds
.filter((c): c is NonNullable<typeof c> => c !== null) .map((id) => [id, userByClerkId.get(id)] as const)
.map((c) => [c.clerkId, c.name]) .filter((entry): entry is [string, NonNullable<typeof entry[1]>] => entry[1] != null)
.map(([id, user]) => [id, getUserDisplayName(user)])
); );
// Count available teams // Count available teams

View file

@ -15,7 +15,7 @@ import {
} from "~/models/commissioner"; } from "~/models/commissioner";
import { findCurrentSeasonWithSports, updateSeason } from "~/models/season"; import { findCurrentSeasonWithSports, updateSeason } from "~/models/season";
import { findTeamsBySeasonId, deleteTeam, removeTeamOwner, claimTeam } from "~/models/team"; import { findTeamsBySeasonId, deleteTeam, removeTeamOwner, claimTeam } from "~/models/team";
import { findAllUsers, findUserByClerkId, getUserDisplayName, isUserAdminByClerkId } from "~/models/user"; import { findAllUsers, findUserByClerkId, findUsersByClerkIds, getUserDisplayName, isUserAdminByClerkId } from "~/models/user";
import { unlinkSportFromSeason, linkMultipleSportsToSeason } from "~/models/season-sport"; import { unlinkSportFromSeason, linkMultipleSportsToSeason } from "~/models/season-sport";
import { findAllSportsSeasons } from "~/models/sports-season"; import { findAllSportsSeasons } from "~/models/sports-season";
import { findDraftSlotsBySeasonId, setDraftOrder, randomizeDraftOrder } from "~/models/draft-slot"; import { findDraftSlotsBySeasonId, setDraftOrder, randomizeDraftOrder } from "~/models/draft-slot";
@ -112,30 +112,28 @@ export async function loader(args: Route.LoaderArgs) {
// Get commissioners for this league with user info // Get commissioners for this league with user info
const commissioners = await findCommissionersByLeagueId(leagueId); const commissioners = await findCommissionersByLeagueId(leagueId);
const commissionerUserData = await Promise.all(
commissioners.map(async (c) => { // Batch-fetch all users needed for commissioner and owner lookups in one query
const user = await findUserByClerkId(c.userId); const uniqueOwnerIds = [...new Set(teams.map((t) => t.ownerId).filter((id): id is string => id !== null))];
const commissionerUserIds = commissioners.map((c) => c.userId);
const allUserIds = [...new Set([...commissionerUserIds, ...uniqueOwnerIds])];
const userRows = await findUsersByClerkIds(allUserIds);
const userByClerkId = new Map(userRows.map((u) => [u.clerkId, u]));
const commissionerUserData = commissioners.map((c) => {
const user = userByClerkId.get(c.userId);
return { return {
...c, ...c,
userName: user ? (getUserDisplayName(user) ?? "Unknown User") : "Unknown User", userName: user ? (getUserDisplayName(user) ?? "Unknown User") : "Unknown User",
}; };
}) });
);
// Get owner details for teams const validOwners = uniqueOwnerIds
const ownerIds = teams .map((ownerId) => {
.map((t) => t.ownerId) const user = userByClerkId.get(ownerId);
.filter((id): id is string => id !== null); return user ? { clerkId: ownerId, name: getUserDisplayName(user), id: user.id } : null;
const uniqueOwnerIds = [...new Set(ownerIds)];
const owners = await Promise.all(
uniqueOwnerIds.map(async (ownerId) => {
const user = await findUserByClerkId(ownerId);
return user
? { clerkId: ownerId, name: getUserDisplayName(user), id: user.id }
: null;
}) })
); .filter((o): o is NonNullable<typeof o> => o !== null);
const validOwners = owners.filter((o): o is NonNullable<typeof o> => o !== null);
const ownerMap = new Map(validOwners.map((o) => [o.clerkId, o.name])); const ownerMap = new Map(validOwners.map((o) => [o.clerkId, o.name]));
// League members (team owners) - available to all commissioners for adding co-commissioners // League members (team owners) - available to all commissioners for adding co-commissioners