There are as many reusable web architectures championed by their inventors and proponents as there are technologies available to an Enterprise development team. This series of articles proposes, demonstrates and validates one such architecture based on the Microsoft Windows DNA TM 3-tier framework and the Rational Unified Process modeling methodology. We begin with the use of UML in modeling use cases and distilling the object and data models. We then outline some Active Server page proto-patterns or templates, followed by an extensive and detailed look at some COM design patterns. We then lead into a discussion of database modeling techniques. At several key waypoints in this series, we stress and highlight the twin virtues of scalability and performance as being the ultimate test of the success and validity of any web architecture. To prove the re-usability of these patterns and methodologies, most of the source code has been stripped of almost all business context in order to better highlight their framework .
It is assumed that the reader has some familiarity with Active Server Pages, Javascript, COM, Visual Basic and SQL. A working knowledge of IIS, MTS and any SQL database platform such as MS SQLServer, Oracle or DB2 will be useful while implementing some of the solutions presented. Familiarity with RUP and UML is also assumed. All the scripting examples are presented in Javascript and all COM components have been written using Microsoft Visual Basic 6.0TM
· Modeling the Enterprise
o Planning the process through RUP
§ Describing the business charter
§ Assessing technological boundaries
- the bleeding edge effect!
o Life-cycle tools through UML
§ Describing the Use Case
§ Static Model
§ Sequence Diagrams
· The User Interface
o An Active Server Page
proto-pattern.
o Reusable server-side scripts.
o Reusable client-side scripts.
o The XML/XSL Transform
pattern.
o The stateless
design pattern.
o COM Design
Pattern - I (implementation of the stateless pattern).
o Data Access Review.
o COM Design Pattern - II (extending
COM with ADO).
§ Basic ADO and the data tier
§ Enhancing the COM Template for
data retrieval/update
o COM Design Pattern - III
(The C.R.U.D. Pattern).
o COM Design Pattern - IV (C.R.U.D.
on hierarchical data)
§ Using Shaped Recordsets for
retrieval and updates
§ Custom Recordsets
· Reusable Security Models
o COM & LDAP
· Effective Data Tiers.
o Spreadsheet Origami (A reusable
data-cube pattern)
· Scalability and Performance Review.
Using a Visual Basic class module we demonstrate a physical implementation of the stateless design pattern we expounded upon in the previous section. An understanding of the Visual Basic programming language is required for this section. For a 30 second Intro to Visual Basic click here.
Using your Visual Basic IDE, use the following steps to create a ActiveX DLL project
· Click on the File..New project
menu and click on the ActiveX DLL option. See accompanying
image
· Your IDE will create a class module
file which is going to be the home for your stateless object definition. See accompanying
image
· Since we are going to expand this
template later to include database retrieval/updates ina transactional
environment, we will also include references to ADO and MTS. We will cover both
these topics in detail later on in the show. You can access these properties by
going to the Project..References menu in the IDE See
accompanying image
The sample code below describes the structure of any Stateless object design
Option Explicit ' 'The Public keyword identifies this as an object 'interface that can be used by an application ' '----------------------------------------------------- Public Function BuyCar(ByVal IsAdult As Boolean, _ ByVal Name As String, _ ByVal Address As String, _ ByVal CarMake As String, _ ByVal CarModel As String, _ ByVal CarYear As String, _ ByVal CreditCardNumber As Long, _ ByVal BankId As Long, _ ByVal DriversLicenseId As String) As Boolean On Error GoTo Err_Handler ' 'conduct business logic here.. '------------------------------------ BuyCar = True Exit Function 'if everything went well Err_Handler: ' 'conduct error-handling here '--------------------------------- BuyCar = False End Function |
What this implementation demonstrates is the framework of any stateless COM component independant of the business context. The framework has certain key components
· The class module comprises of one
or more method calls and no public attributes (properties). This is really what
stateless means in real terms. Stateless components perform actions
only.
· Each method call is implemented as
a function in order to provide a return value. In Visual BasicTM, possible
return values range from basic data types (such as strings and numbers) to
objects (other COM components) and arrays.
· Each method call must have built-in
error handling and garbage collection.
· Garbage collection is necessary
after both successful and failed method invocations.
· Always use ByVal to
declare the method parameters if you can. This will send the parameters in by
value, not by reference.
o ByRef is the default in VB6, and is a
hold-over from the days when applications were always desktop applications, and
passing "Pointers" to the data was more efficient. Unfortunately, ByRef
requires a network trip from client to server and one more back again to the
client to get the pointer to the variable.
o Using ByVal, we make a
copy of the variable, and eliminate an unnecessary network trip. In a
distributed environment, this boosts scalability by an enormous amount
· Make sure your threading model is
"Apartment Threaded". This is the model required for a COM component
in VB6 to run concurrently under MTS.
· We also need to set the following
properties of the Project as well in order to run these components smoothly in
MTS (Microsoft Transaction Server) See
accompanying image You can get to this menu by clicking on Project..[Project
Name] properties through the IDE
o The two options 'Unattended
Execution' and 'Retained in Memory' must be checked on for an
ActiveX DLL project to run successfully in MTS.
o Unattended Execution will treat any
MessageBoxes in you DLL as NT events which you can view through the Event Log
Viewer. This is an invaluable debugging tool.
o Retained in memory will prevent
your DLL from being released unexpectedly. (This is a little known Microsoft
feature/bug that can cause headaches is a component is used frequently by an
application)
· Do not use Global variables in your
classes. Global variables are stored on a per-thread basis, and when you use a
middleware tool like MTS to host your components, there is no guarantee that
your component will get the expected value (unless they accidently happen to
share the same thread).
There will be more programming commandments as we get into database access using COM objects, which is the next object of our attention
The sample code for this COM object along with the other class files used in later articles as well can be found by downloading this Zip file SampleCOM01.vbp After unzipping into a direcotry of choice open up the project by clicking on the Sample.vbp file. You must have Visual Basic 6.0 installed on your machine for this to work.
We now do continue our autopsy of the
Visual Basic implementation of our stateless design pattern.
A recurring design problem in most Web applications is data retrieval and
updates from one or more data stores. This stateless object model is ideally
suited to do this.
The sample code below describes the structure of our enhanced Design pattern implementation.
Public Function BuyCar(ByVal IsAdult As Boolean, _ ByVal Name As String, _ ByVal Address As String, _ ByVal CarMake As String, _ ByVal CarModel As String, _ ByVal CarYear As String, _ ByVal CreditCardNumber As Long, _ ByVal BankId As Long, _ ByVal DriversLicenseId As String) As Boolean On Error GoTo Err_Handler ' 'set up the database connectivity here Dim sDSN As String Dim oConn As ADODB.Connection ' 'a typical connection string to a database 'the DSN is the ODBC data ssource name on the 'server; 'the UID and PWD are the user-id and password 'strings. ' 'this connection string is often provider specific '------------------------------------------------------ sDSN = "DSN=eSRP_Auto; UID=test;pwd=test;" ' 'open the connection Set oConn = New ADODB.Connection oConn.Open sDSN ' 'conduct business logic here.. '------------------------------------ oConn.Execute _ "INSERT INTO CAR_TABLE " & _ "(Make, Model, NewOwner) " & _ "VALUES('" & _ CarMake & "','" & _ CarModel & "', '" & _ Name & "')" oConn.Close Set oConn = Nothing 'complete the transaction '----------------------------- GetObjectContext.SetComplete 'return a success flag '--------------------------- BuyCar = True Exit Function 'if everything went well Err_Handler: ' 'Error handling code here '--------------------------- ' 'always close connections If oConn.State = adStateOpen Then oConn.Close 'abort the transaction GetObjectContext.SetAbort 'return a false code to the application BuyCar = False End Function |
· Use Early Binding wherever
possible.
o Early binding is declaring the
object type explicitly. Dim oConn as ADODB.Connection
o Late binding is declaring the
object type as in Dim oConn as Object. Using late-binding results in
two method calls, one to get th ID of the method, and the second to actually
make the call.
· Aquire resources late, release them
early. This is a great performance saving tip. note how the database connection
is opened right before the SQL query, and is terminated immediately after, and
all expensive database resources are releases shortly thereafter.
· In VB, you can often save a line of
code by writing Dim oConn as New ADODB.Connection.
o This will cause every reference to
the object to be slower. This is because we are telling VB to create an object
instance for us whenever we need one, and it does this check every time
an object variable is used.
o Also prevents you from using the
GetObjectContext.CreateInstance() method to create objects within MTS.
In the previous examples, you saw code that involved the GetObjectContext object, and are probably wondering what Transactions are, what they mean to us, do we need them, how do we use them, and a myriad of other perfectly reasonable questions. Let's try and answer them as simply as possible
A transaction is one unit of work, defined as an All or Nothing approach to the work unit. A transaction consists of three pieces
· Creating a Transaction (the transaction
context)
· Committing the work within a
transaction
· Aborting and rolling back all the
work within a transaction on error.
For precisely the reasons outlined above. It lets us treat a unit of work (which may in reality be comprised of multiple retrievals and updates from and to the database) as an indivisible unit. This provides for incredible safetly in modeling business processes.
· For example, if the BuyCar() method
needed to withdraw money from the Person's checking account and in the next
step credit it to the Dealers bank account, these two steps need to be treated
as one.
· If you successfully withdraw the
money from one account but fail to deposit it in the other, the entire unit of
work should be treated as a failure. In this example, the withdrawl from the
Person's account would be nullified, and no money would change hands..!
· Creating a Transaction: Easy, all you have to do is to go
to the properties of a class, and set the MTS Transaction mode to either Supports
Transactions or Requires Transactions . See supporting
image here.
o Supports Transactions will allow methods in the class to
participate in transactions created by other classes
o Requires Transactions will create a transaction if a
method in the class is invoked without any transactions as well as let it
participate in already created transactions.
· Committing the work: Even easier, just write GetObjectContext.SetComplete
just before exiting the unit of work, and MTS will commit the transaction
for you. it will manage any Database provider transactions it may have created
in the background for you.
· Roll-back: Typically, rollback occurs on an
error. So always include error handling and make the last line of code in your
method GetObjectContext.SetAbort
Now we enhance our understanding of a COM/MTS design by
looking at a new design pattern. This is the C.R.U.D. design pattern
In part III of our COM design pattern marathon, we now investigate a kind of design pattern unique to the Stateless object world. We call it C.R.U.D., which stands for the very boring Create Read, Update, Delete pattern. The reasoning behind this pattern is that these four operations are a recurring design problem. Most Web applications will at some point in time, Create database rows, Retrieve them, Update them and finally Delete them as well.
The sample code below describes the structure of our enhanced Design pattern implementation. First we talk about the Read method since it is the most interesting, followed by the Update method. The Create and Read methods are available in the Sample application
The Read method Option Explicit Public Function getCarDetail( _ ByVal VinNumber As String) _ As ADODB.Recordset On Error GoTo Err_Handler ' 'set up the database connectivity here Dim sDSN As String Dim oRst As ADODB.Recordset sDSN = "DSN=eSRP_Auto; UID=test;pwd=test;" Set oRst = New ADODB.Recordset ' 'create a client-side recordset oRst.CursorLocation = adUseClient ' 'conduct business logic here.. ' 'using the DSN string with the recordset object 'you can have ADO open up a connection in the 'background '------------------------------------ oRst.Open _ "SELECT * FROM CAR_TABLE WHERE VIN_NUM = '" & _ VinNumber & "'", _ sDSN, _ adOpenStatic, _ adLockOptimistic, _ adCmdText 'disconnect the recordset Set oRst.ActiveConnection = Nothing 'set the return vlue to the recordset '-------------------------------------- Set getCarDetail = oRst ' 'garbage collection '----------------------- oRst.Close Set oRst = Nothing 'complete the transaction '----------------------------- GetObjectContext.SetComplete Exit Function 'if everything went well Err_Handler: ' 'Error handling code here '--------------------------- ' 'always close connections If oRst.State = adStateOpen Then oRst.Close ' Create a custom recordset with the error ' message as the only field. Must create a 'Static cursor to be able to add fields and rows 'to a client-side recordset '----------------------------------------------- oRst.CursorLocation = adUseClient oRst.Fields.Append "ERROR_MSG", adVarChar, 255 oRst.Open oRst.AddNew oRst.Fields("ERROR_MSG").Value = _ "An error occurred: " & Err.Description oRst.Update Set getCarDetail = oRst 'abort the transaction GetObjectContext.SetAbort 'return a false code to the application End Function |
· Think about when you need
transactions, and when you simply need to support them.
o In the above example, we created a
new class called getCar, which has an MTS transaction mode of "Supports
Transactions". We do this because a retrieval from a database does not
require a transaction on it's own. However, the class may be required to
participate in a transaction, so we leave that design door open for future
developers to exploit.
· In this design pattern, data is
returned as an ADO recordset
o Advantages:
§ A rowset allows for the most
flexible treatment of data, data can be sorted, filtered easily after being
sent to the client.
§ A rowset contains meta-data, such
as field type, size, etc.. this is often very useful information for the UI if
it needs to validate user updates to this recordset
o Disadvantages:
§ Recordset size can be often be an
issue when costing out bandwidth requirements. it is usually offset by the fact
that recordsets can be used to send back more data in one network call.
· The recordset being returned is a
disconnected recordset. Disconnected recordsets provide a huge scalability and
performance advantage to the Web application.
o We do that by two lines of code
§ loRs.CursorLocation = adUseClient.
This tells the COM component that the cursor associated with the rowset is the
responsibility of the client and not the database server.
§ Set loRs.ActiveConnection =
nothing. This line of code severs the connection of the recordset from the
database, and makes it a disconnected recordset.
o We specify the cursorType =
adOpenStatic, because we would like to be able to update this recordset when it
comes back to the COM component through the application interface.
o We specify the lock type =
adLockOptimistic. This allows the database to lock the rows record by record,
only doing so when the Update method is called.
· Note another advantage of telling
the component that we want a disconnected recordset. If there is an error
during the retireval of data, we use a custom recordset
to send back error messages to the client.
o We do this by first closing the
recordset if it ever got opened
o Then we append a field to this
empty recordset, call it ERROR_MSG.
o This field is set to a datatype of
VARCHAR(255)
o A new row is added to this empty
custom rcordset and the value of the field is the complete error message that
was returned from the database
o This error message is richer and
more complete, and helps the client figure out what to do if there was an
error.
o
In the Update method, we see how to use a design pattern where a disconnected recordset is passed into the Save method and returns itself back to the client application
Option Explicit Public Function saveCarDetail( _ ByRef ThisRs As ADODB.Recordset) _ As ADODB.Recordset On Error GoTo Err_Handler ' 'set up the database connectivity here Dim sDSN As String Dim oRst As ADODB.Recordset, oConn As ADODB.Connection sDSN = "DSN=eSRP_Auto; UID=test;pwd=test;" ' 'open the connection oConn.CursorLocation = adUseClient Set oRst = New ADODB.Recordset oConn.Open sDSN ' 'conduct business logic here.. '------------------------------------ oRst.Open ThisRs, oConn oRst.UpdateBatch 'set the return vlue to the recordset '-------------------------------------- Set saveCarDetail = oRst ' 'garbage collection '----------------------- oRst.Close Set oRst = Nothing 'complete the transaction '----------------------------- GetObjectContext.SetComplete Exit Function 'if everything went well Err_Handler: ' 'Error handling code here '--------------------------- ' 'always close connections If oRst.State = adStateOpen Then oRst.Close ' Create a custom recordset with the error ' message as the only field '-------------------------------------------- oRst.CursorLocation = adUseClient oRst.Fields.Append "ERROR_MSG", adVarChar, 255 oRst.Open oRst.AddNew oRst.Fields("ERROR_MSG").Value = _ "An error occurred: " & Err.Description oRst.Update Set saveCarDetail = oRst 'abort the transaction GetObjectContext.SetAbort 'return a failure code to the application End Function |
· In the above example, we
created a new class called updCar, which has an MTS transaction mode of
"Requires Transactions". We do this because an update to a database
requires a transaction on it's own.
· Note that in this example, the
recordset is actually sent back ByRef. This is because we want to work
with the actual recordset and not a copy of the recordset. The increase in size
associated with the addition of this meta-data information is compensated for
by the fact that the returned information is compressed into one single
recordset object.
Now we move on to PartIV of our discussion of COM design patterns, where we will talk about the final enhancement to the CRUD design pattern as applied to hierarchical data sets.
In part IV of our COM design pattern marathon, we now investigate an enhancement to the CRUD design pattern we talked about in the earlier section. Much of enterprise data is hierarchical, in that they obey parent-child relationship hierarchies. An example shown below extends the simple example of relationships between people, their cars and computers under a typical organizational umbrella.
This model describes an organizational hierarchy. The top-level parent is the Company which is comprised of divisions. Each division is simplistically modeled as a collection of Employees. Employees have a sematic relationship with cars as they are required to operate one or more company vehicles. Often, it is necessary to traverse this hierarchical tree in several ways
In the traditional approach, if we wish to traverse from Company to Cars, we would have to use several (inefficient) SQL Outer Joins. Joins have one inherent flaw, they return too much data. A Join has no other representation that one rowset. To traverse the structure described above, it would be necessary for the query to provide a vey large denormalized rowset at the level of individual cars, with duplicate parent information at all of the four levels this hierarchy models.
The Shaped approach utilizes a provider independant feature of ADO called Data Shaping. Data Shaping allows the creation of nested hierarchical data sets that retuen data in a Parent-Child format. The shaped syntax is quite simple and elegant.
Using the above structure as an example, we can write a shaped query to retrieve all divisions under one or more companies. We assume that the data structures have a one-to-one mapping to this data model. We must also assume that each object in the database is related to it's parent with a foreign key that is part of the primary key of the parent. In the absence of this definining relationship, the Shaped Provider may behave unpredictably.
SHAPE { SELECT * FROM Company }
APPEND ( { SELECT * FROM Division }
RELATE CompanyId TO CompanyId)
AS chDiv
We can nest a grandchild inside the child recordset and find out which employees are in each Division.
SHAPE {SELECT * FROM Company }
APPEND( ( SHAPE {SELECT * FROM Division }
APPEND ( {SELECT * FROM Employee }
RELATE DivisionId TO DivisionId ) AS chEmp )
RELATE CompanyId TO CompanyId ) AS chDiv
The sample code below describes the structure of our enhanced Shaped Design pattern implementation. First we talk about the Read method followed by the Update method. The Create and Read methods are available in the Sample application
Option Explicit Public Function getShapedCarDetail() _ As ADODB.Recordset On Error GoTo Err_Handler ' 'set up the database connectivity here Dim sDSN As String Dim oRst As ADODB.Recordset, oConn As ADODB.Connection sDSN = "DSN=eSRP_Auto; UID=test;pwd=test;" ' 'open the connection set oConn = new ADODB.Connection oConn.Open sDSN ' 'set some connection properties: client side recordset cursors 'and set the data shape provider '-------------------------------------------------------- oConn.CursorLocation = adUseClient oConn.Provider = "MSDataShape" Set oRst = New ADODB.Recordset ' 'conduct business logic here.. '------------------------------------ oRst.Open _ "SHAPE{SELECT * FROM Employee } " & _ "APPEND ( {SELECT * FROM Car} " & _ "RELATE EmployeeId to EmployeeId ) AS OPERATOR_OF " & _ VinNumber & "'", _ oConn, _ adOpenStatic, _ adLockOptimistic, _ adCmdText 'disconnect the recordset Set oRst.ActiveConnection = Nothing 'set the return vlue to the recordset '-------------------------------------- Set getShapedCarDetail = oRst ' 'garbage collection '----------------------- oConn.Close 'complete the transaction '----------------------------- GetObjectContext.SetComplete Exit Function 'if everything went well Err_Handler: ' 'Error handling code here '--------------------------- ' 'always close connections If oRst.State = adStateOpen Then oRst.Close ' Create a custom recordset with the error message as the only field '-------------------------------------------------------------------- oRst.CursorLocation = adUseClient oRst.Fields.Append "ERROR_MSG", adVarChar, 255 oRst.Open oRst.AddNew oRst.Fields("ERROR_MSG").Value = _ "An error occurred: " & Err.Description oRst.Update Set getShapedCarDetail = oRst 'abort the transaction GetObjectContext.SetAbort 'return a false code to the application End Function |
· Notice that the connections Data
Provider has been set to MSDataShape
· Everything else about this method
is the same as the earlier design pattern implementations.
Next, we discuss how to update a shaped recordset through the COM object
Public Function updateShapedCarDetail( _ shapeRs As ADODB.Recordset) _ As ADODB.Recordset On Error GoTo Err_Handler ' 'set up the database connectivity here Dim sDSN As String Dim oRst As ADODB.Recordset Dim oRstChild As ADODB.Recordset Dim oField As ADODB.Field Dim oConn As ADODB.Connection sDSN = "DSN=eSRP_Auto; UID=test;pwd=test;" ' 'open the connection oConn.CursorLocation = adUseClient oConn.Provider = "MSDataShape" Set oRst = New ADODB.Recordset Set oRstChild = New ADODB.Recordset set oConn = new ADODB.Connection oConn.Open sDSN ' 'first update every child recordset '------------------------------------ For Each oField In shapeRs.Fields If oField.Type = adChapter Then Set oRstChild = oField.Value oRst.Open oRstChild, oConn oRst.UpdateBatch oRstChild.Close End If Next ' 'finally update the parent oRst.Open shapeRs, oConn oRst.UpdateBatch 'disconnect the recordset Set oRst.ActiveConnection = Nothing 'set the return vlue to the recordset '-------------------------------------- Set updateShapedCarDetail = shapeRs ' 'garbage collection '----------------------- oRst.Close Set oRst = Nothing Set oRstChild = Nothing oConn.Close Set oConn = Nothing 'complete the transaction '----------------------------- GetObjectContext.SetComplete Exit Function 'if everything went well Err_Handler: ' 'Error handling code here '--------------------------- ' 'always close connections If oRst.State = adStateOpen Then oRst.Close If oRstChild.State = adStateOpen Then Set oRstChild = Nothing If oConn.State = adStateOpen Then oConn.Close Set oConn = Nothing ' Create a custom recordset with the error message as the only field '-------------------------------------------------------------------- oRst.CursorLocation = adUseClient oRst.Fields.Append "ERROR_MSG", adVarChar, 255 oRst.Open oRst.AddNew oRst.Fields("ERROR_MSG").Value = _ "An error occurred: " & Err.Description oRst.Update Set updateShapedCarDetail = oRst 'abort the transaction GetObjectContext.SetAbort 'return a error code to the application End Function |