To change a stored object, read it, copy it, edit the copy, then write the copy back:
var obj = await dao.find(id); // 1. read (the returned object is shared; do not mutate it)
var clone = obj.clone(); // 2. copy (a mutable duplicate)
clone.status = 'DONE'; // 3. edit (set the exact property)
await dao.put(clone); // 4. write (this is the save)
The put is the step people miss. A DAO does not watch your object for changes. It only records what you hand back to it. No put, no update.
A DAO (Data Access Object) is the read/write interface to a collection of objects. Updating works the same way as creating: you call put with an object that carries the id you want to replace. The DAO overwrites the stored value with the one you pass.
The twist is that objects coming out of a DAO are shared with the DAO, and in Java they are frozen. You must not edit them in place. You clone first, edit the clone, and put the clone back.
| File | Purpose |
|---|---|
foam/lang/FObject.js |
Defines clone and copyFrom for the JavaScript side |
foam/lang/FObject.java |
Defines fclone; throws Object is frozen. from assertNotFrozen |
foam/dao/MDAO.java |
In-memory DAO; clones and freezes objects on store |
foam/dao/MapDAO.java |
Map-backed DAO; calls obj.freeze() on store |
foam/dao/FreezingDAO.js |
Decorator that freezes objects passing through |
A DAO is rarely a single object. It is a stack of decorators wrapping a storage core. A put travels down the stack, so validation, authorization, logging, and journaling all run on the object you hand in:
+-------------------------------------------------------------+
| DAO Stack |
| |
| put(clone) |
| | |
| v |
| +-------------+ +-------------+ +-------------------+ |
| | Validating |-->| Auth / |-->| Logging / | |
| | Decorator | | Permission | | Journaling | |
| +-------------+ +-------------+ +-------------------+ |
| | |
| v |
| +-----------------+ |
| | Storage Core | |
| | (MDAO / MapDAO)| |
| | clone + freeze | |
| +-----------------+ |
+-------------------------------------------------------------+
The storage core freezes what it keeps. MDAO.java clones and freezes on the way in; MapDAO.java calls obj.freeze(). That frozen instance is what later find and select calls return, and it may be shared across threads, which is why mutating it is unsafe.
| Concept | Description |
|---|---|
frozen |
In Java, a stored object is read-only. Calling a setter on it fails. The DAO caches and shares these instances, so editing one would corrupt shared state. |
clone / fclone |
Produces a mutable, independent copy. clone is the JavaScript method; fclone is the Java method. |
copyFrom |
Bulk-applies property values from a map or another object onto a target. Useful for refreshing a live object after a write returns. |
put / put_ |
Writes an object into the DAO, keyed by id. put uses the DAO’s own context; put_(x, obj) takes an explicit context. |
id |
The key that decides insert versus update. Same id replaces the existing row; new id inserts. |
put return value |
put returns the stored object (often re-frozen, sometimes enriched by decorators). Use it, do not assume your input is now the source of truth. |
+----------+ +----------+ +-----------+ +---------+ +---------+
| find / |--->| clone |--->| set the |--->| put |--->| stored |
| select | | (mutable | | property | | (clone) | | & frozen|
| (frozen) | | copy) | | you want | | | | again |
+----------+ +----------+ +-----------+ +---------+ +---------+
^ |
| returns the saved object |
+--------------------------------------------------------------+
Read returns a shared object (frozen in Java). Clone makes it editable. You set the exact properties. Put hands the clone to the DAO stack, which validates, authorizes, journals, and stores it, re-freezing the result. The value put returns is the canonical stored object.
The storage core deliberately freezes objects so they can be cached and shared safely. A getter on a frozen object works; a setter does not. On the Java side, the setter path runs assertNotFrozen, which throws Object is frozen when the object is frozen. On the JavaScript side, you would be mutating an instance the DAO still holds and shares, which silently corrupts cached state. Cloning sidesteps both problems: you own the copy outright.
Cloning and editing change nothing in storage. The clone is a private copy in your code. The DAO has no reference to it and no change listener on it. The update becomes real only when you call put, which sends the clone through the decorator stack down to the storage core. Treat find as read, clone/edit as staging, and put as commit.
var fresh = await dao.find(id); // shared with the DAO
var clone = fresh.clone(); // mutable copy
clone.status = 'APPROVED';
clone.reviewedBy = currentUser;
await dao.put(clone); // committed
Re-reading with find right before the clone is a good habit. It reduces the window in which another writer changed the record between your read and your write.
When the same object instance must reflect what was stored (for example, a view model bound to the UI), copy the returned value back onto it:
var saved = await dao.put(clone);
this.copyFrom(saved); // pull stored values, including any set by decorators
copyFrom accepts either a plain map or another object. From a map it copies every key that names a property; from another object it copies the properties the two share, skipping values that are still at their default.
MyModel frozen = (MyModel) dao.find(id);
MyModel clone = (MyModel) frozen.fclone(); // mutable copy
clone.setStatus("APPROVED");
dao.put_(x, clone); // committed with explicit context
Use put_(x, obj) when you need to pass a specific context, such as inside a service, decorator, or rule. Use put(obj) when the DAO’s own context is correct.
Each object from a select is frozen, so clone inside the loop before editing:
List<MyModel> rows = ((ArraySink) dao.select(new ArraySink())).getArray();
for ( MyModel row : rows ) {
MyModel clone = (MyModel) row.fclone();
clone.setProcessed(true);
dao.put_(x, clone);
}
select with no sink resolves to an ArraySink; read its results from .array. Each object in that array is shared with the DAO, so clone inside the loop before editing, and await each put:
var sink = await dao.where(M.EQ(MyModel.PROCESSED, false)).select();
for ( var row of sink.array ) {
var clone = row.clone(); // each row from select() is shared with the DAO
clone.processed = true;
await dao.put(clone);
}
The same rule as the single-object case applies per row: clone, edit the clone, put the clone. Editing row directly would either fail on a frozen instance (Java) or change an object the DAO still shares (JavaScript).
| Need | Use |
|---|---|
One object by id |
dao.find(id) |
| One object by predicate | dao.find(predicate) |
| A list to iterate | dao.select(new ArraySink()) |