The business object layer is implemented in part by a small runtime and in part by a small set of interface and behavior requirements for business objects. Code generation support is supplied (in the ObjectExtender tool set) to generate code (and selectively update previously-generated code) for business object and relationship classes from a high-level description of the classes and their relationships.
Business objects and their relationships are described using an extended entity relationship model using a UML vocabulary. Supported features include business object inheritance and relationship cardinality, navigability and inverses. Business objects are not required to inherit from a common root, although an abstract class with suitable behaviors is provided for convenience. Separate classes are generated for relationships to allow business rules to be written governing relationships. In addition to the business object classes that are generated, Key classes and HomeCollection classes are generated for each business object.
New methods can be freely added to business objects using normal browsers without any need to regenerate any part of the runtime system. Only when structural changes are made to objects (adding, deleting or changing an attribute or relationship definition) is there any requirement to regenerate. Non-managed fields can also be added without regeneration.
ObjectExtender generates a HomeCollection class for each business object class. The protocol on HomeCollection classes is the one defined by Component Broker (createFromKey:, findByPrimaryKey:).
ObjectExtender does not prescribe how the HomeCollections are located by the application code. A default mechanism is supplied that will store a singleton HomeCollection instance in a static variable of each HomeCollection class, and this mechanism may be used by application code to locate appropriate HomeCollection instances. However, the ObjectExtender framework never uses this access path, and application programmers are free to store and locate HomeCollection instances in other ways.
HomeCollections also have the following responsibilities:
This is a responsibility of HomeCollections for persistent business objects only. A DataStore represents a particular backend store (for example, a database or a set of CICS TPs) that stores objects from a model. The HomeCollection provides the mapping of a set of classes to a data store.
In fact, the HomeCollection is the object that maps the business object layer to a particular DataStore and service object implementation. HomeCollection classes for persistent objects subclass from a different class from HomeCollection classes for non-persistent classes. The persistent HomeCollection classes have extra protocol for dealing with this mapping of objects to a backend.
This means understanding whether objects in this home use optimistic (non-locking) or pessimistic (locking) transaction isolation, and providing an appropriate implementation object for the same.
A HomeCollection knows about the home for its business object's superclass and also about the homes for its business object's subclasses. It also knows about the homes for all of the relationships from its business object. (Relationships have homes as well).
The transaction model supports concurrent and nested transactions.
A nested transaction is a tree of transactions. The subtrees can be flat or nested transactions. At the leaf level, transactions are flat. The root of the tree is the top-level transaction; all others are subtransactions. A transaction's predecessor in the tree is a parent; a subtransaction at the next lower level is a child. A subtransaction can either commit or roll back; its commit will not take effect, unless the parent transaction commits. Therefore, any subtransaction can finally commit only if the top-level transaction commits. The rollback of a transaction anywhere in the tree causes all its subtransactions to roll back.
The commit of a subtransaction makes its results accessible only to the parent transaction. All objects held by a parent transaction can be made accessible to its subtransactions. Changes made by a subtransaction are not visible to its siblings, in case they execute concurrently.
There is always at least one active transaction: a global read-only transaction, which is called the shared transaction. When a new (read/write) top-level transaction is created, it becomes a child of the shared transaction. When there are multiple transactions, the application must explicitly set which of the transactions is the current one. All the modifications to the business objects are recorded by the current transaction.
Figure 17. Transaction instance coordination. The UML diagram shows the coordination of the shared, top-level, and child transaction instances .
Each transaction has its own view. A view is snapshot (of a subset) of the application's business object model. Each business object is divided into two parts: a shell and a version. An object's business behavior is in the shell, and the instance data is in the version. When a business object is first accessed (get/set a property) within a transaction, a new version of the object is added to the current transaction's view. The new version is based on the version in the parent transaction's view (if the parent transaction's view does not contain a version of the object, the parent will first create a version for itself based on its parent). When any object refers to a business object, it actually refers to the business object's shell. The shell dynamically connects itself to the current version in the current transaction's view. This way the shell/version pair implements a dynamic reference to a business object.
Each transaction has a resource coordinator. The resource coordinator's responsibility is to ensure that the modifications made within the transaction become persistent across multiple resources. A resource provides the ACID properties for a particular connection to a data store.
Figure 18. Transaction/View/Version flow. The UML diagram shows the flow of the Transaction/View/Version model.
On commit, a transaction's view is merged to its parent transaction's view (the top-level view is merged to the shared view), and the view and the resources are synchronized. When receiving the commit request, the transaction first tests if its view can be merged to its parent's view. The default test is that if the same object has been modified in both parent and child views, the views cannot be merged. If the test fails, the transaction will be rolled back. If the test succeeds, the transaction requests its resource coordinator to synchronize the view and the resources. The resource coordinator passes each version in the view to a corresponding resource. The coordinator then prepares and commits the changes to the resources. For nested transactions, the coordinator includes only resources that support nesting (if none of the resources supports nesting, the nesting happens only within the application memory). If the synchronization fails, the transaction will
Figure 19. Transaction states. The UML diagram depicts the flow of the Transaction states.
be rolled back. If the synchronization succeeds, the transaction's view is merged with the parent view and the transaction is marked to be committed.
ObjectExtender has a Transaction class. Transactions represent paths of code execution. You access and manipulate data in your business objects within transactions.
New transactions are created by invoking the static method begin on Transaction. This always creates a new top-level transaction. To create a nested transaction, you send beginChild to an existing transaction. Each thread of execution has a notion of a current transaction. You can get the current transaction for the current thread of execution by sending current to Transaction.
ObjectExtender manages isolation of changes between transactions. Isolation of changes between transactions means that outside the scope of a transaction, changes made within the transaction will not be seen until the transaction commits.
ObjectExtender supports both optimistic and pessimistic approaches to implementation of transaction isolation.
ObjectExtender implements transaction isolation according to the policies for the transaction and for the (service implementations for) HomeCollections.
The Transaction specifies either Repeatable Read or Unrepeatable Read isolation level.
The service implementation for the HomeCollection specifies either non-locking or locking implementation. The service objects therefore have implementation for:
Any given service implementation is required only to contain implementations of an non-locking or locking set, because the non-locking/locking flag is not changeable for a service. The locking capability for a class is specified by checking or unchecking the Enable pessimistic locking menu item for a persistent class in the Map Browser. The transaction isolation policy is specified by calling the methods supportRepeatableReads() or supportUnrepeatableReads() on a Transaction.
This is an isolation policy value that can be specified on a transaction-by-transaction basis. Repeatable Read guarantees that if the same object is fetched multiple times within the same transaction, (for example, using findByPrimaryKey:), then the fetched object will have the same attribute values each time. It is the rough analog of the DB2(R) read stability isolation level
This is an isolation policy value that can be specified on a transaction-by-transaction basis. Unrepeatable Read guarantees that if a business object is used within a transaction, then the attribute values of the business object within a transaction will not be affected by uncommitted changes to the business object in sibling transactions. However, if a sibling transaction commits before the current transaction, changes made in the sibling may become visible in the current transaction. This is the rough analog of the DB2 cursor stability isolation level.
This is an implementation of Repeatable Read specified on a type-by-type basis that uses copying. A copy of the data of each business object (including its associated data object) will be made the first time it is read within a transaction. Within this transaction, subsequent read or write access to the object, or navigation to the object, or query returning the object will use the data from this copy.
This is an implementation of Unrepeatable Read specified on a type-by-type basis that uses copying. A copy of the data of each business object (including its associated data object) will be made the first time it is updated within a transaction. (Read-only access prior to the first write will use the shared copy of the data for the BO.) Within this transaction, subsequent read or write access to the object, or navigation to the object, or query returning the object will use this copy. Copies are not made on read. This means that if an application reads an object, and later re-reads the object (or reuses a stored reference to the object) it may see changed attribute values. In the current implementation, if you keep a reference to the object, you will only see changes made by sibling committed transactions that execute within the same process. If no reference to the object is held, you may see changes committed by transactions executing in other processes. However, we do not preclude the possibility that changes made by sibling transactions in other processes will be visible in the future even if a reference to the object is held.
This is an implementation of Repeatable Read specified on a type-by-type basis that uses locking. An object-level shared lock is acquired on each business object the first time it is read within a transaction. All subsequent attempts to acquire an update lock on this object in other transactions (except child transactions of the current transaction) will cause blocking or an exception. If the object is subsequently updated within the transaction, the lock will be upgraded to an update lock. In the persistence layer (see later) locks will also be acquired on the underlying data store to block other writers (if the object has only been read) and readers (if the object has also been updated) outside the current process. One option that can be specified for locking is not to procure a lock at all. This is typically used when it is known that the access patterns of the application require procurement of a lock on another object first. For example, it might be possible to specify no locking on LineItems if the only way to get to LineItems is through the owning Invoice object (which would be locked). Note that this is not the same as non-locking. Non-locking says that we expect conflicts on an object, but will detect and resolve these conflicts at commit time. Locking says that we expect no conflict.
This is an implementation of Unrepeatable Read specified on a type-by-type basis that uses locking. A lock is acquired on each business object the first time it is updated within a transaction. All subsequent attempts to acquire a write lock on this object in other transactions (except child transactions of the current transaction) will cause blocking or an exception. In the persistence layer locks will also be acquired on the underlying data store to block other writers and readers outside the current process. In the current implementation, if you keep a reference to the object, you will only see changes made by sibling committed transactions that execute within the same process. If no reference to the object is held, you may see changes committed by transactions executing in other processes. However, we do not preclude the possibility that changes made by sibling transactions in other processes will be visible in the future even if a reference to the object is held. One option that can be specified for locking is not to procure a lock at all. This is typically used when it is known that the access patterns of the application require procurement of a lock on another object first. For example, it might be possible to specify no locking on LineItems if the only way to get to LineItems is through the owning Invoiceobject (which would be locked). Note that this is not the same as non-locking. Non-locking says that we expect conflicts on an object, but will detect and resolve these conflicts at commit time. Locking says that we expect no conflict.
Transaction provides the following class protocol:
Transaction provides the following instance protocol:
This section contains a collection of code examples for using the collision management policies for your transaction layer.
Isolation policy: tries to lock objects. To set the transaction's isolation policy such that it tries to lock objects, use the following API.
tx1 supportRepeatableReads "this is the default"
Isolation policy: do not try to lock objects. To set the transaction's isolation policy such that it does not try to lock objects:
tx1 supportUnrepeatableReads
Timing: when the objects lock. If the transaction supports repeatable reads, the locking-capable objects are locked when
faculty name: 'Dr. Salo'
Handling exceptions. In the above cases, the application must be prepared to handle the ExVapObjectLocked exception, which is raised when the attempt to acquire the lock fails.
tx1 := Transaction begin. [depts := VapDepartment singleton allInstances. "..."] when: ExVapObjectLocked do: [:aSignal | "notify the user that the object is locked"]
Explicit locking. An object can be explicitly locked regardless of the transaction's locking policy.
[faculty lock] when: ExVapObjectLocked do:[:aSignal | "notify the user that the object is already locked"]
Explicit refresh. An object can be explicitly refreshed from the database.
faculty refresh
Collision detection predicates. If the object has collision detection predicates, the transaction fails when the object is updated in the database.
faculty := VapFaculty singleton findByPrimaryKey: aKey. faculty name: 'Dr. Rich'. tx1 commitWhenFailureDo: [:aSignal | tx1 rollback. "notify the user that there is a collision"].
The failed object is always the first argument of the signal.
[aSignal | aBo := "aSignal argument ..."]
Depending on the exception the signal may have additional arguments, like the native SQL error.
Every business object defined to ObjectExtender must be identifiable by a unique key. One of the features of ObjectExtender is that it guarantees that only one instance of an object will exist within a transaction scope for a particular unique key. Each transaction keeps a registry of objects that have been read or written within that transaction. Whenever an object is requested, either in a query or by navigating a relationship, ObjectExtender will check if this object is already in the registry of this transaction or one of its parents. If it is, the object in the registry will be used.
For client applications with a UI, it is very important to manage reads of objects outside of any transaction scope. This is not just a matter of convenience, but of semantics.
The model that ObjectExtender supports is as follows:
To support this model, ObjectExtender supports the notion of a special read-only Shared Transaction that is the parent of all top-level transactions.
Note that this model cannot be supported using simply top-level and nested transactions, because the model depends on the fact that the level of transaction that commits to the external store is the level above the Shared Transaction. In ObjectExtender, only top-level transactions commit to backing stores.
The SharedTransaction always runs with a Unrepeatable Read isolation policy. This allows the transaction registry to be weak, so that objects read in the shared transaction are not held onto after the last application reference to them is gone.
For transactions whose isolation policy is Unrepeatable Read, the registry is a weak structure. This means that if there are no other references to the object in the image, it may be reclaimed by the garbage collector. If the transaction isolation policy is Repeatable Read, the registry structure is strong. This guarantees that if the same query is executed twice in succession, the same objects with the same attribute and relationship values will be returned the second time, even if no reference to the objects were kept in between queries.
Weakness is only implemented in the Smalltalk version of the product at this time.
Within the scope of a single transaction, all references to a business object of a particular key value are guaranteed to point to the same business object instance. Since the transaction may have numerous nested transactions, and since ObjectExtender supports optimistic (non-locking) transaction isolation, this means that ObjectExtender has to manage different values of the data of this business object in different nested transaction scopes.
In fact, because of the outer shared transaction, each top-level transaction is in fact a nested transaction for this purpose. This means that the same business object (BO) key value is resolved to reference the same BO instance, even across top-level transactions, so this mechanism is required for the simple non-nested top-level transaction case as well as for the nested case.
ObjectExtender implements this capability of having different values for the data of a single instance of an object in different transactions by generating special get and set methods that must be used to access the fields of an object. When an object is first touched within a transaction, a copy of the values of all the managed fields of the object is taken and stored associated with the transaction. The generated get and set methods will always return the value of the fields from the copy held in the transaction.
Some important characteristics of the way this works follow:
An overview of the ObjectExtender event signaling follows.
ObjectExtender supports the registration of listeners and signaling of events on business objects and relationships.
A code-generation object will cause all ObjectExtender-managed attributes to be created as attributes.
Listener registration is transaction-sensitive in ObjectExtender. Whenever a listener is registered for an object within a transaction, ObjectExtender will create a listener list within the transaction for that object if it has not already been created and add the listener to the list.
The initial listener list for a propertyChanged event in a transaction is always empty, even if the parent transaction had a non-empty property list. This means that listeners in the parent transaction will not be (immediately) notified of changes that happen within a child transaction.
ObjectExtender will signal a changed event whenever a property value is signaled. Only listeners that were added within the current transaction will be notified.
Whenever a transaction is committed, its modified objects are promoted to its parent, and any resulting changes to attributes will trigger a corresponding propertyChanged notification. This notification will go to listeners registered in the parent transaction.
ObjectExtender LinkCollections for relationships signal changes for adding and removing objects within them. ObjectExtender LinkCollections also signal an elementChanged event whenever one of their elements signals a propertyChanged event. This allows containers that display details of elements within the collection to get notifications of changes without having to add themselves as propertyChanged listeners of every element of the relationship.
Whenever a change to a LinkCollections is promoted to a parent transaction as part of transaction commit, a suitable sequence of add and remove events is signaled in the parent transaction.
Whenever a property of an object is modified, ObjectExtender will not only signal the propertyChanged event for the object itself, but will also signal a corresponding elementChanged event for every relationship that the object participates in.
ObjectExtender requires each "shell" business object (BO) to be able to return a BOManager object.
The BOManager object has the following responsibilities:
In order to track the state and data values of an object within a particular transaction, ObjectExtender creates Version and VersionState objects for each object that is read or updated within a transaction. The Version objects are the objects that are actually stored in the transaction registries, keyed by BO key. (Recall that it is the transaction registries that are used to implement object uniqueness within a transaction and to track which objects are "dirty" in a transaction). Version objects are also cached by BOManagers for rapid repeat access within the same transaction.
A Version object has the following responsibilities:
VersionState objects are helper objects for version objects.