Introduction
Recently I wrote a C# library XMLDB.dll for the .NET environment. This dll contains a single class XMLDBComponent which greatly simplified my database programming work. It can, for example, execute a SQL query and convert the retrieved data into an XML document object with a single method call.
There are three projects included in the source code package.
XMLDBLib builds the library, XMLDB.dll.
XMLDBTest is an ASP.NET test application.
XMLDBService is a web service making it possible for clients running on various machines and platforms to access XMLDB.dll.
Unless otherwise stated, the sample code in this article is in C#.
The class XMLDBComponent is implemented using classes from the System.Data.OleDb name space. Instead of using more than five classes, now you can use only one class for all of your basic database work in .NET. The trade-off is, you won't have all the power and control provided by the .NET Framework classes. The reason I did not build my class on classes from the System.Data.SqlClient name space is that I don't want to restrict my databases to just SQL server. I would like to include MS Access, for example. Too bad the System.Data.OleDb classes do not support ODBC data sources.
The code has only been tested on Windows 2000 against SQL 7, SQL 2000, and MS Access databases.
Simple database access
First, we have to create an instance of the XMLDBComponent object. This is done by calling the static method CreateObject(), like the following:
XMLDBLib.XMLDBComponent obj = XMLDBLib.XMLDBComponent.CreateObject(
"Provider=myProvider;Database=myDB;Server=myServer;" +
"User ID=me;Password=");
The parameter in the above code is the connection string. For details about the connection string, please read the documentation for the .NET Framework class System.Data.OleDb.OleDbConnection. After the object is created, the following code will make a SQL query and save the returned data in a file.
System.Xml.XmlDocument doc = obj.GetXmlDocument(
"select ID, firstname, lastname, hiringdate " +
"from employee");
obj.Close();
if(doc==null)
{
String strError = obj.GetLastError();
}
else
{
doc.Save("c:\temp\employee.xml");
}
The Close() method will close the database connection and release all internal resources. You don't have to call it if the same XMLDBComponent object needs to be reused later. Even if you called Close(), the object knows how to automatically reconnect to the same database the next time it is used. In fact, the Close() method is called internally each time a database error occurs to refresh the database connection. We will talk about the Release() method in the comment later.
Here is the XML file representing the returned data in the above code.
<XMLData>
<Record>
<ID>334</ID>
<firstname>Albert</firstname>
<lastname>Einstein</lastname>
<hiringdate>3/12/1999</hiringdate>
</Record>
<Record>
<ID>404</ID>
<firstname>Clint</firstname>
<lastname>Eastwood</lastname>
<hiringdate>12/21/2000</hiringdate>
</Record>
<Record>
<ID>536</ID>
<firstname>James</firstname>
<lastname>Bond</lastname>
<hiringdate>10/23/2002</hiringdate>
</Record>
<RecordCount>3</RecordCount>
<FieldCount>4</FieldCount>
</XMLData>
GetXmlDocument() is the method that retrieves data from the database and builds the output document object. It returns null on error. In the output XML document, each record is contained in a <Record> element and each field of the record is a subelement. If a field value is null, then the corresponding subelement will be missing from the output document. The column names in the SQL query will be used as the names of the field elements. If the columns are not named (such as select * from ...), the corresponding database field names will be used as the element names. For a calculated column you must give it a name in the SQL query, for example, select count(*) as total from employee. At the end of the XML document, there are two elements <RecordCount> and <FieldCount> indicating the size of the output record set.
The GetXmlString() method is the same as the GetXmlDocument() method except that the output is a string instead of a document object. This method returns the empty string on error. By the way, there is no exception thrown by XMLDBComponent. You can check error by the return value and the GetLastError() method returns a descriptive error message for the most recent error.
Thread safety and object pooling
All static methods in the class XMLDBComponent are thread safe, but the class is not thread safe at the object level meaning you cannot have multiple threads accessing the same object simultaneously. In a multi-threaded application such as the web server, there may be many threads running simultaneously. We could create a separate XMLDBComponent object in each new thread. However, this is not always the most efficient way. Because not all threads are doing database related work all the time, we should reuse existing objects as much as possible.
XMLDBComponent maintains a global pool of objects that makes sharing objects an easy thing to do. When you call the CreateObject method, the global object pool is checked to see if there is already a unused object with exactly the same connection string. If such an object is found, it will be returned to you without creating a new one. If an object is no longer needed, you should call the Release() method so that other threads can reuse it. Note that you must not use an object after its Release() method is called.
Another thing to remember is that you do not have to call the Close() method before releasing an object to the pool, because the same object can be reused later. At the end of your program, you should call the CloseAll() static method, which will call Close on all unused objects in the global object pool. Therefore, as long as you remember to release an object after using it, the CloseAll() method will clean up everything.
You don't have to keep a reference to the XMLDBComponent object you created. Instead you can obtain its ID string by calling the GetID() method. Later in your program, you can call the GetObject method using the ID string as parameter to retrieve the exact object you used before. However, you cannot do this if you already called Release() on this object. This feature is very helpful in implementing the web service for XMLDBComponent, as you will see later.
I am aware that the .NET Framework classes already support database connection pooling.
Using parameters in SQL queries or stored procedures
Besides retrieving data from the database, you can also modify records in the database (insert, update, or delete). Suppose you have a stored procedure UpdateTotal that takes a string parameter ID and a integer parameter total, here is a VB subroutine that calls this procedure.
Public Sub CallProcUpdateTotal( _
ByVal strConnect as String, ByVal strID as String, _
ByVal nTotal as Integer)
Dim obj as XMLDBLib.XMLDBComponent = _
XMLDBLib.XMLDBComponent.CreateObject(strConnect)
obj.AddParameter(strID)
obj.AddParameter(nTotal)
If obj.Execute("exec UpdateTotal @ID=?, @total=?") Then
Else
Dim strError as String = obj.GetLastError()
End If
obj.Release()
End Sub
The parameters you add using the AddParameter() method has to be in the same order as they appear in the SQL command. The Execute() method will run a SQL command that updates the database. You can also use parameters with the GetXmlDocument() or the GetXmlString() methods to retrieve data. After a SQL query or command is executed by an XMLDBComponent object, the added parameters are cleared automatically. You need to add the parameters again the next time you want to make the same call.
Simple transactions
XMLDBComponent supports simple transactions. Call the BeginTransaction() method to, well, begin a new transaction. Then do some database work. And finally call the Commit() method to complete the transaction. If any database error occurs, the transaction will be rolled back automatically so you don't have to remember to rollback your transaction. You can call the Rollback method manually if you change your mind about the work you have done to the database since the start of the transaction.
There is no problem to have multiple transactions in your application (just use multiple XMLDBComponent objects). However, nested transaction is not permitted. If you call BeginTransaction() twice on an object, the previous transaction will be rolled back before the next one is started.
Here is a more complicated "virtual" example. Suppose you have a .NET web application which consists of multiple pages. The user is supposed to go through the pages one by one and saving the data to the database along the way. At the last page, all the data collected from the user should be committed or ralled back. In the first page, your app will call the CreateObject() method and then call BeginTransaction() to start database work. When the user moves to the next page, the ID string of the object will be passed along and the next page will call GetObject() using the ID string to obtain the same object created in the first page. On the last page, your app will call the Commit() method to complete the work and call the Release() method to return the object to the pool. Actually, I have never tried the idea in real applications, there could be many problems with this kind of approach.
The web service
The project XMLDBService is a simple web service written in VB. It exposes the methods of XMLDBComponent to all potential web clients. That means your client program doesn't have to create a new XMLDBComponent object in its own address space and the XMLDB.dll doesn't have to be copied onto the same machine the client program is running. In fact, you don't even need the .NET Framework or the Windows operating system installed on the client machine (theoretically, at least).
The methods in the web service are almost the same as those in the XMLDBComponent class except an extra object ID parameter. The typical way to use the web service is, you first call the CreateObject() method, instead of returning an XMLDBComponent object, the method returns an ID string which identifies a unused object within the object pool located in the remote web service process. Later when you call other web service methods, you need to pass the ID string along so that the SQL queries or stored procedures will be executed on the same object you obtained before. Internally, the web service just uses the ID string you passed to call GetObject(), then executes your SQL statements on the returned object. By the way, the Release() method has to be called for each object accessed, otherwise the global object pool in the web service process will get bigger and bigger.
With such a web service, there can be some very unconventional applications. For example, a program A will create an XMLDBComponent object using the web service, start a database transaction on it, and then pass the ID string of this object to program B. Program B will do some database work on this object using the ID string, either commit the transaction or pass the ID string to a third program, program C. Program C can do the same as program B, and so on. So it seems the transaction has crossed process or machine boundaries (actually it all happens within the object in the remote web service process).
Summary
Here is the complete list of public methods of the XMLDBComponent class and we have covered almost all of them. As mentioned before, the methods for the corresponding web service are almost the same except an extra string parameter for the object ID.
static public XMLDBComponent CreateObject(
String strConnect);
public String GetLastError();
public String GetID();
public bool Open();
public bool Open(String strConnect);
public void Close();
public void Release();
public void AddParameter(Object par);
public XmlDocument GetXmlDocument(String strSQL);
public String GetXmlString(String strSQL);
public bool Execute(String strSQL);
public bool BeginTransaction();
public bool Commit();
public bool Rollback();
static public XMLDBComponent GetObject(String strID);
static public void CloseAll();
I hope you will find the code interesting or even useful. Thank you.
Finally, a shameless plug
Some ideas in this article are taken directly out of my free software package XYSystem Components. By the way, it can be used easily in the .NET environment.
History
19 July 2002 - updated source