The most interesting and powerful access mode is STGM_TRANSACTED, which means that any changes made to that storage are not published to its own parent until the client calls Commit. (An element opened with STGM_DIRECT publishes changes immediately.) If that parent is a storage element itself, these changes are scoped by that parent's own access mode. If that parent is a disk file, changes are then written to disk. Changes include the creation, deletion, or modification of any of the elements in the storage, regardless of whether they are substorages, streams, or streams within the substorage of a substorage of a substorage.
In other words, transactioning is a relationship between the transactioned element and its parent element or file. When multiple levels of transactioning exist, changes at the lowest level are not permanent until they percolate upward to the root and the root is committed. In other words, a Commit only publishes changes to the next layer up. This is very important in File Save (and File Save As) operations because a client must be sure to commit every transacted element from the bottom up. If it doesn't, changes will be lost when the root storage is committed.
To illustrate this concept, let's say we have three element objects—A, B, and C—all opened with STGM_TRANSACTED:
If we modify information in C and call its Commit, only B becomes aware of those changes. B is now considered dirty in the eyes of A, so we must commit B in order to save its changes within the scope of A:
On the other hand, if B is opened in direct mode, it immediately publishes the changes in C up to A. If A is a direct mode root storage, those changes go directly to the underlying storage medium.
The whole trick with multilevel transactioning is to walk through the hierarchy and commit everything that has been changed, starting from the bottom and working your way up. The Patron sample we'll see later distributes a Save command from the document to each page, and each page distributes the command to each tenant. When each tenant has committed its storage, each page can commit its storage, and the document can perform the final commit.
An interesting feature of transactioning is that you can open an element as read-only transacted and manipulate it as if it were read/write. The only operation that will not succeed is Commit.
The Commit member functions in IStorage and IStream support a number of different semantics depending on a combination of the following flags passed as the only argument to Commit:
Flag | Description |
STGC_DEFAULT | No special semantics; simply commit changes. |
STGC_ONLYIFCURRENT | When an element is shared between two threads (or processes), this flag prevents one thread from overwriting changes made by another thread or process that has already committed changes. Commit will return STG_E_NOTCURRENT when another commit has already happened. In this case, the calling thread has to reconcile its changes with the new contents of the element before calling Commit without the STGC_ONLYIFCURRENT flag. This flag allows a finer granularity of sharing to occur between end users on a network than was previously available through traditional files—multiple applications can open the same element as read/write as long as they don't specify exclusive access. |
STGC_OVERWRITE | Allows new data to overwrite old data, resulting in smaller space requirements. This should be used only in a low-memory situation or when the storage medium is already full. It is risky because the status of the element is in limbo during the commit, so a failure to complete the operation (such as a power failure) would result in data corruption—the element would contain a mixture of old and new information. |
STGC_DANGEROUSLY- | OLE designers never said they couldn't be verbose. This flag specifies that the commit should not attempt to flush buffers after changes are written (which would ensure that the data ends up on the storage medium before Commit returns). Using this flag will make Commit faster but riskier because for a short time the data will exist only in whatever cache is being used on the underlying medium. Using this flag is not any riskier than using traditional file I/O today because such file I/O is always at the whim of the file- system caching. |
Closely related to the Commit function and equally important in the scheme of transactioning is Revert. This function discards any changes that have been made to a transacted element since the last Commit (or open); it is a mere no-op when direct mode is in operation. It is very important to remember that calling a transacted element's Release without calling Commit first implicitly calls Revert.
A Revert call on a storage object will effectively revert down the hierarchy all changes to everything within it, so a Revert call to a root storage would effectively reset everything to the last committed state. When that root storage is connected to a file, this makes for an easy implementation of a Revert To Last Save command and makes it simple to close a file without saving changes—just call Release.
When you revert a storage, all open substorages and streams underneath that storage in the hierarchy must be closed (you must call Release) and reopened; otherwise, subsequent calls to those objects will fail outright with an STG_E_REVERTED code.
Transacted mode enables a new technique in application design. Because changes made to transacted storages and streams will be recorded in memory or in temporary files, making changes to a transacted element might be only slightly slower than writing directly to memory. Applications usually need to read structures out of a disk file into memory, modify them in memory, and then write them back to the disk file during a save. Transactioning allows these structures to remain in the file as a stream with little effect on overall performance. In using streams for such structures, you also gain a number of benefits:
When a memory structure contains pointers, keeping the pointers in a stream makes committing the stream senseless. However, even for small structures that you will store persistently in a file, this technique can save you from keeping the same data in a memory structure. When you need it, load it from a stream. When you change it, write it to the stream. When you save, commit everything. Because you want the data in permanent storage anyway, why not leave it there?
Configuration structures, such as LOGFONT, and any structure that is likely to grow over time are great candidates for this technique. An application seldom reads and writes configuration data, but such data is always written to storage. Keeping things in the streams they occupy in the permanent file can greatly simplify or eliminate the tedious memory management code that we all hate to write ourselves.