Dbase Programming
Dbase Programming
™ ®
Mastering Visual Basic .NET
Database Programming
Evangelos Petroutsos; Asli Bilgin
Copyright © 2002 SYBEX Inc., 1151 Marina Village Parkway, Alameda, CA 94501. World rights reserved. No part of this
publication may be stored in a retrieval system, transmitted, or reproduced in any way, including but not limited to
photocopy, photograph, magnetic or other record, without the prior agreement and written permission of the publisher.
ISBN: 0-7821-2878-5
SYBEX and the SYBEX logo are either registered trademarks or trademarks of SYBEX Inc. in the USA and other
countries.
TRADEMARKS: Sybex has attempted throughout this book to distinguish proprietary trademarks from descriptive terms
by following the capitalization style used by the manufacturer. Copyrights and trademarks of all products and services
listed or described herein are property of their respective owners and companies. All rules and laws pertaining to said
copyrights and trademarks are inferred.
This document may contain images, text, trademarks, logos, and/or other material owned by third parties. All rights
reserved. Such material may not be copied, distributed, transmitted, or stored without the express, prior, written consent
of the owner.
The author and publisher have made their best efforts to prepare this book, and the content is based upon final release
software whenever possible. Portions of the manuscript may be based upon pre-release versions supplied by software
manufacturers. The author and the publisher make no representation or warranties of any kind with regard to the
completeness or accuracy of the contents herein and accept no liability of any kind including but not limited to
performance, merchantability, fitness for any particular purpose, or any losses or damages of any kind caused or alleged
to be caused directly or indirectly from this book.
Sybex Inc.
1151 Marina Village Parkway
Alameda, CA 94501
U.S.A.
Phone: 510-523-8233
www.sybex.com
2878c06.qxd 01/31/02 2:14 PM Page 227
Chapter 6
the data activity is hidden from you. With client-side disconnected RecordSets, you can’t control how
your updates occur. They just happen “magically.” ADO.NET opens that black box, giving you
more granularity with your data manipulations. ADO 2.x is about common data access. ADO.NET
extends this model and factors out data storage from common data access. Factoring out functional-
ity makes it easier for you to understand how ADO.NET components work. Each ADO.NET com-
ponent has its own specialty, unlike the RecordSet, which is a jack-of-all-trades. The RecordSet could
be disconnected or stateful; it could be read-only or updateable; it could be stored on the client or on
the server—it is multifaceted. Not only do all these mechanisms bloat the RecordSet with function-
ality you might never use, it also forces you to write code to anticipate every possible chameleon-like
metamorphosis of the RecordSet. In ADO.NET, you always know what to expect from your data
access objects, and this lets you streamline your code with specific functionality and greater control.
Although a separate chapter is dedicated to XML (Chapter 10, “The Role of XML”), we must
touch upon XML in our discussion of ADO.NET. In the .NET Framework, there is a strong syn-
ergy between ADO.NET and XML. Although the XML stack doesn’t technically fall under
ADO.NET, XML and ADO.NET belong to the same architecture. ADO.NET persists data as
XML. There is no other native persistence mechanism for data and schema. ADO.NET stores data
as XML files. Schema is stored as XSD files.
There are many advantages to using XML. XML is optimized for disconnected data access.
ADO.NET leverages these optimizations and provides more scalability. To scale well, you can’t main-
tain state and hold resources on your database server. The disconnected nature of ADO.NET and
XML provide for high scalability.
In addition, because XML is a text-based standard, it’s simple to pass it over HTTP and through
firewalls. Classic ADO uses a binary format to pass data. Because ADO.NET uses XML, a ubiqui-
tous standard, more platforms and applications will be able to consume your data. By using the
XML model, ADO.NET provides a complete separation between the data and the data presentation.
ADO.NET takes advantage of the way XML splits the data into an XML document, and the
schema into an XSD file.
By the end of this chapter, you should be able to answer the following questions:
◆ What are .NET data providers?
◆ What are the ADO.NET classes?
◆ What are the appropriate conditions for using a DataReader versus a DataSet?
◆ How does OLE DB fit into the picture?
◆ What are the advantages of using ADO.NET over classic ADO?
◆ How do you retrieve and update databases from ADO.NET?
◆ How does XML integration go beyond the simple representation of data as XML?
Let’s begin by looking “under the hood” and examining the components of the ADO.NET stack.
2878c06.qxd 01/31/02 2:14 PM Page 229
Is OLE DB Dead?
Not quite. Although you can still use OLE DB data providers with ADO.NET, you should try to use the man-
aged .NET data providers whenever possible. If you use native OLE DB, your .NET code will suffer because
it’s forced to go through the COM interoperability layer in order to get to OLE DB. This leads to performance
degradation. Native .NET providers, such as the System.Data.SqlClient library, skip the OLE DB layer
entirely, making their calls directly to the native API of the database server.
However, this doesn’t mean that you should avoid the OLE DB .NET data providers completely. If you are
using anything other than SQL Server 7 or 2000, you might not have another choice. Although you will expe-
rience performance gains with the SQL Server .NET data provider, the OLE DB .NET data provider compares
favorably against the traditional ADO/OLE DB providers that you used with ADO 2.x. So don’t hold back
from migrating your non-managed applications to the .NET Framework for performance concerns. In addi-
tion, there are other compelling reasons for using the OLE DB .NET providers. Many OLE DB providers are
very mature and support a great deal more functionality than you would get from the newer SQL Server
.NET data provider, which exposes only a subset of this full functionality. In addition, OLE DB is still the way
to go for universal data access across disparate data sources. In fact, the SQL Server distributed process
relies on OLE DB to manage joins across heterogeneous data sources.
Another caveat to the SQL Server .NET data provider is that it is tightly coupled to its data source. Although
this enhances performance, it is somewhat limiting in terms of portability to other data sources. When
you use the OLE DB providers, you can change the connection string on the fly, using declarative code such
as COM+ constructor strings. This loose coupling enables you to easily port your application from an SQL
Server back-end to an Oracle back-end without recompiling any of your code, just by swapping out the con-
nection string in your COM+ catalog.
Keep in mind, the only native OLE DB provider types that are supported with ADO.NET are SQLOLEDB for
SQL Server, MSDAORA for Oracle, and Microsoft.Jet.OLEDB.4 for the Microsoft Jet engine. If you are so
inclined, you can write your own .NET data providers for any data source by inheriting from the Sys-
tem.Data namespace.
At this time, the .NET Framework ships with only the SQL Server .NET data provider for data access within
the .NET runtime. Microsoft expects the support for .NET data providers and the number of .NET data
providers to increase significantly. (In fact, the ODBC.NET data provider is available for download on
Microsoft’s website.) A major design goal of ADO.NET is to synergize the native and managed interfaces,
advancing both models in tandem.
2878c06.qxd 01/31/02 2:14 PM Page 230
You can find the ADO.NET objects within the System.Data namespace. When you create a new
VB .NET project, a reference to the System.Data namespace will be automatically added for you, as
you can see in Figure 6.1.
Figure 6.1
To use ADO.NET,
reference the
System.Data
namespace.
To comfortably use the ADO.NET objects in an application, you should use the Imports state-
ment. By doing so, you can declare ADO.NET variables without having to fully qualify them. You
could type the following Imports statement at the top of your solution:
Imports System.Data.SqlClient
After this, you can work with the SqlClient ADO.NET objects without having to fully qualify the
class names. If you want to dimension the SqlClientDataAdapter, you would type the following short
declaration:
Dim dsMyAdapter as New SqlDataAdapter
Alternately, you can use the visual database tools to automatically generate your ADO.NET code
for you. As you saw in Chapter 3, “The Visual Database Tools,” the various wizards that come with
VS .NET provide the easiest way to work with the ADO.NET objects. Nevertheless, before you use
these tools to build production systems, you should understand how ADO.NET works program-
matically. In this chapter, we don’t focus too much on the visual database tools, but instead concen-
trate on the code behind the tools. By understanding how to program against the ADO.NET object
model, you will have more power and flexibility with your data access code.
2878c06.qxd 01/31/02 2:14 PM Page 231
DataAdapter Serves as an ambassador between your DataSet and data source, proving the mapping
instructions between the two
Figure 6.2 summarizes the ADO.NET object model. If you’re familiar with classic ADO, you’ll
see that ADO.NET completely factors out the data source from the actual data. Each object exposes
a large number of properties and methods, which are discussed in this and following chapters.
Connection DataSet
DB XML
Command DataTable
DataAdapter
DataReader
2878c06.qxd 01/31/02 2:14 PM Page 232
Note If you have worked with collection objects, this experience will be a bonus to programming with ADO.NET.
ADO.NET contains a collection-centric object model, which makes programming easy if you already know how to work
with collections.
Four core objects belong to .NET data providers, within the ADO.NET managed provider archi-
tecture: the Connection, Command, DataReader, and DataAdapter objects. The Connection object is the
simplest one, because its role is to establish a connection to the database. The Command object exposes a
Parameters collection, which contains information about the parameters of the command to be exe-
cuted. If you’ve worked with ADO 2.x, the Connection and Command objects should seem familiar
to you. The DataReader object provides fast access to read-only, forward-only data, which is reminiscent
of a read-only, forward-only ADO RecordSet. The DataAdapter object contains Command objects that
enable you to map specific actions to your data source. The DataAdapter is a mechanism for bridging
the managed providers with the disconnected DataSets.
The DataSet object is not part of the ADO.NET managed provider architecture. The DataSet
exposes a collection of DataTables, which in turn contain both DataColumn and DataRow collec-
tions. The DataTables collection can be used in conjunction with the DataRelation collection to
create relational data structures.
First, you will learn about the connected layer by using the .NET data provider objects and
touching briefly on the DataSet object. Next, you will explore the disconnected layer and examine
the DataSet object in detail.
Note Although there are two different namespaces, one for OleDb and the other for the SqlClient, they are quite
similar in terms of their classes and syntax. As we explain the object model, we use generic terms, such as Connection, rather
than SqlConnection. Because this book focuses on SQL Server development, we gear our examples toward SQL Server data
access and manipulation.
In the following sections, you’ll look at the five major objects of ADO.NET in detail. You’ll
examine the basic properties and methods you’ll need to manipulate databases, and you’ll find
examples of how to use each object. ADO.NET objects also recognize events, which we discuss
in Chapter 12, “More ADO.NET Programming.”
Connecting to a Database
The first step to using ADO.NET is to connect to a data source, such as a database. Using the Con-
nection object, you tell ADO.NET which database you want to contact, supply your username and
password (so that the DBMS can grant you access to the database and set the appropriate privileges),
and, possibly, set more options. The Connection object is your gateway to the database, and all the
operations you perform against the database must go through this gateway. The Connection object
encapsulates all the functionality of a data link and has the same properties. Unlike data links, how-
ever, Connection objects can be accessed from within your VB .NET code. They expose a number of
properties and methods that enable you to manipulate your connection from within your code.
Note You don’t have to type this code by hand. The code for all the examples in this chapter is located on the companion
CD that comes with this book. You can find many of this chapter’s code examples in the solution file Working with
ADO.NET.sln. Code related to the ADO.NET Connection object is listed behind the Connect To Northwind button on
the startup form.
Let’s experiment with creating a connection to the Northwind database. Create a new Win-
dows Application solution and place a command button on the Form; name it Connect to
Northwind. Add the Imports statement for the System.Data.SqlClient name at the top of
the form module. Now you can declare a Connection object with the following statement:
Dim connNorthwind As New SqlClient.SqlConnection()
2878c06.qxd 01/31/02 2:14 PM Page 234
As soon as you type the period after SqlClient, you will see a list with all the objects exposed by
the SqlClient component, and you can select the one you want with the arrow keys. Declare the
connNorthwind object in the button’s click event.
Note All projects on the companion CD use the setting (local) for the data source. In other words, we’re assuming
you have SQL Server installed on the local machine. Alternately, you could use localhost for the data source value.
Replace the data source value with the name of your SQL Server, or keep the local setting if you
are running SQL Server on the same machine. If you aren’t using Windows NT integrated security,
then set your user ID and password like so:
connNorthwind.ConnectionString=”data source=(local);”& _
“initial catalog=Northwind; user ID=sa;password=xxx”
Tip Some of the names in the connection string also go by aliases. You can use Server instead of data source to
specify your SQL Server. Instead of initial catalog, you can specify database.
Those of you who have worked with ADO 2.x might notice something missing from the connec-
tion string: the provider value. Because you are using the SqlClient namespace and the .NET Frame-
work, you do not need to specify an OLE DB provider. If you were using the OleDb namespace, then
you would specify your provider name-value pair, such as Provider=SQLOLEDB.1.
Or you could overload the constructor of the connection string by using the following:
Dim myConnectString As String = “data source=localhost; initial
catalog=Northwind; user ID=sa;password=xxx”
2878c06.qxd 01/31/02 2:14 PM Page 235
You have just established a connection to the SQL Server Northwind database. As you remember
from Chapter 3, you can also do this visually from the Server Explorer. The ConnectionString prop-
erty of the Connection object contains all the information required by the provider to establish a
connection to the database. As you can see, it contains all the information that you see in the Con-
nection properties tab when you use the visual tools.
Keep in mind that you can also create connections implicitly by using the DataAdapter object.
You will learn how to do this when we discuss the DataAdapter later in this section.
In practice, you’ll never have to build connection strings from scratch. You can use the Server
Explorer to add a new connection, or use the appropriate ADO.NET data component wizards, as
you did in Chapter 3. These visual tools will automatically build this string for you, which you can
see in the properties window of your Connection component.
Tip The connection pertains more to the database server rather than the actual database itself. You can change the database
for an open SqlConnection, by passing the name of the new database to the ChangeDatabase() method.
Note Unlike ADO 2.x, the Open() method doesn’t take any optional parameters. You can’t change this feature
because the Open() method is not overridable.
You must call the Close() or Dispose() method, or else the connection will not be released back
to the connection pool. The .NET garbage collector will periodically remove memory references for
expired or invalid connections within a pool. This type of lifetime management improves the per-
formance of your applications because you don’t have to incur expensive shutdown costs. However,
this mentality is dangerous with objects that tie down server resources. Generational garbage collec-
tion polls for objects that have been recently created, only periodically checking for those objects that
have been around longer. Connections hold resources on your server, and because you don’t get deter-
ministic cleanup by the garbage collector, you must make sure you explicitly close the connections
that you open. The same goes for the DataReader, which also holds resources on the database server.
2878c06.qxd 01/31/02 2:14 PM Page 236
TableDirect The command is a table’s name. The Command object passes the name of the table
to the server.
When you choose StoredProcedure as the CommandType, you can use the Parameters property to
specify parameter values if the stored procedure requires one or more input parameters, or it returns
one or more output parameters. The Parameters property works as a collection, storing the various
attributes of your input and output parameters. For more information on specifying parameters with
the Command object, see Chapter 8, “Data-Aware Controls.”
Executing a Command
After you have connected to the database, you must specify one or more commands to execute
against the database. A command could be as simple as a table’s name, an SQL statement, or the
name of a stored procedure. You can think of a Command object as a way of returning streams of
data results to a DataReader object or caching them into a DataSet object.
Command execution has been seriously refined since ADO 2.x., now supporting optimized execu-
tion based on the data you return. You can get many different results from executing a command:
◆ If you specify the name of a table, the DBMS will return all the rows of the table.
◆ If you specify an SQL statement, the DBMS will execute the statement and return a set of
rows from one or more tables.
◆ If the SQL statement is an action query, some rows will be updated, and the DBMS will
report the number of rows that were updated but will not return any data rows. The same is
true for stored procedures:
◆ If the stored procedure selects rows, these rows will be returned to the application.
◆ If the stored procedure updates the database, it might not return any values.
2878c06.qxd 01/31/02 2:14 PM Page 237
Tip As we have mentioned, you should prepare the commands you want to execute against the database ahead of time and,
if possible, in the form of stored procedures. With all the commands in place, you can focus on your VB .NET code. In
addition, if you are performing action queries and do not want results being returned, specify the NOCOUNT ON option in
your stored procedure to turn off the “rows affected” result count.
You specify the command to execute against the database with the Command object. The
Command objects have several methods for execution: the ExecuteReader() method returns a
forward-only, read-only DataReader, the ExecuteScalar() method retrieves a single result value, and
the ExecuteNonQuery() method doesn’t return any results. We discuss the ExecuteXmlReader()
method, which returns the XML version of a DataReader, in Chapter 7, “ADO.NET Programming.”
Note ADO.NET simplifies and streamlines the data access object model. You no longer have to choose whether to exe-
cute a query through a Connection, Command, or RecordSet object. In ADO.NET, you will always use the Command
object to perform action queries.
You can also use the Command object to specify any parameter values that must be passed to the
DBMS (as in the case of a stored procedure), as well as specify the transaction in which the com-
mand executes. One of the basic properties of the Command object is the Connection property,
which specifies the Connection object through which the command will be submitted to the DBMS
for execution. It is possible to have multiple connections to different databases and issue different
commands to each one. You can even swap connections on the fly at runtime, using the same Com-
mand object with different connections. Depending on the database to which you want to submit a
command, you must use the appropriate Connection object. Connection objects are a significant
load on the server, so try to avoid using multiple connections to the same database in your code.
Selection queries return a set of rows from the database. The following SQL statement will return
the company names for all customers in the Northwind database:
SELECT CompanyName FROM Customers
As you recall from Chapter 4, “Structured Query Language,” SQL is a universal language for
manipulating databases. The same statement will work on any database (as long as the database con-
tains a table called Customers and this table has a CompanyName column). Therefore, it is possible to
execute this command against the SQL Server Northwind database to retrieve the company names.
Note For more information on the various versions of the sample databases used throughout this book, see the sections
“Exploring the Northwind Database,” and “Exploring the Pubs Database” in Chapter 2, “Basic Concepts of Relational
Databases.”
Let’s execute a command against the database by using the connNorthwind object you’ve just cre-
ated to retrieve all rows of the Customers table. The first step is to declare a Command object vari-
able and set its properties accordingly. Use the following statement to declare the variable:
Dim cmdCustomers As New SqlCommand
Note If you do not want to type these code samples from scratch as you follow along, you can take a shortcut and load
the code from the companion CD. The code in this walk-through is listed in the click event of the Create DataReader but-
ton located on the startup form for the Working with ADO.NET solution.
Alternately, you can use the CreateCommand() method of the Connection object.
cmdCustomers = connNorthwind.CreateCommand()
Then set its CommandText property to the name of the Customers table:
cmdCustomers.CommandType = CommandType.TableDirect
The TableDirect property is supported only by the OLE DB .NET data provider. The TableDirect is equiv-
alent to using a SELECT * FROM tablename SQL statement. Why doesn’t the SqlCommand object support
this? Microsoft feels that when using specific .NET data providers, programmers should have better knowl-
edge and control of what their Command objects are doing. You can cater to your Command objects more
efficiently when you explicitly return all the records in a table by using an SQL statement or stored proce-
dure, rather than depending on the TableDirect property to do so for you. When you explicitly specify
SQL, you have tighter reign on how the data is returned, especially considering that the TableDirect prop-
erty might not choose the most efficient execution plan.
2878c06.qxd 01/31/02 2:14 PM Page 239
The CommandText property tells ADO.NET how to interpret the command. In this example, the
command is the name of a table. You could have used an SQL statement to retrieve selected rows
from the Customers table, such as the customers from Germany:
strCmdText = “SELECT ALL FROM Customers”
strCmdText = strCmdText & “WHERE Country = ‘Germany’”
cmdCustomers.CommandText = strCmdText
cmdCustomers.CommandType = CommandType.Text
By setting the CommandType property to a different value, you can execute different types of com-
mands against the database.
Note In previous versions of ADO, you are able to set the command to execute asynchronously and use the State prop-
erty to poll for the current fetch status. In VB .NET, you now have full support of the threading model and can execute
your commands on a separate thread with full control, by using the Threading namespace. We touch on threading and
asynchronous operations in Chapter 11, “More ADO.NET Programming.”
Regardless of what type of data you are retuning with your specific Execute() method, the Com-
mand object exposes a ParameterCollection that you can use to access input and output parameters
for a stored procedure or SQL statement. If you are using the ExecuteReader() method, you must
first close your DataReader object before you are able to query the parameters collection.
Warning For those of you who have experience working with parameters with OLE DB, keep in mind that you must
use named parameters with the SqlClient namespace. You can no longer use the question mark character (?) as an indi-
cator for dynamic parameters, as you had to do with OLE DB.
SqlCommand (InsertCommand)
SqlCommand
SqlConnection
SqlParameterCollection
The DataAdapter works with ADO.NET Command objects, mapping them to specific database
update logic that you provide. Because all this logic is stored outside of the DataSet, your DataSet
becomes much more liberated. The DataSet is free to collect data from many different data sources,
relying on the DataAdapter to propagate any changes back to its appropriate source.
Populating a DataSet
Although we discuss the DataSet object in more detail later in this chapter, it is difficult to express
the power of the DataAdapter without referring to the DataSet object.
The DataAdapter contains one of the most important methods in ADO.NET: the Fill() method.
The Fill() method populates a DataSet and is the only time that the DataSet touches a live data-
base connection. Functionally, the Fill() method’s mechanism for populating a DataSet works
much like creating a static, client-side cursor in classic ADO. In the end, you end up with a discon-
nected representation of your data.
The Fill() method comes with many overloaded implementations. A notable version is the one
that enables you to populate an ADO.NET DataSet from a classic ADO RecordSet. This makes
interoperability between your existing native ADO/OLE DB code and ADO.NET a breeze. If you
wanted to populate a DataSet from an existing ADO 2.x RecordSet called adoRS, the relevant seg-
ment of your code would read:
Dim daFromRS As OleDbDataAdapter = New OleDbDataAdapter
Dim dsFromRS As DataSet = New DataSet
daFromRS.Fill(dsFromRS, adoRS)
Warning You must use the OleDb implementation of the DataAdapter to populate your DataSet from a classic
ADO RecordSet. Accordingly, you would need to import the System.Data.OleDb namespace.
Tip The DataAdapter maps commands to the DataSet via the DataTable. Although the DataAdapter maps only one
DataTable at a time, you can use multiple DataAdapters to fill your DataSet by using multiple DataTables.
Note The code for the walkthrough in this section can be found in the Updating Data Using ADO.NET.sln solu-
tion file. Listing 6.1 is contained within the click event of the Inserting Data Using DataAdapters With Mapped Insert
Commands button.
The DataAdapter gives you a simple way to map the commands by using its SelectCommand,
UpdateCommand, DeleteCommand, and InsertCommand properties. When you call the Update() method,
the DataAdapter maps the appropriate update, add, and delete SQL statements or stored procedures
to their appropriate Command object. (Alternately, if you use the SelectCommand property, this
command would execute with the Fill() method.) If you want to perform an insert into the Cus-
tomers table of the Northwind database, you could type the code in Listing 6.1.
Listing 6.1: Insert Commands by Using the DataAdapter Object with Parameters
Dim strSelectCustomers As String = “SELECT * FROM Customers ORDER BY CustomerID”
Dim strConnString As String = “data source=(local);” & _
“initial catalog=Northwind;integrated security=SSPI;”
‘ We can’t use the implicit connection created by the
‘ DataSet since our update command requires a
‘ connection object in its constructor, rather than a
‘ connection string
Dim connNorthwind As New SqlConnection(strConnString)
‘ String to update the customer record - it helps to
‘ specify this in advance so the CommandBuilder doesn’t
‘ affect our performance at runtime
Dim strInsertCommand As String = _
“INSERT INTO Customers(CustomerID,CompanyName) VALUES (@CustomerID,
@CompanyName)”
Dim daCustomers As New SqlDataAdapter()
Dim dsCustomers As New DataSet()
Dim cmdSelectCustomer As SqlCommand = New SqlCommand _
(strSelectCustomers, connNorthwind)
Dim cmdInsertCustomer As New SqlCommand(strInsertCommand, connNorthwind)
daCustomers.SelectCommand = cmdSelectCustomer
daCustomers.InsertCommand = cmdInsertCustomer
connNorthwind.Open()
daCustomers.Fill(dsCustomers, “dtCustomerTable”)
cmdInsertCustomer.Parameters.Add _
(New SqlParameter _
(“@CustomerID”, SqlDbType.NChar, 5)).Value = “ARHAN”
cmdInsertCustomer.Parameters.Add _
(New SqlParameter _
2878c06.qxd 01/31/02 2:14 PM Page 242
This code sets up both the SelectCommand and InsertCommand for the DataAdapter and executes
the insert query with no results. To map the insert command with the values you are inserting, you
use the Parameters property of the appropriate SqlCommand objects. This example adds parameters
to the InsertCommand of the DataAdapter. As you can see from the DataAdapter object model in
Figure 6.3, each of the SqlCommand objects supports a ParameterCollection.
As you can see, the Insert statement need not contain all the fields in the parameters—and it
usually doesn’t. However, you must specify all the fields that can’t accept Null values. If you don’t,
the DBMS will reject the operation with a trappable runtime error. In this example, only two of the
new row’s fields are set: the CustomerID and the CompanyName fields, because neither can be Null.
Warning In this code, notice that you can’t use the implicit connection created by the DataSet. This is because the
InsertCommand object requires a Connection object in its constructor rather than a connection string. If you don’t have
an explicitly created Connection object, you won’t have any variable to pass to the constructor.
Tip Because you create the connection explicitly, you must make sure to close your connection when you are finished with
it. Although implicitly creating your connection takes care of cleanup for you, it’s not a bad idea to explicitly open the con-
nection, because you might want to leave it open so you can execute multiple fills and updates.
Each of the DataSet’s Command objects have their own CommandType and Connection properties,
which make them very powerful. Consider how you can use them to combine different types of com-
mand types, such as stored procedures and SQL statements. In addition, you can combine com-
mands from multiple data sources, by using one database for retrievals and another for updates.
As you can see, the DataAdapter with its Command objects is an extremely powerful feature of
ADO.NET. In classic ADO, you don’t have any control of how your selects, inserts, updates, and
deletes are handled. What if you wanted to add some specific business logic to these actions? You
would have to write custom stored procedures or SQL statements, which you would call separately
from your VB code. You couldn’t take advantage of the native ADO RecordSet updates, because
ADO hides the logic from you.
In summary, you work with a DataAdapter by using the following steps:
1. Instantiate your DataAdapter object.
2. Specify the SQL statement or stored procedure for the SelectCommand object. This is the only
Command object that the DataAdapter requires.
3. Specify the appropriate connection string for the SelectCommand’s Connection object.
4. Specify the SQL statements or stored procedures for the InsertCommand, UpdateCommand, and
DeleteCommand objects. Alternately, you could use the CommandBuilder to dynamically map
your actions at runtime. This step is not required.
2878c06.qxd 01/31/02 2:14 PM Page 243
5. Call the Fill() method to populate the DataSet with the results from the SelectCommand
object.
6. If you used step 4, call the appropriate Execute() method to execute your command objects
against your data source.
Warning Use the CommandBuilder sparingly, because it imposes a heavy performance overhead at runtime. You’ll
find out why in Chapter 9, “Working with DataSets.”
Note The code in Listing 6.2 can be found in the click event of the Create DataReader button on the startup form for
the Working with ADO.NET solution on the companion CD.
Notice that you can’t directly instantiate the DataReader object, but must go through the Com-
mand object interface.
Warning You cannot update data by using the DataReader object.
2878c06.qxd 01/31/02 2:14 PM Page 244
The DataReader absolves you from writing tedious MoveFirst() and MoveNext() navigation. The
Read() method of the DataReader simplifies your coding tasks by automatically navigating to a posi-
tion prior to the first record of your stream and moving forward without any calls to navigation meth-
ods, such as the MoveNext() method. To continue our example from Listing 6.2, you could retrieve the
first column from all the rows in your DataReader by typing in the following code:
While(drCustomers.Read())
Console.WriteLine(drCustomers.GetString(0))
End While
Note The Console.WriteLine statement is similar to the Debug.Print() method you used in VB6.
Because the DataReader stores only one record at a time in memory, your memory resource load is
considerably lighter. Now if you wanted to scroll backward or make updates to this data, you would
have to use the DataSet object, which we discuss in the next section. Alternately, you can move the
data out of the DataReader and into a structure that is updateable, such as the DataTable or DataRow
objects.
Warning By default, the DataReader navigates to a point prior to the first record. Thus, you must always call the
Read() method before you can retrieve any data from the DataReader object.
Realize that DataSets stand alone. A DataSet is not a part of the managed data providers and
knows nothing of its data source. The DataSet has no clue about transactions, connections, or even a
database. Because the DataSet is data source agnostic, it needs something to get the data to it. This is
where the DataAdapter comes into play. Although the DataAdapter is not a part of the DataSet, it
understands how to communicate with the DataSet in order to populate the DataSet with data.
customized application data without using XML or a database, by creating custom DataTables and
DataRows. We show you how to create DataSets on the fly in this chapter in the section “Creating
Custom DataSets.”
DataSets are perfect for working with data transfer across Internet applications, especially when
working with WebServices. Unlike native OLE DB/ADO, which uses a proprietary COM protocol,
DataSets transfer data by using native XML serialization, which is a ubiquitous data format. This
makes it easy to move data through firewalls over HTTP. Remoting becomes much simpler with
XML over the wire, rather than the heavier binary formats you have with ADO RecordSets. We
demonstrate how you do this in Chapter 16, “Working with WebServices.”
As we mentioned earlier, DataSet objects take advantage of the XML model by separating the
data storage from the data presentation. In addition, DataSet objects separate navigational data
access from the traditional set-based data access. We show you how DataSet navigation differs from
RecordSet navigation later in this chapter in Table 6.4.
What’s so great about DataSets? You’re happy with the ADO 2.x RecordSets. You want to know
why you should migrate over to using ADO.NET DataSets. There are many compelling reasons.
First, DataSet objects separate all the disconnected logic from the connected logic. This makes them
easier to work with. For example, you could use a DataSet to store a web user’s order information for
their online shopping cart, sending deltagrams to the server as they update their order information.
In fact, almost any scenario where you collect application data based on user interaction is a good
candidate for using DataSets. Using DataSets to manage your application data is much easier than
working with arrays, and safer than working with connection-aware RecordSets.
2878c06.qxd 01/31/02 2:14 PM Page 246
Another motivation for using DataSets lies in their capability to be safely cached with web appli-
cations. Caching on the web server helps alleviate the processing burden on your database servers.
ASP caching is something you really can’t do safely with a RecordSet, because of the chance that the
RecordSet might hold a connection and state. Because DataSets independently maintain their own
state, you never have to worry about tying up resources on your servers. You can even safely store the
DataSet object in your ASP.NET Session object, which you are warned never to do with RecordSets.
RecordSets are dangerous in a Session object; they can crash in some versions of ADO because of
issues with marshalling, especially when you use open client-side cursors that aren’t streamed. In
addition, you can run into threading issues with ADO RecordSets, because they are apartment
threaded, which causes your web server to run in the same thread
DataSets are great for remoting because they are easily understandable by both .NET and non-
.NET applications. DataSets use XML as their storage and transfer mechanism. .NET applications
don’t even have to deserialize the XML data, because you can pass the DataSet much like you would
a RecordSet object. Non-.NET applications can also interpret the DataSet as XML, make modifica-
tions using XML, and return the final XML back to the .NET application. The .NET application
takes the XML and automatically interprets it as a DataSet, once again.
Last, DataSets work well with systems that require tight user interaction. DataSets integrate
tightly with bound controls. You can easily display the data with DataViews, which enable scrolling,
searching, editing, and filtering with nominal effort. You will have a better understanding of how this
works when you read Chapter 8.
Now that we’ve explained how the DataSet gives you more flexibility and power than using the ADO
RecordSet, examine Table 6.3, which summarizes the differences between ADO and ADO.NET.
Table 6.3: Why ADO.NET Is a Better Data Transfer Mechanism than ADO
Feature Set ADO ADO.NET ADO.NET’s Advantage
Data persistence format RecordSet Uses XML With ADO.NET, you don’t have data
type restrictions.
Data transfer format COM marshalling Uses XML ADO.NET uses a ubiquitous format
that is easily transferable and that
multiple platforms and sites can read-
ily translate. In addition, XML strings
are much more manageable than
binary COM objects.
Web transfer protocol You would need to Uses HTTP ADO.NET data is more readily transfer-
use DCOM to tunnel able though firewalls.
through Port 80 and
pass proprietary COM
data, which firewalls
could filter out.
Let’s explore how to work with the various members of the DataSet object to retrieve and manip-
ulate data from your data source. Although the DataSet is designed for data access with any data
source, in this chapter we focus on SQL Server as our data source.
2878c06.qxd 01/31/02 2:14 PM Page 247
Table 6.4: Why ADO.NET Is a Better Data Storage Mechanism than ADO
Feature Set ADO ADO.NET ADO.NET’s Advantage
Disconnected data Uses disconnected Uses DataSets that Storing multiple result sets is
cache RecordSets, which store one or many simple in ADO.NET. The result sets
store data into a DataTables. can come from a variety of data
single table. sources. Navigating between these
result sets is intuitive, using the
standard collection navigation.
DataSets never maintain state,
unlike RecordSets, making
them safer to use with n-tier,
disconnected designs.
Relationship Uses JOINs, which Uses the DataRelation ADO.NET’s DataTable collection
management pull data into a single object to associate sets the stage for more robust rela-
result table. Alter- multiple DataTables tionship management. With ADO,
nately, you can use to one another. JOINs bring back only a single
the SHAPE syntax result table from multiple tables.
with the shaping OLE You end up with redundant data.
DB service provider. The SHAPE syntax is cumbersome
and awkward. With ADO.NET,
DataRelations provide an object-
oriented, relational way to manage
relations such as constraints and
cascading referential integrity, all
within the constructs of ADO.NET.
The ADO shaping commands are in
an SQL-like format, rather than
being native to ADO objects.
Table 6.4: Why ADO.NET Is a Better Data Storage Mechanism than ADO (continued)
Feature Set ADO ADO.NET ADO.NET’s Advantage
Navigation mechanism RecordSets give you DataSets have a DataSets enable you to traverse the
the option to only view nonlinear navigation data among multiple DataTables,
data sequentially. model. using the relevant DataRelations to
skip from one table to another. In
addition, you can view your rela-
tional data in a hierarchical fashion
by using the tree-like structure
of XML.
Assuming you’ve prepared your DataAdapter object, all you would have to call is the Fill()
method. Listing 6.3 shows you the code to populate your DataSet object with customer information.
daCustomers.Fill(dsCustomers, “dtCustomerTable”)
MsgBox(dsCustomers.GetXml, , “Results of Customer DataSet in XML”)
2878c06.qxd 01/31/02 2:14 PM Page 249
Note The code in Listing 6.3 can be found in the click event of the Create Single Table DataSet button on the startup
form for the Working with ADO.NET solution on the companion CD.
This code uses the GetXml() method to return the results of your DataSet as XML. The rows
of the Customers table are retrieved through the dsCustomers object variable. The DataTable object
within the DataSet exposes a number of properties and methods for manipulating the data by using
the DataRow and DataColumn collections. You will explore how to navigate through the DataSet
in the upcoming section, “Navigating Through DataSets.” However, first you must understand the
main collections that comprise a DataSet, the DataTable, and DataRelation collections.
The DataTableCollection
Unlike the ADO RecordSet, which contained only a single table object, the ADO.NET DataSet
contains one or more tables, stored as a DataTableCollection. The DataTableCollection is what
makes DataSets stand out from disconnected ADO RecordSets. You never could do something like
this in classic ADO. The only choice you have with ADO is to nest RecordSets within RecordSets
and use cumbersome navigation logic to move between parent and child RecordSets. The ADO.NET
navigation model provides a user-friendly navigation model for moving between DataTables.
In ADO.NET, DataTables factor out different result sets that can come from different data
sources. You can even dynamically relate these DataTables to one another by using DataRelations,
which we discuss in the next section.
Note If you want, you can think of a DataTable as analogous to a disconnected RecordSet, and the DataSet as a
collection of those disconnected RecordSets.
Let’s go ahead and add another table to the DataSet created earlier in Listing 6.3. Adding tables is
easy with ADO.NET, and navigating between the multiple DataTables in your DataSet is simple and
straightforward. In the section “Creating Custom DataSets,” we show you how to build DataSets on
the fly by using multiple DataTables. The code in Listing 6.4 shows how to add another DataTable
to the DataSet that you created in Listing 6.3.
Note The code in Listing 6.4 can be found in the click event of the Create DataSet With Two Tables button on the startup
form for the Working with ADO.NET solution on the companion CD.
Warning DataTables are conditionally case sensitive. In Listing 6.4, the DataTable is called dtCustomerTable.
This would cause no conflicts when used alone, whether you referred to it as dtCustomerTable or dtCUSTOMERTABLE.
However, if you had another DataTable called dtCUSTOMERTABLE, it would be treated as an object separate from
dtCustomerTable.
As you can see, all you had to do was create a new DataAdapter to map to your Orders table,
which you then filled into the DataSet object you had created earlier. This creates a collection of two
DataTable objects within your DataSet. Now let’s explore how to relate these DataTables together.
Note The code in Listing 6.5 can be found in the click event of the Using Simple DataRelations button on the startup
form for the Working with ADO.NET solution on the companion CD.
As you can with other ADO.NET objects, you can overload the DataRelation constructor. In this
example, you pass in three parameters. The first parameter indicates the name of the relation. This is
similar to how you would name a relationship within SQL Server. The next two parameters indicate
2878c06.qxd 01/31/02 2:14 PM Page 251
the two columns that you wish to relate. After creating the DataRelation object, you add it to the
Relations collection of the DataSet object.
Warning The data type of the two columns you wish to relate must be identical.
Listing 6.6 shows you how to use DataRelations between the Customers and Orders tables of the
Northwind database to ensure that when a customer ID is deleted or updated, it is reflected within
the Orders table.
Note The code in Listing 6.6 can be found in the click event of the Using Cascading Updates button on the startup form
for the Working with ADO.NET solution on the companion CD.
In this example, you create a foreign key constraint with cascading updates and add it to the
ConstraintCollection of your DataSet. First, you declare and instantiate a ForeignKeyConstraint
object, as you did earlier when creating the DataRelation object. Afterward, you set the properties
of the ForeignKeyConstraint, such as the UpdateRule and AcceptRejectRule, finally adding it
to your ConstraintCollection. You have to ensure that your constraints activate by setting the
EnforceConstraints property to True.
In ADO 2.x, a fundamental concept in programming for RecordSets is that of the current row: to
read the fields of a row, you must first move to the desired row. The RecordSet object supports a
number of navigational methods, which enable you to locate the desired row, and the Fields prop-
erty, which enables you to access (read or modify) the current row’s fields. With ADO.NET, you no
longer have to use fixed positioning to locate your records; instead, you can use array-like navigation.
Unlike ADO RecordSets, the concept of the current row no longer matters with DataSets.
DataSets work like other in-memory data representations, such as arrays and collections, and use
familiar navigational behaviors. DataSets provide an explicit in-memory representation of data in
the form of a collection-based model. This enables you to get rid of the infamous Do While Not
rs.EOF() And Not rs.BOF() loop. With ADO.NET, you can use the friendly For Each loop to iter-
ate through the DataTables of your DataSet. If you want to iterate through the rows and columns
within an existing DataTable named tblCustomers, stored in a dsCustomers DataSet, you could use
the following loop in Listing 6.7.
This will print out the values in each column of the customers DataSet created in Listing 6.3. As
you can see, the For Each logic saves you from having to monitor antiquated properties such as EOF
and BOF of the ADO RecordSet.
DataTables contain collections of DataRows and DataColumns, which also simplify your naviga-
tion mechanism. Instead of worrying about the RecordCount property of RecordSets, you can use the
traditional UBound() property to collect the number of rows within a DataTable. For the example in
Listing 6.7, you can calculate the row count for the customer records by using the following statement:
UBound(rowCustomer)
DataTable Capacities
In classic ADO, you could specify paged RecordSets—the type of RecordSets displayed on web pages
when the results of a query are too many to be displayed on a single page. The web server displays 20
or so records and a number of buttons at the bottom of the page that enable you to move quickly to
another group of 20 records. This technique is common in web applications, and ADO supports a
few properties that simplify the creation of paged RecordSets, such as the AbsolutePage, PageSize,
and PageCount properties.
2878c06.qxd 01/31/02 2:14 PM Page 253
With ADO.NET, you can use the MinimumCapacity property to specify the number of rows you
wish to bring back for a DataTable. The default setting is 25 rows. This setting is especially useful if
you want to improve performance on your web pages in ASP.NET. If you want to ensure that only
50 customer records display for the Customers DataTable, you would specify the following:
dtCustomers.MinimumCapacity = 50
If you have worked with paged RecordSets, you will realize that this performance technique is
much less involved than the convoluted paging logic you had to use in ADO 2.x.
◆ Load data from an XML file by using the ReadXml() method. Map the resulting DataSet to
your data source by using the DataAdapter.
◆ Merge multiple DataSets by using the Merge() method, passing the results to the data source
via the DataAdapter.
◆ Create a new DataSet with new schema and data on the fly, mapping it to a data source by
using the DataAdapter.
As you can see, all these options have one thing in common: your changes are not committed back
to the server until the DataAdapter intervenes. DataSets are completely unaware of where their data
comes from and how their changes relate back to the appropriate data source. The DataAdapter takes
care of all this.
Realize that updating a record is not always a straightforward process. What happens if a user
changes the record after you have read it? And what will happen if the record you’re about to update
has already been deleted by another user? In this chapter, you will learn the basics of updating data-
bases through the ADO.NET DataSet, assuming no concurrency is involved. However, we discuss
the implications of concurrency at the end of this chapter. In the meantime, let’s set up your
ADO.NET objects to insert a customer row into the Northwind database.
First, let’s look at the code that pulls down the data that you want to work with from your data-
base into a DataSet. Using the existing DataSet, you will add a new row directly to the DataSet by
using the DataTable and DataRow collections of the DataSet.
Note The code depicted in Figure 6.5 can be found in the Updating Data using ADO.NET.sln solution file,
within the click event of the Inserting Data With DataSets and DataTables button.
As you see in Figure 6.5, DataSet updates are very straightforward. All you have to do is fill your
DataSet, as we’ve shown you earlier in the chapter. Then you set up a new DataRow object with the
DataTable’s NewRow() method. The Add() collection of the Rows collection will add your new row to
the collection. Finally, you call the AcceptChanges() method of the DataSet, which will automati-
cally cascade all changes down to its inner DataTables and DataRows. Alternately, you could call the
AcceptChanges() method specifically on the inner object you wish to update because the DataTable
and DataRow also support the AcceptChanges() method.
As the note indicates, the source code for this example is available on the accompanying CD. Go
ahead and load the code into Visual Studio .NET and place a breakpoint on the Add() method. Exe-
cute the code by pressing F5. When you get to your breakpoint, type the following in the Command
window:
?dtcustomer.rows.count
Figure 6.5
Updating your
DataSet object
Warning If you have difficulty working with the Command window, it might be because you are not in Immediate
mode. If you see a > prompt, then this is most likely the case. Toggle the mode from Command mode to Immediate mode by
typing immed at the prompt and pressing Enter. Now you should be able to debug your code.
2878c06.qxd 01/31/02 2:14 PM Page 256
You will see the number of rows in your Customers table, within your DataSet, prior to making
changes. Hit F11 to step into the Add() method. This will update your DataSet with the newly added
row. Go back to the Command window and hit the Up arrow key and Enter to re-execute the row
count statement. The results will show that the Add() method increments your row count in your
DataRow by one record. However, if you compare the result to the data in the database, you will see
that your data still has the same number of original rows. This is an important point. None of your
changes will be committed to the data source until you call the Update() method of the DataAdapter
object. Finish the execution of the code to commit the changes in your DataSet.
In summary, all you have to do is execute the following steps to commit updates to your DataSet:
1. Instantiate your DataSet and DataAdapter objects.
2. Fill your DataSet object from the DataAdapter object.
3. Manipulate your DataSet by using the DataRow objects.
4. Call the AcceptChanges() method of the DataSet, DataTable, or DataRow object to commit
your changes to your DataSet.
In summary, all you have to do is execute the following steps to update your data source from
your DataSet, after you’ve made your changes to the DataSet:
1. Create a new row object that contains all the modified rows. You can use the DataViewRowState
property to extract the appropriate rows. In our case, we used the DataViewRowState.Added
value.
2. Call the Update() method of the DataAdapter object to send your changes back to the appro-
priate data source(s). Pass a copy of the DataRow containing your changes.
That’s it. As you see, it’s quite simple to add new rows to your database. Updates and deletes work
the same way.
Detached Indicates that the row is “floating” and not yet attached to a DataRowCollection
Unchanged Indicates that either the row was never touched in the first place, or the
AcceptChanges() method was called, committing the changes to the row
If you want advanced information on how the RowState property works, please refer to Chapter 11,
where we show you its importance with event-based programming.
doing this, you can filter out specific change types. If you only wanted to query if the DataSet had any
deletions, you would type:
If dsCustomers.HasChanges(DataRowState.Deleted)Then
‘ Do some logic to get the changes
End If
Merging
Another technique for working with DataSets uses the ability to merge results from multiple
DataTables or DataSets. The merge operation can also combine multiple schemas together. The
Merge() method enables you to extend one schema to support additional columns from the other,
and vice versa. In the end, you end up with a union of both schemas and data. This is useful when
you want to bring together data from heterogeneous data sources, or to add a subset of data to an
existing DataSet. The merge operation is quite simple:
dsCustomers.Merge (dsIncomingCustomers)
Typed DataSets
There are many data typing differences between ADO and ADO.NET. In classic ADO, you have more
memory overhead than ADO because the fields in a RecordSet are late-bound, returning data as the
Variant data type. ADO.NET supports stricter data typing. ADO.NET uses the Object, rather than
the Variant data type for your data. Although Objects are more lightweight than Variants, your code
will be even more efficient if you know the type ahead of time. You could use the GetString() method
to convert your column values to strings. This way, you avoid boxing your variables to the generic
Object type. You can use similar syntax for the other data types, such as GetBoolean() or GetGuid().
Try to convert your values to the native format to reduce your memory overhead.
When you work with classic ADO, you experience performance degradation when you refer to
your fields by name. You would type the following:
strName = rsCustomers.Fields(“CustomerName”).Value
Now, with ADO.NET, you can use strong typing to reference the fields of a DataSet directly by
name, like so:
strName = dsCustomers.CustomerName
2878c06.qxd 01/31/02 2:14 PM Page 260
Because the values are strictly typed in ADO.NET, you don’t have to write type-checking code.
ADO.NET will generate a compile-time error if your have a type mismatch, unlike the ADO run-
time errors you get much too late. With ADO.NET, if you try to pass a string to an integer field, you
will raise an error when you compile the code.
Now, all you have to do is set a DataTable variable to the results of your method and populate it. If
you load the code from the companion CD, place a breakpoint on the Add() method of the DataRow
collection, as shown in Figure 6.6. This way, you can use the Immediate mode of the Command win-
dow to see if your custom DataSet was successfully updated. With ADO.NET, it’s easy to use array-like
navigation to return the exact value you are looking for. In this example, you query the value of the cus-
tomer name in the first row by using the tblCart.Rows(0).Item(2)statement. Figure 6.6 shows you the
results.
Tip Again, you can see the power of constructors. In this sample, you see how you can set your constructor to a method
result.
2878c06.qxd 01/31/02 2:14 PM Page 261
Figure 6.6
Populating your
custom DataSet
object
Being able to create your own DataSet from within your code enables you to apply many of the
techniques discussed in this book. You can use these custom DataSets to store application data, with-
out incurring the cost of crossing your network until you need to commit your changes.
Managing Concurrency
When you set up your DataSet, you should consider the type of locking, or concurrency control,
that you will use. Concurrency control determines what will happen when two users attempt to
update the same row.
ADO.NET uses an optimistic architecture, rather than a pessimistic model. Pessimistic locking locks
the database when a record is retrieved for editing. Be careful when you consider pessimistic locking.
Pessimistic locking extremely limits your scalability. You really can’t use pessimistic locking in a sys-
tem with a large number of users. Only certain types of designs can support this type of locking.
Consider an airline booking system. A passenger (let’s call her Sam) makes a request to book a
seat and retrieves a list of the available seats from the database. Sam selects a seat and updates the
information in the database. Under optimistic locking, if someone else took her seat, she would see a
message on her screen asking her to select a new one. Now let’s consider what happens under pes-
simistic locking. After Sam makes a request for the list of available seats, she decides to go to lunch.
Because pessimistic locking prevents other users from making changes when Sam is making edits,
everyone else would be unable to book their seats. Of course, you could add some logic for lock
timeouts, but the point is still the same. Pessimistic locking doesn’t scale very well. In addition, dis-
connected architecture cannot support pessimistic locking because connections attach to the database
only long enough to read or update a row, not long enough to maintain an indefinite lock. In classic
ADO, you could choose between different flavors of optimistic and pessimistic locks. This is no
longer the case. The .NET Framework supports only an optimistic lock type.
2878c06.qxd 01/31/02 2:14 PM Page 262
An optimistic lock type assumes that the data source is locked only at the time the data update com-
mitment occurs. This means changes could have occurred while you were updating the disconnected
data cache. A user could have updated the same CompanyName while you were making changes to the
disconnected DataSet. Under optimistic locking, when you try to commit your CompanyName changes
to the data source, you will override the changes made by the last user. The changes made by the
last user could have been made after you had retrieved your disconnected DataSet. You could have
updated the CompanyName for a customer, after someone else had updated the Address. When you
push your update to the server, the updated address information would be lost. If you expect concur-
rency conflicts of this nature, you must make sure that your logic detects and rejects conflicting
updates.
If you have worked with ADO 2.x, you can think of the Update() method of the DataAdapter
object as analogous to the UpdateBatch() method you used with the RecordSet object. Both models
follow the concept of committing your deltagram to the data source by using an optimistic lock type.
Understanding how locking works in ADO.NET is an essential part of building a solid architec-
ture. ADO.NET makes great strides by advancing the locking mechanism. Let’s take a look at how it
changes from classic ADO in order to get an idea of how much power ADO.NET gives you.
In ADO 2.x, when you make changes to a disconnected RecordSet, you call the UpdateBatch()
method to push your updates to the server. You really don’t know what goes on under the covers and
you hope that your inserts, updates, and deletes will take. You can’t control the SQL statements that
modify the database.
When you use optimistic concurrency, you still need some way to determine whether your server
data has been changed since the last read. You have three choices with managing concurrency: time-
date stamps, version numbers, and storing the original values.
Time-date stamps are a commonly used approach to tracking updates. The comparison logic
checks to see if the time-date of the updated data matches the time-date stamp of original data in
the database. It’s a simple yet effective technique. Your logic would sit in your SQL statements or
stored procedures, such as:
UPDATE Customers SET CustomerID = “SHAMSI”,
CustomerName = “Irish Twinkle SuperMart”
WHERE DateTimeStamp = olddatetimestamp
The second approach is to use version numbers, which is similar to using the time-date stamp, but
this approach labels the row with version numbers, which you can then compare.
The last approach is to store the original values so that when you go back to the database with your
updates, you can compare the stored values with what’s in the database. If they match, you can safely
update your data because no one else has touched it since your last retrieval. ADO.NET does data rec-
onciliation natively by using the HasVersion() method of your DataRow object. The HasVersion()
method indicates the condition of the updated DataRow object. Possible values for this property are
Current, Default, Original, or Proposed. These values fall under the DataRowVersion enumeration. If
you wanted to see whether the DataRow changes still contained original values, you could check to see
if the DataRow has changed by using the HasVersion() method:
If r.HasVersion(datarowversion.Proposed) Then
‘ Add logic
End if
2878c06.qxd 01/31/02 2:14 PM Page 263
SUMMARY 263
Summary
This concludes our discussion of the basic properties of the ADO.NET objects. After reading this
chapter, you should be able to answer the questions that we asked you in the beginning:
◆ What are .NET data providers?
◆ What are the ADO.NET classes?
◆ What are the appropriate conditions for using a DataReader versus a DataSet?
◆ How does OLE DB fit into the picture?
◆ What are the advantages of using ADO.NET over classic ADO?
◆ How do you retrieve and update databases from ADO.NET?
◆ How does XML integration go beyond the simple representation of data as XML?
Although you covered a lot of ground in this chapter, there is still a good amount of ADO.NET
functionality we haven’t discussed. We use this chapter as a building block for the next few chapters.
In the next chapter, we show you how to search and filter ADO.NET DataSets. You will learn about
such things as data binding and data-aware controls in Chapter 8.