|
| 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