From the February 2002 issue of MSDN Magazine
| Establishing Relationships Between Rowsets with ADO.NET |
| Download the code for this article:
Data0202.exe (103KB) |
| X ML has taken the development community by
storm. But until the introduction of ADO.NET, the basic tenets of XML have not been
fully implemented by a data access toolset. Because ADO.NET has XML at its core,
it can handle full-feature XML data structures as well as relational data structures. |
This wasn't always the case. ADO 2.x
required the use of either the hierarchical recordset structure (which was very
slow and not sufficiently intuitive) or the standard Recordset object with a matrix
of columns and rows, in order to manage hierarchical data in an object. The Recordset
object in ADO 2.x manages a two-dimensional matrix very well, but it's not
great for representing hierarchical data.
In the previous two installments of the Data
Points column I began a series on writing applications using the DataSet and other
features of ADO.NET (see the
November 2001 and January 2002 issues of MSDNÂ® Magazine).
In the January column, I demonstrated how you can use a DataSet to retrieve data,
track modifications made by the user, and send the data back to the database. This
month I will build on this application so that the user can save several rows of
changes for customers, orders, and order details all in one shot. Using ADO.NET,
this type of relational yet hierarchical structure can be retrieved from and applied
to a database in one fell swoop.
Using Visual BasicÂ® .NET, I'll show you how
to use a business services application that's written in C# to retrieve customers,
orders, and order details from the Northwind database and plug them into a DataSet.
The DataSet will contain three DataTables, which I'll link using DataRelation objects
to create a hierarchical data structure. At that point, I'll load the information
into the presentation tier. Finally, I'll present examples that use the DataSet
to save several rows of data to the database all at once.
The application built in this article can be
broken into two distinct projects: a WindowsÂ®-based app to present the data to the
user and a class library project to handle the business rules and data manipulation
calls. I'll start by examining the Windows Form (written in Visual Basic .NET),
which allows the user to interact with the application. Then, I'll show how the
business services (written in C#) can retrieve the hierarchical data and make database
modifications. In the business services, I'll explore class inheritance as well
as the overriding and overloading of methods.
The key to managing data in ADO.NET is to understand
the convergence of hierarchical and relational data structures. An ADO.NET DataTable
stores a two-dimensional rowset, which is similar to the way you used the Recordset
in ADO 2.x. Currently, if you take two DataTables within the same DataSet
object and create a relationship between them using a DataRelation object, the result
is a hierarchical structure. A relational database such as Northwind is now represented
with its true relationships. In the past, you would have had to combine the customers
and their orders into a single rowset, thus producing a flattened data structure
by forcing the data into a single two-dimensional matrix. Now, using a DataSet and
its child objects, you can maintain the relational and hierarchical structure. Figure 1
shows how the DataSet object can contain multiple DataTable and DataRelation objects.
Figure 1 ADO.NET DataSet
Let's look at a Windows Form that controls the
presentation of this application. The form frmMain.vb contains a DataGrid control
that will be filled with the customers, their orders, and each order's details.
The DataGrid is a server control that is easily manipulated to render a rowset by
binding itself to a DataSet. Because the DataSet will have a hierarchical structure,
the DataGrid will automatically adjust to that structure by displaying all customers
first. All changes made to the DataGrid's data are reflected onto the underlying
DataSet. The DataGrid shows the customer rows with a + sign to the left of each
row (see Figure 2). This allows the user to drill down into each customer's
child records (in this case, the customer's orders).
Figure 2 The Customers
The DataGrid allows the user to navigate up
and down the parent and child relationships from a customer to their order and onto
the order details. The grid allows for changes to be made at each level of the hierarchy.
So whether a row is deleted, added, or simply modified, the DataGrid manages the
changes. All changes are then reflected in the underlying DataSet. To navigate to
the child records, simply click on the link for the DataRelation, called CustomerToOrder
(see Figure 2).
Figure 3 The Customer's Orders
After clicking on the CustomerToOrder DataRelation
hyperlink in the DataGrid, the customer's orders will be displayed (see Figure 3).
The grid can be a useful tool to represent and manage multiple levels of hierarchical
data and their relationships.
To load the data into the DataGrid, I've declared
and instantiated the business services' CustomerOrder class. Then the DataGrid's
data binding settings are cleared and the business services' CustomerOrder object
is called to retrieve the DataSet into the frmMain class property m_oDS. Now that
the DataSet is local, I bind it to the DataGrid, loading three levels of related
rowsets into the DataGrid (see Figure 4).
Public Class frmMain Inherits System.Windows.Forms.Form Private m_oDS As
The user can interact with the data and then
save the changes to the database by clicking on the Save button. Figure 5 shows the code in which the SaveData method
declares and instantiates the business services' CustomerOrder class. If no changes
were made to the DataSet, the method is exited. I check the HasChanges method of
the DataSet to see if any changes were made to the data.
If there are changes to the data in the DataGrid,
I set the oDS_Delta DataSet variable to contain the changed rows in the original
DataSet. The GetChanges method of the DataSet copies the changed rows from the DataSet
and puts them into a new DataSet called oDS_Delta. This reduces the network traffic
since I pass only the modified rows to the business services.
The Business Services
Up to this point the code is the same whether
the DataGrid is loaded with a single DataTable or the DataSet contains multiple
related DataTables. Where things really begin to change is in the business services.
The code in the following examples shows how to define and create instances of the
Customer, Order, and OrderDetail classes. These classes will be used to retrieve
and save data to and from the database. Since all three classes need to retrieve
and save data in the same manner, I have created a base class, Base_BusinessServices,
from which all three classes can inherit properties and methods.
Base Classes and Inheritance
The Base_BusinessServices class has two public
methods: GetData and SaveData. It also contains a protected method called InitializeConnection,
which defines the connection string. I made this method protected so that it can
be accessed by any class that inherits from or implements the Base_BusinessServices
class. The GetData method, shown in
Figure 6, works the same way whether the data retrieved is customer,
order, or order detail data. Therefore, I can use this method in the base class
and the Customer, Order, and OrderDetail classes can inherit it.
I also created a fifth class called CustomerOrder,
which creates an instance of each of the Customer, Order, and OrderDetail classes.
The CustomerOrder class also inherits from the Base_BusinessServices class since
it needs the GetData and SaveData methods. It is not necessary for this class to
inherit from the base class, but it provides a means to discuss overriding an inherited
method. You may have noticed in Figure 6
that the GetData method was declared with the virtual keyword. This tells any class
that inherits from this base class that it may override the method. In this application,
I wanted to have the GetData method execute code other than what was in the base
class, so I chose to override this method.
The Base_BusinessServices class also contains
a SaveData method, as shown in Figure 7.
Since I used the virtual keyword on this method, any class that's derived from this
base class will be able to override this method. Notice that the SaveData method
uses the try-catch-finally construct to apply the data changes to the database.
It opens the connection to the database and executes the SqlDataAdapter object's
Each row of the DataSet is traversed in the
same order that it was sent to the SaveData method. Assuming six rows were changed
(as shown in Figure 7),
the corresponding SqlCommand would be issued in the order that the rows appear in
The Inheriting Classes
The Base_BusinessServices class contains the
basic functionality that the Customer, Order, and OrderDetail classes require. Therefore,
by having each of these classes inherit from the Base_BusinessServices class, the
GetData and SaveData methods do not need to be redefined. Figure 8 shows the Customer class. (The Order and OrderDetail
classes are very similar to the Customer class, differing only in their SQL statements.)
To inherit the Base_BusinessServices class,
the Customer class's definition is followed by a colon and the name of the Base_BusinessServices
class, as shown in Figure 8.
The constructor for the Customer class is used to set up the SqlDataAdapter object.
The SqlDataAdapter takes four SqlCommand objects, each of which represents one of
the four main types of data manipulation and retrieval in SQL: SELECT, INSERT, UPDATE,
and DELETE. First, I created the SqlCommand to represent the query that retrieves
the customer data via a SELECT statement. Then I created the SqlCommand to represent
the query that updates the customer data via an UPDATE statement, and so on. Finally,
I created a SqlDataAdapter object and assigned the corresponding SqlCommand object
to each of its command properties.
The CustomerOrder Class
The CustomerOrder class puts all of the pieces
together in the business services. It combines the customer, order, and order detail
data and relates them within a single DataSet. The constructor for CustomerOrder
defines and creates the instances of each of these classes, as shown in Figure 9.
The GetData method overrides the base class
Base_BusinessServices' GetData method using the override keyword (see Figure 10). Here is where the data from the three classes
is combined into a single DataSet. The GetData method of each of the Customer, Order
and OrderDetail classes is called and a DataTable is retrieved from each. Then each
DataTable is added to a new DataSet. Stopping at this point in the process would
yield a DataSet comprised of all three rowsets, but the data would not yet be linked
in a hierarchical structure.
To relate the data, my next step was to create
a DataRelation object for each relationship. First, I created the relationship that
links the individual customers to their orders. The DataSet contains a Relations
collection, which has an Add method. I used this method to create the new DataRelation
object and add it to the DataSet. I called the relationship CustomerToOrder and
passed to it the columns from the DataSet that defines the relationship. By establishing
this relationship, you can traverse the DataSet from a customer to his orders.
To complete the relationships, I added another
DataRelation to link each order with its order details. With both DataRelation objects
linking the three DataTables within the DataSet, I can walk the tree from the parent
to its child records. At this point I'm ready to return the DataSet to the presentation
services (the Windows-based app, in this case).
Once the user saves the data modifications in
the Windows-based app, the delta DataSet is passed to the business services' CustomerOrder
class via the SaveData method (shown in
Figure 11). The CustomerOrder class's SaveData method overrides the
SaveData method from the inherited base class and uses the try-catch-finally construct
to save the data to the database.
First, the connection to the database is defined
and opened. Next, a transaction is begun on the connection. Creating the transaction
allows the code to use a two-phase commit on the data changes. The first phase prepares
the data changes, and the second phase writes the data changes if there are no problems
and the data doesn't have to be rolled back.
Next, each of the Customer, Order, and OrderDetail
classes's SaveData methods are executed. If any one of them fails, it will return
an error message and an exception will be thrown in the try-catch-finally construct.
Then the exception transfers the next execution to the catch clause where the transaction
is rolled back. Unlike previous versions of Visual Basic, Visual Basic .NET and
C# error handling techniques do not rely on errors bubbling up the call stack. Instead,
it is sometimes recommended that your methods return a status that can be evaluated
to determine if an error occurred. In the SaveData methods, a return value of a
string message means that an error occurred while saving the data. An empty string
indicates that the method encountered no difficulties. Assuming that each of the
Customer, Order, and OrderDetail class's SaveData methods executed without incident,
the transaction is committed and control is passed back to the calling Windows-based
This example shows how to create a business
service (the Class Library written in C#) and a presentation service (the Windows-based
app with the Windows Form written in Visual Basic .NET) using different .NET languages.
To execute the application, compile the two projects and make sure that the Windows-based
app references the business services assembly. Being able to relate different rowsets
so that they resemble a relational database with one-to-many relationships makes
the use of XML for data manipulation even more powerful. Using ADO.NET you don't
need to combine rowsets through SQL joins or manipulate an XML document directly.
Or course, the option of joining rowsets into a single rowset is entirely feasible
in ADO.NET. In fact, to retain backward compatibility, you could return a single
rowset of customer, orders, and order details to an application.
In this installment of the Data Points column
I showed you how to take advantage of the DataSet by relating rowsets to one another
and creating a single interface to modify all related rowsets. I also demonstrated
how to use inheritance to derive methods and properties from a base class when there
are commonalities that can be reused. All of the code used in this article can be
downloaded from the link at the top of this article.
Send questions and comments to Johnny at
| Johnny Papa is VP of Information Technology at MJM Investigations in Raleigh,
NC and the author of Professional ADO 2.5 RDS Programming with ASP 3.0
(Wrox, 2000) and contributing author of Professional XML Databases (Wrox,
2000). You can reach him at email@example.com. |