Skip to content

Commit fa0c3ec

Browse files
committed
docs: add user documentation for scopes, query builder, enums, and batch processing
Four new documentation files in .ai/wheels/models/ covering the full API, usage patterns, and examples for each new ORM feature: - scopes.md — defining and chaining query scopes - query-builder.md — fluent, injection-safe query building - enums.md — named property values with auto-generated methods - batch-processing.md — findEach() and findInBatches() https://claude.ai/code/session_01TYLbmcU97RcvZUDdyUfS1t
1 parent 58fd542 commit fa0c3ec

File tree

4 files changed

+1016
-0
lines changed

4 files changed

+1016
-0
lines changed
Lines changed: 250 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,250 @@
1+
# Batch Processing
2+
3+
## Description
4+
5+
Batch processing methods let you work with large result sets memory-efficiently. Instead of loading thousands of records into memory at once, `findEach()` and `findInBatches()` paginate through the data internally and hand you records in manageable chunks. This is essential for background jobs, data migrations, bulk emails, and any operation that touches many records.
6+
7+
## Key Points
8+
9+
- `findEach()` — processes one record at a time (callback receives a single object or struct)
10+
- `findInBatches()` — processes groups of records (callback receives a query/array batch)
11+
- Both use pagination internally, fetching `batchSize` records per database query
12+
- Support all standard finder arguments: `where`, `order`, `include`, `select`, `parameterize`, `includeSoftDeletes`
13+
- Compose with query scopes and the chainable query builder
14+
- Default ordering is primary key ascending (for consistent pagination)
15+
16+
## findEach()
17+
18+
Iterates through records one at a time. Internally loads records in batches (default 1,000) but invokes the callback for each individual record.
19+
20+
### Basic Usage
21+
22+
```cfm
23+
// Send a reminder email to every active user
24+
model("User").findEach(
25+
where="status = 'active'",
26+
batchSize=500,
27+
callback=function(user) {
28+
sendEmail(
29+
to=user.email,
30+
subject="Monthly Reminder",
31+
from="app@example.com"
32+
);
33+
}
34+
);
35+
```
36+
37+
### With Scopes
38+
39+
```cfm
40+
// Using a scope to filter
41+
model("User").active().findEach(
42+
batchSize=1000,
43+
callback=function(user) {
44+
user.lastNotifiedAt = Now();
45+
user.save();
46+
}
47+
);
48+
```
49+
50+
### With the Query Builder
51+
52+
```cfm
53+
model("Order")
54+
.where("status", "pending")
55+
.where("createdAt", "<", DateAdd("d", -30, Now()))
56+
.findEach(batchSize=200, callback=function(order) {
57+
order.status = "expired";
58+
order.save();
59+
});
60+
```
61+
62+
### Returning Structs Instead of Objects
63+
64+
By default `findEach()` yields model objects. Set `returnAs="struct"` if you only need data and want to avoid object creation overhead:
65+
66+
```cfm
67+
model("User").findEach(
68+
returnAs="struct",
69+
batchSize=1000,
70+
callback=function(user) {
71+
writeOutput("Processing: #user.email#<br>");
72+
}
73+
);
74+
```
75+
76+
### findEach() Reference
77+
78+
| Argument | Type | Default | Description |
79+
|----------|------|---------|-------------|
80+
| `batchSize` | numeric | `1000` | Records to load per internal query. |
81+
| `callback` | function | *required* | Closure called for each record. Receives one argument: the record (object or struct). |
82+
| `where` | string | `""` | WHERE clause to filter records. |
83+
| `order` | string | PK ASC | ORDER BY clause. Defaults to primary key for consistent pagination. |
84+
| `include` | string | `""` | Associations to JOIN. |
85+
| `select` | string | `""` | Column list (default: all). |
86+
| `parameterize` | any || Whether to use `cfqueryparam`. |
87+
| `includeSoftDeletes` | boolean | `false` | Include soft-deleted records. |
88+
| `returnAs` | string | `"object"` | `"object"` or `"struct"`. |
89+
90+
## findInBatches()
91+
92+
Processes records in groups. The callback receives the entire batch (query result set, array of objects, or array of structs) rather than individual records. Useful when the operation benefits from bulk processing (e.g., batch API calls, bulk inserts into another system).
93+
94+
### Basic Usage
95+
96+
```cfm
97+
// Export users in CSV batches
98+
model("User").findInBatches(
99+
batchSize=500,
100+
callback=function(users) {
101+
// 'users' is a query result set (default returnAs="query")
102+
writeBatchToCSV(users);
103+
}
104+
);
105+
```
106+
107+
### With Objects
108+
109+
```cfm
110+
model("Order").findInBatches(
111+
where="status = 'pending'",
112+
batchSize=100,
113+
returnAs="objects",
114+
callback=function(orders) {
115+
// 'orders' is an array of model objects
116+
for (var order in orders) {
117+
order.process();
118+
}
119+
}
120+
);
121+
```
122+
123+
### With Scopes
124+
125+
```cfm
126+
model("Product").active().findInBatches(
127+
batchSize=200,
128+
returnAs="structs",
129+
callback=function(products) {
130+
// 'products' is an array of structs
131+
syncToExternalCatalog(products);
132+
}
133+
);
134+
```
135+
136+
### With the Query Builder
137+
138+
```cfm
139+
model("LogEntry")
140+
.where("createdAt", "<", DateAdd("m", -6, Now()))
141+
.findInBatches(batchSize=1000, callback=function(logs) {
142+
archiveLogBatch(logs);
143+
});
144+
```
145+
146+
### findInBatches() Reference
147+
148+
| Argument | Type | Default | Description |
149+
|----------|------|---------|-------------|
150+
| `batchSize` | numeric | `500` | Records per batch. |
151+
| `callback` | function | *required* | Closure called for each batch. Receives one argument: the batch. |
152+
| `where` | string | `""` | WHERE clause to filter records. |
153+
| `order` | string | PK ASC | ORDER BY clause. |
154+
| `include` | string | `""` | Associations to JOIN. |
155+
| `select` | string | `""` | Column list (default: all). |
156+
| `parameterize` | any || Whether to use `cfqueryparam`. |
157+
| `includeSoftDeletes` | boolean | `false` | Include soft-deleted records. |
158+
| `returnAs` | string | `"query"` | `"query"`, `"objects"`, or `"structs"`. |
159+
160+
## Choosing Between findEach() and findInBatches()
161+
162+
| Use case | Method | Why |
163+
|----------|--------|-----|
164+
| Send individual emails | `findEach()` | One email per record |
165+
| Update records one by one | `findEach()` | Each record saved individually |
166+
| Export data to CSV in chunks | `findInBatches()` | Write many rows at once |
167+
| Sync batch to external API | `findInBatches()` | API accepts arrays |
168+
| Aggregate/report generation | `findInBatches()` | Process sets of data |
169+
| Simple per-record logic | `findEach()` | Cleaner callback |
170+
171+
## Choosing batchSize
172+
173+
| Scenario | Recommended batchSize |
174+
|----------|-----------------------|
175+
| Simple reads (few columns) | 1000–5000 |
176+
| Object creation (many columns) | 200–1000 |
177+
| Heavy processing per record | 50–200 |
178+
| External API calls per record | 10–50 |
179+
| Memory-constrained environment | 100–500 |
180+
181+
The default values (1000 for `findEach`, 500 for `findInBatches`) are good starting points for most applications.
182+
183+
## Common Patterns
184+
185+
### Data Migration
186+
187+
```cfm
188+
// Backfill a new column
189+
model("User").findEach(
190+
batchSize=500,
191+
callback=function(user) {
192+
if (!Len(user.displayName)) {
193+
user.displayName = user.firstName & " " & user.lastName;
194+
user.save(callbacks=false);
195+
}
196+
}
197+
);
198+
```
199+
200+
### Bulk Email
201+
202+
```cfm
203+
// Send newsletter to subscribers
204+
model("Subscriber").active().findEach(
205+
batchSize=200,
206+
callback=function(subscriber) {
207+
sendEmail(
208+
to=subscriber.email,
209+
subject="Weekly Newsletter",
210+
from="newsletter@example.com",
211+
template="emails/newsletter"
212+
);
213+
}
214+
);
215+
```
216+
217+
### Cleanup Old Records
218+
219+
```cfm
220+
// Archive old log entries in batches
221+
model("AuditLog")
222+
.where("createdAt", "<", DateAdd("yyyy", -1, Now()))
223+
.findInBatches(batchSize=1000, callback=function(logs) {
224+
// Move to archive table, then delete
225+
for (var i = 1; i <= logs.recordCount; i++) {
226+
archiveLog(logs.id[i], logs.action[i], logs.createdAt[i]);
227+
}
228+
});
229+
230+
// Then delete the originals
231+
model("AuditLog").deleteAll(where="createdAt < '#DateAdd('yyyy', -1, Now())#'");
232+
```
233+
234+
### Progress Reporting
235+
236+
```cfm
237+
local.processed = 0;
238+
local.total = model("User").active().count();
239+
240+
model("User").active().findEach(
241+
batchSize=500,
242+
callback=function(user) {
243+
// ... process user ...
244+
local.processed++;
245+
if (local.processed MOD 100 == 0) {
246+
writeLog(text="Processed #local.processed# / #local.total# users", type="information");
247+
}
248+
}
249+
);
250+
```

0 commit comments

Comments
 (0)