Saving Data
A save writes what the object holds. It doesn’t depend on whether the object was loaded, or on anything the manager remembers, because the manager remembers nothing.
await gom.save(view); // every field that is set; relationships are added, never removedawait gom.save(view, { replace: ['assignedTo'] }); // this field's list is the whole listawait gom.save(view, { replace: 'all' }); // every relationship field's list is the whole listawait gom.save(chunk, { except: ['embedding'] }); // write everything but theseawait gom.save(person, { only: ['name'] }); // write just theseawait gom.update(Person, id, (it) => { it.name = 'Ada'; }); // load, change, save what differsawait gom.create(person); // make it only if it is not there; undefined when it isawait gom.edges.unrelate(nodeRef(Issue, a), nodeRef(Person, b), 'ASSIGNED_TO');A save is one statement: the root, the relationships it drops, each related node and the relationship to it. It is applied whole or not at all, with or without a transaction, on every engine.
only and except name fields of the object, and for a view fields of its root. A view’s relationships are written
whatever they name. Give one or the other, not both.
save returns the saved object. On a class with a @NodeStamp
field, use the returned object from then on: it carries the stamps the save left, on the root and on each related node
that has one.
Relationships
Section titled “Relationships”A save adds the relationships the object holds and removes none. To remove, name the field in replace: its list is
then the whole list.
- A field removes only what it loads: relationships of its type and direction, to nodes with its target’s labels.
removedTargets: RemovedTargets.DELETE_UNREFERENCEDalso deletes a removed target that nothing else refers to the way the field did. The root is never deleted, nor is a node the object still holds in another field.replace: 'all'covers every relationship field of the view itself. It is refused for an object that carries no stamp, because the lists of an object built from scratch are not what the store holds.- A list to replace that is null or undefined is refused. An empty list removes every relationship of the field, and a single-valued field that is null or undefined removes its relationship.
replaceis refused, on a root that carries a stamp, if any relationship of the root was added, removed or changed since the object was loaded. A save that only adds is not: two writers who loaded the same view can each add to it.- An
UNDIRECTEDfield is met by a relationship stored either way. One is made, from the root, only when there is none. - A read-only field is never written: a
@ReadOnly()field, a@GraphPath, a@Countor@Aggregate, and a list of fragments read over several hops.
Load, Change and Save (update)
Section titled “Load, Change and Save (update)”update loads the object, applies your change, and writes only what the change altered: the fields that differ, a
field set to null, and for a view the relationships it added or dropped. Your change may alter the object it is given,
or return a copy.
const saved = await gom.update(Person, id, (person) => { person.name = 'Ada';});- If the node changed between the load and the save,
updateloads it again and re-applies the change: three attempts by default (the fourth argument), thenStaleObjectException. That needs a@NodeStampfield; without one a change by another writer goes unnoticed. - It returns undefined when there is no such node, or the node is deleted in between. It never makes a node.
- A change that gives the object another id or another class is refused.
Create If Absent (create)
Section titled “Create If Absent (create)”create makes the node only if no node has its id, in one statement. If one does, it writes nothing and returns
undefined. With update, that saves an aggregate without losing any writer’s data:
const stored = (await gom.create(proposition)) ?? (await gom.update(Proposition, proposition.id, (it) => it.merge(proposition)));- An object that carries a stamp was loaded, and is refused.
- Two writers at the same moment can both find the node absent. On Neo4j, Memgraph and Oracle only a uniqueness
constraint on the id keeps the second from making the node again:
uniquenessConstraint('Proposition', 'id'). FalkorDB runs one write at a time, so there the second always finds the first’s node.
Null-Write Policy (NullPolicy)
Section titled “Null-Write Policy (NullPolicy)”await gom.save(chunk); // IGNORE (default): merge-patchawait gom.save(chunk, { nullPolicy: NullPolicy.CLEAR }); // a full overwrite: nulls clearIGNOREwrites only fields that are set; a null or undefined field leaves the stored value. A partly-loaded object never destroys stored data, an embedding included.CLEARmakes the object authoritative: a null field clears its property. In JavaScript only keys the object holds can be seen, so a declared field that was never assigned is not cleared. Use it with a complete object.
In a view the policy governs the root. A related node is written as under IGNORE.
Batch Save (saveAll)
Section titled “Batch Save (saveAll)”const saved = await gom.saveAll(views);await gom.saveAll(views, { replace: ['mentions'] });await gom.saveAll(chunks, { only: ['summary', 'summaryModel'] });- Fragments go in one statement for each 1,000, of any classes and labels. A view is a statement of its own. Outside a transaction of yours the statements run in one transaction, so a failure leaves nothing written; inside one they join it. FalkorDB has no transactions: see FalkorDB.
- The objects come back in the order given, carrying the stamps the batch left.
- A batch that only adds doesn’t check a fragment’s stamp: a stale one is written anyway, and keeps the stamp it had, so
a later
saveof it is refused. replaceapplies to views; a batch holding a fragment is refused with it. A stale view throwsStaleObjectException, and the batch then writes nothing.- With
onlyorexcept, a node deleted since its object was loaded is made again. To write only where the node still exists, usepatchAll.
Conditional Write by Id (patch)
Section titled “Conditional Write by Id (patch)”Write named fields of one node, by its id, if the node meets a condition, in one statement. The node isn’t loaded: the store evaluates the condition under the node’s lock.
const result: PatchResult = await gom.patch(Session, sessionId, { set: { lastActivityAt: activityAt }, where: (q) => anyOf(q.lastActivityAt.isNull(), q.lastActivityAt.lt(activityAt))});result.found; // a node has the idresult.applied; // the condition held, so the fields were writtenresult.changed; // a value differed, and the node's stamp was renewed| Outcome | found |
applied |
changed |
|---|---|---|---|
| No node has that id | false | false | false |
| The condition didn’t hold | true | false | false |
| It held, and every value was already as set | true | true | false |
| It held, and a value differed | true | true | true |
On a view the fields are the root’s, and the condition may name a relationship:
const narrated = (await gom.patch(MessageInSession, messageId, { set: { narration }, where: (q) => q.session.sessionId.eq(sessionId)})).applied; // false for a message that is there, but in another sessionsetnames fields to write, andclearfields to remove.- It never makes a node, isn’t checked against a stamp, and hands none back. A node it changed has a new stamp, so an object loaded before is refused when saved: load it again.
- The id, the stamp, a bag’s key, a
@NodeLabelsfield and a related node’s field can’t be written.
Batch Patch (patchAll)
Section titled “Batch Patch (patchAll)”Write named fields of many nodes, where each is there, making none:
const results = await gom.patchAll(embedded, { only: ['embedding', 'embeddingModel'] });const rewritten = results.filter((it) => it.found).length;There is one PatchResult for each object, in order. only or except is required: to write every field, save.
Under NullPolicy.IGNORE a named field that is null is left as the store has it.
Saves and Transactions
Section titled “Saves and Transactions”- A save joins the transaction it runs in, a
@Transactional()method’s orrunInTransaction’s. Outside one it is its own. StaleObjectExceptionleaving a@Transactional()method rolls the transaction back. A refused save has written nothing; what the transaction wrote before it is undone only if the transaction rolls back.- A statement the engine turns away because another writer was changing the same node is run again, after a pause, outside a transaction. Inside one it fails, and the transaction’s owner decides.

