ADO.NET 1.x was the successor to ActiveX Data Objects 2.6 (ADO). The main goal of ADO.NET 1.x was to enable developers to easily create distributed, data-sharing applications in the .NET Framework. The main goals of ADO.NET today are to improve the performance of existing features in ADO.NET 1.x, to provide easier use and to add new features without breaking backward compatibility.
Throughout this chapter, when ADO.NET is mentioned without a version number after it (that is, 1.x, 2.0, or 3.5), the statement applies to all versions of ADO.NET.
ADO.NET 1.x was built upon industry standards such as XML, and it provided a data-access interface to communicate with data sources such as SQL Server and Oracle. ADO.NET 3.5 builds upon these concepts, while increasing performance. Applications can use ADO.NET to connect to these data sources and retrieve, manipulate, and update data. ADO.NET 3.5 does not break any compatibility with ADO.NET 2.0 or 1.x; it only adds to the stack of functionality.
In solutions that require disconnected or remote access to data, ADO.NET 3.5 uses XML to exchange data between programs or with Web pages. Any component that can read XML can make use of ADO.NET components. A receiving component does not even have to be an ADO.NET component if a transmitting ADO.NET component packages and delivers a data set in an XML format. Transmitting information in XML-formatted data sets enables programmers to easily separate the data-processing and user interface components of a data-sharing application onto separate servers. This can greatly improve both the performance and maintainability of systems that support many users.
For distributed applications, ADO.NET 1.x proved that the use of XML data sets provided performance advantages relative to the COM marshaling used to transmit disconnected data sets in ADO. Because transmission of data sets occurred through XML streams in a simple text-based standard accepted throughout the industry, receiving components did not require any of the architectural restrictions required by COM. XML data sets used in ADO.NET 1.x also avoided the processing cost of converting values in the Fields collection of a Recordset
to data types recognized by COM. Virtually any two components from different systems can share XML data sets, provided that they both use the same XML schema for formatting the data set. This continues to be true in ADO.NET 3.5, but the story gets better. The XML integration in ADO.NET today is even stronger, and extensive work was done to improve the performance of the DataSet
object, particularly in the areas of serialization and memory usage.
ADO.NET also supports the scalability required by Web-based data-sharing applications. Web applications must often serve hundreds, or even thousands, of users. By default, ADO.NET does not retain lengthy database locks or active connections that monopolize limited resources. This enables the number of users to grow with only a small increase in the demands made on the resources of a system.
In this chapter, you will see that ADO.NET is a very extensive and flexible API for accessing many types of data, and because ADO.NET 3.5 is an incremental change to the previous versions of ADO.NET, all previous ADO.NET knowledge already learned can be leveraged. In fact, to get the most out of this chapter, you should be fairly familiar with earlier versions of ADO.NET and the entire .NET Framework.
This chapter demonstrates how to use the ADO.NET object model in order to build flexible, fast, scalable data-access objects and applications. Specifically, it covers the following:
The main design goals of ADO.NET 3.5 are as follows:
Customer-driven features that are still backwardly compatible with ADO.NET 1.x
Improving performance on your data-store calls
Providing more power for power users
Taking advantage of SQL Server 2005/2008 features
ADO.NET addresses a couple of the most common data-access strategies used for applications today. When classic ADO was developed, many applications could be connected to the data store almost indefinitely. Today, with the explosion of the Internet as the means of data communication, a new data technology is required to make data accessible and updateable in a disconnected architecture.
The first of these common data-access scenarios is one in which a user must locate a collection of data and iterate through this data just a single time. This is a popular scenario for Web pages. When a request for data from a Web page that you have created is received, you can simply fill a table with data from a data store. In this case, you go to the data store, grab the data that you want, send the data across the wire, and then populate the table. In this scenario, the goal is to get the data in place as fast as possible.
The second way to work with data in this disconnected architecture is to grab a collection of data and use this data separately from the data store itself. This could be on the server or even on the client. Even though the data is disconnected, you want the capability to keep the data (with all of its tables and relations in place) on the client side. Classic ADO data was represented by a single table that you could iterate through; but ADO.NET can be a reflection of the data store itself, with tables, columns, rows, and relations all in place. When you are done with the client-side copy of the data, you can persist the changes that you made in the local copy of data directly back into the data store. The technology that gives you this capability is the DataSet
, which is covered shortly.
Although classic ADO was geared for a two-tiered environment (client-server), ADO.NET addresses a multi-tiered environment. ADO.NET is easy to work with because it has a unified programming model. This unified programming model makes working with data on the server the same as working with data on the client. Because the models are the same, you find yourself more productive when working with ADO.NET.
This chapter begins with a quick look at the basics of ADO.NET and then provides an overview of ADO.NET capabilities, namespaces, and classes. It also reviews how to work with the Connection, Command, DataAdapter, DataSet
, and DataReader
objects.
Before jumping into the depths of ADO.NET, step back and make sure that you understand some of the common tasks you might perform programmatically within ADO.NET. This section looks at the process of selecting, inserting, updating, and deleting data.
The following example makes use of the
Northwind.mdf
SQL Server Express Database file. To get this database, search for "Northwind and pubs Sample Databases for SQL Server 2000." You can find this link atwww.microsoft.com/downloads/details.aspx?familyid=06616212-0356-46a0-8da2-eebc53a68034&displaylang=en
. Once installed, you will find theNorthwind.mdf
file in theC: SQL Server 2000 Sample Databases
directory. To add this database to your ASP.NET application, create anApp_Data
folder within your project (if it isn't already there) and right-click on the folder and select Add Existing Item. From the provided dialog, you can browse to the location of theNorthwind.mdf
file that you just installed. If you have trouble getting permissions to work with the database, make a data connection to the file from the Visual Studio Server Explorer. You will be asked to be made the appropriate user of the database and VS will make the appropriate changes on your behalf for this to occur.
After the connection to the data source is open and ready to use, you probably want to read the data from the data source. If you do not want to manipulate the data, but simply to read it or transfer it from one spot to another, you use the DataReader
class.
The following example uses the GetCompanyNameData
function to provide a list of company names from the SQL Northwind database.
Imports Microsoft.VisualBasic Imports System.Collections.Generic
Imports System.Data Imports System.Data.SqlClient Public Class SelectingData Public Function GetCompanyNameData() As List(Of String) Dim conn As SqlConnection Dim cmd As SqlCommand Dim cmdString As String = "Select CompanyName from Customers" conn = New SqlConnection("Data Source=.SQLEXPRESS;AttachDbFilename= |DataDirectory|NORTHWND.MDF;Integrated Security=True; User Instance=True") ' Put this string on one line in your code cmd = New SqlCommand(cmdString, conn) conn.Open() Dim myReader As SqlDataReader Dim returnData As List(Of String) = New List(Of String) myReader = cmd.ExecuteReader(CommandBehavior.CloseConnection) While myReader.Read() returnData.Add(myReader("CompanyName").ToString()) End While Return returnData End Function End Class
In this example, you create an instance of both the SqlConnection
and the SqlCommand
classes. Then, before you open the connection, you simply pass the SqlCommand
class a SQL command selecting specific data from the Northwind database. After your connection is opened (based upon the commands passed in), you create a DataReader
. To read the data from the database, you iterate through the data with the DataReader
by using the myReader.Read
method. After the List(Of String)
object is built, the connection is closed and the object is returned from the function.
When working with data, you often insert the data into the data source. The next code sample shows you how to do this. This data may have been passed to you by the end user through the XML Web Service, or it may be data that you generated within the logic of your class.
Public Sub InsertData() Dim conn As SqlConnection Dim cmd As SqlCommand Dim cmdString As String = "Insert Customers (CustomerID, _ CompanyName, ContactName) Values ('BILLE', 'XYZ Company', 'Bill Evjen')" conn = New SqlConnection("Data Source=.SQLEXPRESS;AttachDbFilename= |DataDirectory|NORTHWND.MDF;Integrated Security=True; User Instance=True") ' Put this string on one line in your code cmd = New SqlCommand(cmdString, conn) conn.Open() cmd.ExecuteNonQuery() conn.Close() End Sub
Inserting data into SQL is pretty straightforward and simple. Using the SQL command string, you insert specific values for specific columns. The actual insertion is initiated using the cmd.ExecuteNonQuery
command. This executes a command on the data when you don't want anything in return.
In addition to inserting new records into a database, you frequently update existing rows of data in a table. Imagine a table in which you can update multiple records at once. In the next example, you want to update an employee table by putting a particular value in the emp_bonus
column if the employee has been at the company for five years or longer:
Public Function UpdateEmployeeBonus() As Integer Dim conn As SqlConnection Dim cmd As SqlCommand Dim RecordsAffected as Integer Dim cmdString As String = "UPDATE Employees SET emp_bonus=1000 WHERE " & _ "yrs_duty>=5" conn = New SqlConnection("Data Source=.SQLEXPRESS;AttachDbFilename= |DataDirectory|NORTHWND.MDF;Integrated Security=True; User Instance=True") ' Put this string on one line in your code cmd = New SqlCommand(cmdString, conn) conn.Open() RecordsAffected = cmd.ExecuteNonQuery() conn.Close() Return RecordsAffected End Function
This update function iterates through all the employees in the table and changes the value of the emp_bonus
field to 1000
if an employee has been with the company for more than five years. This is done with the SQL command string. The great thing about these update capabilities is that you can capture the number of records that were updated by assigning the ExecuteNonQuery
command to the RecordsAffected
variable. The total number of affected records is then returned by the function.
Along with reading, inserting, and updating data, you sometimes need to delete data from the data source. Deleting data is a simple process of using the SQL command string and then the ExecuteNonQuery
command as you did in the update example. The following bit of code illustrates this:
Public Function DeleteEmployee() As Integer Dim conn As SqlConnection Dim cmd As SqlCommand Dim RecordsAffected as Integer Dim cmdString As String = "DELETE Employees WHERE LastName='Evjen'" conn = New SqlConnection("Data Source=.SQLEXPRESS;AttachDbFilename= |DataDirectory|NORTHWND.MDF;Integrated Security=True; User Instance=True") ' Put this string on one line in your code cmd = New SqlCommand(cmdString, conn) conn.Open() RecordsAffected = cmd.ExecuteNonQuery()
conn.Close() Return RecordsAffected End Function
You can assign the ExecuteNonQuery
command to an Integer
variable (just as you did for the update function) to return the number of records deleted.
The six core ADO.NET namespaces are shown in the following table. In addition to these namespaces, each new data provider can have its own namespace. As an example, the Oracle .NET data provider adds a namespace of System.Data.OracleClient
(for the Microsoft-built Oracle data provider).
ADO.NET has three distinct types of classes commonly referred to as disconnected, shared, and data providers. The disconnected classes provide the basic structure for the ADO.NET Framework. A good example of this type of class is the DataTable
class. The objects of this class are capable of storing data without any dependency on a specific data provider. The Shared
classes form the base classes for data providers and are shared among all data providers. The data provider classes are meant to work with different kinds of data sources. They are used to perform all data-management operations on specific databases. The SqlClient
data provider, for example, works only with the SQL Server database.
A data provider contains Connection, Command, DataAdapter
, and DataReader
objects. Typically, in programming ADO.NET, you first create the Connection
object and provide it with the necessary information, such as the connection string. You then create a Command
object and provide it with the details of the SQL command that is to be executed. This command can be an inline SQL text command, a stored procedure, or direct table access. You can also provide parameters to these commands if needed.
After you create the Connection
and the Command
objects, you must decide whether the command returns a result set. If the command doesn't return a result set, then you can simply execute the command by calling one of its several Execute
methods. Conversely, if the command returns a result set, you must decide whether you want to retain the result set for future use without maintaining the connection to the database. If you want to retain the result set, then you must create a DataAdapter
object and use it to fill a DataSet
or a DataTable
object. These objects are capable of maintaining their information in a disconnected mode. However, if you don't want to retain the result set, but rather to simply process the command in a swift fashion, then you can use the Command
object to create a DataReader
object. The DataReader
object needs a live connection to the database, and it works as a forward-only, read-only cursor.
To better support the disconnected model as defined above, the ADO.NET components separate data access from data manipulation. This is accomplished via two main components: the DataSet
and the .NET Data Provider. Figure 9-1 illustrates the concept of separating data access from data manipulation.
The DataSet
is the core component of the disconnected architecture of ADO.NET. It is explicitly designed for data access independent of any data source. As a result, it can be used with multiple and differing data sources, with XML data, or even to manage data local to an application such as an in-memory data cache. The DataSet
contains a collection of one or more DataTable
objects made up of rows and columns of data, as well as primary key, foreign key, constraint, and relation information about the data in the DataTable
objects. It is basically an in-memory database, but what sets it apart is that it doesn't care whether its data is obtained from a database, an XML file, a combination of the two, or somewhere else. You can apply inserts, updates, and deletes to the DataSet
and then push the changes back to the data source, no matter where the data source lives! This chapter offers an in-depth look at the DataSet
object family.
The other core element of the ADO.NET architecture is the .NET Data Provider, whose components are designed for data manipulation (as opposed to data access with the DataSet
). These components are listed in the following table.
The DataAdapter
uses Command
objects to execute SQL commands at the data source, both to load the DataSet
with data and to reconcile changes made to the data in the DataSet
with the data source. You will take a closer look at this later in the detailed discussion of the DataAdapter
object.
.NET Data Providers can be written for any data source, though this topic is beyond the scope of this chapter.
Object | Activity |
---|---|
Provides connectivity to a data source | |
| Enables access to database commands to return and modify data, run stored procedures, and send or retrieve parameter information |
Provides a high-performance, read-only stream of data from the data source | |
| Provides the bridge between the |
The .NET Framework 3.5 ships with three .NET Data Providers: the SQL Server .NET Data Provider, the Oracle .NET Data Provider, and the OLE DB .NET Data Provider.
Do not confuse the OLE DB .NET Data Provider with generic OLE DB Providers.
The rule of thumb when deciding which data provider to use is to first use a .NET Relational Database Management System (RDBMS)–specific data provider if it is available, and to use the .NET OLE DB Provider when connecting to any other data source. (Most RDBMS vendors are now producing their own .NET Data Providers in order to encourage .NET developers to use their databases.)
For example, if you were writing an application that uses SQL Server, then you would want to use the SQL Server .NET Data Provider. The .NET OLE DB Provider is used to access any data source exposed through OLE DB, such as Microsoft Access, Open DataBase Connectivity (ODBC), and so on. You will be taking a closer look at these later.
.NET Data Providers are used for connecting to a RDBMS-specific database (such as SQL Server or Oracle), executing commands, and retrieving results. Those results are either processed directly (via a DataReader
) or placed in an ADO.NET DataSet
(via a DataAdapter
) in order to be exposed to the user in an ad hoc manner, combined with data from multiple sources, or passed around between tiers. NET Data Providers are designed to be lightweight, to create a minimal layer between the data source and the .NET programmer's code, and to increase performance while not sacrificing any functionality.
To connect to a specific data source, you use a data Connection
object. To connect to Microsoft SQL Server 7.0 or later, you need to use the SqlConnection
object of the SQL Server .NET Data Provider. You need to use the OleDbConnection
object of the OLE DB .NET Data Provider to connect to an OLE DB data source, or the OLE DB Provider for SQL Server (SQLOLEDB) to connect to versions of Microsoft SQL Server earlier than 7.0.
For the OLE DB .NET Data Provider, the connection string format is the same as the connection string format used in ADO, with the following exceptions:
Here is an example OleDbConnection
connection string connecting to an Oracle database:
Provider=msdaora;Data Source=MyOracleDB;UserId=myUsername;Password=myPassword;
The SQL Server .NET Data Provider supports a connection-string format that is similar to the OLE DB (ADO) connection-string format. The only thing that you need to omit, obviously, is the provider name-value pair, as you know you are using the SQL Server .NET Data Provider. Here is an example of a SqlConnection
connection string:
Data Source=(local);Initial Catalog=pubs;Integrated Security=SSPI;
After establishing a connection, you can execute commands and return results from a data source (such as SQL Server) using a Command
object. A Command
object can be created using the Command
constructor, or by calling the CreateCommand
method of the Connection
object. When creating a Command
object using the Command
constructor, you need to specify a SQL statement to execute at the data source, and a Connection
object. The Command
object's SQL statement can be queried and modified using the CommandText
property. The following code is an example of executing a SELECT
command and returning a DataReader
object:
' Build the SQL and Connection strings. Dim sql As String = "SELECT * FROM authors" Dim connectionString As String = "Initial Catalog=pubs;" _ & "Data Source=(local);Integrated Security=SSPI;" ' Initialize the SqlCommand with the SQL ' and Connection strings. Dim command As SqlCommand = New SqlCommand(sql, _
New SqlConnection(connectionString)) ' Open the connection. command.Connection.Open() ' Execute the query, return a SqlDataReader object. ' CommandBehavior.CloseConnection flags the ' DataReader to automatically close the DB connection ' when it is closed. Dim dataReader As SqlDataReader = _ command.ExecuteReader(CommandBehavior.CloseConnection)
The CommandText
property of the Command
object executes all SQL statements in addition to the standard SELECT, UPDATE, INSERT
, and DELETE
statements. For example, you could create tables, foreign keys, primary keys, and so on, by executing the applicable SQL from the Command
object.
The Command
object exposes several Execute
methods to perform the intended action. When returning results as a stream of data, ExecuteReader
is used to return a DataReader
object. ExecuteScalar
is used to return a singleton value. In ADO.NET, the ExecuteRow
method has been added, which returns a single row of data in the form of a SqlRecord
object. ExecuteNonQuery
is used to execute commands that do not return rows, which usually includes stored procedures that have output parameters and/or return values. (You will learn about stored procedures in a later section.)
When using a DataAdapter
with a DataSet, Command
objects are used to return and modify data at the data source through the DataAdapter
object's SelectCommand, InsertCommand, UpdateCommand
, and DeleteCommand
properties.
Note that the DataAdapter
object's SelectCommand
property must be set before the Fill
method is called.
The InsertCommand, UpdateCommand
, and DeleteCommand
properties must be set before the Update
method is called. You will take a closer look at this when you look at the DataAdapter
object.
This section offers a quick look at how to use stored procedures, before delving into a more complex example later in the chapter demonstrating how you can build a reusable data-access component that also uses stored procedures. The motivation for using stored procedures is simple. Imagine you have the following code:
SELECT au_lname FROM authors WHERE au_id='172-32-1176'
If you pass that to SQL Server using ExecuteReader
on SqlCommand
(or any execute method, for that matter), SQL Server has to compile the code before it can run it, in much the same way that VB .NET applications have to be compiled before they can be executed. This compilation takes up SQL Server's time, so it is easy to deduce that if you can reduce the amount of compilation that SQL Server has to do, database performance should increase. (Compare the speed of execution of a compiled application against interpreted code.)
That's what stored procedures are all about: you create a procedure, store it in the database, and because the procedure is recognized and understood ahead of time, it can be compiled ahead of time and ready for use in your application.
Stored procedures are very easy to use, but the code to access them is sometimes a little verbose. The next section demonstrates some code that can make accessing stored procedures a bit more straightforward, but to make things clearer, let's start by building a simple application that demonstrates how to create and call a stored procedure.
To create a stored procedure, you can either use the tools in Visual Studio .NET or you can use the tools in SQL Server's Enterprise Manager if you are using SQL Server 2000, or in SQL Server Management Studio if you are using SQL Server 2005/2008. (Technically, you can use a third-party tool or just create the stored procedure in a good, old-fashioned SQL script.)
This example builds a stored procedure that returns all of the columns for a given author ID. The SQL to do this looks like this:
SELECT au_id, au_lname, au_fname, phone, address, city, state, zip, contract FROM authors WHERE au_id = whatever author ID you want
The "whatever author ID you want" part is important. When using stored procedures, you typically have to be able to provide parameters into the stored procedure and use them from within code. This is not a book about SQL Server, so this example focuses only on the principle involved. You can find many resources on the Web about building stored procedures (they have been around a very long time, and they are most definitely not a .NET-specific feature).
Variables in SQL Server are prefixed by the @
symbol, so if you have a variable called au id
, then your SQL will look like this:
SELECT au_id, au_lname, au_fname, phone, address, city, state, zip, contract FROM authors WHERE au_id = @au_id
In Visual Studio 2008, stored procedures can be accessed using the Server Explorer. Simply add a new data connection (or use an existing data connection), and then drill down into the Stored Procedures folder in the management tree. A number of stored procedures are already loaded. The byroyalty
procedure is a stored procedure provided by the sample pubs database developers. Figure 9-2 illustrates the stored procedures of the pubs database in Visual Studio 2008.
To create a new stored procedure, just right-click the Stored Procedures folder in the Server Explorer and select Add New Stored Procedure to invoke the Editor window.
A stored procedure can be either a single SQL statement or a complex set of statements. T-SQL supports branches, loops, and other variable declarations, which can make for some pretty complex stored procedure code. However, your stored procedure is just a single line of SQL. You need to declare the parameter that you want to pass in (@au_id
) and the name of the procedure: usp_authors_Get_By_ID
. Here's code for the stored procedure:
CREATE PROCEDURE usp_authors_Get_By_ID @au_id varchar(11) AS SELECT au_id, au_lname, au_fname, phone, address, city, state, zip, contract FROM authors WHERE au_id = @au_id
Click OK to save the stored procedure in the database. You are now able to access this stored procedure from code.
Calling the stored procedure is just a matter of creating a SqlConnection
object to connect to the database, and a SqlCommand
object to run the stored procedure.
The sample code for this chapter demonstrates a solution called Examples.sln
, which includes a project called AdoNetFeaturesTest.
For all of the data-access examples in this chapter, you need the pubs database, which can be downloaded from MSDN. In addition, be sure to run the examples.sql
file — available with the code download for this chapter — in SQL Server 2005 Management Studio before running the code examples. This creates the necessary stored procedures and functions in the pubs database. You can also use the SQL Server Express Edition of the pubs database, PUBS.MDF, also found on MSDN.
Now you have to decide what you want to return by calling the stored procedure. In this case, you return an instance of the SqlDataReader
object. The TestForm.vb
file contains a method called GetAuthorSqlReader
that takes an author ID and returns an instance of a SqlDataReader
. Here is the code for the method:
Private Function GetAuthorSqlReader(ByVal authorId As String) As SqlDataReader ' Build a SqlCommand Dim command As SqlCommand = New SqlCommand("usp_authors_Get_By_ID", _ GetPubsConnection()) ' Tell the command we are calling a stored procedure command.CommandType = CommandType.StoredProcedure ' Add the @au_id parameter information to the command command.Parameters.Add(New SqlParameter("@au_id", authorId)) ' The reader requires an open connection command.Connection.Open() ' Execute the sql and return the reader Return command.ExecuteReader(CommandBehavior.CloseConnection) End Function
Notice that in the SqlCommand
's constructor call, you have factored out creating a connection to the pubs database into a separate helper method. This is used later in other code examples in your form.
Here is the code for the GetPubsConnection
helper method:
Private Function GetPubsConnection() As SqlConnection ' Build a SqlConnection based on the config value. Return New _ SqlConnection(ConfigurationSettings.AppSettings("dbConnectionString")) End Function
The most significant thing this code does is grab a connection string to the database from the application's configuration file, app.config
. Here is what the entry in the app.config
file looks like:
<appSettings> <add key="dbConnectionString" value="data source=(local);initial catalog=pubs;Integrated Security=SSPI;" /> </appSettings>
Although the helper method does not do much, it is nice to place this code in a separate method. This way, if the code to get a connection to the databases needs to be changed, then the code only has to be changed in one place.
Accessing a stored procedure is more verbose (but not more difficult) than accessing a normal SQL statement through the methods discussed thus far. The approach is as follows:
There is no real need to build an impressive UI for this application, as we're about to add a button named getAuthorByIdButton
that calls the GetAuthorSqlRecord
helper method and displays the selected author's name. Here is the button's Click
event handler:
Private Sub _getAuthorByIdButton_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles _getAuthorByIdButton.Click Dim reader As SqlDataReader = Me. GetAuthorSqlReader ("409-56-7008") If reader.Read() MessageBox.Show(reader("au_fname").ToString() & " " _ & reader("au_lname").ToString()) End If reader.Close() End Sub
This has hard-coded an author ID of 409-56-7008. Run the code now and you should see the result shown in Figure 9-3.
You can use the DataReader
to retrieve a read-only, forward-only stream of data from the database. Using the DataReader
can increase application performance and reduce system overhead because only one buffered row at a time is ever in memory. With the DataReader
object, you are getting as close to the raw data as possible in ADO.NET; you do not have to go through the overhead of populating a DataSet
object, which sometimes may be expensive if the DataSet
contains a lot of data. The disadvantage of using a DataReader
object is that it requires an open database connection and increases network activity.
After creating an instance of the Command
object, a DataReader
is created by calling the ExecuteReader
method of the Command
object. Here is an example of creating a DataReader
and iterating through it to print out its values to the screen:
Private Sub TraverseDataReader() ' Build the SQL and Connection strings. Dim sql As String = "SELECT * FROM authors" Dim connectionString As String = "Initial Catalog=pubs;" _ & "Data Source=(local);Integrated Security=SSPI;" ' Initialize the SqlCommand with the SQL query and connection strings. Dim command As SqlCommand = New SqlCommand(sql, _ New SqlConnection(connectionString)) ' Open the connection. command.Connection.Open() ' Execute the query, return a SqlDataReader object. ' CommandBehavior.CloseConnection flags the ' DataReader to automatically close the DB connection ' when it is closed. Dim reader As SqlDataReader = _ command.ExecuteReader(CommandBehavior.CloseConnection) ' Loop through the records and print the values. Do While reader.Read Console.WriteLine(reader.GetString(1) & " " & reader.GetString(2)) Loop ' Close the DataReader (and its connection). reader.Close() End Sub
This code snippet uses the SqlCommand
object to execute the query via the ExecuteReader
method. This method returns a populated SqlDataReader
object, which you loop through and then print out the author names. The main difference between this code and looping through the rows of a DataTable
is that you have to stay connected while you loop through the data in the DataReader
object; this is because the DataReader
reads in only a small stream of data at a time to conserve memory space.
At this point, an obvious design question is whether to use the DataReader
or the DataSet
. The answer depends upon performance. If you want high performance and you are only going to access the data you are retrieving once, then the DataReader
is the way to go. If you need access to the same data multiple times, or if you need to model a complex relationship in memory, then the DataSet
is the way to go. As always, test each option thoroughly before deciding which one is the best.
The Read
method of the DataReader
object is used to obtain a row from the results of the query. Each column of the returned row may be accessed by passing the name or ordinal reference of the column to the DataReader
; or, for best performance, the DataReader
provides a series of methods that enable you to access column values in their native data types (GetDateTime, GetDouble, GetGuid, GetInt32
, and so on). Using the typed accessor methods when the underlying data type is known reduces the amount of type conversion required (converting from type Object
) when retrieving the column value.
The DataReader
provides a nonbuffered stream of data that enables procedural logic to efficiently process results from a data source sequentially. The DataReader
is a good choice when retrieving large amounts of data; only one row of data is cached in memory at a time. You should always call the Close
method when you are through using the DataReader
object, as well as close the DataReader
object's database connection; otherwise, the connection will not be closed until the garbage collector gets around to collecting the object.
Note how you use the CommandBehavior.CloseConnection
enumeration value on the SqlDataReader.ExecuteReader
method. This tells the SqlCommand
object to automatically close the database connection when the SqlDataReader.Close
method is called.
If your command contains output parameters or return values, they will not be available until the DataReader
is closed.
In ADO.NET, additional support enables Command
objects to execute their commands asynchronously, which can result in a huge perceived performance gain in many applications, especially in Windows Forms applications. This can come in very handy, especially if you ever have to execute a long-running SQL statement. This section examines how this functionality enables you to add asynchronous processing to enhance the responsiveness of an application.
The SqlCommand
object provides three different asynchronous call options: BeginExecuteReader, BeginExecuteNonQuery
, and BeginExecuteXmlReader
. Each of these methods has a corresponding "end" method — that is, EndExecuteReader, EndExecutreNonQuery
, and EndExecuteXmlReader
. Now that you are familiar with the DataReader
object, let's look at an example using the BeginExecuteReader
method to execute a long-running query.
In the AdoNetFeaturesTest project, I have added a Button
and an associated Click
event handler to the form that will initiate the asynchronous call to get a DataReader
instance:
Private Sub _testAsyncCallButton_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles _testAsyncCallButton.Click ' Build a connection for the async call to the database. Dim connection As SqlConnection = GetPubsConnection() connection.ConnectionString &= "Asynchronous Processing=true;" ' Build a command to call the stored procedure. Dim command As New SqlCommand("usp_Long_Running_Procedure", _ connection) ' Set the command type to stored procedure. command.CommandType = CommandType.StoredProcedure ' The reader requires an open connection. connection.Open()
' Make the asynchronous call to the database. command.BeginExecuteReader(AddressOf Me.AsyncCallback, _ command, CommandBehavior.CloseConnection) End Sub
The first thing you do is reuse your helper method GetPubsConnection
to get a connection to the pubs database. Next, and this is very important, you append the statement Asynchronous Processing=true
to your Connection
object's connection string. This must be set in order for ADO.NET to make asynchronous calls to SQL Server.
After getting the connection set, you then build a SqlCommand
object and initialize it to be able to execute the usp_Long_Running_Procedure
stored procedure. This procedure uses the SQL Server 2005 WAITFOR DELAY
statement to create a 20-second delay before it executes the usp_Authors_Get_All
stored procedure. As you can probably guess, the usp_authors_Get_All
stored procedure simply selects all of the authors from the authors table. The delay is added simply to demonstrate that while this stored procedure is executing, you can perform other tasks in your Windows Forms application. Here is the SQL code for the usp_Long_Running_Procedure
stored procedure:
CREATE PROCEDURE usp_Long_Running_Procedure AS SET NOCOUNT ON WAITFOR DELAY '00:00:20' EXEC usp_authors_Get_All
The last line of code in the Button
's Click
event handler is the call to BeginExecuteReader
. In this call, the first thing you are passing in is a delegate method (Me.AsyncCallback
) for the System.AsyncCallback
delegate type. This is how the .NET Framework calls you back once the method is finished running asynchronously. You then pass in your initialized SqlCommand
object so that it can be executed, as well as the CommandBehavior
value for the DataReader
. In this case, you pass in the CommandBehavior.CloseConnection
value so that the connection to the database will be closed once the DataReader
has been closed. You will look at the DataReader
in more detail in the next section.
Now that you have initiated the asynchronous call, and have defined a callback for your asynchronous call, let's look at the actual method that is being called back, the AsyncCallback
method:
Private Sub AsyncCallback(ByVal ar As IAsyncResult) ' Get the command that was passed from the AsyncState of the IAsyncResult. Dim command As SqlCommand = CType(ar.AsyncState, SqlCommand) ' Get the reader from the IAsyncResult. Dim reader As SqlDataReader = command.EndExecuteReader(ar) ' Get a table from the reader. Dim table As DataTable = Me.GetTableFromReader(reader, "Authors") ' Call the BindGrid method on the Windows main thread, ' passing in the table. Me.Invoke(New BindGridDelegate(AddressOf Me.BindGrid), _ New Object() {table}) End Sub
The first line of the code is simply retrieving the SqlCommand
object from the AsyncState
property of the IAsyncResult
that was passed in. Remember that when you called BeginExecuteReader
earlier, you passed in your SqlCommand
object. You need it so that you can call the EndExecuteReader
method on the next line. This method gives you your SqlDataReader
. On the next line, you then transform the SqlDataReader
into a DataTable
(covered later when the DataSet
is discussed).
The last line of this method is probably the most important. If you tried to just take your DataTable
and bind it to the grid, it would not work, because right now you are executing on a thread other than the main Windows thread. The helper method named BindGrid
can do the data binding, but it must be called only in the context of the Windows main thread. To bring the data back to the main Windows thread, it must be marshaled via the Invoke
method of the Form
object. Invoke
takes two arguments: the delegate of the method you want to call and (optionally) any parameters for that method. In this case, you define a delegate for the BindGrid
method, called BindGridDelegate
. Here is the delegate declaration:
Private Delegate Sub BindGridDelegate(ByVal table As DataTable)
Notice how the signature is exactly the same as the BindGrid
method shown here:
Private Sub BindGrid(ByVal table As DataTable) ' Clear the grid. Me._authorsGridView.DataSource = Nothing ' Bind the grid to the DataTable. Me._authorsGridView.DataSource = table End Sub
Here is another look at the call to the form's Invoke
method:
Me.Invoke(New BindGridDelegate(AddressOf Me.BindGrid), _ New Object() {table})
You pass in a new instance of the BindGridDelegate
delegate and initialize it with a pointer to the BindGrid
method. As a result, the .NET worker thread that was executing your query can now safely join up with the main Windows thread.
Each .NET Data Provider included with the .NET Framework has a DataAdapter
object. The OLE DB .NET Data Provider includes an OleDbDataAdapter
object, and the SQL Server .NET Data Provider includes a SqlDataAdapter
object. A DataAdapter
is used to retrieve data from a data source and populate DataTable
objects and constraints within a DataSet
. The DataAdapter
also resolves changes made to the DataSet
back to the data source. The DataAdapter
uses the Connection
object of the .NET Data Provider to connect to a data source, and Command
objects to retrieve data from, and resolve changes to, the data source from a DataSet
object.
This differs from the DataReader
, in that the DataReader
uses the Connection
object to access the data directly, without having to use a DataAdapter
. The DataAdapter
essentially decouples the DataSet
object from the actual source of the data, whereas the DataReader
is tightly bound to the data in a read-only fashion.
The SelectCommand
property of the DataAdapter
is a Command
object that retrieves data from the data source. A nice, convenient way to set the DataAdapter
's SelectCommand
property is to pass in a Command
object in the DataAdapter
's constructor. The InsertCommand, UpdateCommand
, and DeleteCommand
properties of the DataAdapter
are Command
objects that manage updates to the data in the data source according to the modifications made to the data in the DataSet
. The Fill
method of the DataAdapter
is used to populate a DataSet
with the results of the SelectCommand
of the DataAdapter
. It also adds or refreshes rows in the DataSet
to match those in the data source. The following example code demonstrates how to fill a DataSet
object with information from the authors table in the pubs database:
Private Sub TraverseDataSet() ' Build the SQL and Connection strings. Dim sql As String = "SELECT * FROM authors" Dim connectionString As String = "Initial Catalog=pubs;" _ & "Data Source=(local);Integrated Security=SSPI;" ' Initialize the SqlDataAdapter with the SQL ' and Connection strings, and then use the ' SqlDataAdapter to fill the DataSet with data. Dim adapter As New SqlDataAdapter(sql, connectionString) Dim authors As New DataSet adapter.Fill(authors) ' Iterate through the DataSet's table. For Each row As DataRow In authors.Tables(0).Rows Console.WriteLine(row("au_fname").ToString _ & " " & row("au_lname").ToString) Next ' Print the DataSet's XML. Console.WriteLine(authors.GetXml()) Console.ReadLine() End Sub
Note how you use the constructor of the SqlDataAdapter
to pass in and set the SelectCommand
, as well as pass in the connection string in lieu of a SqlCommand
object that already has an initialized Connection
property. You then just call the SqlDataAdapter
object's Fill
method and pass in an initialized DataSet
object. If the DataSet
object is not initialized, then the Fill
method raises an exception (System.ArgumentNullException
).
Ever since ADO.NET 2.0, a significant performance improvement was made in the way that the DataAdapter
updates the database. In ADO.NET 1.x, the DataAdapter
's Update
method would loop through each row of every DataTable
object in the DataSet
and subsequently make a trip to the database for each row being updated. In ADO.NET 2.0, batch update support was added to the DataAdapter
. This means that when the Update
method is called, the DataAdapter
batches all of the updates from the DataSet
in one trip to the database.
Now let's take a look at a more advanced example. Here, you use a DataAdapter
to insert, update, and delete data from a DataTable
back to the pubs database:
Private Sub _batchUpdateButton_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles _batchUpdateButton.Click ' Build insert, update, and delete commands.
' Build the parameter values. Dim insertUpdateParams() As String = {"@au_id", "@au_lname", _ "@au_fname", _ "@phone", "@address", "@city", "@state", "@zip", "@contract"}
The preceding code begins by initializing a string array of parameter names to pass into the BuildSqlCommand
helper method:
' Insert command. Dim insertCommand As SqlCommand = _ BuildSqlCommand("usp_authors_Insert", _ insertUpdateParams)
Next, you pass the name of the stored procedure to execute and the parameters for the stored procedure to the BuildSqlCommand
helper method. This method returns an initialized instance of the SqlCommand
class. Here is the BuildSqlCommand
helper method:
Private Function BuildSqlCommand(ByVal storedProcedureName As String, _ ByVal parameterNames() As String) As SqlCommand ' Build a SqlCommand. Dim command As New SqlCommand(storedProcedureName, GetPubsConnection()) ' Set the command type to stored procedure. command.CommandType = CommandType.StoredProcedure ' Build the parameters for the command. ' See if any parameter names were passed in. If Not parameterNames Is Nothing Then ' Iterate through the parameters. Dim parameter As SqlParameter = Nothing For Each parameterName As String In parameterNames ' Create a new SqlParameter. parameter = New SqlParameter() parameter.ParameterName = parameterName ' Map the parameter to a column name in the DataTable/DataSet. parameter.SourceColumn = parameterName.Substring(1) ' Add the parameter to the command. command.Parameters.Add(parameter) Next End If Return command End Function
This method first initializes a SqlCommand
class and passes in the name of a stored procedure; it then uses the GetPubsConnection
helper method to pass in a SqlConnection
object to the SqlCommand
. The next step is to set the command type of the SqlCommand
to a stored procedure. This is important because ADO.NET uses this to optimize how the stored procedure is called on the database server. You then check whether any parameter names have been passed (via the parameterNames
string array); if so, you iterate through them. While iterating through the parameter names, you build up SqlParameter
objects and add them to the SqlCommand
's collection of parameters.
The most important step in building up the SqlParameter
object is setting its SourceColumn
property. This is what the DataAdapter
later uses to map the name of the parameter to the name of the column in the DataTable
when its Update
method is called. An example of such a mapping is associating the @au_id
parameter name with the au_id
column name. As shown in the code, the mapping assumes that the stored procedure parameters all have exactly the same names as the columns, except for the mandatory @
character in front of the parameter. That's why when assigning the SqlParameter
's SourceColumn
property value, you use the Substring
method to strip off the @
character to ensure that it maps correctly.
You then call the BuildSqlCommand
method two more times to build your update and delete SqlCommand
objects:
' Update command. Dim updateCommand As SqlCommand = _ BuildSqlCommand("usp_authors_Update", _ insertUpdateParams) ' Delete command. Dim deleteCommand As SqlCommand = _ BuildSqlCommand("usp_authors_Delete", _ New String() {"@au_id"})
Now that the SqlCommand
objects have been created, the next step is to create a SqlDataAdapter
object. Once the SqlDataAdapter
is created, you set its InsertCommand, UpdateCommand
, and DeleteCommand
properties with the respective SqlCommand
objects that you just built:
' Create an adapter. Dim adapter As New SqlDataAdapter() ' Associate the commands with the adapter. adapter.InsertCommand = insertCommand adapter.UpdateCommand = updateCommand adapter.DeleteCommand = deleteCommand
The next step is to get a DataTable
instance of the authors table from the pubs database. You do this by calling the GetAuthorsSqlReader
helper method to first get a DataReader
and then the GetTableFromReader
helper method to load a DataTable
from a DataReader
:
' Get the authors reader. Dim reader As SqlDataReader = GetAuthorsSqlReader() ' Load a DataTable from the reader. Dim table As DataTable = GetTableFromReader(reader, "Authors")
Once you have your DataTable
filled with data, you begin modifying it so you can test the new batch update capability of the DataAdapter
. The first change to make is an insert in the DataTable
. In order to add a row, you first call the DataTable
's NewRow
method to give you a DataRow
initialized with the same columns as your DataTable
:
' Add a new author to the DataTable. Dim row As DataRow = table.NewRow
Once that is done, you can set the values of the columns of the DataRow
:
row("au_id") = "335-22-0707" row("au_fname") = "Bill"
row("au_lname") = "Evjen" row("phone") = "800-555-1212" row("contract") = 0
Then you call the Add
method of the DataTable
's DataRowCollection
property and pass in the newly populated DataRow
object:
table.Rows.Add(row)
Now that there is a new row in the DataTable
, the next test is to update one of its rows:
' Change an author in the DataTable. table.Rows(0)("au_fname") = "Updated Name!"
Finally, you delete a row from the DataTable
. In this case, it is the second-to-last row in the DataTable
:
' Delete the second to last author from the table table.Rows(table.Rows.Count - 2).Delete()
Now that you have performed an insert, update, and delete action on your DataTable
, it is time to send the changes back to the database. You do this by calling the DataAdapter
's Update
method and passing in either a DataSet
or a DataTable
. Note that you are calling the GetChanges
method of the DataTable
; this is important, because you only want to send the changes to the DataAdapter
:
' Send only the changes in the DataTable to the database for updating. adapter.Update(table.GetChanges())
To prove that the update worked, you get back a new DataTable
from the server using the same technique as before, and then bind it to the grid with your helper method to view the changes that were made:
' Get the new changes back from the server to show that the update worked. reader = GetAuthorsSqlReader() table = GetTableFromReader(reader, "Authors") ' Bind the grid to the new table data. BindGrid(table) End Sub
The SQL Server .NET Data Provider uses Tabular Data Stream (TDS) to communicate with the SQL Server. This offers a great performance increase, as TDS is SQL Server's native communication protocol. As an example of how much of an increase you can expect, when I ran some simple tests accessing the authors table of the pubs database, the SQL Server .NET Data Provider performed about 70 percent faster than the OLE DB .NET Data Provider.
The SQL Server .NET Data Provider is lightweight and performs very well, thanks to not having to go through the OLE DB or ODBC layer. What it actually does is establish a network connection (usually sockets-based) and drag data from this directly into managed code and vice versa.
This is very important, as going through the OLE DB or ODBC layers means that the CLR has to marshal (convert) all of the COM data types to .NET CLR data types each time data is accessed from a data source. When using the SQL Server .NET Data Provider, everything runs within the .NET CLR, and the TDS protocol is faster than the other network protocols previously used for SQL Server.
To use this provider, you need to include the System.Data.SqlClient
namespace in your application. Note that it works only for SQL Server 7.0 and later. I highly recommend using the SQL Server .NET Data Provider any time you are connecting to a SQL Server 7.0 and later database server. The SQL Server .NET Data Provider requires the installation of MDAC 2.6 or later.
The OLE DB .NET Data Provider uses native OLE DB through COM interop to enable data access. The OLE DB .NET Data Provider supports both manual and automatic transactions. For automatic transactions, the OLE DB .NET Data Provider automatically enlists in a transaction and obtains transaction details from Windows 2000 Component Services. The OLE DB .NET Data Provider does not support OLE DB 2.5 interfaces. OLE DB Providers that require support for OLE DB 2.5 interfaces will not function properly with the OLE DB .NET Data Provider. This includes the Microsoft OLE DB Provider for Exchange and the Microsoft OLE DB Provider for Internet Publishing. The OLE DB .NET Data Provider requires the installation of MDAC 2.6 or later. To use this provider, you need to include the System.Data.OleDb
namespace in your application.
The DataSet
object is central to supporting disconnected, distributed data scenarios with ADO.NET. The DataSet
is a memory-resident representation of data that provides a consistent relational programming model regardless of the data source. The DataSet
represents a complete set of data, including related tables, constraints, and relationships among the tables; basically, it's like having a small relational database residing in memory.
Because the
DataSet
contains a lot of metadata, you need to be careful about how much data you try to stuff into it, as it consumes memory.
The methods and objects in a DataSet
are consistent with those in the relational database model. The DataSet
can also persist and reload its contents as XML, and its schema as XSD. It is completely disconnected from any database connections, so it is totally up to you to fill it with whatever data you need in memory.
Ever since ADO.NET 2.0, there have been several new features to the DataSet
and the DataTable
classes, as well as enhancements to existing features. The features covered in this section are as follows:
An ADO.NET DataSet
contains a collection of zero or more tables represented by DataTable
objects. The DataTableCollection
contains all of the DataTable
objects in a DataSet
.
A DataTable
is defined in the System.Data
namespace and represents a single table of memory-resident data. It contains a collection of columns represented by the DataColumnCollection
, which defines the schema and rows of the table. It also contains a collection of rows represented by the DataRowCollection
, which contains the data in the table. Along with the current state, a DataRow
retains its original state and tracks changes that occur to the data.
A DataSet
contains relationships in its DataRelationCollection
object. A relationship (represented by the DataRelation
object) associates rows in one DataTable
with rows in another DataTable
. The relationships in the DataSet
can have constraints, which are represented by UniqueConstraint
and ForeignKeyConstraint
objects. It is analogous to a JOIN
path that might exist between the primary and foreign key columns in a relational database. A DataRelation
identifies matching columns in two tables of a DataSet
.
Relationships enable you to see what links information within one table to another. The essential elements of a DataRelation
are the name of the relationship, the two tables being related, and the related columns in each table. Relationships can be built with more than one column per table, with an array of DataColumn
objects for the key columns. When a relationship is added to the DataRelationCollection
, it may optionally add ForeignKeyConstraints
that disallow any changes that would invalidate the relationship.
DataSet
(as well as DataTable
and DataColumn
) has an ExtendedProperties
property. ExtendedProperties
is a PropertyCollection
in which a user can place customized information, such as the SELECT
statement that is used to generate the result set, or a date/time stamp indicating when the data was generated. Because the ExtendedProperties
contains customized information, this is a good place to store extra user-defined data about the DataSet
(or DataTable
or DataColumn
), such as a time when the data should be refreshed. The ExtendedProperties
collection is persisted with the schema information for the DataSet
(as well as DataTable
and DataColumn
). The following code is an example of adding an expiration property to a DataSet
:
Private Shared Sub DataSetExtended() ' Build the SQL and Connection strings. Dim sql As String = "SELECT * FROM authors" Dim connectionString As String = "Initial Catalog=pubs;" _ & "Data Source=(local);Integrated Security=SSPI;" ' Initialize the SqlDataAdapter with the SQL ' and Connection strings, and then use the ' SqlDataAdapter to fill the DataSet with data.
Dim adapter As SqlDataAdapter = _ New SqlDataAdapter(sql, connectionString) Dim authors As New DataSet adapter.Fill(authors) ' Add an extended property called "expiration." ' Set its value to the current date/time + 1 hour. authors.ExtendedProperties.Add("expiration", _ DateAdd(DateInterval.Hour, 1, Now)) Console.Write(authors.ExtendedProperties("expiration").ToString) Console.ReadLine() End Sub
This code begins by filling a DataSet
with the authors table from the pubs database. It then adds a new extended property, called expiration
, and sets its value to the current date and time plus one hour. You then simply read it back. As you can see, it is very easy to add extended properties to DataSet
objects. The same pattern also applies to DataTable
and DataColumn
objects.
The ADO.NET DataSet
is a memory-resident representation of the data that provides a consistent relational programming model, regardless of the source of the data it contains. A DataSet
represents a complete set of data, including the tables that contain, order, and constrain the data, as well as the relationships between the tables. The advantage to using a DataSet
is that the data it contains can come from multiple sources, and it is fairly easy to get the data from multiple sources into the DataSet
. In addition, you can define your own constraints between the DataTables
in a DataSet
.
There are several methods for working with a DataSet
, which can be applied independently or in combination:
Here is a typical usage scenario for a DataSet
object:
A client makes a request to a Web service.
Based on this request, the Web service populates a DataSet
from a database using a DataAdapter
and returns the DataSet
to the client.
The client then views the data and makes modifications.
When finished viewing and modifying the data, the client passes the modified DataSet
back to the Web service, which again uses a DataAdapter
to reconcile the changes in the returned DataSet
with the original data in the database.
The Web service may then return a DataSet
that reflects the current values in the database.
Optionally, the client can then use the DataSet
class's Merge
method to merge the returned DataSet
with the client's existing copy of the DataSet
; the Merge
method will accept successful changes and mark with an error any changes that failed.
The design of the ADO.NET DataSet
makes this scenario fairly easy to implement. Because the DataSet
is stateless, it can be safely passed between the server and the client without tying up server resources such as database connections. Although the DataSet
is transmitted as XML, Web services and ADO.NET automatically transform the XML representation of the data to and from a DataSet
, creating a rich, yet simplified, programming model.
In addition, because the DataSet
is transmitted as an XML stream, non-ADO.NET clients can consume the same Web service consumed by ADO.NET clients. Similarly, ADO.NET clients can interact easily with non-ADO.NET Web services by sending any client DataSet
to a Web service as XML and by consuming any XML returned as a DataSet
from the Web service. However, note the size of the data; if your DataSet
contains a large number of rows, then it will eat up a lot of bandwidth.
You can programmatically create a DataSet
object to use as a data structure in your programs. This could be quite useful if you have complex data that needs to be passed around to another object's method. For example, when creating a new customer, instead of passing 20 arguments about the new customer to a method, you could just pass the programmatically created DataSet
object with all of the customer information to the object's method.
Here is the code for building an ADO.NET DataSet
object that is comprised of related tables:
Private Sub BuildDataSet() Dim customerOrders As New Data.DataSet("CustomerOrders") Dim customers As Data.DataTable = customerOrders.Tables.Add("Customers") Dim orders As Data.DataTable = customerOrders.Tables.Add("Orders") Dim row As Data.DataRow With customers .Columns.Add("CustomerID", Type.GetType("System.Int32")) .Columns.Add("FirstName", Type.GetType("System.String")) .Columns.Add("LastName", Type.GetType("System.String")) .Columns.Add("Phone", Type.GetType("System.String")) .Columns.Add("Email", Type.GetType("System.String")) End With With orders .Columns.Add("CustomerID", Type.GetType("System.Int32")) .Columns.Add("OrderID", Type.GetType("System.Int32")) .Columns.Add("OrderAmount", Type.GetType("System.Double")) .Columns.Add("OrderDate", Type.GetType("System.DateTime")) End With customerOrders.Relations.Add("Customers_Orders", _ customerOrders.Tables("Customers").Columns("CustomerID"), _ customerOrders.Tables("Orders").Columns("CustomerID"))
row = customers.NewRow() row("CustomerID") = 1 row("FirstName") = "Bill" row("LastName") = "Evjen" row("Phone") = "555-1212" row("Email") = "[email protected]" customers.Rows.Add(row) row = orders.NewRow() row("CustomerID") = 1 row("OrderID") = 22 row("OrderAmount") = 0 row("OrderDate") = #11/10/1997# orders.Rows.Add(row) Console.WriteLine(customerOrders.GetXml()) Console.ReadLine() End Sub
Here is what the resulting XML of the DataSet
looks like:
<CustomerOrders> <Customers> <CustomerID>1</CustomerID> <FirstName>Bill</FirstName> <LastName>Evjen</LastName> <Phone>555-1212</Phone> <Email>[email protected]</Email> </Customers> <Orders> <CustomerID>1</CustomerID> <OrderID>22</OrderID> <OrderAmount>0</OrderAmount> <OrderDate>1997-11-10T00:00:00.0000</OrderDate> </Orders> </CustomerOrders>
You begin by first defining a DataSet
object (CustomerOrders
) named CustomerOrders
. You then create two tables: one for customers (customers
) and one for orders (orders
). Then you define the columns of the tables. Note that you call the Add
method of the DataSet
's Tables
collection. You then define the columns of each table and create a relation in the DataSet
between the customers table and the orders table on the CustomerID column. Finally, you create instances of Rows
for the tables, add the data, and then append the Rows
to the Rows
collection of the DataTable
objects.
If you create a
DataSet
object with no name, it is given the default name ofNewDataSet
.
A DataSet
is made up of a collection of tables, relationships, and constraints. In ADO.NET, DataTable
objects are used to represent the tables in a DataSet
. A DataTable
represents one table of in-memory relational data. The data is local to the .NET application in which it resides, but can be populated from a data source such as SQL Server using a DataAdapter
.
The DataTable
class is a member of the System.Data
namespace within the .NET Framework class library. You can create and use a DataTable
independently or as a member of a DataSet
, and DataTable
objects can be used by other .NET Framework objects, including the DataView
. You access the collection of tables in a DataSet
through the DataSet
object's Tables
property.
The schema, or structure, of a table is represented by columns and constraints. You define the schema of a DataTable
using DataColumn
objects as well as ForeignKeyConstraint
and UniqueConstraint
objects. The columns in a table can map to columns in a data source, contain calculated values from expressions, automatically increment their values, or contain primary key values.
If you populate a DataTable
from a database, then it inherits the constraints from the database, so you don't have to do all of that work manually. A DataTable
must also have rows in which to contain and order the data. The DataRow
class represents the actual data contained in the table. You use the DataRow
and its properties and methods to retrieve, evaluate, and manipulate the data in a table. As you access and change the data within a row, the DataRow
object maintains both its current and original state.
You can create parent-child relationships between tables within a database, such as SQL Server, using one or more related columns in the tables. You create a relationship between DataTable
objects using a DataRelation
, which can then be used to return a row's related child or parent rows.
One of the main complaints developers had about ADO.NET 1.x was related to the performance of the DataSet
and its DataTable
children — in particular, when they contained a large amount of data. The performance hit comes in two different ways. The first way is the time it takes to actually load a DataSet
with a lot of data. As the number of rows in a DataTable
increases, the time to load a new row increases almost proportionally to the number of rows. The second way is when the large DataSet
is serialized and remoted. A key feature of the DataSet
is the fact that it automatically knows how to serialize itself, especially when you want to pass it between application tiers. Unfortunately, the serialization is quite verbose and takes up a lot of memory and network bandwidth. Both of these performance problems have been addressed since ADO.NET 2.0.
The first improvement made since ADO.NET 2.0 to the DataSet
family was a complete rewrite of the indexing engine for the DataTable
, which now scales much better for large DataSet
s. The addition of the new indexing engine results in faster basic inserts, updates, and deletes, which also means faster Fill
and Merge
operations. Just as in relational database design, if you are dealing with large DataSet
s, then it pays big dividends if you first add unique keys and foreign keys to your DataTable
. Even better, you don't have to change any of your code at all to take advantage of this new feature.
The second improvement made to the DataSet
family was adding new options to the way the DataSet
and DataTable
are serialized. The main complaint about retrieving DataSet
objects from Web services and remoting calls was that they were way too verbose and took up too much network bandwidth. In ADO.NET 1.x, the DataSet
serializes as XML, even when using the binary formatter. Using ADO.NET, you can also specify true binary serialization by setting the newly added RemotingFormat
property to SerializationFormat.Binary
, rather than (the default) SerializationFormat.XML
. In the AdoNetFeaturesTest project of the Examples solution, I have added a Button
(serializationButton
) to the form and its associated Click
event handler that demonstrates how to serialize a DataTable
in binary format:
Private Sub _serializationButton_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles _serializationButton.Click ' Get the authors reader. Dim reader As SqlDataReader = GetAuthorsSqlReader() ' Load a DataTable from the reader Dim table As DataTable = GetTableFromReader(reader, "Authors")
This code begins by calling the helper methods GetAuthorsSqlReader
and GetTableFromReader
to get a DataTable
of the authors from the pubs database. The next code block, shown here, is where you are actually serializing the DataTable
out to a binary format:
Using fs As New FileStream("c:authors.dat", FileMode.Create) table.RemotingFormat = SerializationFormat.Binary Dim format As New BinaryFormatter() format.Serialize(fs, table) End Using ' Tell the user what happened. MessageBox.Show("Successfully serialized the DataTable!") End Sub
This code takes advantage of the newly added Using
statement for Visual Basic to wrap up creating and disposing of a FileStream
instance that will hold your serialized DataTable
data. The next step is to set the DataTable
's RemotingFormat
property to the SerializationFormat.Binary
enumeration value. Once that is done, you simply create a new BinaryFormatter
instance, and then call its Serialize
method to serialize your DataTable
into the FileStream
instance. You then finish by showing users a message box indicating that the data has been serialized.
Another nice feature of the DataSet
and DataTable
classes is the capability to both read from and write out to a stream of data in the form of a DataReader
. You will first take a look at how you can load a DataTable
from a DataReader
. To demonstrate this, I have added a Button
(loadFromReaderButton
) and its associated Click
event handler to TestForm.vb
of the AdoNetFeaturesTest project in the Examples solution:
Private Sub _loadFromReaderButton_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles _loadFromReaderButton.Click ' Get the authors reader. Dim reader As SqlDataReader = GetAuthorsSqlReader() ' Load a DataTable from the reader. Dim table As DataTable = GetTableFromReader(reader, "Authors") ' Bind the grid to the table. BindGrid(table) End Sub
This method is a controller method, meaning that it only calls helper methods. It begins by first obtaining a SqlDataReader
from the GetAuthorsReader
helper method. It then calls the GetTableFromReader
helper method to transform the DataReader
into a DataTable
. The GetTableFromReader
method is where you actually get to see the DataTable
's new load functionality:
Private Function GetTableFromReader(ByVal reader As SqlDataReader, _ ByVal tableName As String) As DataTable ' Create a new DataTable using the name passed in. Dim table As New DataTable(tableName) ' Load the DataTable from the reader. table.Load(reader) ' Close the reader. reader.Close() Return table End Function
This method begins by first creating an instance of a DataTable
and initializing it with the name passed in from the tableName
argument. Once the new DataTable
has been initialized, you call the new Load
method and pass in the SqlDataReader
that was passed into the method via the reader
argument. This is where the DataTable
takes the DataReader
and populates the DataTable
instance with the column names and data from the DataReader
. The next step is to close the DataReader
, as it is no longer needed; and finally, you return the newly populated DataTable
.
One of the most convenient capabilities in ADO.NET is the inclusion of several methods from the DataSet
class in the DataTable
class. The DataTable
is now much more versatile and useful than it was in the early ADO.NET days. The DataTable
now supports all of the same read and write methods for XML as the DataSet
— specifically, the ReadXml, ReadXmlSchema, WriteXml
, and WriteXmlSchema
methods.
The Merge
method of the DataSet
has now been added to the DataTable
as well; and in addition to the existing functionality of the DataSet
class, some of the new features of the DataSet
class have been added to the DataTable
class — namely, the RemotingFormat
property, the Load
method, and the GetDataReader
method.
In ADO.NET 1.x, you could either code to the provider-specific classes, such as SqlConnection
, or the generic interfaces, such as IDbConnection
. If there was a possibility that the database you were programming against would change during your project, or if you were creating a commercial package intended to support customers with different databases, then you had to use the generic interfaces. You cannot call a constructor on an interface, so most generic programs included code that accomplished the task of obtaining the original IDbConnection
by means of their own factory method, such as a GetConnection
method that would return a provider-specific instance of the IDbConnection
interface.
ADO.NET today has a more elegant solution for getting the provider-specific connection. Each data provider registers a ProviderFactory
class and a provider string in the .NET machine.config
file. A base ProviderFactory
class (DbProviderFactory
) and a System.Data.Common.ProviderFactories
class can return a DataTable
of information about different data providers registered in machine.config
, and can return the correct ProviderFactory
given the provider string (called ProviderInvariantName
) or a DataRow
from the DataTable
. Instead of writing your own framework to build connections based on the name of the provider, ADO.NET now makes it much more straightforward, flexible, and easy to solve this problem.
Let's look at an example of using the common provider model to connect to the pubs database and display some rows from the authors table. In the AdoNetFeaturesTest project, on the TestForm.vb
form, the providerButton
button's Click
event handler shows this functionality. The code is broken down into six steps. The first step is get the provider factory object based on a configuration value of the provider's invariant name:
Private Sub _providerButton_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles _providerButton.Click ' 1. Factory ' Create the provider factory from config value. Dim factory As DbProviderFactory = DbProviderFactories.GetFactory( _ ConfigurationSettings.AppSettings("providerInvariantName"))
You are able to get the factory via the DbProviderFactories
object's GetFactory
method and pass in the string name of the provider invariant that you are storing in the project's app.config
file. Here is the entry in the app.config
file:
<add key="providerInvariantName" value="System.Data.SqlClient" />
In this case, you are using the SQL Server Data Provider. Once you have the factory object, the next step is to use it to create a connection:
' 2. Connection ' Create the connection from the factory. Dim connection As DbConnection = factory.CreateConnection() ' Get the connection string from config. connection.ConnectionString = _ ConfigurationSettings.AppSettings("dbConnectionString")
The connection is created by calling the DbProviderFactory
's CreateConnection
method. In this case, the factory is returning a SqlConnection
, because you chose to use the System.Data.SqlClient
provider invariant. To keep your code generic, you will not be directly programming against any of the classes in the System.Data.SqlClient
namespace. Note how the connection class you declare is a DbConnection
class, which is part of the System.Data
namespace.
The next step is to create a Command
object so you can retrieve the data from the authors table:
' 3. Command ' Create the command from the connection. Dim command As DbCommand = connection.CreateCommand() ' Set the type of the command to stored procedure. command.CommandType = CommandType.StoredProcedure ' Set the name of the stored procedure to execute. command.CommandText = "usp_authors_Get_All"
You begin by declaring a generic DbCommand
class variable and then using the DbConnection
's CreateCommand
method to create the DbCommand
instance. Once you have done that, you set the command type to StoredProcedure
and then set the stored procedure name.
This example uses a DbDataAdapter
to fill a DataTable
with the authors' data. Here is how you create and initialize the DbDataAdapter
:
' 4. Adapter ' Create the adapter from the factory. Dim adapter As DbDataAdapter = factory.CreateDataAdapter() ' Set the adapter's select command. adapter.SelectCommand = command
Just as you did when you created your DbConnection
instance, you use the factory to create your DbDataAdapter
. After creating it, you then set the SelectCommand
property's value to the instance of the previously initialized DbCommand
instance.
After finishing these steps, the next step is to create a DataTable
and fill it using the DataAdapter
:
' 5. DataTable ' Create a new DataTable. Dim authors As New DataTable("Authors") ' Use the adapter to fill the DataTable. adapter.Fill(authors)
The final step is to bind the table to the form's grid:
' 6. Grid ' Populate the grid with the data. BindGrid(authors)
You already looked at the BindGrid
helper method in the asynchronous example earlier. In this example, you are simply reusing this generic method again:
Private Sub BindGrid(ByVal table As DataTable) ' Clear the grid. Me._authorsGridView.DataSource = Nothing ' Bind the grid to the DataTable. Me._authorsGridView.DataSource = table End Sub
The main point to take away from this example is that you were able to easily write database-agnostic code with just a few short lines. ADO.NET 1.x required a lot of lines of code to create this functionality; you had to write your own abstract factory classes and factory methods in order to create instances of the generic database interfaces, such as IDbConnection, IDbCommand
, and so on.
Pooling connections can significantly enhance the performance and scalability of your application. Both the SQL Client .NET Data Provider and the OLE DB .NET Data Provider automatically pool connections using Windows Component Services and OLE DB Session Pooling, respectively. The only requirement is that you must use the exact same connection string each time if you want a pooled connection.
ADO.NET now enhances the connection pooling functionality offered in ADO.NET 1.x by enabling you to close all of the connections currently kept alive by the particular managed provider that you are using. You can clear a specific connection pool by using the shared SqlConnection.ClearPool
method or clear all of the connection pools in an application domain by using the shared SqlConnection.ClearPools
method. Both the SQL Server and Oracle managed providers implement this functionality.
To better demonstrate what you have learned so far about ADO.NET, in this section you are going to build a data-access component. This component is designed to abstract the processing of stored procedures. The component you build is targeted at SQL Server, and it is assumed that all data access to the database will be through stored procedures. The idea of only using stored procedures to access data in a database has a number of advantages, such as scalability, performance, flexibility, and security. The only disadvantage is that you have to use stored procedures, and not SQL strings. Through the process of building this component, you will see how stored procedures are implemented in ADO.NET. You will also be building on the knowledge that you have gained from the previous chapters.
This component's main job is to abstract stored procedure calls to SQL Server, and one of the ways you do this is by passing in all of your stored procedure parameter metadata as XML (covered later in this section). The other job of the component is to demonstrate the use of some of the new objects in ADO.NET.
The code for this project is quite extensive, and you will only examine the key parts of it in this chapter. The full source is available in the code download (
www.wrox.com
).
Let's start with the beginning of the component. The first thing you do is declare your class and the private members of the class:
Option Explicit On Option Strict On Imports System Imports System.Data Imports System.Data.SqlClient Imports System.Xml Imports System.Collections Imports System.Diagnostics '' ' <summary> '' ' This class wraps stored procedure calls to SQL Server. '' ' It requires that all '' ' stored procedures and their parameters be defined in an '' ' XML document before '' ' calling any of its methods. The XML can be passed in as an XmlDocument '' ' instance or as a string of XML. The only exceptions to this rule are '' ' stored procedures that do not have parameters. This class also caches '' ' SqlCommand objects. Each time a stored procedure is executed, a SqlCommand '' ' object is built and cached into memory so that the next time the stored '' ' procedure is called the SqlCommand object can be retrieved from memory. '' ' </summary> Public NotInheritable Class StoredProcedureHelper
Private _connectionString As String = "" Private _spParamXml As String = "" Private _spParamXmlDoc As XmlDocument = Nothing Private _spParamXmlNode As XmlNode = Nothing Private _commandParametersHashTable As New Hashtable() Private Const ExceptionMsg As String = _ "There was an error in the method. " _ & "Please see the Windows Event Viewer Application log for details"
You begin with your Option
statements. Note that you are using the Option Strict
statement. This helps prevent logic errors and data loss that can occur when you work between variables of different types. Next, you import the namespaces that you need for your component. In this case, most of your dependencies are on System.Data.SqlClient
. You call your class StoredProcedureHelper
to indicate that it wraps calling stored procedures to SQL Server. Next, you declare your private data members. You use the ExceptionMsg
constant to indicate a generic error message for any exceptions thrown.
Now you get to declare your constructors for the StoredProcedureHelper
class. This is where you can really take advantage of method overloading, and it gives you a way to pass data to your class upon instantiation. First, you declare a default constructor:
'' ' <summary> '' ' Default constructor. '' ' </summary> Public Sub New() End Sub
The default constructor is provided in case users want to pass data to your class through public properties instead of through constructor arguments.
The next constructor you create allows a database connection string to be passed into it. By abstracting the database connection string out of this component, you give users of your component more flexibility regarding how they store and retrieve their database connection strings. Here is the code for the constructor:
`´` <summary> `´` Overloaded constructor. `´` </summary> `´` <param name="connectionString">The connection string to the `´` SQL Server database.</param> Public Sub New(ByVal connectionString As String) Me._connectionString = connectionString End Sub
The only difference between this constructor and the default constructor is that you are passing in a database connection string.
In the next constructor, you pass in both a database connection string and a string of XML representing the stored procedure parameters for the stored procedures you want to call:
`´` <summary> `´` Overloaded constructor. `´` </summary> `´` <param name="connectionString">The connection string to the `´` SQL Server database.</param> `´` <param name="spParamXml">A valid XML string which conforms to `´` the correct schema for stored procedure(s) and their `´` associated parameter(s).</param> Public Sub New(ByVal connectionString As String, ByVal spParamXml As String) Me.New(connectionString) Me._spParamXml = spParamXml Me._spParamXmlDoc = New XmlDocument Try Me._spParamXmlDoc.LoadXml(spParamXml) Me._spParamXmlNode = Me._spParamXmlDoc.DocumentElement Catch e As XmlException LogError(e) Throw New Exception(ExceptionMsg, e) End Try End Sub
This constructor sets the database connection string by calling the first overloaded constructor. This handy technique enables you to avoid writing duplicate code in your constructors. The constructor then loads the stored procedure parameter configuration into a private XmlDocument
instance variable as well as a private XmlNode
instance variable.
The remaining constructors enable you to pass in combinations of database connection strings as well as either a valid XmlDocument
instance representing the stored procedure parameters or a valid XmlNode
instance that represents the stored procedure parameters.
Now let's look at the properties of your class. Your object contains the following properties: ConnectionString, SpParamXml
, and SpParamXmlDoc
. These properties are provided as a courtesy in case the user of your object does not want to supply them via a constructor call. The ConnectionString
property provides the same functionality as the first overloaded constructor you looked at. The SpParamXml
property enables the user of the object to pass in a valid XML string representing the stored procedures' parameter metadata. All of the properties are read-write. The SpParamXmlDoc
property enables users to pass in an XmlDocument
instance representing the stored procedures' parameter metadata.
Here is the code for the SpParamXml
property:
`´` <summary> `´` A valid XML string which conforms to the correct schema for `´` stored procedure(s) and their associated parameter(s). `´` </summary> Public Property SpParamXml() As String
Get Return Me._spParamXml End Get Set(ByVal Value As String) Me._spParamXml = Value ' Set the XmlDocument instance to null, since ' an XML string is being passed in. Me._spParamXmlDoc = Nothing Try Me._spParamXmlDoc.LoadXml(Me._spParamXml) Me._spParamXmlNode = Me._spParamXmlDoc.DocumentElement Catch e As XmlException LogError(e) Throw New Exception(ExceptionMsg) End Try End Set End Property
Note that this property resets the XmlDocument
instance to Nothing
before trying to load the document. This is done in case it was already set in one of the overloaded constructors, or from a previous call to this property. It also sets the XmlNode
instance to the DocumentElement
property of the XmlDocument
instance, thus keeping them both in sync.
In this case, rather than have the user of this class be responsible for populating the Parameters
collection of a Command
object, you will abstract it out into an XML structure. The structure is very simple; it basically enables you to store the metadata for one or more stored procedures at a time. This has a huge advantage because you can change all of the parameters on a stored procedure without having to recompile the project. Shown here is the XML structure for the metadata:
<StoredProcedures> <StoredProcedure name> <Parameters> <Parameter name size datatype direction isNullable sourceColumn /> </Parameters> </StoredProcedure> </StoredProcedures>
Here is what some sample data for the XML structure looks like:
<?xml version="1.0"?> <StoredProcedures> <StoredProcedure name="usp_Get_Authors_By_States"> <Parameters> <Parameter name="@states" size="100" datatype="VarChar" direction="Input" isNullabel="True" /> <Parameter name="@state_delimiter" size="1" datatype="Char" direction="Input" isNullabel="True" /> </Parameters> </StoredProcedure> </StoredProcedures>
The valid values for the direction
attribute are Input, Output, ReturnValue
, and InputOutput
. These values map directly to the System.Data.Parameter
enumeration values. The valid values for the data type attribute are BigInt, Binary, Bit, Char, DateTime, Decimal, Float, Image, Int, Money, NChar, NText, NVarChar, Real, SmallDateTime, SmallInt, SmallMoney, Text, Timestamp, TinyInt, UniqueIdentifier, VarBinary, VarChar
, and Variant
. These values map directly to the System.Data.SqlDbType
enumeration values.
That completes our look at the stored procedure XML structure the class expects, as well as the public properties and public constructors for the class. Now let's turn our attention to the public methods of your class.
This public function executes a stored procedure and returns a DataSet
object. It takes a stored procedure name (String
), an optional DataSet
name (String
), and an optional list of parameter names and values (IDictionary
). Here is the code for ExecSpReturnDataSet
:
`´` <summary> `´` Executes a stored procedure with or without parameters and returns a `´` populated DataSet object. `´` </summary> `´` <param name="spName">The name of the stored procedure to execute.</param> `´` <param name="dataSetName">An optional name for the DataSet instance.</param> `´` <param name="paramValues">A name-value pair of stored procedure parameter `´` name(s) and value(s).</param> `´` <returns>A populated DataSet object.</returns> Public Function ExecSpReturnDataSet(ByVal spName As String, _ ByVal dataSetName As String, _ ByVal paramValues As IDictionary) As DataSet Dim command As SqlCommand = Nothing Try ' Get the initialized SqlCommand instance. command = GetSqlCommand(spName) ' Set the parameter values for the SqlCommand. SetParameterValues(command, paramValues) ' Initialize the SqlDataAdapter with the SqlCommand object. Dim sqlDA As New SqlDataAdapter(command) ' Initialize the DataSet. Dim ds As New DataSet() If Not (dataSetName Is Nothing) Then If dataSetName.Length > 0 Then ds.DataSetName = dataSetName End If End If ' Fill the DataSet. sqlDA.Fill(ds) ' Return the DataSet.
Return ds Catch e As Exception LogError(e) Throw New Exception(ExceptionMsg, e) Finally ' Close and release resources. DisposeCommand(command) End Try End Function
This function uses three main objects to accomplish its mission: SqlCommand, SqlDataAdapter
, and DataSet
. You first wrap everything in a Try-Catch-Finally
block to ensure that you trap any exceptions that are thrown and to properly close and release the SqlCommand
and SqlConnection
resources. You call a helper method, GetSqlCommand
, in order to get a fully initialized SqlCommand
instance, to include any SqlParameter
objects the SqlCommand
may have based on your object's internal XmlDocument
. Here is the code for GetSqlCommand
and its overload:
`´` <summary> `´` Initializes a SqlCommand object based on a stored procedure name `´` and a SqlTransaction instance. Verifies that the stored procedure `´` name is valid, and then tries to get the SqlCommand object from `´` cache. If it is not already in cache, then the SqlCommand object `´` is initialized and placed into cache. `´` </summary> `´` <param name="transaction">The transaction that the stored `´` procedure will be executed under.</param> `´` <param name="spName">The name of the stored procedure to execute.</param> `´` <returns>An initialized SqlCommand object.</returns> Public Function GetSqlCommand(ByVal transaction As SqlTransaction, _ ByVal spName As String) As SqlCommand Dim command As SqlCommand = Nothing ' Get the name of the stored procedure. If spName.Length < 1 Or spName.Length > 127 Then Throw New ArgumentOutOfRangeException("spName", _ "Stored procedure name must be from 1 - 128 characters.") End If ' See if the command object is already in memory. Dim hashKey As String = Me._connectionString & ":" & spName command = CType(_commandParametersHashTable(hashKey), SqlCommand) If command Is Nothing Then ' It was not in memory. ' Initialize the SqlCommand. command = New SqlCommand(spName, GetSqlConnection(transaction)) ' Tell the SqlCommand that you are using a stored procedure. command.CommandType = CommandType.StoredProcedure ' Build the parameters, if there are any. BuildParameters(command) ' Put the SqlCommand instance into memory.
Me._commandParametersHashTable(hashKey) = command Else ' It was in memory, but you still need to set the ' connection property. command.Connection = GetSqlConnection(transaction) End If ' Return the initialized SqlCommand instance. Return command End Function `´` <summary> `´` Overload. Initializes a SqlCommand object based on a stored `´` procedure name, with no SqlTransaction instance. `´` Verifies that the stored procedure name is valid, and then tries `´` to get the SqlCommand object from cache. If it is not already in `´` cache, then the SqlCommand object is initialized and placed into cache. `´` </summary> `´` <param name="spName">The name of the stored procedure to execute.</param> `´` <returns>An initialized SqlCommand object.</returns> Public Function GetSqlCommand(ByVal spName As String) As SqlCommand ' Return the initialized SqlCommand instance. Return GetSqlCommand(Nothing, spName) End Function
The difference between this method and its overload is that the first method takes in a SqlTransaction
instance argument, and the overload does not require the SqlTransaction
instance to be passed in. The overload simply calls the first method and passes in a value of Nothing
for the SqlTransaction
argument.
This method first performs a check to ensure that the stored procedure name is between 1 and 128 characters long, in accordance with SQL Server's object-naming conventions. If it is not, then you throw an exception. The next step this method performs is to try to get an already initialized SqlCommand
object from your object's private Hashtable
variable, commandParametersHashTable
, using your object's database connection string and the name of the stored procedure as the key. If the SqlCommand
is not found, then you go ahead and build the SqlCommand
object by calling its constructor and passing in the stored procedure name and a SqlConnection
instance returned from the GetSqlConnection
helper method. The code then sets the SqlCommand
's CommandType
property. You should ensure that you pass in the CommandType.StoredProcedure
enumeration value, as you are executing a stored procedure.
Once the SqlCommand
object is properly initialized, you pass it to the BuildParameters
method. You will look at this method in more detail later. After this step, the SqlCommand
is fully initialized, and you place it into your object's internal cache (the commandParametersHashTable Hashtable
variable). Finally, the SqlCommand
is returned to the calling code.
Getting back to the ExecSpReturnDataSet
method, now that the SqlCommand
object has been properly initialized, you need to set the values of the parameters. This is done via another helper method called SetParameterValues. SetParameterValues
takes two arguments: a reference to a SqlCommand
object and an IDictionary
interface. You are using an IDictionary
interface instead of a class such as a Hashtable
(which implements the IDictionary
interface) in order to make your code more flexible. This is a good design practice and works quite well — for example, in situations where the user of your class has built his or her own custom dictionary
object that implements the IDictionary
interface. It then loops through the SqlCommand
's Parameters
collection and sets each SqlParameter
's value based on the corresponding name-value pair in the IDictionary
object, as long as the parameter's direction is not Output
. Following is the code for the SetParameterValues
method:
`´` <summary> `´` Traverses the SqlCommand's SqlParameters collection and sets the values `´` for all of the SqlParameter(s) objects whose direction is not Output and `´` whose name matches the name in the dictValues IDictionary that was `´` passed in. `´` </summary> `´` <param name="command">An initialized SqlCommand object.</param> `´` <param name="dictValues">A name-value pair of stored procedure parameter `´` name(s) and value(s).</param> Public Sub SetParameterValues(ByVal command As SqlCommand, _ ByVal dictValues As IDictionary) If command Is Nothing Then Throw New ArgumentNullException("command", _ "The command argument cannot be null.") End If ' Traverse the SqlCommand's SqlParameters collection. Dim parameter As SqlParameter For Each parameter In command.Parameters ' Do not set Output parameters. If parameter.Direction <> ParameterDirection.Output Then ' Set the initial value to DBNull. parameter.Value = TypeCode.DBNull ' If there is a match, then update the parameter value. If dictValues.Contains(parameter.ParameterName) Then parameter.Value = dictValues(parameter.ParameterName) Else ' There was not a match. ' If the parameter value cannot be null, throw an exception. If Not parameter.IsNullable Then Throw New ArgumentNullException(parameter.ParameterName, _ "Error getting the value for the " _ & parameter.ParameterName & " parameter.") End If End If End If Next parameter End Sub
When traversing the SqlCommand
's Parameters
collection, if a SqlParameter
's value cannot be found in the IDictionary
instance, then a check is made to determine whether the SqlParameter
's value is allowed to be null or not. If it is allowed, then the value is set to DBNull
; otherwise, an exception is thrown.
After setting the values of the parameters, the next step is to pass the SqlCommand
object to the SqlDataAdapter
's constructor:
' Initialize the SqlDataAdapter with the SqlCommand object. Dim sqlDA As New SqlDataAdapter(command)
Then try to set the name of the DataSet
using the dataSetName
method argument:
' Try to set the name of the DataSet. If Not (dataSetName Is Nothing) Then If dataSetName.Length > 0 Then ds.DataSetName = dataSetName End If End If
After doing this, you call the Fill
method of the SqlDataAdapter
to fill your DataSet
object:
' Fill the DataSet. sqlDA.Fill(ds)
You then return the DataSet
object back to the caller:
' Return the DataSet. Return ds
If an exception was caught, then you log the exception data to the Windows Application Log via the LogError
private method, and then throw a new exception with your generic exception message. Nest the original exception inside of the new exception via the innerException
constructor parameter:
Catch e As Exception LogError(e) Throw New Exception(ExceptionMsg, e)
In the Finally
block, you close and release the SqlCommand
object's resources via the DisposeCommand
helper method:
Finally ' Close and release resources DisposeCommand(command)
The DisposeCommand
helper function closes the SqlCommand
's SqlConnection
property and disposes of the SqlCommand
object:
`´` <summary> `´` Disposes a SqlCommand and its underlying SqlConnection. `´` </summary> `´` <param name="command"></param> Private Sub DisposeCommand(ByVal command As SqlCommand) If Not (command Is Nothing) Then If Not (command.Connection Is Nothing) Then command.Connection.Close() command.Connection.Dispose() End If command.Dispose() End If End Sub
This private method is the heart of this object and does the most work. It is responsible for parsing the stored procedure parameter XML and mapping all of the SqlParameter
objects into the Parameters
property of the SqlCommand
object. Here is the signature of the method:
`´` <summary> `´` Finds the parameter information for the stored procedure from the `´` stored procedures XML document and then uses that information to `´` build and append the parameter(s) for the SqlCommand's `´` SqlParameters collection. `´` </summary> `´` <param name="command">An initialized SqlCommand object.</param> Private Sub BuildParameters(ByVal command As SqlCommand)
The first thing you do in this method is determine whether any XML is being passed in or not. Here is the code that checks for the XML:
' See if there is an XmlNode of parameter(s) for the stored procedure. If Me._spParamXmlNode Is Nothing Then ' No parameters to add, so exit. Return End If
The last bit of code simply checks whether there is an XmlNode
instance of parameter information. If the XmlNode
has not been initialized, then you exit the method. It is entirely possible that users of this object may have stored procedures with no parameters at all. You choose an XmlNode
object to parse the XML because loading all of the stored procedure XML into memory will not hurt performance; it is a small amount of data. As an alternative, you could use an XmlReader
object to load into memory only what you need at runtime.
The next step is to clear the SqlCommand
object's Parameters
collection:
' Clear the parameters collection for the SqlCommand command.Parameters.Clear()
You then use the name of the stored procedure as the key in your XPath query of the XML, and execute the following XPath query to get the list of parameters for the stored procedure:
' Get the node list of <Parameter>'s for the stored procedure. Dim xpathQuery As String = "//StoredProcedures/StoredProcedure[@name='" _ & command.CommandText & "']/Parameters/Parameter" Dim parameterNodes As XmlNodeList = Me._spParamXmlNode.SelectNodes(xpathQuery)
This query is executed off the XmlDocument
object and returns an XmlNodeList
object. You start the loop through the Parameter
elements in the XML and retrieve all of the mandatory Parameter
attributes:
Dim parameterNode As XmlElement For Each parameterNode In parameterNodes ' Get the attribute values for the <Parameter> element. ' Get the attribute values for the <Parameter> element.
' name Dim parameterName As String = parameterNode.GetAttribute("name") If parameterName.Length = 0 Then Throw New ArgumentNullException("name", "Error getting the 'name' " _ & "attribute for the <Parameter> element.") End If ' size Dim parameterSize As Integer = 0 If parameterNode.GetAttribute("size").Length = 0 Then Throw New ArgumentNullException("size", "Error getting the 'size' " _ & "attribute for the <Parameter> element.") Else parameterSize = Convert.ToInt32(parameterNode.GetAttribute("size")) End If ' datatype Dim sqlDataType As SqlDbType If parameterNode.GetAttribute("datatype").Length = 0 Then Throw New ArgumentNullException("datatype", "Error getting the " _ & "'datatype' attribute for the <Parameter> element.") Else sqlDataType = CType([Enum].Parse(GetType(SqlDbType), _ parameterNode.GetAttribute("datatype"), True), SqlDbType) End If ' direction Dim parameterDirection As ParameterDirection = parameterDirection.Input If parameterNode.GetAttribute("direction").Length > 0 Then parameterDirection = CType([Enum].Parse(GetType(ParameterDirection), _ parameterNode.GetAttribute("direction"), True), ParameterDirection) End If End If
Because these attributes are mandatory, if any of them are missing, then you throw an exception. The interesting part of this code is using the Enum.Parse
static method to convert the string value from the XML into the correct .NET enumeration data type for the sqlDataType
and parameterDirection
variables. This is possible because the probable values in your XML for these attributes map directly to the names of their respective enumeration data types in .NET. Next, you get the optional attributes:
' Get the optional attribute values for the <Parameter> element. ' isNullable Dim isNullable As Boolean = False Try If parameterNode.GetAttribute("isNullable").Length > 0 Then isNullable = Boolean.Parse(parameterNode.GetAttribute("isNullable")) End If Catch End Try ' sourceColumn - This must map to the name of a column in a DataSet. Dim sourceColumn As String = "" Try If parameterNode.GetAttribute("sourceColumn").Length > 0 Then
sourceColumn = parameterNode.GetAttribute("sourceColumn") End If Catch End Try
These attributes are optional mainly because of their data types. Because isNullable
is Boolean, you go ahead and convert it to False
if it is missing; and if sourceColumn
is missing, then you just ignore it entirely.
Now you are ready to create the SqlParameter
object and set its Direction
property:
' Create the parameter object. Pass in the name, datatype, ' and size to the constructor. Dim sqlParameter As SqlParameter = New SqlParameter(parameterName, _ sqlDataType, parameterSize) 'Set the direction of the parameter. sqlParameter.Direction = parameterDirection
You then set the optional property values of the SqlParameter
object:
' If the optional attributes have values, then set them. ' IsNullable If isNullable Then sqlParameter.IsNullable = isNullable End If ' SourceColumn sqlParameter.SourceColumn = sourceColumn
Finally, you add the SqlParameter
object to the SqlCommand
object's Parameters
collection, complete your loop, and finish the method:
' Add the parameter to the SqlCommand's parameter collection. command.Parameters.Add(sqlParameter) Next parameterNode End Sub
Now it's time to look at ExecSpReturnDataReader
. This function is almost identical to ExecSpReturnDataSet
except that it returns a SqlDataReader
object instead of a DataSet
object.
This public function executes a stored procedure and returns a SqlDataReader
object. Similar to the ExecSpReturnDataSet
method, it takes a stored procedure name (String
) and an optional list of parameter names and values (IDictionary
). Here is the code for ExecSpReturnDataReader
:
`´` <summary> `´` Executes a stored procedure with or without parameters and returns a `´` SqlDataReader instance with a live connection to the database. It is `´` very important to call the Close method of the SqlDataReader as soon `´` as possible after using it. `´` </summary>
`´` <param name="spName">The name of the stored procedure to execute.</param> `´` <param name="paramValues">A name-value pair of stored procedure parameter `´` name(s) and value(s).</param> `´` <returns>A SqlDataReader object.</returns> Public Function ExecSpReturnDataReader(ByVal spName As String, _ ByVal paramValues As IDictionary) As SqlDataReader Dim command As SqlCommand = Nothing Try ' Get the initialized SqlCommand instance. command = GetSqlCommand(spName) ' Set the parameter values for the SqlCommand. SetParameterValues(command, paramValues) ' Open the connection. command.Connection.Open() ' Execute the sp and return the SqlDataReader. Return command.ExecuteReader(CommandBehavior.CloseConnection) Catch e As Exception LogError(e) Throw New Exception(ExceptionMsg, e) End Try End Function
This function uses two objects to accomplish its mission: SqlCommand
and SqlDataReader
. The only part where this function differs from ExecSpReturnDataSet
is right after you call the SetParameterValues
private method. In this case, you have to ensure that the SqlCommand
object's SqlConnection
is opened because the SqlDataReader
requires an open connection. You then call the ExecuteReader
method of the SqlCommand
object to get your SqlDataReader
object, passing in the CommandBehavior.CloseConnection
value for the method's behavior argument.
Because this method returns a SqlDataReader
object, which requires an open database connection, you do not close the connection in this method. It is up to the caller to close the SqlDataReader
and the connection when finished. Because you used the CommandBehavior.CloseConnection
value for the behavior argument, the user of the method only has to remember to call the SqlDataReader
's Close
method in order to close the underlying SqlConnection
object.
The next function you are going to look at, ExecSpReturnXmlReader
, is almost identical to the last two functions, except that it returns an XmlReader
instead of a DataSet
or a SqlDataReader
.
This public function executes a stored procedure and returns an XmlReader
instance. The function requires the stored procedure to contain a FOR XML
clause in its SQL statement. Once again, it takes a stored procedure name (String
) and an optional list of parameter names and values (IDictionary
). Here is the code for ExecSpReturnXmlReader
:
`´` <summary> `´` Executes a stored procedure with or without parameters and returns an `´` XmlReader instance with a live connection to the database. It is
`´` very important to call the Close method of the XmlReader as soon `´` as possible after using it. Only use this method when calling stored `´` procedures that return XML results (FOR XML ...). `´` </summary> `´` <param name="spName">The name of the stored procedure to execute.</param> `´` <param name="paramValues">A name-value pair of stored procedure parameter `´` name(s) and value(s).</param> `´` <returns>An XmlReader object.</returns> Public Function ExecSpReturnXmlReader(ByVal spName As String, _ ByVal paramValues As IDictionary) As XmlReader Dim command As SqlCommand = Nothing Try ' Get the initialized SqlCommand instance. command = GetSqlCommand(spName) ' Set the parameter values for the SqlCommand. SetParameterValues(command, paramValues) ' Open the connection. command.Connection.Open() ' Execute the sp and return the XmlReader. Return command.ExecuteXmlReader() Catch e As Exception LogError(e) Throw New Exception(ExceptionMsg, e) End Try End Function
The only difference between this method and ExecSpReturnDataReader
is that you call the ExecuteXmlReader
method of the SqlCommand
object instead of the ExecuteReader
method. Like the ExecSpReturnDataReader
method, users of this method need to close the returned XmlReader
after using it in order to properly release resources.
This method works only with SQL Server 2000 and later.
Next up is the ExecSp
method, which needs only the SqlCommand
object to get its work done. Its job is to execute stored procedures that do not return result sets.
This public method executes a stored procedure and does not return a value. It takes a stored procedure name (String
) and an optional list of parameter names and values (IDictionary
) for its arguments. Here is the code for ExecSp
:
`´` <summary> `´` Executes a stored procedure with or without parameters that `´` does not return output values or a resultset. `´` </summary> `´` <param name="transaction">The transaction that the stored procedure `´` will be executed under.</param> `´` <param name="spName">The name of the stored procedure to execute.</param> `´` <param name="paramValues">A name-value pair of stored procedure parameter `´` name(s) and value(s).</param> Public Sub ExecSp(ByVal spName As String, ByVal paramValues As IDictionary)
Dim command As SqlCommand = Nothing Try ' Get the initialized SqlCommand instance. command = GetSqlCommand(transaction, spName) ' Set the parameter values for the SqlCommand. SetParameterValues(command, paramValues) ' Run the stored procedure. RunSp(command) Catch e As Exception LogError(e) Throw New Exception(ExceptionMsg, e) Finally ' Close and release resources. DisposeCommand(command) End Try End Sub
It is almost identical to the other Exec*
functions, except when it executes the stored procedure. The code inside of the private RunSp
method opens the SqlCommand
's SqlConnection
object and then calls the SqlCommand
object's ExecuteNonQuery
method. This ensures that the SqlCommand
does not return any type of DataReader
object to read the results. This method is used mostly to execute INSERT, UPDATE
, and DELETE
stored procedures that do not return any results. It also has an overload that does not include the SqlTransaction
argument.
Following is the code for RunSp
:
`´` <summary> `´` Opens the SqlCommand object's underlying SqlConnection and calls `´` the SqlCommand's ExecuteNonQuery method. `´` </summary> `´` <param name="command">An initialized SqlCommand object.</param> Private Sub RunSp(ByRef command As SqlCommand) ' Open the connection. command.Connection.Open() 'a Execute the stored procedure. command.ExecuteNonQuery() End Sub
Finally, the last public function you are going to create is ExecSpOutputValues
.
This last public function in your component executes a stored procedure and returns an IDictionary
object that contains output parameter name-value pairs. It is not meant for stored procedures that return result sets. As with the previous examples, this function takes a stored procedure name (String
) and an optional list of parameter names and values (IDictionary
) for its arguments. Here is the code for ExecSpOutputValues
:
`´` <summary> `´` Executes a stored procedure with or without parameters and returns an
`´` IDictionary instance with the stored procedure's output parameter `´` name(s) and value(s). `´` </summary> `´` <param name="transaction">The transaction that the stored procedure `´` will be executed under.</param> `´` <param name="spName">The name of the stored procedure to execute.</param> `´` <param name="paramValues">A name-value pair of stored procedure parameter `´` name(s) and value(s).</param> `´` <returns>An IDictionary object.</returns> Public Function ExecSpOutputValues(ByVal transaction As SqlTransaction, _ ByVal spName As String, _ ByVal paramValues As IDictionary) As IDictionary Dim command As SqlCommand = Nothing Try ' Get the initialized SqlCommand instance. command = GetSqlCommand(transaction, spName) ' Set the parameter values for the SqlCommand. SetParameterValues(command, paramValues) ' Run the stored procedure. RunSp(command) ' Get the output values. Dim outputParams As New Hashtable() Dim param As SqlParameter For Each param In command.Parameters If param.Direction = ParameterDirection.Output _ Or param.Direction = ParameterDirection.InputOutput Then outputParams.Add(param.ParameterName, param.Value) End If Next param Return outputParams Catch e As Exception LogError(e) Throw New Exception(ExceptionMsg, e) Finally ' Close and release resources. DisposeCommand(command) End Try End Function
This function is almost identical to ExecSp
except that after the SqlCommand.ExecuteNonQuery
method is called, you iterate through the SqlCommand
object's Parameters
collection and look for all of the parameters that are output parameters. Next, you take the values of the output parameters and add the name-value pair to the IDictionary
instance that you return. This method also has an overload that does not include the SqlTransaction
argument.
Now that you have built your data-access component, it is time to test it. A nice way to do that is to call the ExecSpReturnDataSet
method, take the DataSet
object that was created, and then bind the DataSet
to a DataGrid
. You also get to see how easily the DataSet
and the DataGrid
control integrate together.
This exercise uses a Windows Application project called SqlServerWrapperTestHarness, added to the Examples solution. It contains references to System, System.Data, System.Drawing, System.Windows.Forms
, and System.Xml
, as well as a project reference to the SqlServerWrapper project. Added to the project is a form named TestForm.vb
with two buttons, one for testing the ExecSpReturnDataSet
method and one for testing the ExecSpReturnSqlRecord
method. In this example, you will be looking only at the code for testing the ExecSpReturnDataSet
method. Figure 9-4 shows what the test form looks like.
Figure 9-5 shows what your references should look like.
Here is the code for the declarations and private members of the form:
Option Explicit On Option Strict On Imports SqlServerWrapper Imports System.Data.SqlClient Imports System.Xml Imports System.Configuration Public Class TestForm Inherits System.Windows.Forms.Form Private _helper As StoredProcedureHelper = Nothing
These declarations should look pretty familiar by now. Note that you are declaring a private variable (_helper
) for the StoredProcedureHelper
class that you are using so you can get to the class from other parts of the form instead of just a Button Click
event handler.
Next, you initialize the _helper
variable in the form's Load
event handler:
Private Sub TestForm_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load ' Set the SQL connection string Dim connectionString As String = _ ConfigurationSettings.AppSettings("dbConnectionString") ' Call the SqlServer wrapper constructor and ' pass the DB connection string and the stored procedures config. helper = New StoredProcedureHelper(connectionString, _ CType(ConfigurationSettings.GetConfig("StoredProcedureSettings"), _ XmlNode)) End Sub
As in the earlier examples, this code begins by retrieving a connection string to the pubs database from the app.config
file. You then create a new instance of the StoredProcedureHelper
and assign it to the _helper
class variable. During the constructor call to the StoredProcedureHelper
class, you first pass in your connection string, and then you pass in an XmlNode
of the stored procedure metadata for the StoredProcedureHelper
class to consume. Note that you are passing the stored procedure metadata in to your class via the GetConfig
method of the ConfigurationSettings
class. This is because you have created a section inside of your app.config
file called StoredProcedureSettings
, and you have configured a SectionHandler
to let the .NET Framework application configuration functionality consume your XML and give it back to you as an XmlNode
. Here is what this section looks like inside of the app.config
file:
<configSections> <section name="StoredProcedureSettings" type="SqlServerWrapper.StoredProcedureSectionHandler, SqlServerWrapper" />
</configSections> <StoredProcedureSettings> <StoredProcedures> <StoredProcedure name="usp_Get_Authors_By_States"> <Parameters> <Parameter name="@states" datatype="VarChar" direction="Input" isNullabel="false" size="100" /> <Parameter name="@state_delimiter" datatype="Char" direction="Input" isNullabel="false" size="1" /> </Parameters> </StoredProcedure> <StoredProcedure name="usp_Get_Author_By_ID"> <Parameters> <Parameter name="@au_id" datatype="VarChar" direction="Input" isNullabel="false" size="11" /> </Parameters> </StoredProcedure> </StoredProcedures> </StoredProcedureSettings>
This is nice because you don't need to include a separate XML file for your project; you just integrate seamlessly into the app.config
file. Note how you are defining what class in what assembly will handle consuming your <StoredProcedureSettings>
section in the <section>
element. In order for this to work, the class defined must implement the System.Configuration.IConfigurationSectionHandler
interface. Here is the code for your section handler:
Option Explicit On Option Strict On Imports System Imports System.Configuration Imports System.Xml Imports System.Xml.Serialization Imports System.Xml.XPath Public Class StoredProcedureSectionHandler Implements IConfigurationSectionHandler Public Function Create(ByVal parent As Object, _ ByVal configContext As Object, _ ByVal section As System.Xml.XmlNode) As Object _ Implements IConfigurationSectionHandler.Create Return section("StoredProcedures") End Function End Class
This code is pretty simple; you just return the XML node named StoredProcedures
to the caller of your handler.
Back to your Button
's Click
event handler, once you have the StoredProcedureHelper
class instance fully initialized, you then create the parameter values for the stored procedure you want to execute and pass these arguments to the ExecSpReturnDataSet
method:
' Add the two parameter name-values. Dim params As New Hashtable
params.Add("@states", "CA") params.Add("@state_delimiter", "^") ' Execute the sp, and get the DataSet object back. Dim ds As DataSet = _helper.ExecSpReturnDataSet("usp_Get_Authors_By_States", _ "", params)
The last step is to actually bind the data to the form's grid:
' Bind the DataGrid to the DataSet object. dgdAuthors.SetDataBinding(ds.Tables(0), Nothing)
The results should look like the dialog shown in Figure 9-6.
This chapter took a look at ADO.NET and some of its more advanced features. You have seen and used the main objects in ADO.NET that you need to quickly get up and running in order to build data-access into your .NET applications. You took a fairly in-depth look at the DataSet
and DataTable
classes, as these are the core classes of ADO.NET.
You also looked at stored procedures, including how to create them in SQL Server and how to access them from your code. Finally, you built your own custom data-access component, which makes it easy to call stored procedures, and separate data-access code from the rest of business-logic code in a .NET application.