The persistence layer is implemented by generating parallel class hierarchies of classes to the business object hierarchy. Instances of these parallel classes are responsible for mapping the business objects and relationships to and from the backend store.
Two of these hierarchies are:
Data objects contain the data for a business object in the form in which it was retrieved from the persistent store. Each persistent business object points to a data object, and there is a direct correspondence between the managed fields of the business object and the fields of the data object. DataObjects are the principle entries in the cache (discussed later).
Service objects read data from the persistent store to create data objects, and store data from data objects in the persistent store to support the Create/Read/Update/Delete (CRUD) operations and navigation operations required by the persistent object application.
ObjectExtender can generate stubs for these services if it does not understand the backend datastore.
For SQL, ObjectExtender can generate full service implementations.
The SQL statements executed are all pre-calculated and stored in methods. Currently, both dynamic and static SQL are suppported.
The ServiceObjects have a couple of generated helper classes that understand the shape of the data objects, the shape of the SQL result rows (which may contain data for several objects) and how to map between them. These helper classes include Extractors, Injectors, and QueryPools.
For some cases, it is necessary to execute multiple statements of SQL (for example, to update, insert an object that spans multiple tables), and to extract multiple objects of differing types from a result set. This is handled transparently to the application.
For fetch-ahead, BOs are made immediately for the root object being fetched. For the fetch-ahead objects, only DOs are created and are entered in the DO cache for future use.
Modification of generated code is supported, even encouraged. This may be necessary or desirable for performance tuning and to handle complex legacy data cases.
ObjectExtender generates service implementations for relational databases from a set of mapping specifications. The things you can specify include:
ObjectExtender implements a two-level caching scheme to enhance performance.
The first level is an object cache, in the form of transaction registries that are scoped by transaction. They ensure uniqueness of BOs and help to implement the isolation required. The existence of the object cache allows the BOs to remain in the client application for the duration of the transaction without having to reread from the datastore each time they are referenced by the application.
The second level of caching is the DO cache, which comes into play during fetch-ahead. Entries in the cache are (wrappers around) DataObjects (DOs) and special cache entries for relationships. All DOs created from a read service invocation that are not part of the target object type's extent are placed in the corresponding DO caches until needed.
The DO cache should not be confused with the transaction registries:
There are no implemented mechanisms for flushing or controlling the size of the cache. This is a potential problem, because aggressive pre-fetch of DOs that are not subsequently converted to BOs will cause the cache to grow in an unmanaged manner.
There is no implemented policy for controlling the staleness of DO data. This is a potential problem, because arbitrarily stale data in DOs can be used for an arbitrary length of time. This problem only exists for optimistic (non-locking) transaction isolation. The following are the two scenarios that can cause problems:
You can specify the depth to which data for an object should be retrieved from the database in a single read query.
Preload is specified in ObjectExtender by defining paths. An example of a path is invoices.lineItems. A path is relative to an existing object or relationship, and can be thought of as a sequence of relationship names to navigate through. So for a Customer object (or for a relationship collection of Customer objects), invoices.lineItems would be a path to all lineItems for that Customer (or those Customers).
ObjectExtender allows paths to be defined as part of the map for an object. Thus invoices.lineItems would be a reasonable path for the Customer object.
You can define a default preload path for a class. This does not generate extra services or protocol, but modifies the default retrieve services used by findByPrimaryKey: and by relationships that point to the class.
To set a default preload for a class map you could do something similar to the following:
| tableMap | tableMap := ((VapDataStoreMap mapAt: 'CeducCLIDB2') mapAt: 'VapDepartment') defaultPreloadPath: #('faculty').This would set the faculty data as the retrieval depth for VapDepartment. Thus, whenever VapDepartment was retrieved from the database, Faculty data would be retrieved also.
Restrictions. The following restrictions apply:
ObjectExtender provides a simple framework that allows you to add your own custom query methods to HomeCollections with associated service implementations. A custom query will return a Vector of business objects that currently exist in the database. The query will not retrieve an object that was created by the Home but not yet committed to the database.
ObjectExtender does not support static or dynamic queries on relationship collections, nor does it automatically generate custom queries on HomeCollections.
The class, DataStore, is responsible for owning and managing a pool of database connections. For each database connnection, it registers a home collection.
ODBC restriction: The ODBC spec does not include the types BLOB and CLOB which are IBM CLI extensions to the spec. When using the JDBC-ODBC Bridge, this type is not supported. The DB2 JDBC Drivers work well with these types.
Each top-level transaction has a ResourceManager instance. The resource manager instance for a top-level transaction will create a Resource instance for each Resource used by the transaction.
A Resource instance manages a resource, for example, a database connection, for a single transaction instance.
The flow through Transaction commit processing is as follows:
Transaction commit Get the resourceManager for the Transaction and tell it to synchronize the transaction ResourceManager synchronize: aTransaction Sort all the versions according to the resource they belong to For each version in the transaction, add the version to the version list of the correct resource for that version Prepare all the resources For each resource sort the versions to satisfy referential integrity Go through the versions in order telling them to synchronize using the resource's session For each version, perform appropriate CRUD Commit all the resources Commit the DB transaction
When using optimistic (non-locking) transaction isolation, ObjectExtender detects collisions on the database by overqualifying the update SQL query. You can specify which columns of the table should be included in the overqualified query in the map for the class. Use the Be part of optimistic predicate selection from the Property Maps menu item in the Map Browser.
For example, if you were building a view to display selective data from Course objects for a University, a lite collection for Course would only retrieve the attributes: name, credit, and courseNumber from the data store.
Lite collections, therefore, read only a subset of attributes for an object. In this respect, they are very useful for displaying a choice in a list, for example. Lite collections return partially to completely populated data objects, not business objects. As a result, when displaying attributes retrieved in a lite collection, you must use the data object's methods.
Lite collections support the "drill-down" data access approach often seen in GUI intensive applications. GUI applications that open a series of nested dialogs on a set of data are prime candidates for using lite collections.
Lite collections have protocol to instantiate persistent objects. The message #getBusinessObject can be sent to an element of the collection to instantiate it, for example, say you have a lite collection with twenty objects and you want to instantiate the last one, you could do the following:
aLiteCollection last getBusinessObject.The notion of "packeting" lite collections is supported as well. For example, say you have a Student object. Consider there are twenty thousand Students enrolled in the University. You can "packet" the amount of Students retrieved from the data store.
Using the Course scenario described earlier, a code example follows. Three lite collections for Course are shown:
| courseClass key nameProperty credit dept | courseClass := (Model modelNamed: 'University') classNamed: 'Course'. key := courseClass attributeNamed: 'number'. nameProperty := courseClass attributeNamed: 'name'. credit := courseClass attributeNamed: 'credit'. dept := courseClass associationEndNamed: 'department'. LightCollectionSpec name: 'byDepartment' namespace: courseClass properties: (Array with: key with: nameProperty with: dept) filterProperty: dept. LightCollectionSpec name: 'names' namespace: courseClass properties: (Array with: key with: nameProperty). LightCollectionSpec name: 'byCredit' namespace: courseClass properties: (Array with: key with: nameProperty) filterProperty: credit.This example is just for illustration purposes. You do not need to hand code lite collections. Lite collections are much easier to create using the Model Browser.
ObjectExtender supports defining complex mappings of object attributes to column values.
ObjectExtender supports the notion of a converter.
A converter is an object that converts a column value to and from a corresponding object format. The conversion performed by a converter may be arbitrarily complex. You can code your own converter classes.
Converters are associated with columns in a schema definition, and are used wherever that schema is used. Converters encode the "real meaning" of a database column in object terms. An example of a converter is one that interprets a Y or an N in a CHAR field of a database as a Boolean object.
You likely will need to create your own converters from time to time. For example, you might have to convert a database integer to an IP address object. To create your own converters, subclass under VapAbstractConverter, which has a simple protocol that converters must implement.
ObjectExtender also supports the notion of a composer.
A composer is responsible for mapping a number of separate DataObject attribute values as a single complex BusinessObject attribute value.
The aggregation performed by a composer may be arbitrarily complex, and you can code your own composer classes.
Composers are associated with maps: composers define how schema values are mapped into a particular object model. An example of a composer would be one that mapped street, city and zip columns into an Address object.
Composers are used to create attribute values that are complex objects. These attributes do not have unique keys, and so cannot be referenced from other objects and cannot be modeled as separate top level objects with relationships to their owners.