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
|
@@ -72,10 +72,24 @@ class EntityTrackerModel {
|
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
});
|
|
75
|
-
}
|
|
75
|
+
}
|
|
76
76
|
}
|
|
77
|
-
|
|
78
|
-
|
|
77
|
+
|
|
78
|
+
// Add Active Record-style .save() method
|
|
79
|
+
modelClass.save = async function() {
|
|
80
|
+
if (!this.__context) {
|
|
81
|
+
throw new Error('Cannot save: entity is not attached to a context');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Ensure entity is tracked
|
|
85
|
+
if (!this.__context.__trackedEntitiesMap.has(this.__ID)) {
|
|
86
|
+
this.__context.__track(this);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Save all tracked changes in the context
|
|
90
|
+
return await this.__context.saveChanges();
|
|
91
|
+
};
|
|
92
|
+
|
|
79
93
|
return modelClass;
|
|
80
94
|
}
|
|
81
95
|
|
|
@@ -465,6 +465,21 @@ class queryMethods{
|
|
|
465
465
|
}
|
|
466
466
|
}
|
|
467
467
|
|
|
468
|
+
// Add Active Record-style .save() method
|
|
469
|
+
newEntity.save = async function() {
|
|
470
|
+
if (!this.__context) {
|
|
471
|
+
throw new Error('Cannot save: entity is not attached to a context');
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// Ensure entity is tracked
|
|
475
|
+
if (!this.__context.__trackedEntitiesMap.has(this.__ID)) {
|
|
476
|
+
this.__context.__track(this);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Save all tracked changes in the context
|
|
480
|
+
return await this.__context.saveChanges();
|
|
481
|
+
};
|
|
482
|
+
|
|
468
483
|
// Track the entity
|
|
469
484
|
this.__context.__track(newEntity);
|
|
470
485
|
return newEntity;
|
package/context.js
CHANGED
|
@@ -662,6 +662,117 @@ class context {
|
|
|
662
662
|
this.clearQueryCache();
|
|
663
663
|
}
|
|
664
664
|
|
|
665
|
+
/**
|
|
666
|
+
* Attach a detached entity and mark it as modified
|
|
667
|
+
* Use this when an entity was loaded in a different context or passed from another service
|
|
668
|
+
* Similar to Entity Framework's context.Update() or Hibernate's session.merge()
|
|
669
|
+
*
|
|
670
|
+
* @param {object} entity - The detached entity to attach
|
|
671
|
+
* @param {object} changes - Optional: specific fields that were modified
|
|
672
|
+
*
|
|
673
|
+
* @example
|
|
674
|
+
* // Attach entity loaded elsewhere
|
|
675
|
+
* const task = await taskService.getTask(taskId);
|
|
676
|
+
* task.status = 'completed';
|
|
677
|
+
* db.attach(task); // Mark as modified
|
|
678
|
+
* await db.saveChanges();
|
|
679
|
+
*
|
|
680
|
+
* @example
|
|
681
|
+
* // Attach with specific changed fields
|
|
682
|
+
* db.attach(task, { status: 'completed', updated_at: new Date() });
|
|
683
|
+
* await db.saveChanges();
|
|
684
|
+
*/
|
|
685
|
+
attach(entity, changes = null) {
|
|
686
|
+
if (!entity) {
|
|
687
|
+
throw new Error('Cannot attach null or undefined entity');
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Ensure entity has required metadata
|
|
691
|
+
if (!entity.__entity || !entity.__entity.__name) {
|
|
692
|
+
throw new Error('Entity must have __entity metadata. Make sure it was loaded through MasterRecord.');
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
// Mark entity as modified
|
|
696
|
+
entity.__state = 'modified';
|
|
697
|
+
|
|
698
|
+
// If specific changes provided, mark only those fields as dirty
|
|
699
|
+
if (changes) {
|
|
700
|
+
entity.__dirtyFields = entity.__dirtyFields || [];
|
|
701
|
+
for (const fieldName in changes) {
|
|
702
|
+
entity[fieldName] = changes[fieldName];
|
|
703
|
+
if (!entity.__dirtyFields.includes(fieldName)) {
|
|
704
|
+
entity.__dirtyFields.push(fieldName);
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
} else {
|
|
708
|
+
// Mark all fields as potentially modified
|
|
709
|
+
entity.__dirtyFields = entity.__dirtyFields || [];
|
|
710
|
+
|
|
711
|
+
// If no dirty fields yet, mark all non-metadata fields as dirty
|
|
712
|
+
if (entity.__dirtyFields.length === 0) {
|
|
713
|
+
for (const fieldName in entity.__entity) {
|
|
714
|
+
if (!fieldName.startsWith('__') &&
|
|
715
|
+
entity.__entity[fieldName].type !== 'hasMany' &&
|
|
716
|
+
entity.__entity[fieldName].type !== 'hasOne') {
|
|
717
|
+
entity.__dirtyFields.push(fieldName);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
// Ensure context reference
|
|
724
|
+
entity.__context = this;
|
|
725
|
+
|
|
726
|
+
// Track the entity
|
|
727
|
+
this.__track(entity);
|
|
728
|
+
|
|
729
|
+
return entity;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
/**
|
|
733
|
+
* Attach multiple detached entities at once
|
|
734
|
+
*
|
|
735
|
+
* @example
|
|
736
|
+
* const tasks = await taskService.getTasks();
|
|
737
|
+
* tasks.forEach(t => t.status = 'completed');
|
|
738
|
+
* db.attachAll(tasks);
|
|
739
|
+
* await db.saveChanges();
|
|
740
|
+
*/
|
|
741
|
+
attachAll(entities) {
|
|
742
|
+
if (!Array.isArray(entities)) {
|
|
743
|
+
throw new Error('attachAll() requires an array of entities');
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
return entities.map(entity => this.attach(entity));
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
/**
|
|
750
|
+
* Update a detached entity by primary key
|
|
751
|
+
* Loads the entity, applies changes, and marks as modified
|
|
752
|
+
* Similar to Sequelize's Model.update()
|
|
753
|
+
*
|
|
754
|
+
* @example
|
|
755
|
+
* // Update without loading first
|
|
756
|
+
* await db.update('Task', { id: taskId }, { status: 'completed' });
|
|
757
|
+
* await db.saveChanges();
|
|
758
|
+
*/
|
|
759
|
+
async update(entityName, primaryKey, changes) {
|
|
760
|
+
// Get entity class
|
|
761
|
+
const EntityClass = this[entityName];
|
|
762
|
+
if (!EntityClass) {
|
|
763
|
+
throw new Error(`Entity ${entityName} not found in context`);
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
// Load entity
|
|
767
|
+
const entity = EntityClass.findById(primaryKey);
|
|
768
|
+
if (!entity) {
|
|
769
|
+
throw new Error(`${entityName} with id ${primaryKey} not found`);
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Apply changes and attach
|
|
773
|
+
return this.attach(entity, changes);
|
|
774
|
+
}
|
|
775
|
+
|
|
665
776
|
// __track(model){
|
|
666
777
|
// this.__trackedEntities.push(model);
|
|
667
778
|
// return model;
|
|
@@ -0,0 +1,477 @@
|
|
|
1
|
+
# Active Record Pattern - entity.save()
|
|
2
|
+
|
|
3
|
+
MasterRecord now supports **both** patterns:
|
|
4
|
+
|
|
5
|
+
1. **Entity Framework style** - `context.saveChanges()`
|
|
6
|
+
2. **Active Record style** - `entity.save()` ✨ NEW!
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Active Record Style (NEW)
|
|
11
|
+
|
|
12
|
+
### Basic Usage
|
|
13
|
+
|
|
14
|
+
```javascript
|
|
15
|
+
// Load entity
|
|
16
|
+
const task = db.Task.findById(1);
|
|
17
|
+
|
|
18
|
+
// Modify
|
|
19
|
+
task.status = 'completed';
|
|
20
|
+
task.completed_at = new Date();
|
|
21
|
+
|
|
22
|
+
// Save (like Active Record!)
|
|
23
|
+
await task.save(); // ✅ Entity saves itself!
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Create New Entity
|
|
27
|
+
|
|
28
|
+
```javascript
|
|
29
|
+
// Create
|
|
30
|
+
const task = db.Task.new();
|
|
31
|
+
task.name = 'New Task';
|
|
32
|
+
task.status = 'pending';
|
|
33
|
+
|
|
34
|
+
// Save
|
|
35
|
+
await task.save(); // ✅ Like Active Record!
|
|
36
|
+
|
|
37
|
+
console.log(task.id); // Auto-generated ID
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### Update Multiple Fields
|
|
41
|
+
|
|
42
|
+
```javascript
|
|
43
|
+
const user = db.User.findById(userId);
|
|
44
|
+
user.name = 'Updated Name';
|
|
45
|
+
user.email = 'new@example.com';
|
|
46
|
+
user.updated_at = new Date();
|
|
47
|
+
|
|
48
|
+
await user.save(); // ✅ All changes saved
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
---
|
|
52
|
+
|
|
53
|
+
## Solving the Detached Entity Problem
|
|
54
|
+
|
|
55
|
+
### Before (Manual attach)
|
|
56
|
+
|
|
57
|
+
```javascript
|
|
58
|
+
// ❌ Required manual attach
|
|
59
|
+
const task = await taskService.getTask(taskId);
|
|
60
|
+
task.status = 'completed';
|
|
61
|
+
db.attach(task); // Manual attach
|
|
62
|
+
await db.saveChanges();
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### After (Active Record style)
|
|
66
|
+
|
|
67
|
+
```javascript
|
|
68
|
+
// ✅ Just call .save()!
|
|
69
|
+
const task = await taskService.getTask(taskId);
|
|
70
|
+
task.status = 'completed';
|
|
71
|
+
await task.save(); // Entity saves itself!
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
**Why this works:**
|
|
75
|
+
- Entity has `__context` reference
|
|
76
|
+
- `.save()` automatically tracks the entity
|
|
77
|
+
- No manual `attach()` needed!
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Your Original Problem - SOLVED
|
|
82
|
+
|
|
83
|
+
### Your Original Code (BROKEN)
|
|
84
|
+
|
|
85
|
+
```javascript
|
|
86
|
+
class AnnotationService {
|
|
87
|
+
async createAnnotation(taskId, data) {
|
|
88
|
+
const task = await this._taskService.getTask(taskId);
|
|
89
|
+
|
|
90
|
+
const annotation = this._qaContext.Annotation.new();
|
|
91
|
+
annotation.task_id = taskId;
|
|
92
|
+
this._qaContext.Annotation.add(annotation);
|
|
93
|
+
|
|
94
|
+
task.status = 'completed'; // ❌ Not tracked
|
|
95
|
+
|
|
96
|
+
await this._qaContext.saveChanges(); // ❌ Task not saved
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Solution 1: Active Record Style (EASIEST) ✨
|
|
102
|
+
|
|
103
|
+
```javascript
|
|
104
|
+
class AnnotationService {
|
|
105
|
+
async createAnnotation(taskId, data) {
|
|
106
|
+
const task = await this._taskService.getTask(taskId);
|
|
107
|
+
|
|
108
|
+
const annotation = this._qaContext.Annotation.new();
|
|
109
|
+
annotation.task_id = taskId;
|
|
110
|
+
annotation.data = data;
|
|
111
|
+
|
|
112
|
+
task.status = 'completed';
|
|
113
|
+
|
|
114
|
+
// Save both (Active Record style!)
|
|
115
|
+
await annotation.save(); // ✅ Saves annotation
|
|
116
|
+
await task.save(); // ✅ Saves task
|
|
117
|
+
|
|
118
|
+
return annotation;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Solution 2: Entity Framework Style
|
|
124
|
+
|
|
125
|
+
```javascript
|
|
126
|
+
class AnnotationService {
|
|
127
|
+
async createAnnotation(taskId, data) {
|
|
128
|
+
const task = await this._taskService.getTask(taskId);
|
|
129
|
+
|
|
130
|
+
const annotation = this._qaContext.Annotation.new();
|
|
131
|
+
annotation.task_id = taskId;
|
|
132
|
+
this._qaContext.Annotation.add(annotation);
|
|
133
|
+
|
|
134
|
+
task.status = 'completed';
|
|
135
|
+
this._qaContext.attach(task); // Attach detached entity
|
|
136
|
+
|
|
137
|
+
await this._qaContext.saveChanges(); // Save all
|
|
138
|
+
return annotation;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
### Solution 3: Pure Active Record
|
|
144
|
+
|
|
145
|
+
```javascript
|
|
146
|
+
class AnnotationService {
|
|
147
|
+
async createAnnotation(taskId, data) {
|
|
148
|
+
const task = await this._taskService.getTask(taskId);
|
|
149
|
+
task.status = 'completed';
|
|
150
|
+
await task.save(); // Save task first
|
|
151
|
+
|
|
152
|
+
const annotation = this._qaContext.Annotation.new();
|
|
153
|
+
annotation.task_id = taskId;
|
|
154
|
+
annotation.data = data;
|
|
155
|
+
await annotation.save(); // Save annotation
|
|
156
|
+
|
|
157
|
+
return annotation;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Comparison: Both Patterns
|
|
165
|
+
|
|
166
|
+
### Entity Framework Style
|
|
167
|
+
|
|
168
|
+
```javascript
|
|
169
|
+
// Load entities
|
|
170
|
+
const user = db.User.findById(1);
|
|
171
|
+
const task = db.Task.findById(2);
|
|
172
|
+
|
|
173
|
+
// Modify both
|
|
174
|
+
user.name = 'Updated';
|
|
175
|
+
task.status = 'completed';
|
|
176
|
+
|
|
177
|
+
// Save all at once
|
|
178
|
+
await db.saveChanges(); // Batch save
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
**Pros:**
|
|
182
|
+
- ✅ Batch operations (efficient)
|
|
183
|
+
- ✅ Transaction-like behavior
|
|
184
|
+
- ✅ One save call for multiple entities
|
|
185
|
+
|
|
186
|
+
**Cons:**
|
|
187
|
+
- ❌ Less intuitive
|
|
188
|
+
- ❌ Need to track which context has which entities
|
|
189
|
+
|
|
190
|
+
### Active Record Style (NEW)
|
|
191
|
+
|
|
192
|
+
```javascript
|
|
193
|
+
// Load entities
|
|
194
|
+
const user = db.User.findById(1);
|
|
195
|
+
const task = db.Task.findById(2);
|
|
196
|
+
|
|
197
|
+
// Modify and save individually
|
|
198
|
+
user.name = 'Updated';
|
|
199
|
+
await user.save(); // Save user
|
|
200
|
+
|
|
201
|
+
task.status = 'completed';
|
|
202
|
+
await task.save(); // Save task
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
**Pros:**
|
|
206
|
+
- ✅ More intuitive (entity saves itself)
|
|
207
|
+
- ✅ No detached entity issues
|
|
208
|
+
- ✅ Works across contexts
|
|
209
|
+
- ✅ Familiar to Rails developers
|
|
210
|
+
|
|
211
|
+
**Cons:**
|
|
212
|
+
- ❌ Multiple database calls
|
|
213
|
+
- ❌ No automatic batching
|
|
214
|
+
|
|
215
|
+
---
|
|
216
|
+
|
|
217
|
+
## When to Use Each Pattern
|
|
218
|
+
|
|
219
|
+
### Use `entity.save()` when:
|
|
220
|
+
|
|
221
|
+
✅ Working with single entities
|
|
222
|
+
✅ Entity from external service (detached)
|
|
223
|
+
✅ Quick updates to one entity
|
|
224
|
+
✅ Familiar with Active Record (Rails)
|
|
225
|
+
✅ Want explicit control
|
|
226
|
+
|
|
227
|
+
```javascript
|
|
228
|
+
// Single entity updates
|
|
229
|
+
const user = db.User.findById(userId);
|
|
230
|
+
user.last_login = new Date();
|
|
231
|
+
await user.save(); // Clear and simple
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
### Use `context.saveChanges()` when:
|
|
235
|
+
|
|
236
|
+
✅ Batch operations (multiple entities)
|
|
237
|
+
✅ Need transaction-like behavior
|
|
238
|
+
✅ Performance critical (fewer DB calls)
|
|
239
|
+
✅ Familiar with Entity Framework
|
|
240
|
+
|
|
241
|
+
```javascript
|
|
242
|
+
// Batch updates
|
|
243
|
+
const users = db.User.where(u => u.active == false).toList();
|
|
244
|
+
users.forEach(u => u.deleted_at = new Date());
|
|
245
|
+
await db.saveChanges(); // One batch save
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
## Complete Examples
|
|
251
|
+
|
|
252
|
+
### Example 1: User Registration
|
|
253
|
+
|
|
254
|
+
```javascript
|
|
255
|
+
async function registerUser(data) {
|
|
256
|
+
const db = new AppContext();
|
|
257
|
+
|
|
258
|
+
// Create user
|
|
259
|
+
const user = db.User.new();
|
|
260
|
+
user.email = data.email;
|
|
261
|
+
user.name = data.name;
|
|
262
|
+
user.password_hash = await hash(data.password);
|
|
263
|
+
|
|
264
|
+
// Save (Active Record style)
|
|
265
|
+
await user.save(); // ✅
|
|
266
|
+
|
|
267
|
+
// Send welcome email
|
|
268
|
+
await sendWelcomeEmail(user);
|
|
269
|
+
|
|
270
|
+
return user;
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
### Example 2: Task Completion
|
|
275
|
+
|
|
276
|
+
```javascript
|
|
277
|
+
async function completeTask(taskId) {
|
|
278
|
+
const db = new AppContext();
|
|
279
|
+
|
|
280
|
+
const task = db.Task.findById(taskId);
|
|
281
|
+
task.status = 'completed';
|
|
282
|
+
task.completed_at = new Date();
|
|
283
|
+
|
|
284
|
+
// Save (Active Record style)
|
|
285
|
+
await task.save(); // ✅
|
|
286
|
+
|
|
287
|
+
// Notify user
|
|
288
|
+
await notifyUser(task.user_id, 'Task completed');
|
|
289
|
+
|
|
290
|
+
return task;
|
|
291
|
+
}
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Example 3: Bulk Update (Entity Framework style)
|
|
295
|
+
|
|
296
|
+
```javascript
|
|
297
|
+
async function archiveOldTasks() {
|
|
298
|
+
const db = new AppContext();
|
|
299
|
+
|
|
300
|
+
const oldTasks = db.Task
|
|
301
|
+
.where(t => t.status == $$, 'completed')
|
|
302
|
+
.where(t => t.completed_at < $$, thirtyDaysAgo)
|
|
303
|
+
.toList();
|
|
304
|
+
|
|
305
|
+
oldTasks.forEach(task => {
|
|
306
|
+
task.archived = true;
|
|
307
|
+
task.archived_at = new Date();
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
// Batch save (Entity Framework style)
|
|
311
|
+
await db.saveChanges(); // ✅ Efficient batch update
|
|
312
|
+
|
|
313
|
+
return oldTasks.length;
|
|
314
|
+
}
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
### Example 4: Mixed Pattern
|
|
318
|
+
|
|
319
|
+
```javascript
|
|
320
|
+
async function processOrder(orderId) {
|
|
321
|
+
const db = new AppContext();
|
|
322
|
+
|
|
323
|
+
// Load order
|
|
324
|
+
const order = db.Order.findById(orderId);
|
|
325
|
+
order.status = 'processing';
|
|
326
|
+
await order.save(); // Save immediately (Active Record)
|
|
327
|
+
|
|
328
|
+
// Create line items
|
|
329
|
+
const items = order.items.map(item => {
|
|
330
|
+
const lineItem = db.LineItem.new();
|
|
331
|
+
lineItem.order_id = orderId;
|
|
332
|
+
lineItem.product_id = item.productId;
|
|
333
|
+
lineItem.quantity = item.quantity;
|
|
334
|
+
return lineItem;
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
// Batch save line items (Entity Framework)
|
|
338
|
+
await db.saveChanges(); // ✅ Efficient for multiple items
|
|
339
|
+
|
|
340
|
+
return order;
|
|
341
|
+
}
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## Rails vs MasterRecord
|
|
347
|
+
|
|
348
|
+
### Rails (Active Record)
|
|
349
|
+
|
|
350
|
+
```ruby
|
|
351
|
+
# Create
|
|
352
|
+
user = User.new
|
|
353
|
+
user.name = 'John'
|
|
354
|
+
user.save
|
|
355
|
+
|
|
356
|
+
# Update
|
|
357
|
+
user = User.find(1)
|
|
358
|
+
user.name = 'Jane'
|
|
359
|
+
user.save
|
|
360
|
+
|
|
361
|
+
# Delete
|
|
362
|
+
user.destroy
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
### MasterRecord (Active Record style)
|
|
366
|
+
|
|
367
|
+
```javascript
|
|
368
|
+
// Create
|
|
369
|
+
const user = db.User.new();
|
|
370
|
+
user.name = 'John';
|
|
371
|
+
await user.save();
|
|
372
|
+
|
|
373
|
+
// Update
|
|
374
|
+
const user = db.User.findById(1);
|
|
375
|
+
user.name = 'Jane';
|
|
376
|
+
await user.save();
|
|
377
|
+
|
|
378
|
+
// Delete
|
|
379
|
+
db.remove(user);
|
|
380
|
+
await db.saveChanges(); // Or await user.save()
|
|
381
|
+
```
|
|
382
|
+
|
|
383
|
+
---
|
|
384
|
+
|
|
385
|
+
## Important Notes
|
|
386
|
+
|
|
387
|
+
### 1. `.save()` saves ALL tracked changes
|
|
388
|
+
|
|
389
|
+
```javascript
|
|
390
|
+
const db = new AppContext();
|
|
391
|
+
|
|
392
|
+
const user1 = db.User.findById(1);
|
|
393
|
+
user1.name = 'Updated 1';
|
|
394
|
+
|
|
395
|
+
const user2 = db.User.findById(2);
|
|
396
|
+
user2.name = 'Updated 2';
|
|
397
|
+
|
|
398
|
+
// Calling save() on either entity saves BOTH
|
|
399
|
+
await user1.save(); // Saves user1 AND user2!
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
**Why?** `.save()` calls `context.saveChanges()`, which saves all tracked entities in that context.
|
|
403
|
+
|
|
404
|
+
**To save only one entity:** Use separate contexts or save immediately:
|
|
405
|
+
|
|
406
|
+
```javascript
|
|
407
|
+
// Option 1: Separate contexts
|
|
408
|
+
const db1 = new AppContext();
|
|
409
|
+
const user1 = db1.User.findById(1);
|
|
410
|
+
user1.name = 'Updated 1';
|
|
411
|
+
await user1.save(); // Only saves user1
|
|
412
|
+
|
|
413
|
+
const db2 = new AppContext();
|
|
414
|
+
const user2 = db2.User.findById(2);
|
|
415
|
+
user2.name = 'Updated 2';
|
|
416
|
+
await user2.save(); // Only saves user2
|
|
417
|
+
|
|
418
|
+
// Option 2: Save immediately
|
|
419
|
+
const db = new AppContext();
|
|
420
|
+
const user1 = db.User.findById(1);
|
|
421
|
+
user1.name = 'Updated 1';
|
|
422
|
+
await user1.save(); // Saves and clears tracking
|
|
423
|
+
|
|
424
|
+
const user2 = db.User.findById(2);
|
|
425
|
+
user2.name = 'Updated 2';
|
|
426
|
+
await user2.save(); // Only user2 tracked at this point
|
|
427
|
+
```
|
|
428
|
+
|
|
429
|
+
### 2. Entity must have `__context`
|
|
430
|
+
|
|
431
|
+
```javascript
|
|
432
|
+
// ✅ WORKS: Entity loaded through MasterRecord
|
|
433
|
+
const user = db.User.findById(1);
|
|
434
|
+
await user.save(); // Has __context reference
|
|
435
|
+
|
|
436
|
+
// ❌ ERROR: Plain object
|
|
437
|
+
const user = { id: 1, name: 'Test' };
|
|
438
|
+
await user.save(); // Error: no __context
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### 3. Async/await required
|
|
442
|
+
|
|
443
|
+
```javascript
|
|
444
|
+
// ✅ GOOD
|
|
445
|
+
await user.save();
|
|
446
|
+
|
|
447
|
+
// ❌ BAD: Doesn't wait for save
|
|
448
|
+
user.save();
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
---
|
|
452
|
+
|
|
453
|
+
## Summary
|
|
454
|
+
|
|
455
|
+
**MasterRecord now supports Active Record style!** 🎉
|
|
456
|
+
|
|
457
|
+
```javascript
|
|
458
|
+
// Both patterns work:
|
|
459
|
+
|
|
460
|
+
// Active Record (NEW)
|
|
461
|
+
await entity.save();
|
|
462
|
+
|
|
463
|
+
// Entity Framework
|
|
464
|
+
await context.saveChanges();
|
|
465
|
+
|
|
466
|
+
// Choose what feels natural for your use case!
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
**Solves the detached entity problem:**
|
|
470
|
+
- No need for manual `attach()`
|
|
471
|
+
- Entity knows its context
|
|
472
|
+
- Just call `.save()`!
|
|
473
|
+
|
|
474
|
+
**Best of both worlds:** ✨
|
|
475
|
+
- Active Record: Intuitive entity-level saves
|
|
476
|
+
- Entity Framework: Efficient batch operations
|
|
477
|
+
- You choose which to use!
|