Layering of Transactional Applications, and handling of state
Case being considered:
A single database is in use, by multiple users, possibly concurrently.
The distributed case of multiple databases in use is much
more difficult and should be avoided if reasonably possible. The use of
O-R Mapping is equally valid for the distributed case, but the full
system is more complicated, so for simplicity, not treated here.
Basic Rules
1. All persistent state resides in the database. The database is the
expert at
handling shared data
safely in face of concurrent access, so we use it fully. Per-user data
(which is unshared) may live outside the database.
2. All interactions with the database data, even read-only access, are
brought about through actions inside transactions.
3. No user interaction (necessarily causing long delay) may occur
during a
transaction because a transaction should be atomic, or nearly so, and
the database may
hold locks to make this true, or if it doesn't, the lack of atomicity
gets worse and worse the longer the transaction lasts..
To make it easier to obey rule 2, the transaction code is usually
organized into methods, so that each
transaction runs completely inside a call to some particular method.
The transactional methods are grouped together to make the service
layer. The collection of method signatures of the transactional methods
defines the Service API. The
user interaction code makes up the presentation layer, which calls into
the service layer as needed to carry out the actions of the app.
We think of the Presentation layer as above the Service layer,
separated by the Service API:
Presentation layer: line-oriented, GUI, or web scripts
---------------Service API-------------------------
Service layer: methods with transaction(s) within them.
Thus UI actions and their huge delays occur strictly in the
presentation layer between
the calls into the
service
layer. Here is a timeline for one user's execution: starting in
presentation layer,
calling into service layer, returning to presentation layer:
----UI-------<call service>--<start
Tx>----work with database data----<end Tx>--<return from
service>--UI-----
The service API defines what the app can do for any presentation layer,
whether
it's a console (line-oriented) app, a local GUI app, a client-server
app, or a web app. One service layer implementation can support many
presentation layers without change except to initial configuration code
and/or files.
Domain objects: changes are done in
service
layer, treated as read-only in presentation layer
In general, all changes to domain objects should be done in the service
layer, in a transaction, since their data is persistent and "belongs"
to the database. Once domain objects are finalized in the service
layer they can be sent up to the presentation layer for read-only use
there. This is the conservative approach that will work in any O-R
mapping and is used in the pizza app. The current Topping and PizzaSize
objects are supplied via service-layer calls for the order-pizza form,
and the ids of the toppings and size picked by the user are assembled
in the presentation layer and the "makeOrder()" service is called with
them as arguments. In the service layer, the order object is created
and saved. Alternatively, you could imagine creating a new order object
in the presentation layer as a "scratch" copy, not immediately
persistent, filling it in with referenced Topping and PizzaSize objects
(also now just scratch copies, since we are outside of a transaction
here in the presentation layer) and then sending it down to the service
layer in a makeOrder(PizzaOrder) call. However, now the scratch objects
need to be converted to persistent objects, a more complicated and
error-prone operation than simply creating a new one under a
transaction. To keep things simple, the conservative approach is used
everywhere in the pizza app.
In the supplied example, the service layer API is provided as a
singleton stateless object in both Hibernate and EDM
implementations, with essentially the same methods in both cases.
Since transactions run strictly inside service-layer calls, the whole
transactional state is created and destroyed within a call, so nothing
needs to be saved from service call to service call for transactions.
All shared
application data is held in the database, and none of it is in the
service layer between transactions. This strategy is called
"session-per-request" strategy in Hibernate language, referring to web
requests, but generalizable to service requests between UI actions in
any app.
Per-User Data
The presentation layer may hold some data related to the current user
running the app across several calls to the service API.. In the pizza
app, the user has a room number for pizza
delivery that is remembered in the presentation layer across various
calls into the service layer to order a pizza, check if it's done yet,
etc. This is unshared data that does not need to be held in the
database. If the computer running the app crashes, the current room
number is lost for the user, except in web app cases where the
application server persists session data. If more persistence is
needed, per-user data can be kept in the database as well as shared
data.
The Data Access Object (DAO) Layer
As discussed above, the crucial layer boundary is between the
presentation
and service layers, to subdivide the work into transaction units that
run between the much longer user interactions. A less important layer
can be set up below the service layer to encapsulate the domain object
access needs of the application. This allows substitution of different
O-R mapping implementations or file-based persistence for some
uses. The pizza project has a DAO layer, with olmost identical methods
in both implementations.
Home