|
Introduction
Many developers argue that the true potential of object-oriented development isn't realized until a proper OODBMS is used for persistent storage of data. Of course this is not true, but it touches an interesting issue. There is a huge and largely unnecessary productivity and quality decrease as a result of developing in different paradigms simultaneously. One such common situation is mixing object-oriented development and relational databases.
The work-around is to introduce a dedicated object-relational mapping layer that transparently maps objects to relational data. Developers using this approach experience a tremendous increase in productivity, escape database vendor lock-in, and simplify maintenance.
On the down side, it requires substantial effort to create an object persistence layer manually. Fortunately there have emerged excellent code-generation tools that can do the time consuming work for you.
This article will take a closer look at the benefits of O/R mapping.
Design Goals of an Object Persistence Layer
When you decide to implement an object persistence layer, you want to achieve three important goals: decreased coupling, increased cohesion, and increased abstraction.
By routing all data access through an encapsulating layer you decrease the coupling between your application and the storage solution. In other words, your application becomes independent of the underlying data storage, usually a relational database.
Low coupling is achieved by delivering transparent persistence services. This allows your application to become agnostic to the "physical" mechanisms of the storage solution, which can be changed in the future without affecting the functionality of the application.
Typically the object persistence layer exposes a full set of access methods and properties that are mapped to the particular underlying storage solution. If you need to change the storage solution, just update the mapping code.
Even seemingly small changes, such as database upgrades, can be a big problem with storage solution specific code scattered all over your application. With the specifics confined to the dedicated object persistence layer, upgrades become a much smoother operation.
Cohesion is about doing one thing great rather than several things poorly. By focusing the persistence code to one separate layer, bugs and performance bottlenecks are easier to isolate and address. Also, the consequences of changes can be predicted with greater certainty.
The object persistence layer should only CRUD (Create/Read/Update/Delete) data - and that is it! You really want to avoid business rules, communication interfaces, or GUI-elements originating from or performing in this part of your application. The reasons are the same that have been driving structured programming for the past two decades: reuse and maintainability.
We all agree that a knife is more versatile than an egg slicer, and it never fails as long as we sharpen it now and then. Yet, when developing software we use feature mania to try to achieve the same thing. An object persistence layer that only CRUDs data, but does it great, is more versatile than one that has integrated business rules and other "great" features.
Finally, but most important, the object persistence layer increases the level of abstraction by hiding the complexity of the underlying data model. You can use advanced object-oriented concepts such as inheritance, polymorphism, and complex relationships without having to worry about how it is implemented.
Abstraction is one of the most important factors driving software quality and developer productivity. High-level programming languages and development environments such as Visual Basic, ASP and .NET have really empowered developers to create fantastic applications without explicit knowledge about the internals. This is really important because software developers create value by solving business problems, not by patching inefficiencies in the underlying technology.
There is a performance penalty with abstraction, but the benefits of increased productivity and quality far outweigh the drawbacks.
Benefits Of Using O/R-Mapping
I assume that you use relational databases for much of your structured storage needs. Relational databases are very popular, thus, most persistent object layers will have to map to a relational database.
Relational database technology is mature. All the popular databases, such as Oracle, SQL-Server, DB2 and Sybase, have plenty of tools allowing easy deployment, data maintenance, and analysis. These tools can be used as usual when persisting the objects to a relational database. Relational databases are effectively an open data storage solution allowing enterprise wide data reuse and straightforward integration with future applications.
Another great advantage is the Structured Query Language that gives access to advanced filter operations that can be used by your object persistence layer. This saves a lot of effort, but care has to be taken if you want to retain portability. There are plenty of proprietary extensions to SQL, and the use of stored procedures also creates vendor lock-in. However, if you use vendor specific SQL and stored procedures carefully, porting can be achieved with reasonable effort.
The overhead of the object persistence layer is usually a consequence of an advanced class model. Sometimes you really need maximum performance, and if you have mapped the objects to a relational database you always have the option to connect straight to the DBMS and execute specific stored procedures or SQL-statements. Basically, you would write 99.9% of your application using the object persistence layer and then add a few performance critical operations accessing the database directly.
The Object Relational Impedance Mismatch
Even though object-oriented and relational theory have fundamental differences, they can be mapped against each other perfectly. The challenge to overcome is the so-called object-relational impedance mismatch. This describes the difficulties that arise due to these fundamental differences.
Both performance and functionality suffer if the impedance mismatch isn't addressed properly, and the key is in making the right trade-offs. There are a couple of papers that discuss these issues in more depth (see useful links section below).
Creating Your Class Model
Creating a class model differs in many ways from ordinary data modeling. Most notably is the higher level of abstraction. You will normally have many tables corresponding to one class, and information regarding relationships will be stored directly in the class model.
For the sake of these examples we will be using the Component Definition Language (CDL), allowing us a very readable way of defining our class model.
Component CommunityServer
Class User
{Name As String}
{HashedPassword As String}
{Roles() As Role, <<=Users}
{Posts() As Post, <<=Creator}
{Articles() As Article, <<=Creator}
End Class
Class Post
{Creator As User, <<=Posts}
{CreationDate As DateTime, default=Now()}
{Header As String}
{Contents As BigString}
{Checked As Boolean, default=False}
{ReplyTo As Object, <<=Replies}
{Replies() As Post, <<=ReplyTo}
End Class
Class Article
{Creator As User, <<=Posts}
{CreationDate As DateTime, default=Now()}
{Editor As Role, <<=Items}
{Header As String}
{Body As BigString}
{Checked As Boolean, default=False}
{Hidden As Boolean, default=False}
{Posts() As Post}
{Replies() As Post, <<=ReplyTo}
End Class
Class Role
{Name As String}
{Items() As Object}
{Users() As User, <<=Roles}
End Class
End Component
We have created a community server that contains the following classes: User, Post, Article and Role. These four classes correspond to 16 tables in the database. When coding your persistence layer manually you would now have to create the tables and map each column to the appropriate entity in the class model. O/R mapping tools do this for you.
If you end up with a lot of classes mapped to single tables, you should seriously question whether you are actually creating the right level of abstraction. You should definitely redesign the class model. The purpose of the object persistence layer is not to become "the emperor's new clothes" to an ordinary relational data model.
Note how we have defined a one-to-many inverse relationship between User.Articles() and Article.Creator in the class model above. This means that we can reference a user's articles by looping through the collection property User.Articles(), also we could reference back through the Creator property: User.Articles(n).Creator = User.
The OO-way of referencing other objects is nice because of the high readability. Also, you will enjoy the added benefit of intellisense and type checking. This will save you from plenty of subtle typos. Another great thing is polymorphism. All derived subclasses of the class Article can be referenced through User.Articles().
Creating relationships are the key to a usable class model. By choosing carefully when to implement them you are also creating a best practice for working with the objects. The goal is to generate a flow in the business methods that make them straightforward to program and easy to read.
One important aspect when creating the class model is not to over-engineer it. In this case it might be questionable whether it is absolutely necessary to have a separate Role class. We have decided it is because we want the users to be self-organizing, creating new roles dynamically. This is an important design consideration. Basically you want to think two steps ahead, but only implement the simplest possible version. Keep it simple when you can!
For the sake of scalability you might wish to put one or more classes in its own component. This way it can be compiled and deployed separately from the rest of the classes. This is really tricky to implement manually. In these cases it is advisable to use an O/R mapping tool. Make sure it supports cross component referencing. You'll notice that this is a great way to get started with component-based development.
In the example above, the classes User and Role might be placed in a separate UserServer component. Thus, when adding a ShoppingServer you could easily share the same UserServer component. This way you will have the choice to omit the CommunityServer in future projects where it is not required. Without much effort you have created reusable components.
Coding With Your Persistent Objects in VB
Now we have reached the fun stuff! You have created your object persistence layer and underlying database, making up the complete logical data tier. It is time to write business methods and start reaping the benefits of the elegant class model. You could write the business methods in ASP, but we'll present the VB code for the sake of diversity.
The example is implemented against an object persistence layer generated by O/R mapping tool Pragmatier Data Tier Builder. Let's start by showing a really simple example (hint, the class CComponent contains the GetObject method that is used both to fetch and create objects):
' Instantiate an object factory that does the o/r mapping.
' Initialise some variables and create an object with
' the GetUser method. The GetUser method with the second
' parameter (create if not found) set to true basically means:
' get me an object of the class User with a certain key,
' and if it doesn't exist create it for me and give me
' the created object.
Dim ObjectFactory As New CommunityServer.CComponent
Dim User As CommunityServer.User
Dim EditorRole As CommunityServer.Role
Set User = ObjectFactory.GetUser("seb@mail.com", True)
Set EditorRole = ObjectFactory.GetRole("FirstEditor", True)
' Assign some values to the object and persist the object.
' Finish off with the clean-up method that helps release cyclic
' relationships that could fool COM not to release the memory.
User.Name = "Sebastian Ware"
User.AddToRoles EditorRole
User.Persist
ObjectFactory.CleanUp()
The code is pretty easy to read even without any comments. A non-programmer could, with very little instructions, verify that we have created a new user named "Sebastian Ware" and assigned him the role "FirstEditor", which was also created.
The point to note is how this code focuses entirely on business rules without even considering whether we have SQL-Server or Oracle running beneath. We don't even have to understand the underlying database schema; we concentrate on the more useful class model.
Let's take a look at a real world example. Imagine that we want to check the write credentials of a user wanting to update an article. That code could look something like this:
Public Function MayEdit(ByRef User As CommunityServer.User, _
ByRef Article As CommunityServer.Article) As Boolean
Dim Editable As Boolean
Dim Role As CommunityServer.Role
Editable = False
' Check whether the user created the article.
If Article.Creator.Qualifier = User.Qualifier then
Editable = True
Else
' Check all the roles the user has to see if she has
' editor rights to this article.
For Each Role in User.Roles()
If Role.Qualifier = _
Article.Editor.Qualifier then
Editable = True
End If
Next
EndIf
MayEdit = Editable
End Function
The code is highly readable and the project manager can easily verify that this was the intended behavior. By using good names the code itself becomes reasonable and accurate project documentation even without any knowledge of the database schema or SQL.
If we decide to subclass the class User or Article defined in the class model, this method would still work fine courtesy of polymorphism. We can simply reuse basic components without any modification.
Let's take a look at another example where we start by selecting a subset of objects by means of a filter operation. We want to hide all the articles that haven't been checked by an editor. For this purpose we filter by the property Article.Checked.
Public Sub HideUnchecked()
Dim ObjectFactory As New CommunityServer.CComponent
Dim Article As CommunityServer.Article
Set Article = ObjectFactory.GetArticle("")
For Each Article in Article.FilterByChecked(False, "=")
Article.Hidden = True
Article.Persist
Next
ObjectFactory.CleanUp()
End Sub
The Articles.FilterBy[PropertyName] method returns a collection of objects matching the argument provided. If you also supply a collection of objects as a parameter, the filter operation will be performed within that collection. In the example above, the filter operation returns all objects of the class Article where Article.Checked = False. Then it sets Article.Hidden = True and saves the update.
As we mentioned earlier you really want to use plenty of relationships in your class model so that you can "navigate" between objects. Using filter operations doesn't give you the same abstraction.
Coding With Your Persistent Objects in ASP
When you have created your business methods, you'll probably want to create some ASP-pages as an interface to your application. This is just as straightforward, making the pages very easy to maintain.
The first example displays a list of users by looping through all the objects and printing the User.Name property:
Users.asp
<%
Dim ObjectFactory, User
Set ObjectFactory = Server.CreateObject( _
"CommunityServer.CComponent")
Set User = ObjectFactory.GetUser("")
' This is where we loop through all the user objects.
' Note that we have sorted the objects by name.
For Each User In User.SortByName()
Response.Write "<a href=""user.asp?UserID=" & _
User.ID & """>" & User.Name & "</a><br>"
Next
ObjectFactory.CleanUp
%>
Another pertinent example is to display all the articles and posts written by a certain user. We assume that the UserID is supplied in the call, which would make the code look something like this:
User.asp:
<%
Dim ObjectFactory, User, UserID, Post, Article
UserID = Request("UserID")
' You can't fool us...
If Len(UserID) < 1 Or Then
Response.Write "You must supply a user id!"
Response.End
End If
Set ObjectFactory = Server.CreateObject( _
"CommunityServer.CComponent")
' Get the User object.
Set User = ObjectFactory.GetUser(UserID)
Response.Write "ID:" & User.ID & "<br>"
Response.Write "Name:" & User.Name & "<br>"
' We use the relationship between class User and
' Article rather than a filter operation in order
' to find the articles written by the user.
Response.Write "Articles by this user:<br>
For Each Article In User.GetAllArticles
If Article.Checked And Not Article.Hidden Then
Response.Write & _
"<a href=""article.asp?aID=" & _
Article.ID & """>" & _
Article.Header & "</a><br>"
End If
Next
' Dito to posts.
Response.Write "<br>Posts by this user:<br>
For Each Post In User.GetAllPosts
If Post.Checked Then
Response.Write & _
"<a href=""post.asp?pID=" & _
Post.ID & """>" & _
Post.Header & "</a><br>"
End If
Next
ObjectFactory.CleanUp
%>
The code is pretty straightforward and can easily be changed to get a new appearance. As long as the interfaces aren't changed, the GUI will work fine, even if the object persistence layer is updated to map to another database or if business methods are rewritten. Adding new properties, such as first name and last name to the class User, can easily be done, and by updating the mapping code of the property User.Name to concatenate the contents of these new properties, the application needs no further modification.
Conclusion
Creating a separate object persistence layer using object-relational mapping is a great way of improving developer productivity and application quality, and simplifying maintenance. Object-oriented features such as inheritance and polymorphism are great enablers of code reuse, if you choose to use it, and reusing the class model doesn't require much overhead. If you haven't explored the possibilities of object-relational mapping technology yet, today is a good day to start. You might never want to write another line of SQL again.
Useful Links
Link to the tool used:
http://www.pragmatier.com
Links to useful resources:
http://www.objectarchitects.de/ObjectArchitects/orpatterns/index.htm
http://www.ambysoft.com/mappingObjects.html
http://www.objectmatter.com/vbsf/docs/maptool/ormapping.html
About the Authors
Sebastian Ware and Mats Helander founded and run the software development company Pragmatier. The company specialises in object-relational mapping tools -- the art of storing persistent objects in ordinary relational
databases.
The flagship product, Pragmatier Data Tier Builder, incorporates industry leading code generation technology allowing developers to maximize productivity while retaining flexibility. The tool supports COM/COM+ and
.NET.
Sebastian Ware can be reached at sebastian.ware@pragmatier.com
|