masterrecord 0.3.8 → 0.3.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Entity/entityTrackerModel.js +17 -3
- package/QueryLanguage/queryMethods.js +15 -0
- package/context.js +111 -0
- package/docs/ACTIVE_RECORD_PATTERN.md +477 -0
- package/docs/DETACHED_ENTITIES_GUIDE.md +445 -0
- package/package.json +1 -1
- package/readme.md +37 -4
- package/test/attachDetached.test.js +303 -0
- /package/{QUERY_CACHING_GUIDE.md → docs/QUERY_CACHING_GUIDE.md} +0 -0
|
@@ -0,0 +1,445 @@
|
|
|
1
|
+
# Detached Entity Problem - Solutions
|
|
2
|
+
|
|
3
|
+
## The Problem
|
|
4
|
+
|
|
5
|
+
```javascript
|
|
6
|
+
// Controller loads task
|
|
7
|
+
const task = await this._taskService.getTask(taskId);
|
|
8
|
+
|
|
9
|
+
// Service receives task and modifies it
|
|
10
|
+
task.status = 'completed';
|
|
11
|
+
|
|
12
|
+
// Try to save - BUT IT DOESN'T WORK! ❌
|
|
13
|
+
this._qaContext.saveChanges(); // Task change NOT saved
|
|
14
|
+
|
|
15
|
+
// Why? The task is "detached" - not tracked by _qaContext
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
**Root Cause:** The task was loaded in a different context (`taskService`) and is now **detached** from the current context (`_qaContext`). MasterRecord's change tracking doesn't see the modification.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## How Other ORMs Solve This
|
|
23
|
+
|
|
24
|
+
### Entity Framework (.NET)
|
|
25
|
+
```csharp
|
|
26
|
+
// Solution 1: Attach
|
|
27
|
+
context.Attach(task);
|
|
28
|
+
context.Entry(task).State = EntityState.Modified;
|
|
29
|
+
context.SaveChanges();
|
|
30
|
+
|
|
31
|
+
// Solution 2: Update (simpler)
|
|
32
|
+
context.Update(task);
|
|
33
|
+
context.SaveChanges();
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
### Hibernate (Java)
|
|
37
|
+
```java
|
|
38
|
+
// Solution: Merge
|
|
39
|
+
session.merge(task);
|
|
40
|
+
session.flush();
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
### Active Record (Rails)
|
|
44
|
+
```ruby
|
|
45
|
+
# No problem - entities have .save()
|
|
46
|
+
task.status = 'completed'
|
|
47
|
+
task.save
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Sequelize (Node.js)
|
|
51
|
+
```javascript
|
|
52
|
+
// No problem - entities have .save()
|
|
53
|
+
task.status = 'completed';
|
|
54
|
+
await task.save();
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
---
|
|
58
|
+
|
|
59
|
+
## MasterRecord Solutions
|
|
60
|
+
|
|
61
|
+
### Solution 1: **attach()** Method (Recommended)
|
|
62
|
+
|
|
63
|
+
Like Entity Framework's `Update()`:
|
|
64
|
+
|
|
65
|
+
```javascript
|
|
66
|
+
// Your original code (BROKEN)
|
|
67
|
+
const task = await this._taskService.getTask(taskId);
|
|
68
|
+
task.status = 'completed';
|
|
69
|
+
this._qaContext.saveChanges(); // ❌ Doesn't work
|
|
70
|
+
|
|
71
|
+
// FIX: Attach the detached entity
|
|
72
|
+
const task = await this._taskService.getTask(taskId);
|
|
73
|
+
task.status = 'completed';
|
|
74
|
+
this._qaContext.attach(task); // ✅ Mark as modified
|
|
75
|
+
await this._qaContext.saveChanges(); // ✅ Now it works!
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
### Solution 2: **Specific Field Changes**
|
|
79
|
+
|
|
80
|
+
Only mark specific fields as dirty:
|
|
81
|
+
|
|
82
|
+
```javascript
|
|
83
|
+
const task = await this._taskService.getTask(taskId);
|
|
84
|
+
|
|
85
|
+
// Attach with specific changes
|
|
86
|
+
this._qaContext.attach(task, {
|
|
87
|
+
status: 'completed',
|
|
88
|
+
completed_at: new Date()
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
await this._qaContext.saveChanges();
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Solution 3: **attachAll()** for Multiple Entities
|
|
95
|
+
|
|
96
|
+
```javascript
|
|
97
|
+
const tasks = await this._taskService.getTasks();
|
|
98
|
+
|
|
99
|
+
// Modify all tasks
|
|
100
|
+
tasks.forEach(task => {
|
|
101
|
+
task.status = 'completed';
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
// Attach all at once
|
|
105
|
+
this._qaContext.attachAll(tasks);
|
|
106
|
+
await this._qaContext.saveChanges();
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
### Solution 4: **update()** Helper
|
|
110
|
+
|
|
111
|
+
Update by primary key without loading first:
|
|
112
|
+
|
|
113
|
+
```javascript
|
|
114
|
+
// No need to load task first
|
|
115
|
+
await this._qaContext.update('Task', taskId, {
|
|
116
|
+
status: 'completed',
|
|
117
|
+
completed_at: new Date()
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
await this._qaContext.saveChanges();
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
---
|
|
124
|
+
|
|
125
|
+
## Complete Example: Your Annotation Service
|
|
126
|
+
|
|
127
|
+
### Before (BROKEN) ❌
|
|
128
|
+
|
|
129
|
+
```javascript
|
|
130
|
+
class AnnotationService {
|
|
131
|
+
constructor() {
|
|
132
|
+
this._qaContext = new QAContext();
|
|
133
|
+
this._taskService = new TaskService();
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async createAnnotation(taskId, data) {
|
|
137
|
+
// Load task from different service
|
|
138
|
+
const task = await this._taskService.getTask(taskId);
|
|
139
|
+
|
|
140
|
+
// Create annotation
|
|
141
|
+
const annotation = this._qaContext.Annotation.new();
|
|
142
|
+
annotation.task_id = taskId;
|
|
143
|
+
annotation.data = data;
|
|
144
|
+
this._qaContext.Annotation.add(annotation);
|
|
145
|
+
|
|
146
|
+
// Modify task - BUT NOT TRACKED! ❌
|
|
147
|
+
task.status = 'completed';
|
|
148
|
+
|
|
149
|
+
// Only annotation is saved, task change is ignored ❌
|
|
150
|
+
await this._qaContext.saveChanges();
|
|
151
|
+
|
|
152
|
+
return annotation;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### After (FIXED) ✅
|
|
158
|
+
|
|
159
|
+
**Option A: Use attach()**
|
|
160
|
+
|
|
161
|
+
```javascript
|
|
162
|
+
class AnnotationService {
|
|
163
|
+
constructor() {
|
|
164
|
+
this._qaContext = new QAContext();
|
|
165
|
+
this._taskService = new TaskService();
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
async createAnnotation(taskId, data) {
|
|
169
|
+
// Load task from different service
|
|
170
|
+
const task = await this._taskService.getTask(taskId);
|
|
171
|
+
|
|
172
|
+
// Create annotation
|
|
173
|
+
const annotation = this._qaContext.Annotation.new();
|
|
174
|
+
annotation.task_id = taskId;
|
|
175
|
+
annotation.data = data;
|
|
176
|
+
this._qaContext.Annotation.add(annotation);
|
|
177
|
+
|
|
178
|
+
// Modify task
|
|
179
|
+
task.status = 'completed';
|
|
180
|
+
|
|
181
|
+
// FIX: Attach detached task ✅
|
|
182
|
+
this._qaContext.attach(task);
|
|
183
|
+
|
|
184
|
+
// Both annotation and task are saved ✅
|
|
185
|
+
await this._qaContext.saveChanges();
|
|
186
|
+
|
|
187
|
+
return annotation;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**Option B: Use attach() with specific fields**
|
|
193
|
+
|
|
194
|
+
```javascript
|
|
195
|
+
async createAnnotation(taskId, data) {
|
|
196
|
+
const task = await this._taskService.getTask(taskId);
|
|
197
|
+
|
|
198
|
+
// Create annotation
|
|
199
|
+
const annotation = this._qaContext.Annotation.new();
|
|
200
|
+
annotation.task_id = taskId;
|
|
201
|
+
annotation.data = data;
|
|
202
|
+
this._qaContext.Annotation.add(annotation);
|
|
203
|
+
|
|
204
|
+
// Attach with specific changes ✅
|
|
205
|
+
this._qaContext.attach(task, {
|
|
206
|
+
status: 'completed',
|
|
207
|
+
completed_at: new Date()
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
await this._qaContext.saveChanges();
|
|
211
|
+
return annotation;
|
|
212
|
+
}
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
**Option C: Use update() helper**
|
|
216
|
+
|
|
217
|
+
```javascript
|
|
218
|
+
async createAnnotation(taskId, data) {
|
|
219
|
+
// No need to load task first
|
|
220
|
+
|
|
221
|
+
// Create annotation
|
|
222
|
+
const annotation = this._qaContext.Annotation.new();
|
|
223
|
+
annotation.task_id = taskId;
|
|
224
|
+
annotation.data = data;
|
|
225
|
+
this._qaContext.Annotation.add(annotation);
|
|
226
|
+
|
|
227
|
+
// Update task by ID ✅
|
|
228
|
+
await this._qaContext.update('Task', taskId, {
|
|
229
|
+
status: 'completed',
|
|
230
|
+
completed_at: new Date()
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
await this._qaContext.saveChanges();
|
|
234
|
+
return annotation;
|
|
235
|
+
}
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
---
|
|
239
|
+
|
|
240
|
+
## Best Practices
|
|
241
|
+
|
|
242
|
+
### ✅ DO: Use One Context Per Request
|
|
243
|
+
|
|
244
|
+
```javascript
|
|
245
|
+
// Express middleware
|
|
246
|
+
app.use((req, res, next) => {
|
|
247
|
+
req.db = new AppContext(); // One context per request
|
|
248
|
+
res.on('finish', () => req.db.endRequest());
|
|
249
|
+
next();
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
// Service uses the request context
|
|
253
|
+
class AnnotationService {
|
|
254
|
+
async createAnnotation(db, taskId, data) {
|
|
255
|
+
// Use passed-in context
|
|
256
|
+
const task = db.Task.findById(taskId); // Loaded in same context ✅
|
|
257
|
+
task.status = 'completed'; // Already tracked ✅
|
|
258
|
+
|
|
259
|
+
const annotation = db.Annotation.new();
|
|
260
|
+
annotation.task_id = taskId;
|
|
261
|
+
db.Annotation.add(annotation);
|
|
262
|
+
|
|
263
|
+
await db.saveChanges(); // Both changes saved ✅
|
|
264
|
+
return annotation;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
// Controller
|
|
269
|
+
app.post('/annotations', async (req, res) => {
|
|
270
|
+
const annotationService = new AnnotationService();
|
|
271
|
+
const annotation = await annotationService.createAnnotation(
|
|
272
|
+
req.db, // Pass request context ✅
|
|
273
|
+
req.body.taskId,
|
|
274
|
+
req.body.data
|
|
275
|
+
);
|
|
276
|
+
res.json(annotation);
|
|
277
|
+
});
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### ✅ DO: Load in Same Context
|
|
281
|
+
|
|
282
|
+
```javascript
|
|
283
|
+
// ❌ BAD: Different contexts
|
|
284
|
+
const taskService = new TaskService(); // Has own context
|
|
285
|
+
const qaContext = new QAContext(); // Different context
|
|
286
|
+
const task = await taskService.getTask(taskId); // Loaded in taskService context
|
|
287
|
+
task.status = 'completed';
|
|
288
|
+
qaContext.saveChanges(); // Doesn't see change ❌
|
|
289
|
+
|
|
290
|
+
// ✅ GOOD: Same context
|
|
291
|
+
const db = new AppContext();
|
|
292
|
+
const task = db.Task.findById(taskId); // Loaded in db context
|
|
293
|
+
task.status = 'completed'; // Already tracked
|
|
294
|
+
await db.saveChanges(); // Change saved ✅
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
### ✅ DO: Use attach() for Detached Entities
|
|
298
|
+
|
|
299
|
+
```javascript
|
|
300
|
+
// When you must use entities from different contexts
|
|
301
|
+
const task = await externalService.getTask(taskId); // Detached
|
|
302
|
+
task.status = 'completed';
|
|
303
|
+
db.attach(task); // Re-attach ✅
|
|
304
|
+
await db.saveChanges();
|
|
305
|
+
```
|
|
306
|
+
|
|
307
|
+
### ❌ DON'T: Modify Detached Entities Without Attaching
|
|
308
|
+
|
|
309
|
+
```javascript
|
|
310
|
+
// ❌ BAD
|
|
311
|
+
const task = await someService.getTask(taskId);
|
|
312
|
+
task.status = 'completed';
|
|
313
|
+
db.saveChanges(); // Won't work!
|
|
314
|
+
|
|
315
|
+
// ✅ GOOD
|
|
316
|
+
const task = await someService.getTask(taskId);
|
|
317
|
+
task.status = 'completed';
|
|
318
|
+
db.attach(task); // Attach first!
|
|
319
|
+
await db.saveChanges();
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
---
|
|
323
|
+
|
|
324
|
+
## Comparison Table
|
|
325
|
+
|
|
326
|
+
| ORM | Method | Example |
|
|
327
|
+
|-----|--------|---------|
|
|
328
|
+
| **Entity Framework** | `context.Update(entity)` | `context.Update(task);` |
|
|
329
|
+
| **Hibernate** | `session.merge(entity)` | `session.merge(task);` |
|
|
330
|
+
| **Active Record** | `entity.save()` | `task.save` |
|
|
331
|
+
| **Sequelize** | `entity.save()` | `await task.save()` |
|
|
332
|
+
| **MasterRecord** | `context.attach(entity)` | `db.attach(task);` |
|
|
333
|
+
|
|
334
|
+
---
|
|
335
|
+
|
|
336
|
+
## API Reference
|
|
337
|
+
|
|
338
|
+
### attach(entity, changes?)
|
|
339
|
+
|
|
340
|
+
Attach a detached entity and mark as modified.
|
|
341
|
+
|
|
342
|
+
**Parameters:**
|
|
343
|
+
- `entity` - The detached entity
|
|
344
|
+
- `changes` (optional) - Object with specific field changes
|
|
345
|
+
|
|
346
|
+
**Returns:** The attached entity
|
|
347
|
+
|
|
348
|
+
**Example:**
|
|
349
|
+
```javascript
|
|
350
|
+
// Attach entire entity
|
|
351
|
+
db.attach(task);
|
|
352
|
+
|
|
353
|
+
// Attach with specific changes
|
|
354
|
+
db.attach(task, { status: 'completed' });
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
### attachAll(entities)
|
|
358
|
+
|
|
359
|
+
Attach multiple entities at once.
|
|
360
|
+
|
|
361
|
+
**Parameters:**
|
|
362
|
+
- `entities` - Array of detached entities
|
|
363
|
+
|
|
364
|
+
**Returns:** Array of attached entities
|
|
365
|
+
|
|
366
|
+
**Example:**
|
|
367
|
+
```javascript
|
|
368
|
+
const tasks = await getTasks();
|
|
369
|
+
tasks.forEach(t => t.status = 'completed');
|
|
370
|
+
db.attachAll(tasks);
|
|
371
|
+
await db.saveChanges();
|
|
372
|
+
```
|
|
373
|
+
|
|
374
|
+
### update(entityName, primaryKey, changes)
|
|
375
|
+
|
|
376
|
+
Update entity by primary key without loading.
|
|
377
|
+
|
|
378
|
+
**Parameters:**
|
|
379
|
+
- `entityName` - Name of entity (e.g., 'Task')
|
|
380
|
+
- `primaryKey` - Primary key value
|
|
381
|
+
- `changes` - Object with field changes
|
|
382
|
+
|
|
383
|
+
**Returns:** The attached entity
|
|
384
|
+
|
|
385
|
+
**Example:**
|
|
386
|
+
```javascript
|
|
387
|
+
await db.update('Task', taskId, {
|
|
388
|
+
status: 'completed',
|
|
389
|
+
completed_at: new Date()
|
|
390
|
+
});
|
|
391
|
+
await db.saveChanges();
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
---
|
|
395
|
+
|
|
396
|
+
## Troubleshooting
|
|
397
|
+
|
|
398
|
+
### Changes not saving?
|
|
399
|
+
|
|
400
|
+
**Check:**
|
|
401
|
+
1. Is entity tracked? `console.log(db.__trackedEntities.includes(entity))`
|
|
402
|
+
2. Is entity state correct? `console.log(entity.__state)` (should be "modified")
|
|
403
|
+
3. Are dirty fields marked? `console.log(entity.__dirtyFields)`
|
|
404
|
+
|
|
405
|
+
**Fix:**
|
|
406
|
+
```javascript
|
|
407
|
+
// If not tracked, attach it
|
|
408
|
+
if (!db.__trackedEntities.includes(entity)) {
|
|
409
|
+
db.attach(entity);
|
|
410
|
+
}
|
|
411
|
+
```
|
|
412
|
+
|
|
413
|
+
### "Entity must have __entity metadata" error?
|
|
414
|
+
|
|
415
|
+
**Cause:** Entity wasn't loaded through MasterRecord
|
|
416
|
+
|
|
417
|
+
**Fix:**
|
|
418
|
+
```javascript
|
|
419
|
+
// ❌ BAD: Plain object
|
|
420
|
+
const task = { id: 1, status: 'completed' };
|
|
421
|
+
db.attach(task); // Error!
|
|
422
|
+
|
|
423
|
+
// ✅ GOOD: Load through MasterRecord
|
|
424
|
+
const task = db.Task.findById(1);
|
|
425
|
+
task.status = 'completed';
|
|
426
|
+
db.attach(task); // Works!
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
---
|
|
430
|
+
|
|
431
|
+
## Summary
|
|
432
|
+
|
|
433
|
+
**The detached entity problem** happens when:
|
|
434
|
+
1. Entity loaded in one context
|
|
435
|
+
2. Passed to different context/service
|
|
436
|
+
3. Modified
|
|
437
|
+
4. saveChanges() called but doesn't see modification
|
|
438
|
+
|
|
439
|
+
**Solutions:**
|
|
440
|
+
1. ✅ Use **`attach()`** to re-attach detached entities (like Entity Framework)
|
|
441
|
+
2. ✅ Use **same context** throughout request (best practice)
|
|
442
|
+
3. ✅ Pass context to services instead of creating multiple contexts
|
|
443
|
+
4. ✅ Use **`update()`** helper for simple updates
|
|
444
|
+
|
|
445
|
+
**MasterRecord now handles this like Entity Framework!** 🎉
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "masterrecord",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.9",
|
|
4
4
|
"description": "An Object-relational mapping for the Master framework. Master Record connects classes to relational database tables to establish a database with almost zero-configuration ",
|
|
5
5
|
"main": "MasterRecord.js",
|
|
6
6
|
"bin": {
|
package/readme.md
CHANGED
|
@@ -68,6 +68,30 @@ MasterRecord includes the following database drivers by default:
|
|
|
68
68
|
- `sync-mysql2@^1.0.8` - MySQL
|
|
69
69
|
- `better-sqlite3@^12.6.0` - SQLite
|
|
70
70
|
|
|
71
|
+
## Two Patterns: Entity Framework & Active Record
|
|
72
|
+
|
|
73
|
+
MasterRecord supports **both** ORM patterns - choose what feels natural:
|
|
74
|
+
|
|
75
|
+
### Active Record Style (Recommended for beginners)
|
|
76
|
+
```javascript
|
|
77
|
+
// Entity saves itself
|
|
78
|
+
const user = db.User.findById(1);
|
|
79
|
+
user.name = 'Updated';
|
|
80
|
+
await user.save(); // ✅ Entity knows how to save
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Entity Framework Style (Efficient for batch operations)
|
|
84
|
+
```javascript
|
|
85
|
+
// Context saves all tracked entities
|
|
86
|
+
const user = db.User.findById(1);
|
|
87
|
+
user.name = 'Updated';
|
|
88
|
+
await db.saveChanges(); // ✅ Batch save
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
**Read more:** [Active Record Pattern Guide](./ACTIVE_RECORD_PATTERN.md) | [Detached Entities Guide](./DETACHED_ENTITIES_GUIDE.md)
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
71
95
|
## Quick Start
|
|
72
96
|
|
|
73
97
|
### 1. Create a Context
|
|
@@ -137,21 +161,21 @@ masterrecord migrate AppContext
|
|
|
137
161
|
const AppContext = require('./app/models/context');
|
|
138
162
|
const db = new AppContext();
|
|
139
163
|
|
|
140
|
-
// Create
|
|
164
|
+
// Create (Active Record style)
|
|
141
165
|
const user = db.User.new();
|
|
142
166
|
user.name = 'Alice';
|
|
143
167
|
user.email = 'alice@example.com';
|
|
144
168
|
user.age = 28;
|
|
145
|
-
await
|
|
169
|
+
await user.save(); // Entity saves itself!
|
|
146
170
|
|
|
147
171
|
// Read with parameterized query
|
|
148
172
|
const alice = db.User
|
|
149
173
|
.where(u => u.email == $$, 'alice@example.com')
|
|
150
174
|
.single();
|
|
151
175
|
|
|
152
|
-
// Update
|
|
176
|
+
// Update (Active Record style)
|
|
153
177
|
alice.age = 29;
|
|
154
|
-
await
|
|
178
|
+
await alice.save(); // Entity saves itself!
|
|
155
179
|
|
|
156
180
|
// Delete
|
|
157
181
|
db.remove(alice);
|
|
@@ -1172,6 +1196,12 @@ context.saveChanges() // MySQL/SQLite (sync)
|
|
|
1172
1196
|
context.EntityName.add(entity)
|
|
1173
1197
|
context.remove(entity)
|
|
1174
1198
|
|
|
1199
|
+
// Attach detached entities (like Entity Framework's Update())
|
|
1200
|
+
context.attach(entity) // Attach and mark as modified
|
|
1201
|
+
context.attach(entity, { field: value }) // Attach with specific changes
|
|
1202
|
+
context.attachAll([entity1, entity2]) // Attach multiple entities
|
|
1203
|
+
await context.update('Entity', id, changes) // Update by primary key
|
|
1204
|
+
|
|
1175
1205
|
// Cache management
|
|
1176
1206
|
context.getCacheStats() // Get cache statistics
|
|
1177
1207
|
context.clearQueryCache() // Clear all cached queries
|
|
@@ -1203,6 +1233,9 @@ context.setQueryCacheEnabled(bool) // Enable/disable caching
|
|
|
1203
1233
|
// Convenience methods
|
|
1204
1234
|
.findById(id) // Find by primary key
|
|
1205
1235
|
.new() // Create new entity instance
|
|
1236
|
+
|
|
1237
|
+
// Entity methods (Active Record style)
|
|
1238
|
+
await entity.save() // Save this entity (and all tracked changes)
|
|
1206
1239
|
```
|
|
1207
1240
|
|
|
1208
1241
|
### Migration Methods
|