@NodeStamp: refusing a save when the node changed
Strongly recommended on any class that is loaded, changed and saved. It plays the part of JPA’s @Version: optimistic
locking, with a random value in place of a counter.
@NodeFragment({ labels: ['Person'] })export class Person { @NodeId() id: string; name: string; @NodeStamp() stamp?: string;}The field is an optional string on a @NodeFragment, one to a fragment. A stamp is a node’s, so it is refused on a
view and on a relationship fragment.
The Stamp
Section titled “The Stamp”The stamp is two random tokens of 64 bits each, stored on the node under __drivine.stamp as
3fa9c1d27b40e8a6:91d0f4b2c7ee5a13, whatever the field is called. Loading fills the field. Treat it as opaque.
- The data token, the first, speaks for the node’s properties and labels.
- The relationship token, the second, speaks for its relationships, of every type, in both directions.
- To mark a node is to replace one of its tokens. A save marks what it changes, and so must Cypher of your own.
Every node a save writes is given a stamp if it has none, whether or not its class declares a @NodeStamp field.
When a Save Is Refused
Section titled “When a Save Is Refused”A save is refused when what it would overwrite changed since the object was loaded. Every save of an object that
carries a stamp compares the data token. A save with replace compares the relationship token too. A refused save
writes nothing and throws StaleObjectException.
| Since the object was loaded, another writer… | save |
save with replace |
update |
|---|---|---|---|
| changed a property or a label of the root | refused | refused | loads again and re-applies the change |
| added or removed a relationship of the root, or changed one’s properties | applied | refused | applied |
| changed a related node | applied: the related node is written over the change | the same | applied |
| deleted the root | refused, deleted is true |
refused, deleted is true |
returns undefined |
| changed anything with Cypher that doesn’t mark it | not noticed | not noticed | not noticed |
An object whose stamp is undefined isn’t checked: it makes the node or overwrites it. The save is one statement that takes the node’s write lock before it compares, so of several writers holding the same stamp exactly one succeeds.
try { await gom.save(edited);} catch (e) { if (e instanceof StaleObjectException && !e.deleted) { // another writer got there first: e.foundStamp is what the node carries now await gom.update(Person, edited.id, (it) => { it.name = edited.name; }); } else { throw e; }}Existing Data
Section titled “Existing Data”A node written before stamps has none, and loads with the field undefined. The first save of it isn’t checked and gives it a stamp. To stamp every node of a label at once:
await pm.execute( new QuerySpecification(`MATCH (n:Person) WHERE n.\`__drivine.stamp\` IS NULL SET ${Stamps.setClause('n')}`));Cypher You Write Yourself
Section titled “Cypher You Write Yourself”Cypher should mark what it changes: a write that doesn’t is not noticed, and a checked save of an object loaded
before it is applied over it. Stamps.setClause marks a node whose properties or labels you change, and
Stamps.linksClause each end of a relationship you add, remove or change:
`MATCH (p:Person {id: $id}) SET p.name = $name, ${Stamps.setClause('p')}`;
`MATCH (a:Person {id: $a}), (b:Person {id: $b}) CREATE (a)-[:KNOWS]->(b) SET ${Stamps.linksClause('a')}, ${Stamps.linksClause('b')}`;The property’s name holds a dot, so Cypher that names it quotes it: n.`__drivine.stamp`. Cypher that replaces
every property (SET n = $props) removes the stamp, and the next checked save of an object loaded before is refused.
What a Stamp Doesn’t Cover
Section titled “What a Stamp Doesn’t Cover”- A related node isn’t checked:
savewrites it whole.updatewrites only what the change altered. - An object with no stamp isn’t checked, nor is a fragment in a
saveAll. - One relationship token covers every relationship: a
replaceof one list is refused when a relationship of another type was added. - Deleting a node removes its relationships without marking the nodes at their other ends.

