findMany is a method that retrieves multiple records matching conditions. It supports pagination, search, filtering, and sorting, returning results along with the total count.
findMany is not defined in BaseModelClass. Itβs a standard pattern automatically generated by the Syncer in each Model class when you create an Entity.
Type Signature
async findMany<T extends SubsetKey, LP extends ListParams>(
subset: T,
params?: LP,
): Promise<ListResult<LP, SubsetMapping[T]>>
Auto-Generated Code
Sonamu automatically generates the following code based on your Entity:
// src/application/user/user.model.ts (auto-generated)
class UserModelClass extends BaseModelClass {
@api({ httpMethod: "GET", clients: ["axios", "tanstack-query"], resourceName: "Users" })
async findMany<T extends UserSubsetKey, LP extends UserListParams>(
subset: T,
rawParams?: LP,
): Promise<ListResult<LP, UserSubsetMapping[T]>> {
// 1. Set default values
const params = {
num: 24,
page: 1,
search: "id" as const,
orderBy: "id-desc" as const,
...rawParams,
} satisfies UserListParams;
// 2. Get subset query builder
const { qb } = this.getSubsetQueries(subset);
// 3. Add filtering conditions
if (params.id) {
qb.whereIn("users.id", asArray(params.id));
}
// 4. Search conditions
if (params.search && params.keyword) {
if (params.search === "id") {
qb.where("users.id", Number(params.keyword));
} else if (params.search === "email") {
qb.where("users.email", "like", `%${params.keyword}%`);
}
}
// 5. Sorting
if (params.orderBy === "id-desc") {
qb.orderBy("users.id", "desc");
}
// 6. Create enhancers
const enhancers = this.createEnhancers({
A: (row) => ({ ...row }),
B: (row) => ({ ...row }),
// ... Calculate virtual fields per subset
});
// 7. Execute query
return this.executeSubsetQuery({
subset,
qb,
params,
enhancers,
debug: false,
});
}
}
How it works:
- getSubsetQueries(): Gets subset query builder from BaseModelClass method
- Add conditions: Add WHERE, search, and sorting conditions
- createEnhancers(): Creates virtual field calculation functions from BaseModelClass method
- executeSubsetQuery(): Executes actual query from BaseModelClass method
- COUNT query (total)
- SELECT query (rows)
- Loader execution (load relationship data)
- Hydrate (flat β nested objects)
- Apply enhancers (calculate virtual fields)
Parameters
Specifies the subset of data to retrieve.
Type: SubsetKey (e.g., "A", "B", "C")
// Fetch basic info only
const { rows } = await UserModel.findMany("A", { num: 10, page: 1 });
// Include relationship data
const { rows } = await UserModel.findMany("B", { num: 10, page: 1 });
Query conditions and pagination settings.
Type: ListParams (optional)
type UserListParams = {
// Pagination
num?: number; // Items per page (default: 24)
page?: number; // Page number (default: 1)
// Filtering
id?: number | number[]; // ID filter
status?: "active" | "inactive"; // Custom filter
// Search
search?: "id" | "email" | "name"; // Search field
keyword?: string; // Search keyword
// Sorting
orderBy?: "id-desc" | "created-desc"; // Sort order
// Query mode
queryMode?: "list" | "count" | "both"; // Query mode
// Semantic search (vector search)
semanticQuery?: Record<string, unknown>; // Semantic search parameters
}
Default Values
const params = {
num: 24,
page: 1,
search: "id",
orderBy: "id-desc",
...rawParams, // Overwrite with user input
};
Return Value
Type: Promise<ListResult<LP, SubsetMapping[T]>>
// When semanticQuery is provided, a similarity field is added to each row.
type WithSimilarity<LP, T> = LP extends { semanticQuery: Record<string, unknown> }
? T & { similarity: number }
: T;
type ListResult<LP, T> =
// queryMode: "list"
| { rows: WithSimilarity<LP, T>[] }
// queryMode: "count"
| { total: number }
// queryMode: "both" (default)
| { rows: WithSimilarity<LP, T>[]; total: number };
When semanticQuery is provided, a similarity field (a score between 0 and 1) is automatically added to each row.
// Normal query
const { rows } = await PostModel.findMany("A", { num: 10 });
// rows[0].id, rows[0].title (no similarity)
// Semantic search
const { rows } = await PostModel.findMany("A", {
num: 10,
semanticQuery: { embedding: queryVector },
});
// rows[0].id, rows[0].title, rows[0].similarity (similarity included)
Array of retrieved records.
const { rows } = await UserModel.findMany("A", { num: 10, page: 1 });
rows.forEach(user => {
console.log(user.id, user.name);
});
Total number of records matching the conditions.
const { total } = await UserModel.findMany("A", { num: 10, page: 1 });
const totalPages = Math.ceil(total / 10);
console.log(`Total pages: ${totalPages}`);
num (Items per page)
Number of records to display per page.
Default: 24
// Fetch 10 at a time
const { rows } = await UserModel.findMany("A", { num: 10 });
// Fetch 50 at a time
const { rows } = await UserModel.findMany("A", { num: 50 });
// Fetch all (num: 0)
const { rows } = await UserModel.findMany("A", { num: 0 });
page (Page number)
Page number to fetch (starts from 1).
Default: 1
// First page
const { rows: page1 } = await UserModel.findMany("A", {
num: 10,
page: 1 // Records 1-10
});
// Second page
const { rows: page2 } = await UserModel.findMany("A", {
num: 10,
page: 2 // Records 11-20
});
// Third page
const { rows: page3 } = await UserModel.findMany("A", {
num: 10,
page: 3 // Records 21-30
});
Filtering
ID Filter
// Single ID
const { rows } = await UserModel.findMany("A", { id: 1 });
// Multiple IDs
const { rows } = await UserModel.findMany("A", { id: [1, 2, 3] });
Custom Filters
Additional filters available depending on Entity definition.
// Status filter
const { rows } = await UserModel.findMany("A", {
status: "active"
});
// Date range filter
const { rows } = await UserModel.findMany("A", {
created_from: "2024-01-01",
created_to: "2024-12-31"
});
// Combine multiple filters
const { rows } = await UserModel.findMany("A", {
status: "active",
role: "admin",
verified: true
});
search + keyword
// Search by ID
const { rows } = await UserModel.findMany("A", {
search: "id",
keyword: "123"
});
// Search by email
const { rows } = await UserModel.findMany("A", {
search: "email",
keyword: "john"
});
// Search by name (partial match)
const { rows } = await UserModel.findMany("A", {
search: "name",
keyword: "Smith"
});
Implementation Example
Generated Model code:
// search-keyword
if (params.search && params.keyword && params.keyword.length > 0) {
if (params.search === "id") {
qb.where("users.id", Number(params.keyword));
} else if (params.search === "email") {
qb.where("users.email", "like", `%${params.keyword}%`);
} else if (params.search === "name") {
qb.where("users.name", "like", `%${params.keyword}%`);
} else {
throw new BadRequestException(`Unimplemented search field ${params.search}`);
}
}
Sorting
orderBy
// ID descending (default)
const { rows } = await UserModel.findMany("A", {
orderBy: "id-desc"
});
// Created date descending
const { rows } = await UserModel.findMany("A", {
orderBy: "created-desc"
});
// Name ascending
const { rows } = await UserModel.findMany("A", {
orderBy: "name-asc"
});
Implementation Example
// orderBy
if (params.orderBy) {
if (params.orderBy === "id-desc") {
qb.orderBy("users.id", "desc");
} else if (params.orderBy === "created-desc") {
qb.orderBy("users.created_at", "desc");
} else if (params.orderBy === "name-asc") {
qb.orderBy("users.name", "asc");
} else {
exhaustive(params.orderBy);
}
}
Query Mode
queryMode: βbothβ (default)
Returns both list and total count.
const { rows, total } = await UserModel.findMany("A", {
num: 10,
page: 1,
queryMode: "both" // Default value
});
console.log(`${rows.length} of ${total}`);
queryMode: βlistβ
Returns list only (skips COUNT query).
const { rows } = await UserModel.findMany("A", {
num: 10,
page: 1,
queryMode: "list" // No total
});
// total is undefined
Use queryMode: "list" for performance improvement when COUNT query is heavy.
queryMode: βcountβ
Returns total count only (skips SELECT query).
const { total } = await UserModel.findMany("A", {
status: "active",
queryMode: "count" // No rows
});
console.log(`Total active users: ${total}`);
Basic Usage
Simple List
import { UserModel } from "./user/user.model";
class UserService {
async getUsers(page: number = 1) {
const { rows, total } = await UserModel.findMany("A", {
num: 20,
page
});
return {
users: rows,
total,
page,
totalPages: Math.ceil(total / 20)
};
}
}
async getActiveUsers(page: number = 1) {
const { rows, total } = await UserModel.findMany("A", {
status: "active",
num: 20,
page
});
return { rows, total };
}
Search + Sorting
async searchUsers(keyword: string) {
const { rows } = await UserModel.findMany("A", {
search: "name",
keyword,
orderBy: "name-asc",
num: 50
});
return rows;
}
Practical Examples
Using BaseModelClass Methods
findMany internally uses the following BaseModelClass methods:
getSubsetQueries()
Gets subset query builder.
const { qb } = this.getSubsetQueries(subset);
createEnhancers()
Creates virtual field calculation functions.
const enhancers = this.createEnhancers({
A: (row) => ({
...row,
full_name: `${row.first_name} ${row.last_name}`
}),
});
executeSubsetQuery()
Executes query and returns results.
return this.executeSubsetQuery({
subset,
qb,
params,
enhancers,
debug: false,
optimizeCountQuery: false
});
Type Safety
Subset Type Inference
// Subset A
const { rows: usersA } = await UserModel.findMany("A", {});
usersA[0].posts; // β Type error
// Subset B (includes posts)
const { rows: usersB } = await UserModel.findMany("B", {});
usersB[0].posts; // β
Post[]
ListParams Type
// params is limited to UserListParams type
await UserModel.findMany("A", {
unknownField: true // β Type error
});
await UserModel.findMany("A", {
status: "active" // β
Defined field
});
queryMode Type Inference
// queryMode: "both"
const { rows, total } = await UserModel.findMany("A", {
queryMode: "both"
});
rows; // β
User[]
total; // β
number
// queryMode: "list"
const { rows } = await UserModel.findMany("A", {
queryMode: "list"
});
total; // β Type error (no total)
// queryMode: "count"
const { total } = await UserModel.findMany("A", {
queryMode: "count"
});
rows; // β Type error (no rows)
1. Use queryMode: βlistβ
When COUNT query is heavy:
// β Slow: executes COUNT(*)
const { rows, total } = await UserModel.findMany("A", {
num: 20,
page: 1
});
// β
Fast: skips COUNT
const { rows } = await UserModel.findMany("A", {
num: 20,
page: 1,
queryMode: "list"
});
2. Use Indexes
Add indexes on frequently filtered fields:
CREATE INDEX idx_users_status ON users(status);
CREATE INDEX idx_users_created_at ON users(created_at);
3. Minimize Subsets
Use subsets that include only necessary fields:
// β Heavy: loads all relationships
const { rows } = await UserModel.findMany("Full", { num: 100 });
// β
Light: basic fields only
const { rows } = await UserModel.findMany("A", { num: 100 });
4. Limit num
Prevent fetching too much data at once:
// β Dangerous: possible memory exhaustion
const { rows } = await UserModel.findMany("A", { num: 0 });
// β
Safe: appropriate page size
const { rows } = await UserModel.findMany("A", { num: 100 });
Cautions
1. Caution with num: 0
num: 0 fetches all records, use carefully.
// β Dangerous: can fetch millions of records
const { rows } = await UserModel.findMany("A", { num: 0 });
// β
Safe: pagination
const { rows } = await UserModel.findMany("A", { num: 100 });
2. page Starts from 1
// β Wrong: starts from 0
const { rows } = await UserModel.findMany("A", { page: 0 });
// β
Correct: starts from 1
const { rows } = await UserModel.findMany("A", { page: 1 });
3. Subset Selection
// β Inefficient: loads unnecessary relationships
const { rows } = await UserModel.findMany("Full", { num: 1000 });
// β
Efficient: only what's needed
const { rows } = await UserModel.findMany("A", { num: 1000 });
Next Steps