Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Pro CSharp 2008 And The .NET 3.5 Platform [eng]

.pdf
Скачиваний:
78
Добавлен:
16.08.2013
Размер:
22.5 Mб
Скачать

762 CHAPTER 22 ADO.NET PART I: THE CONNECTED LAYER

Figure 22-15. Fun with data reader objects

Obtaining Multiple Result Sets Using a Data Reader

Data reader objects are able to obtain multiple result sets using 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 strSQL = "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())

{

Console.WriteLine("***** Record *****");

for (int i = 0; i < myDataReader.FieldCount; i++)

{

Console.WriteLine("{0} = {1}", myDataReader.GetName(i), myDataReader.GetValue(i).ToString().Trim());

}

Console.WriteLine();

}

} while (myDataReader.NextResult());

So, at this point, you should be more aware of the functionality data reader objects bring to the table. Always remember that a data reader can only process SQL Select statements and cannot be used to modify an existing database table via Insert, Update, or Delete requests. To understand how to modify an existing database requires a further investigation of command objects.

CHAPTER 22 ADO.NET PART I: THE CONNECTED LAYER

763

Source Code The AutoLotDataReader project is included under the Chapter 22 subdirectory.

Building a Reusable Data Access Library

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 statements 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.

Note Technically speaking, a nonquery is a SQL statement that does not return a result set. Thus, Select statements are queries, while Insert, Update, and Delete statements are not. Given this, ExecuteNonQuery() returns an int that represents the number of rows affected, not a new set of records.

To illustrate how to modify an existing database using nothing more than a call to ExecuteNonQuery(), your next goal is to build a custom data access library that will encapsulate the process of operating upon the AutoLot database. In a production-level environment, your ADO.NET logic will almost always be isolated to a .NET *.dll assembly for one simple reason: code reuse! The first examples of this chapter have not done so just to keep focused on the task at hand; however, as you might imagine, it is would be a waste of time to author the same connection logic, the same data reading logic, and the same command logic for every application that needs to interact with the AutoLot database.

By isolating data access logic to a .NET code library, multiple applications using any sort of front end (console based, desktop based, web based, etc.) can reference the library at hand in a language-independent manner. Thus, if you author your data library using C#, other .NET programmers would be able to build a UI in his or her language of choice (VB, C++/CLI, etc.).

In this chapter, our data library (AutoLotDAL.dll) will contain a single namespace (AutoLotConnectedLayer) that interacts with AutoLot using the connected types of ADO.NET. The next chapter will add a new namespace (AutoLotDisconnectionLayer) to this same *.dll that contains types to communicate with AutoLot using the disconnected layer. This library will then be used by numerous applications over the remainder of the text.

To begin, create a new C# Class Library project named AutoLotDAL (short for AutoLot Data Access Layer) and rename your initial C# code file to AutoLotConnDAL.cs. Next, rename your namespace scope to AutoLotConnectedLayer and change the name of your initial class to InventoryDAL, as this class will define various members to interact with the Inventory table of the AutoLot database. Finally, import the following .NET namespaces:

using System;

using System.Collections.Generic; using System.Text;

//We will make use of the SQL server

//provider; however, it would also be

//permissible to make use of the ADO.NET

//factory pattern for greater flexibility. using System.Data;

using System.Data.SqlClient;

764CHAPTER 22 ADO.NET PART I: THE CONNECTED LAYER

namespace AutoLotConnectedLayer

{

public class InventoryDAL

{

}

}

Note Recall from Chapter 8 that when objects make use of types managing raw resources (such as a database connection), it is a good practice to implement IDisposable and author a proper finalizer. In a production environment, classes such as InventoryDAL would do the same; however, I’ll avoid doing so to stay focused on the particulars of ADO.NET.

Adding the Connection Logic

The first task we must attend to is to define some methods that allow the caller to connect to and disconnect from the data source using a valid connection string. Because our AutoLotDAL.dll assembly will be hard-coded to make use of the types of System.Data.SqlClient, define a private member variable of SqlConnection that is allocated at the time the InventoryDAL object is created. As well, define a method named OpenConnection() and another named CloseConnection() that interact with this member variable as follows:

public class InventoryDAL

{

// This member will be used by all methods. private SqlConnection sqlCn = new SqlConnection();

public void OpenConnection(string connectionString)

{

sqlCn.ConnectionString = connectionString; sqlCn.Open();

}

public void CloseConnection()

{

sqlCn.Close();

}

}

For the sake of brevity, our InventoryDAL type will not test for possible exceptions nor will it throw custom exceptions under various circumstances (such as a malformed connection string). If you were to build an industrial-strength data access library, you would most certainly want to make use of structured exception handling techniques to account for any runtime anomalies.

Adding the Insertion Logic

Inserting a new record into the Inventory table is as simple as formatting the SQL Insert statement (based on user input) and calling the ExecuteNonQuery() using your command object. To illustrate, add a public method to your InventoryDAL type named InsertAuto() that takes four parameters which map to the four columns of the Inventory table (CarID, Color, Make, and PetName). Using the arguments, format a string type to insert the new record. Finally, using your SqlConnection object, execute the SQL statement:

CHAPTER 22 ADO.NET PART I: THE CONNECTED LAYER

765

public void InsertAuto(int id, string color, string make, string petName)

{

// Format and execute SQL statement.

string sql = string.Format("Insert Into Inventory" + "(CarID, Make, Color, PetName) Values" +

"('{0}', '{1}', '{2}', '{3}')", id, make, color, petName);

// Execute using our connection.

using(SqlCommand cmd = new SqlCommand(sql, this.sqlCn))

{

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). The preferred way to build command text is using a parameterized query, which I describe shortly.

Adding the Deletion Logic

Deleting an existing record is just as simple as inserting a new record. Unlike the code listing for InsertAuto(), 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. Add the following method to the InventoryDAL class type:

public void DeleteCar(int id)

{

// Get ID of car to delete, then do so.

string sql = string.Format("Delete from Inventory where CarID = '{0}'", id);

using(SqlCommand cmd = new SqlCommand(sql, this.sqlCn))

{

try

{

cmd.ExecuteNonQuery();

}

catch(SqlException ex)

{

Exception error = new Exception("Sorry! That car is on order!", ex); throw error;

}

}

}

Adding the Updating Logic

When it comes to the act of updating an existing record in the Inventory table, the first obvious question is what exactly do we wish to allow the caller to change? The car’s color? The pet name or make? All of the above? Of course one way to allow the caller complete flexibility is to simply define a method that takes a string type to represent any sort of SQL statement, but that is risky at best. Ideally, we would have a set of methods that allow the caller to update a record in a variety of

766CHAPTER 22 ADO.NET PART I: THE CONNECTED LAYER

manners. However, for our simple data access library, we will define a single method that allows the caller to update the pet name of a given automobile:

public void UpdateCarPetName(int id, string newPetName)

{

// Get ID of car to modify and new pet name. string sql =

string.Format("Update Inventory Set PetName = '{0}' Where CarID = '{1}'", newPetName, id);

using(SqlCommand cmd = new SqlCommand(sql, this.sqlCn))

{

cmd.ExecuteNonQuery();

}

}

Adding the Selection Logic

The next method to add is a selection method. As seen earlier in this chapter, a data provider’s data reader object allows for a selection of records using a read-only, forward-only server-side cursor. As you call the Read() method, you are able to process each record in a fitting manner. While this is all well and good, we need to contend with the issue of how to return these records to the calling tier of our application.

One approach would be to populate and return a multidimensional array (or other such object, like a generic List<T>) with the data obtained by the Read() method. Another approach (which we will opt for in the current example) is to return a System.Data.DataTable object, which is actually part of the disconnected layer of ADO.NET.

Full coverage of the DataTable type can be found in the next chapter; however, for the time being, simply understand that a DataTable is a class type that represents a tabular block of data (like a grid on a spreadsheet). To do so, the DataTable type maintains a collection of rows and columns. While these collections can be filled programmatically, the DataTable type provides a method named Load(), which will automatically populate these collections using a data reader object! Consider the following:

public DataTable GetAllInventory()

{

//This will hold the records.

DataTable inv = new DataTable();

//Prep command object.

string sql = "Select * From Inventory"; using(SqlCommand cmd = new SqlCommand(sql, this.sqlCn))

{

SqlDataReader dr = cmd.ExecuteReader();

// Fill the DataTable with data from the reader and clean up. inv.Load(dr);

dr.Close();

}

return inv;

}

Working with Parameterized Command Objects

Currently, the insert, update, and delete logic for the InventoryDAL type uses hard-coded string literals for each SQL query. As you may know, a parameterized query can be used to treat SQL

CHAPTER 22 ADO.NET PART I: THE CONNECTED LAYER

767

parameters as objects, rather than a simple blob of text. Treating SQL queries in a more objectoriented manner not only helps reduce the number of typos (given strongly typed properties), but parameterized queries typically 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). Furthermore, parameterized queries also help protect against SQL injection attacks (a well-known data access security issue).

To support parameterized queries, ADO.NET command objects maintain a collection of individual parameter objects. 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 the @ 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-7 describes some key properties of the

DbParameter type.

Table 22-7. 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 (only truly useful for

 

textual data)

Value

Gets or sets the value of the parameter

 

 

To illustrate how to populate a command object’s collection of DBParameter-compatible objects, let’s rework the previous InsertAuto() method to make use of parameter objects (a similar reworking could also be performed for your remaining SQL-centric methods; however, it is not necessary for this example):

public void InsertAuto(int id, string color, string make, string petName)

{

// Note the "placeholders" in the SQL query.

string sql = string.Format("Insert Into Inventory" + "(CarID, Make, Color, PetName) Values" + "(@CarID, @Make, @Color, @PetName)");

// This command will have internal parameters. using(SqlCommand cmd = new SqlCommand(sql, this.sqlCn))

{

// Fill params collection.

SqlParameter param = new SqlParameter(); param.ParameterName = "@CarID";

768 CHAPTER 22 ADO.NET PART I: THE CONNECTED LAYER

param.Value = id; param.SqlDbType = SqlDbType.Int; cmd.Parameters.Add(param);

param = new SqlParameter(); param.ParameterName = "@Make"; param.Value = make; param.SqlDbType = SqlDbType.Char; param.Size = 10; cmd.Parameters.Add(param);

param = new SqlParameter(); param.ParameterName = "@Color"; param.Value = color; param.SqlDbType = SqlDbType.Char; param.Size = 10; cmd.Parameters.Add(param);

param = new SqlParameter(); param.ParameterName = "@PetName"; param.Value = petName; param.SqlDbType = SqlDbType.Char; param.Size = 10; cmd.Parameters.Add(param);

cmd.ExecuteNonQuery();

}

}

Again, notice that our SQL query consists of four embedded placeholder symbols, each of which is prefixed with the @ token. Using the SqlParameter type, we are able to map each placeholder using the ParameterName property and specify various details (its value, data type, size, etc.) in a strongly typed matter. Once each parameter object is hydrated, it is added to the command object’s collection via a call to Add().

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). Also be aware that Visual Studio 2008 provides numerous graphical designers that will generate a good deal of this grungy parameter-centric code on your behalf (see Chapter 23).

While building a parameterized query often 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.

Executing a Stored Procedure

Recall that 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. Currently our AutoLot database defines a single stored procedure named GetPetName, which was formatted as follows:

CHAPTER 22 ADO.NET PART I: THE CONNECTED LAYER

769

CREATE PROCEDURE GetPetName @carID int,

@petName char(10) output AS

SELECT @petName = PetName from Inventory where CarID = @carID

Now, consider the following final method of the InventoryDAL type, which invokes our stored procedure:

public string LookUpPetName(int carID)

{

string carPetName = string.Empty;

// Establish name of stored proc.

using (SqlCommand cmd = new SqlCommand("GetPetName", this.sqlCn))

{

cmd.CommandType = CommandType.StoredProcedure;

// Input param.

SqlParameter param = new SqlParameter(); param.ParameterName = "@carID"; param.SqlDbType = SqlDbType.Int; param.Value = carID;

param.Direction = ParameterDirection.Input; cmd.Parameters.Add(param);

// Output param.

param = new SqlParameter(); param.ParameterName = "@petName"; param.SqlDbType = SqlDbType.Char; param.Size = 10;

param.Direction = ParameterDirection.Output; cmd.Parameters.Add(param);

//Execute the stored proc. cmd.ExecuteNonQuery();

//Return output param.

carPetName = ((string)cmd.Parameters["@petName"].Value).Trim();

}

return carPetName;

}

The first important aspect of invoking a stored procedure is to recall that a command object can represent a SQL statement (the default) or the name of a stored procedure. When you wish to inform a command object that it will be invoking a stored procedure, you pass in the name of the procedure (as a constructor argument or via the CommandText property) and must set the CommandType property to the value CommandType.StoredProcedure (if you fail to do so, you will receive a runtime exception, as the command object is expecting a SQL statement by default):

SqlCommand cmd = new SqlCommand("GetPetName", this.sqlCn); cmd.CommandType = CommandType.StoredProcedure;

Next, notice that the Direction property of a parameter object allows you to specify the direction of travel for each parameter passed to the stored procedure (e.g., input parameters and the output parameter). As before, each parameter object is added to the command object’s parameters collection:

770 CHAPTER 22 ADO.NET PART I: THE CONNECTED LAYER

// Input param.

SqlParameter param = new SqlParameter(); param.ParameterName = "@carID"; param.SqlDbType = SqlDbType.Int; param.Value = carID;

param.Direction = ParameterDirection.Input; cmd.Parameters.Add(param);

Finally, once the stored procedure completes via a call to ExecuteNonQuery(), you are able to obtain the value of the output parameter by investigating the command object’s parameter collection and casting accordingly:

// Return output param.

carPetName = (string)cmd.Parameters["@petName"].Value;

Source Code The AutoLotDAL project is included under the Chapter 22 subdirectory.

Creating a Console UI–Based Front End

At this point, our first iteration of the AutoLotDAL.dll data access library is complete. Using this assembly, we can build any sort of front end to display and edit our data (console based, Windows Forms based, Windows Presentation Foundation applications, or an HTML-based web application). Given that we have not yet examined how to build graphical user interfaces, we will test our data library from a new Console Application named AutoLotCUIClient. Once you create your new project, be sure to add a reference to your AutoLotDAL.dll assembly as well as System.Configuration. dll and update your using statements as follows:

using AutoLotConnectedLayer; using System.Configuration; using System.Data;

Next, insert a new App.config file into your project that contains a <connectionString> element used to connect to your instance of the AutoLot database, for example:

<configuration>

<connectionStrings>

<add name ="AutoLotSqlProvider" connectionString = "Data Source=(local)\SQLEXPRESS;" +

"Integrated Security=SSPI;Initial Catalog=AutoLot"/> </connectionStrings>

</configuration>

Implementing the Main() Method

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

CHAPTER 22 ADO.NET PART I: THE CONNECTED LAYER

771

L: Displays the current inventory using a data reader

S: Shows these options to the user

P: Look up pet name from car ID

Q: Quits the program

Each possible option is handled by a unique static method within the Program class. Here is the complete implementation of Main(). Notice that each method invoked from the do/while loop (with the exception of the ShowInstructions() method) takes an InventoryDAL object as its sole parameter:

static void Main(string[] args)

{

Console.WriteLine("***** The AutoLot Console UI *****\n");

//Get connection string from App.config. string cnStr =

ConfigurationManager.ConnectionStrings["AutoLotSqlProvider"].ConnectionString; bool userDone = false;

string userCommand = "";

//Create our InventoryDAL object.

InventoryDAL invDAL = new InventoryDAL(); invDAL.OpenConnection(cnStr);

// Keep asking for input until user presses the Q key. try

{

ShowInstructions(); do

{

Console.Write("Please enter your command: "); userCommand = Console.ReadLine(); Console.WriteLine();

switch (userCommand.ToUpper())

{

case "I": InsertNewCar(invDAL); break;

case "U": UpdateCarPetName(invDAL); break;

case "D": DeleteCar(invDAL); break;

case "L": ListInventory(invDAL); break;

case "S": ShowInstructions(); break;

case "P": LookUpPetName(invDAL); break;

case "Q": userDone = true; break;

default: