
Pro CSharp And The .NET 2.0 Platform (2005) [eng]
.pdf
784 CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET
Figure 22-7. Fun with data reader objects
Obtaining Multiple Result Sets Using a Data Reader
Data reader objects are able to obtain multiple result sets from a single command object. For example, if you are interested in obtaining all rows from the Inventory table as well as all rows from the Customers table, you are able to specify both SQL select statements using a semicolon delimiter:
string theSQL = "Select * From Inventory;Select * from Customers";
Once you obtain the data reader, you are able to iterate over each result set via the NextResult() method. Do be aware that you are always returned the first result set automatically. Thus, if you wish to read over the rows of each table, you will be able to build the following iteration construct:
do
{
while(myDataReader.Read())
{
// Read the info of the current result set.
}
}while(myDataReader.NextResult());
So, at this point, you should be more aware of the functionality data reader objects bring to the table. While these objects provide additional bits of functionality than I have shown here (such as the ability to execute scalars and single-row queries), I’ll leave it to interested readers to consult the
.NET Framework 2.0 SDK documentation for complete details.
■Source Code The CarsDataReader project is included under the Chapter 22 subdirectory.
Modifying Tables Using Command Objects
As you have just seen, the ExecuteReader() method extracts a data reader object that allows you to examine the results of a SQL Select statement using a forward-only, read-only flow of information. However, when you wish to submit SQL commands that result in the modification of a given table, you will call the ExecuteNonQuery() method of your command object. This single method will perform inserts, updates, and deletes based on the format of your command text.

CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET |
785 |
To illustrate how to modify an existing database using nothing more than a call to ExecuteNonQuery(), you will now build a new console application (CarsInventoryUpdater) that allows the caller to modify the Inventory table of the Cars database. Like in other examples in this text, the Main() method is responsible for prompting the user for a specific course of action and executing that request via a switch statement. This program will allow the user to enter the following commands:
•I: Inserts a new record into the Inventory table
•U: Updates an existing record in the Inventory table
•D: Deletes an existing record from the Inventory table
•L: Displays the current inventory using a data reader
•S: Shows these options to the user
•Q: Quits the program
Each possible option is handled by a unique static method within the Program class. For the purpose of completion, here is the implementation of Main(), which I assume requires no further comment:
static void Main(string[] args)
{
Console.WriteLine("***** Car Inventory Updater *****");
bool userDone = false; string userCommand = "";
SqlConnection cn = new SqlConnection(); cn.ConnectionString =
"uid=sa;pwd=;Initial Catalog=Cars;" + "Data Source=(local);Connect Timeout=30";
cn.Open();
ShowInstructions(); do
{
Console.Write("Please enter your command: "); userCommand = Console.ReadLine(); Console.WriteLine();
switch (userCommand.ToUpper())
{
case "I": InsertNewCar(cn); break;
case "U": UpdateCarPetName(cn); break;
case "D": DeleteCar(cn); break;
case "L": ListInventory(cn); break;
case "S": ShowInstructions(); break;
case "Q":
userDone = true; break;

786 CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET
default:
Console.WriteLine("Bad data! Try again"); break;
}
} while (!userDone); cn.Close();
}
The ShowInstructions() method does what you would expect:
private static void ShowInstructions()
{
Console.WriteLine();
Console.WriteLine("I: Inserts a new car."); Console.WriteLine("U: Updated an existing car."); Console.WriteLine("D: Deletes an existing car."); Console.WriteLine("L: List current inventory."); Console.WriteLine("S: Show these instructions."); Console.WriteLine("Q: Quits program.");
}
As mentioned, ListInventory() prints out the current rows of the Inventory table using a data reader object (the code is identical to the previous CarsDataReader example):
private static void ListInventory(SqlConnection cn)
{
string strSQL = "Select * From Inventory"; SqlCommand myCommand = new SqlCommand(strSQL, cn); SqlDataReader myDataReader;
myDataReader = myCommand.ExecuteReader(); while (myDataReader.Read())
{
for (int i = 0; i < myDataReader.FieldCount; i++)
{
Console.Write("{0} = {1} ", myDataReader.GetName(i), myDataReader.GetValue(i).ToString().Trim());
}
Console.WriteLine();
}
myDataReader.Close();
}
Now that the CUI is in place, let’s move on to the good stuff.
Inserting New Records
Inserting a new record into the Inventory table is as simple as formatting the SQL insert statement (based on user input) and calling ExecuteNonQuery(). To keep the code crisp, I have deleted the necessary try/catch logic that is present in the code download for this text:
private static void InsertNewCar(SqlConnection cn)
{
// Gather info about new car.
Console.Write("Enter CarID: ");
int newCarID = int.Parse(Console.ReadLine()); Console.Write("Enter Make: ");
string newCarMake = Console.ReadLine(); Console.Write("Enter Color: ");

CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET |
787 |
string newCarColor = Console.ReadLine(); Console.Write("Enter PetName: ");
string newCarPetName = Console.ReadLine();
// Format and execute SQL statement.
string sql = string.Format("Insert Into Inventory" + "(CarID, Make, Color, PetName) Values" +
"('{0}', '{1}', '{2}', '{3}')", newCarID, newCarMake, newCarColor, newCarPetName);
SqlCommand cmd = new SqlCommand(sql, cn); cmd.ExecuteNonQuery();
}
■Note As you may know, building a SQL statement using string concatenation can be risky from a security point of view (think: SQL injection attacks). While I use this approach during this chapter for purposes of brevity, the preferred way to build command text is using a parameterized query, which I describe shortly.
Deleting Existing Records
Deleting an existing record is just as simple as inserting a new record. Unlike the code listing for InsertNewCar(), I will show one important try/catch scope that handles the possibility of attempting to delete a car that is currently on order for an individual in the Customers table (which will be used later in this chapter):
private static void DeleteCar(SqlConnection cn)
{
// Get ID of car to delete, then do so.
Console.Write("Enter CarID of car to delete: "); int carToDelete = int.Parse(Console.ReadLine());
string sql = string.Format("Delete from Inventory where CarID = '{0}'", carToDelete);
SqlCommand cmd = new SqlCommand(sql, cn); try { cmd.ExecuteNonQuery(); }
catch { Console.WriteLine("Sorry! That car is on order!"); }
}
Updating Existing Records
If you followed the code behind DeleteCar() and InsertNewCar(), then UpdateCarPetName() is a nobrainer (again, try/catch logic has been removed for clarity):
private static void UpdateCarPetName(SqlConnection cn)
{
//Get ID of car to modify and new pet name.
Console.Write("Enter CarID of car to modify: "); string newPetName = "";
int carToUpdate = carToUpdate = int.Parse(Console.ReadLine()); Console.Write("Enter new pet name: ");
newPetName = Console.ReadLine();
//Now update record.
string sql =
string.Format("Update Inventory Set PetName = '{0}' Where CarID = '{1}'",

788 CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET
newPetName, carToUpdate);
SqlCommand cmd = new SqlCommand(sql, cn); cmd.ExecuteNonQuery();
}
With this, our application is finished. Figure 22-8 shows a test run.
Figure 22-8. Inserting, updating, and deleting records via command objects
Working with Parameterized Command Objects
The previous insert, update, and delete logic works as expected; however, note that each of your SQL queries is represented using hard-coded string literals. As you may know, a parameterized query can be used to treat SQL parameters as objects, rather than a simple blob of text. Typically, parameterized queries execute much faster than a literal SQL string, in that they are parsed exactly once (rather than each time the SQL string is assigned to the CommandText property). As well, parameterized queries also help protect against SQL injection attacks (a well-known data access security issue).
ADO.NET command objects maintain a collection of discrete parameter types. By default this collection is empty, but you are free to insert any number of parameter objects that map to a “placeholder parameter” in the SQL query. When you wish to associate a parameter within a SQL query to a member in the command object’s parameters collection, prefix the SQL text parameter with an at
(@) symbol (at least when using Microsoft SQL Server; not all DBMSs support this notation).
Specifying Parameters Using the DbParameter Type
Before you build a parameterized query, let’s get to know the DbParameter type (which is the base class to a provider’s specific parameter object). This class maintains a number of properties that allow you to configure the name, size, and data type of the parameter, as well as other characteristics such as the parameter’s direction of travel. Table 22-8 describes some key properties of the
DbParameter type.

CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET |
789 |
Table 22-8. Key Members of the DbParameter Type
Property |
Meaning in Life |
DbType |
Gets or sets the native data type from the data source, represented as a CLR |
|
data type |
Direction |
Gets or sets whether the parameter is input-only, output-only, bidirectional, or |
|
a return value parameter |
IsNullable |
Gets or sets whether the parameter accepts null values |
ParameterName |
Gets or sets the name of the DbParameter |
Size |
Gets or sets the maximum parameter size of the data |
Value |
Gets or sets the value of the parameter |
|
|
To illustrate, let’s rework the previous InsertNewCar() method to make use of parameter objects. Here is the relevant code:
private static void InsertNewCar(SqlConnection cn)
{
...
// Note the 'placeholders' in the SQL query.
string sql = string.Format("Insert Into Inventory" + "(CarID, Make, Color, PetName) Values" + "(@CarID, @Make, @Color, @PetName)");
// Fill params collection.
SqlCommand cmd = new SqlCommand(sql, cn); SqlParameter param = new SqlParameter(); param.ParameterName = "@CarID"; param.Value = newCarID;
param.SqlDbType = SqlDbType.Int; cmd.Parameters.Add(param);
param = new SqlParameter(); param.ParameterName = "@Make"; param.Value = newCarMake; param.SqlDbType = SqlDbType.Char; param.Size = 20; cmd.Parameters.Add(param);
param = new SqlParameter(); param.ParameterName = "@Color"; param.Value = newCarColor; param.SqlDbType = SqlDbType.Char; param.Size = 20; cmd.Parameters.Add(param);
param = new SqlParameter(); param.ParameterName = "@PetName"; param.Value = newCarPetName; param.SqlDbType = SqlDbType.Char; param.Size = 20; cmd.Parameters.Add(param); cmd.ExecuteNonQuery();
}

790 CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET
While building a parameterized query requires a larger amount of code, the end result is a more convenient way to tweak SQL statements programmatically as well as better overall performance. While you are free to make use of this technique whenever a SQL query is involved, parameterized queries are most helpful when you wish to trigger a stored procedure.
■Note Here, I made use of various properties to establish a parameter object. Do know, however, that parameter objects support a number of overloaded constructors that allow you to set the values of various properties (which will result in a more compact code base).
Executing a Stored Procedure Using DbCommand
A stored procedure is a named block of SQL code stored in the database. Stored procedures can be constructed to return a set of rows or scalar data types and may take any number of optional parameters. The end result is a unit of work that behaves like a typical function, with the obvious difference of being located on a data store rather than a binary business object.
■Note Although I don’t cover this topic in this chapter, it is worth pointing out that the newest version of Microsoft SQL Server (2005) is a CLR host! Therefore, stored procedures (and other database atoms) can be authored using managed languages (such as C#) rather than traditional SQL. Consult http://www.microsoft.com/sql/2005 for further details.
To illustrate the process, let’s add a new option to the CarInventoryUpdate program that allows the caller to look up a car’s pet name via the GetPetName stored procedure. This database object was established when you installed the Cars database and looks like this:
CREATE PROCEDURE GetPetName @carID int,
@petName char(20) output AS
SELECT @petName = PetName from Inventory where CarID = @carID
First, update the current switch statement in Main() to handle a new case for “P” that calls
a new helper function named LookUpPetName() that takes a SqlConnection parameter and returns void. Update your ShowInstructions() method to account for this new option.
When you wish to execute a stored procedure, you begin as always by creating a new connection object, configuring your connection string, and opening the session. However, when you create your command object, the CommandText property is set to the name of the stored procedure (rather than a SQL query). As well, you must be sure to set the CommandType property to CommandType. StoredProcedure (the default is CommandType.Text).
Given that this stored procedure has one input and one output parameter, your goal is to build a command object that contains two SqlParameter objects within its parameter collection:
private static void LookUpPetName(SqlConnection cn)
{
//Get the CarID.
Console.Write("Enter CarID: ");
int carID = int.Parse(Console.ReadLine());
//Establish name of stored proc.
SqlCommand cmd = new SqlCommand("GetPetName", cn); cmd.CommandType = CommandType.StoredProcedure;


792 CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET
Asynchronous Data Access Under .NET 2.0
As of .NET 2.0, the SQL data provider (represented by the System.Data.SqlClient namespace) has been enhanced to support asynchronous database interactions via the following new members of
SqlCommand:
•BeginExecuteReader()/EndExecuteReader()
•BeginExecuteNonQuery()/EndExecuteNonQuery()
•BeginExecuteXmlReader()/EndExecuteXmlReader()
Given your work in Chapter 14, the naming convention of these method pairs may ring a bell. Recall that the .NET asynchronous delegate pattern makes use of a “begin” method to execute a task on a secondary thread, whereas the “end” method can be used to obtain the result of the asynchronous invocation using the members of IAsyncResult and the optional AsyncCallback delegate. Because the process of working with asynchronous commands is modeled after the standard delegate patterns, a simple example should suffice (so be sure to consult Chapter 14 for full details of asynchronous delegates).
Assume you wish to select the records from the Inventory table on a secondary thread of execution using a data reader object. Here is the complete Main() method, with analysis to follow:
static void Main(string[] args)
{
Console.WriteLine("***** Fun with ASNYC Data Readers *****\n");
//Create an open a connection that is async-aware.
SqlConnection cn = new SqlConnection(); cn.ConnectionString =
"uid=sa;pwd=;Initial Catalog=Cars;" + "Asynchronous Processing=true;Data Source=(local)";
cn.Open();
//Create a SQL command object that waits for approx 2 seconds. string strSQL = "WaitFor Delay '00:00:02';Select * From Inventory"; SqlCommand myCommand = new SqlCommand(strSQL, cn);
//Execute the reader on a second thread.
IAsyncResult itfAsynch;
itfAsynch = myCommand.BeginExecuteReader(CommandBehavior.CloseConnection);
//Do something while other thread works.
while (!itfAsynch.IsCompleted)
{
Console.WriteLine("Working on main thread..."); Thread.Sleep(1000);
}
Console.WriteLine();
// All done! Get reader and loop over results.
SqlDataReader myDataReader = myCommand.EndExecuteReader(itfAsynch); while (myDataReader.Read())
{
Console.WriteLine("-> Make: {0}, PetName: {1}, Color: {2}.", myDataReader["Make"].ToString().Trim(), myDataReader["PetName"].ToString().Trim(), myDataReader["Color"].ToString().Trim());

CHAPTER 22 ■ DATABASE ACCESS WITH ADO.NET |
793 |
}
myDataReader.Close();
}
The first point of interest is the fact that you need to enable asynchronous activity using the new Asynchronous Processing segment of the connection string. Also note that you have padded into the command text of your SqlCommand object a new WaitFor Delay segment simply to simulate a long-running database interaction.
Beyond these points, notice that the call to BeginExecuteDataReader() returns the expected IasyncResult-compatible type, which is used to synchronize the calling thread (via the IsCompleted property) as well as obtain the SqlDataReader once the query has finished executing.
■Source Code The AsyncCmdObject application is included under the Chapter 22 subdirectory.
Understanding the Disconnected Layer of ADO.NET
As you have seen, working with the connected layer allows you to interact with a database using connection, command, and data reader objects. With this small handful of types, you are able to select, insert, update, and delete records to your heart’s content (as well as trigger stored procedures). In reality, however, you have seen only half of the ADO.NET story. Recall that the ADO.NET object model can be used in a disconnected manner.
When you work with the disconnected layer of ADO.NET, you will still make use of connection and command objects. In addition, you will leverage a specific object named a data adapter (which extends the abstract DbDataAdapter) to fetch and update data. Unlike the connected layer, data obtained via a data adapter is not processed using data reader objects. Rather, data adapter objects make use of DataSet objects to move data between the caller and data source. The DataSet type is a container for any number of DataTable objects, each of which contains a collection of DataRow and DataColumn objects.
The data adapter object of your data provider handles the database connection automatically. In an attempt to increase scalability, data adapters keep the connection open for the shortest possible amount of time. Once the caller receives the DataSet object, he is completely disconnected from the DBMS and left with a local copy of the remote data. The caller is free to insert, delete, or update rows from a given DataTable, but the physical database is not updated until the caller explicitly passes the DataSet to the data adapter for updating. In a nutshell, DataSets allow the clients to pretend they are indeed always connected, when in fact they are operating on an in-memory database (see Figure 22-10).
Figure 22-10. Data adapter objects move DataSets to and from the client tier.
Given that the centerpiece of the disconnected layer is the DataSet type, your next task is to learn how to manipulate a DataSet manually. Once you understand how to do so, you will have no problem manipulating the contents of a DataSet retrieved from a data adapter object.