Introduction
The Object Persistence Framework for .Net (OPF.Net) is a complete set of classes that implement an object-relational mapping strategy for object oriented access to traditional relational database management systems and other types of persistent storage types such as XML files. OPF.Net has been designed and implemented for practical use in small to medium size projects and is currently being successfully used in several projects.
Background
The basic idea for OPF.Net and some important concepts behind OPF.Net come from "The Design of a Robust Persistence Layer� written by Scott Ambler (http://www.ambysoft.com/). OPF.Net has been written to bridge the traditional gap between object oriented programming and relational data storage. While Object-Databases have made a big progress, relational database management systems are still unrivalled in terms of performance and represent the de facto standard for most new software development projects. Accessing a relational database from an object oriented application can be implemented in two basic fashions:
- use a persistence mechanism that maps the applications business objects to the database tables and vice versa
- directly access and manipulate database tables and records using data access layers and components provided by most modern RAD programming environments
The first approach generally leads to well designed, real object oriented applications with a clear separation between user interface and business logic implemented through reusable business objects.
The second approach generally leads to poorly designed applications breaking the object oriented programming style with no clear separation of user interface, business logic and data storage and no encapsulation of business logic in business objects.
OPF.Net implements a simple and robust mechanism for building applications using persistent business objects by completely encapsulating database access and thus helping to build well designed software.
The Object Persistent Framework
OPF.Net distinguishes between domain classes and data manager classes.
Domain classes are the classes, also called business objects, that actually represent the data to be read, manipulated and written to the persistent storage. Domain classes do not know how to read and write to the storage, they contain no storage specific commands (e.g. SQL) but instead offer methods such as Load, Save and Delete to load, save and delete themselves from the underlying physical storage. The user interface of an application and all its business logic is implemented using the domain classes; application developer do not need to know what kind of underlying storage is used.
Data manager classes perform the actual loading, saving and deleting of domain classes from the persistence layer. The data manager classes are the only classes that know about the underlying storage mechanism and hold storage commands (SQL for DBMS storages) to perform the requested persistence operations. Therefore, to port an application from one DBMS to another, only the data manager classes have to be modified. The domain classes along with the business logic and the user interface do not need to be changed at all.
Associating a domain class with its corresponding data manager class is done through the ObjectBroker
. The ObjectBroker
is the only part of the framework knowing domain and data manager classes. All persistence methods called for a domain class are passed on to the ObjectBroker
along with the domain class itself. The ObjectBroker
then searches the associated data manager class and instructs it to perform the requested operation passing the domain class as a parameter. The data manager class execute the requested operation and eventually populates the domain class� properties on a load or uses the domain class� properties to fill command parameters on save and delete commands.
Each property of a business object generally maps to a single field in the storage. By loading a business object all its public properties get loaded from the underlying storage. Protected and private properties are by default not persistent. Through custom attributes it is however possible to mark properties as persistent, not persistent and read only.
OPF.Net Architecture in a two tier application
Every application needs to process not only single business objects but also collections of business objects. OPF.Net supports loading, saving and deleting collections of business objects through its Collection domain class and the corresponding Collection data manager class. As with single business objects, the data manager controls how a collection actually gets loaded from the underlying physical storage by defining one or more storage commands (SQL queries for DBMS) to load different collections of business objects of one type.
OPF.Net fully supports transactions if the underlying physical storage supports transactions. Using the StartTransaction
, Commit
and Rollback
methods of the ObjectBroker,
an application can start and complete transactions on one ore more used storages at the same time. All access to the physical storage is handled by the Storage class and is derivatives. To support a new type of physical Storage in OPF.Net a only new Storage class has to be written. At present OPF.Net offers Storage classes for MS SQL Server, ADO ,OLE DB and ODBC.
Step-by-step
This section will take you through the process of using OPF.Net, step by step.
Set up OPF.Net
First of all we have to create the ObjectBroker
, which is one of the central units in your application. For windows applications (single user) it is recommended to use the SingleInProcessObjectBroker
located in the OPF.ObjectBroker
namespace.
If you are developing a web or a server application you should use the MultipleInProcessObjectBroker
. In that case you have to register a class that implements the ISessionRetriever
interface. That interface implements a function that returns a unique ID for each session. In web applications normally this function should return the ID of the current session.
Since we are developing an simple single user windows application we are using the SingleInProcessObjectBroker
. To set the ObjectBroker
we have to instance it and assign it to OPF.Main.ObjectBroker
. OPF.Main
is a static class that holds a global information required by the OPF.Net. (eg. MinDatValue
containing the minimum date value the storage supports, IDFieldName
the name of the field containing the id...)
private static Boolean InitOPF()
{
_ObjectBroker = new SingleInProcessObjectBroker();
OPF.Main.ObjectBroker = _ObjectBroker;
...
At this point it would also be possible to set the the IDGenerator
. The IDGenerator
is a class that generates the IDs for new objects saved to the storage. By default the IDGenerator
generated GUIDs which are guarantee to be unique. You have the possiblity to override the class to generate your own IDs.
The next step while setting up the OPF.Net is creating a storage. The storages are located in the OPF.Storages
namespace. In our sample we use a OleDbStorage
since we are dealing with Access.
We have to set the ConnectionString
property or the connection string while connecting to the storage. To immediately check if the storage is available we set the connection string when connecting. It is also possible to not immediately connect because the ObjectBroker
detects if a connection to a storage is required.
In the sample application the created storage is set as DefaultStorage
of the ObjectBroker
. If we are not setting the storage as DefaultStorage
, we have to associate (register) each Persistent
, Collection
or derived classes with the storage.
...
_Storage = new OleDbStorage();
OPF.Main.ObjectBroker.DefaultStorage = _Storage;
try
{
OPF.Main.ObjectBroker.DefaultStorage.Connect(
@"Provider=Microsoft.Jet.OLEDB.4.0;
Data Source=..\db\database.mdb;");
}
catch
{
MessageBox.Show(
"Not possible to find the database or to " +
"connect with the database! " +
"(Check Global.cs - line 88)",
"Error while connecting with database",
MessageBoxButtons.OK, MessageBoxIcon.Error);
return false;
}
...
Usually in our applications this code is located in a function called InitOPF()
. We create this function as we think, that the stuff initiating the OPF.Net should be located in a function and not in Main()
. InitOPF()
returns a Boolean
a successful initialization of the OPF.Net. You can check in the Main()
function what InitOPF()
returns and proceed in a suitable way.
Registering BusinessObjects
The next step takes is to register the persistents and collections with the corresponding data managers. We also register the storage commands required by the application. A StorageCommand
is a class that allows us directly send a query to the storage and return an ArrayList
containing the results. Storage commands should generally used for performance reasons only, since the circumvent the business objects. We create normally three functions: RegisterPersistents()
, RegisterCollections()
and RegisterStorageCommands()
.
...
RegisterPersistents();
RegisterCollections();
RegisterStorageCommands();
return true;
}
RegisterPersistents()
registers all persistents and derived classes with the corresponding data manager.
private static void RegisterPersistents()
{
_ObjectBroker.RegisterPersistent(typeof(bo.Book), typeof(bo.BookDM));
}
RegisterCollections()
registers all collections and derived classes with the corresponding data manager. It is not necessary to set a storage, as the storage registered while registering the persistent or derived class with is data manager is taken.
private static void RegisterCollections()
{
_ObjectBroker.RegisterCollection(typeof(bo.BookCollection),
typeof(bo.BookCollectionDM),
typeof(bo.Book));
}
At the last step we have to register the storage commands. RegisterStorageCommands()
registers each StorageCommand
with the global ObjectBroker
using an alias. To use this command in an application just call ObjectBroker.ExecuteStorageCommand(..)
specifying alias and save the result to an arraylist.
The StorageCommand
class allows you to set the type of command. There are two types:
Execute
executes a query on the storage and returns nothing. Retrieve
normally returns a data record that is mapped to an arraylist. If result should be returned set the StorageCommandType
to Retrieve
.
private static void RegisterStorageCommands()
{
StorageCommand StorCmd = new StorageCommand("NumberOfBooks");
StorCmd.StorageCommandType = StorageCommandType.Retrieve;
StorCmd.Command = "select count(*) from Books;";
OPF.Main.ObjectBroker.RegisterStorageCommand(StorCmd);
}
So far set up the OPF.Net and registered the collections, persistents and storage commands. Now we can create a business object to show you how that is done and how they are used.
Creating business objects and support classes
All business objects must inherit from the Persistent
class. In order to load, save and delete business objects from the storage we also need to create a data manger class by inheriting from DataManager
. To support collections of this business object the collection and collection data manager have to created respectively inheriting from Collection
and CollectionDataManager
.
Let's start with the derived persistent class.
Creating a derived Persistent class
Creating a derived persistent class is very easy. We only have inherit from Persistent
and add the properties represents the columns in the table of the storage.
As you will see in the persistent data manager we can set a property that tells the data manager to parse the column names while populating the properties of the derived persistent object. The parsing algorithm that we use is very simple, but powerful enough to take a column named LAST_NAME
and convert it to LastName
. If parsing is enabled in the persistent data manager the property in the persistent class has to be named LastName
. If not, we have to name the property LAST_NAME
.
How does the parser work?
First of all the parser converts the column name do lower case. Then the parser eliminates the underscores and converts the first letter following an underscore to upper case. The first letter of the column name is also converted to upper case. The only exception were the underscore isn't eliminated is _ID
. In this case the parser converts to _ID
(brings ID to upper case).
Examples:
- LAST_NAME = LastName
- FIRSTNAME = Firstname
- PICTURE_ID = Picture_ID
- AUTHOR_HOUSE_ID = AuthorHouse_ID
- LAST_MAN_STANDING = LastManStanding
To get property names that are independent of storage field names they PopuplateProperties()
and PopulateParams()
method of the persistent datamanager have to be overridden or you can use the custom attribute MapField
. It allows you to directly connect a property with a field in the storage. It is strongly recommended to use the MapField
custom attribute instead let the OPF.Net parse the properties.
The following code snippet shows the book
business object. As you see also we override the constructors to have the possibility to use all constructors of the base class.
public class Book : Persistent
{
Gender _Gender = 0;
String _Title = "", _Author = "";
Boolean _Released = false;
DateTime _Releasedate = DateTime.MinValue;
public Book() :base() {}
public Book(PersistentFlags PersistentFlags) :base(PersistentFlags) {}
public Book(PersistentFlags PersistentFlags, String XmlDefinition)
:base(PersistentFlags, XmlDefinition) {}
public Book(String XmlDefinition) :base(XmlDefinition) {}
[Mandatory, MapField("Title")]
public String Title { get { return _Title; }
set { _Title = value; } }
[Mandatory, MapField("Released")]
public Boolean Released { get { return _Released; }
set { _Released = value; } }
[Mandatory, MapField("Author")]
public String Author { get { return _Author; }
set { _Author = value; } }
[Mandatory, MapField("Gender")]
public Gender Gender { get { return _Gender; }
set { _Gender = value; } }
[MapField("ReleaseDate")]
public DateTime Releasedate { get { return _Releasedate; }
set { _Releasedate = value; } }
[NotPersistent]public String TitleAndAutor
{
get { return "\"" + Title + "\" written by \"" + Author + "\""; }
}
}
Several custom attributes can be used to mark properties that are loaded from storage. If a property is marked with the [Mandatory]
attribute the property is checked while saving. If such a property is empty the OPF.Net throws an exception. Mark a mandatory field in the storage with the [Mandatory]
attribute. A property marked with [ReadOnly]
is only loaded and not saved back to the storage. This is useful if you join tables. [NotPersistent]
properties are not loaded nor saved. This attribute can be used for calculated properties that have no corresponding storage field, as the property TileAndAutor
in the example above.
If you look at the database coming with this article you will find an additional column named "Dyn_Props
". The OPF.Net detects this field as field for dynamic properties.
What are dynamic properties?
Dynamic properties are added dynamically to a persistent or derived class while the program is running. You need a large text field in the table of the persistent in the storage. This field has no corresponding property in the object. We normally use a field named Dyn_Props
. Dynamic properties are saved to and read from this field. The OPF.Net will detect this field automatically! Dynamic properties are saved as Xml into the dynamic properties field.
To add a dynamic property to a persistent or derived class you have to call the AddDynamicProperty(..)
function of that class. Remove them using RemoveDynamicProperty(..)
. A DynamicProperty
is either of type OPF.SupportedTypes
or of DynamicType
. A DynamicType
is a set of possible values the property value can have. You can see it like an enum. For more information check out the source code of the demo program coming with this article.
Creating a derived DataManager
class
As next step we have to create the DataManager
for our business object. The data manager contains the queries to load, save and delete the business object from storage. If you set ParseProperties
to true the column names are parsed (Look above for more information).
We have to override the SetCommands()
function of the data manager and set the queries using SetCommand(..)
.
public class BookDM : PersistentSqlDataManager
{
public BookDM(System.Type ConnectedObjectType) :base(ConnectedObjectType)
{
ParseProperties = true;
}
protected override void SetCommands()
{
SetCommand(DMOperations.Delete, "delete from Books where ID = :ID;");
SetCommand(DMOperations.Load, "select * from Books where ID = :ID;");
SetCommand(DMOperations.Insert,
"insert into Books (ID, Title, Released, Author," +
" Gender, ReleaseDate, Dyn_Props) values" +
" (:ID, :Title, :Released, :Author, :Gender," +
" :Releasedate, :Dyn_Props);");
SetCommand(DMOperations.Save,
"update Books set Title = :Title, Released =" +
" :Released, Author = :Author, Gender" +
" = :Gender, ReleaseDate = :Releasedate, Dyn_Props" +
" = :Dyn_Props where ID = :ID");
}
}
Parameters in the commands are marked with a ':'. Parameters are replaced by the storage class while saving, loading or deleting the object in the storage. By setting the ParameterRecognitionString
property of the OPF.Main
class it is possible to change the ':' to '@' (.Net standard) or something else. We use the ':' because of a former OPF version written in Delphi.
Creating a derived Collection class
The next step takes us to the Collection
class and creating a derived collection class. Collections are lists of objects of a certain type loaded from the storage. Each collection can be loaded in several ways by defining one or more loading functions. Example given LoadAll()
to load all objects of the given type. For each load function a corresponding load function and command (sql..) has to be defined in the collection's data manger. Each loading function passes an alias name and a parameter collection to the collection data manager for execution (The data manager identifies the command to use using the alias).
The following BookCollection
shows how to create a collection where all objects of the table are loaded and where only object with a certain author name are loaded.
public class BookCollection : Collection
{
public void LoadAll()
{
Load("Book.LoadAll", new ParameterCollection());
}
AuthorName)
{
use:
ParameterCollection();
AuthorName);
PCol);
Load("Book.LoadByAuthorName",
new ParameterCollection("AuthorName", AuthorName));
}
}
As you see in LoadByAuthorName(..)
the ParameterCollection
takes one parameter that contains the variable with the name of the author and a name for the parameter - in this case "AuthorName"
. Attention: LoadAll()
contains an empty ParameterCollection
!! You have always to set a parameter collection!
Creating a derived Collection DataManager
class
public class BookCollectionDM : CollectionSqlDataManager
{
String LoadAllSQL = "select * from Books;";
String LoadByAuthorName =
"select * from Books where Author = :AuthorName";
protected override void DefineCollections()
{
DefineCollection("Book.LoadAll", LoadAllSQL);
DefineCollection("Book.LoadByAuthorName", LoadByAuthorName);
}
}
The collection datamanager is very simple. There are only the queries (defined as strings or compiled dynamically using a delegate) and a function called DefineCollections()
that must be overridden. In DefineCollections()
you have to the connections between the collection names and the queries using DefineCollection(..)
.
It is also possible to set a Delegate
as query. This delegate is called while loading the collection. In this way a query can be assembled dynamically at runtime.
...
private String DynQuery(ParameterCollection PCol)
{
String Query = "select * from table where field = '1' and ";
if (PCol.ContainsParameter("ParameterName"))
{
String Parameter = PCol.GetParameter("ParameterName");
Parameter = Parameter.Replace("*", "%") + "%";
PCol.SetParameterValue("ParameterName", Parameter);
Query += " field1 = :ParameterName and ";
}
if (PCol.ContainsParameter("ParameterName1"))
{
String Parameter1 = PCol.GetParameter("ParameterName1");
PCol.Remove("ParameterName1");
PCol.Add("Parameter2", Parameter1);
Query += " field2 = :ParameterName1 ";
}
return Query;
}
DefineCollection("Book.LoadDynamically",
new DynamicCommandDelegate(DynQuery));
...
Using BusinessObjects
It's about time to use the business objects generated, don't ya think so?!
Persistent and derived classes
Single business objects are generally not loaded since the ID
is generally not known. Normally a collection of objects is loaded.
bo.Book Book = new bo.Book();
Book.Load("1");
To save a business object you have to call the Save()
function. To delete it the Delete()
function.
try
{
Book.Save();
}
catch (ConcurrencyException exc)
{
}
catch (ConstraintsException exc)
{
}
Collection and derived classes
A collection or business objects is loaded by using one of the customized load functions. For the book
object it's possible to use LoadAll()
and LoadByAuthorName(..)
.
bo.BookCollection BookCol = new bo.BookCollection();
BookCol.LoadByAuthorName("Roddenberry");
To delete an object from the collection you have to call Delete(..)
.
if (BookCol.Count > 0)
BookCol.Delete(0);
To return an object use the GetObject(..)
function or to use the Indexer BookCol[..]
.
if (BookCol.Count > 0)
{
bo.Book Book = BookCol[0];
}
The Delete(..)
function of the collection does not directly delete an object from storage. It moves the object to a so-called "DeletedList
". Only when the Save()
function is called all objects on the "DeletedList
" are deleted.
if (BookCol.Count > 0)
BookCol.Delete(0);
try
{
BookCol.Save();
}
catch (ConcurrencyException exc)
{
}
catch (ConstraintsException exc)
{
}
To loop over a collection you can use the Indexer BookCol[..]
or the foreach
directive.
foreach(bo.Book Book in BookCol.Objects)
{
}
for(Int32 i = 0; i < BookCol.Count; i++)
{
bo.Book Book = (bo.Book)BookCol[i];
}
As conclusion a full example that demonstrates how to use the collection:
bo.BookCollection BCol = new bo.BookCollection();
BCol.LoadAll();
if (BCol.Count > 0)
BCol.Delete(0);
for(Int32 i = 0; i < BCol.Count; i++)
{
bo.Book Book = (bo.Book)BookCol[i];
Book.Name = "TestName";
}
try
{
BCol.Save();
}
catch (ConcurrencyException exc)
{
}
catch (ConstraintsException exc)
{
}
For more information about the OPF.Net visit the sourceforge page of the framework (http://www.sourceforge.net/projects/opfnet/) or the official homepage (http://www.littleguru.net/ or http://www.sarix.biz/opf).