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

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

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

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

// Get a specific connection.

IDbConnection myCn = GetConnection(DataProvider.SqlServer); Console.WriteLine("Your connection is a {0}", myCn.GetType().Name);

// Open, use and close connection...

Console.ReadLine();

}

//This method returns a specific connection object

//based on the value of a DataProvider enum. static IDbConnection GetConnection(DataProvider dp)

{

IDbConnection conn = null; switch (dp)

{

case DataProvider.SqlServer: conn = new SqlConnection(); break;

case DataProvider.OleDb:

conn = new OleDbConnection(); break;

case DataProvider.Odbc:

conn = new OdbcConnection(); break;

case DataProvider.Oracle:

conn = new OracleConnection(); break;

}

return conn;

}

}

}

The benefit of working with the general interfaces of System.Data (or for that matter, the abstract base classes of System.Data.Common) is that you have a much better chance of building a flexible code base that can evolve over time. For example, perhaps today you are building an application targeting Microsoft SQL Server, but what if your company switches to Oracle months down the road? If you build a solution that hard-codes the MS SQL Server–specific types of System.Data. SqlClient, you will obviously need to edit, recompile, and redeploy the assembly should the backend database management system change.

Increasing Flexibility Using Application Configuration Files

To further increase the flexibility of your ADO.NET applications, you could incorporate a client-side *.config file that makes use of custom key/value pairs within the <appSettings> element. Recall from Chapter 15 that custom data stored within a *.config file can be programmatically obtained using types within the System.Configuration namespace. For example, assume you have specified a data provider value within a configuration file as follows:

<configuration>

<appSettings>

<!-- This key value maps to one of our enum values-->

<add key="provider" value="SqlServer"/> </appSettings>

</configuration>

CHAPTER 22 ADO.NET PART I: THE CONNECTED LAYER

743

With this, you could update Main() to programmatically obtain the underlying data provider. By doing so, you have essentially build a connection object factory that allows you to change the provider without requiring you to recompile your code base (simply change the *.config file). Here are the relevant updates to Main():

Note To use the ConfigurationManager type, be sure to set a reference to the System.Configuration. dll assembly and import the System.Configuration namespace.

static void Main(string[] args)

{

Console.WriteLine("**** Very Simple Connection Factory *****\n");

// Read the provider key.

string dataProvString = ConfigurationManager.AppSettings["provider"];

//Transform string to enum.

DataProvider dp = DataProvider.None; if(Enum.IsDefined(typeof(DataProvider), dataProvString))

dp = (DataProvider)Enum.Parse(typeof(DataProvider), dataProvString); else

Console.WriteLine("Sorry, no provider exists!");

//Get a specific connection.

IDbConnection myCn = GetConnection(dp); if(myCn != null)

Console.WriteLine("Your connection is a {0}", myCn.GetType().Name);

// Open, use, and close connection...

Console.ReadLine();

}

At this point we have authored some ADO.NET code that allows us to specify the underlying connection dynamically. One obvious problem, however, is that this abstraction is only used within the MyConnectionFactory.exe application. If we were to rework this example within a .NET code library (for example, MyConnectionFactory.dll), you would be able to build any number of clients that could obtain various connection objects using layers of abstraction.

However, obtaining a connection object is only one aspect of working with ADO.NET. To make a worthwhile data provider factory library, you would also have to account for command objects, data readers, data adapters, transaction objects, and other data-centric types. While building such a code library would not necessarily be difficult, it would require a good amount of code and a considerable amount of time.

Thankfully, since the release of .NET 2.0, the kind folks in Redmond have built this very functionality directly within the .NET base class libraries. We will examine this formal API in just a moment, but first we need to create custom database for use throughout this chapter, as well as many chapters to come.

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

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

Creating the AutoLot Database

As we work through this chapter, we will execute queries against a simple SQL Server test database named AutoLot. In keeping with the automotive theme used throughout this text, this database will contain three interrelated tables (Inventory, Orders, and Customers) that contain various bits of data representing order information for a fictional automobile sales company.

The assumption in this text is that you have a copy of Microsoft SQL Server (7.0 or higher) or a copy of Microsoft SQL Server 2005 Express Edition (http://msdn.microsoft.com/vstudio/express/ sql). This lightweight database server is perfect for our needs, in that (a) it is free, (b) it provides a GUI front end (the SQL Server Management Tool) to create and administer your databases, and (c) it integrates with Visual Studio 2008/Visual C# Express Edition.

To illustrate the last point, the remainder of this section will walk you through the construction of the AutoLot database using Visual Studio 2008. If you are using Visual C# Express, you can perform similar operations to what is explained here, using the Database Explorer window (which can be loaded from the View Other Windows menu option).

Note Do be aware that the AutoLot database will be used throughout the remainder of this text.

Creating the Inventory Table

To begin building our testing database, launch Visual Studio 2008 and open the Server Explorer perspective using the View menu of the IDE. Next, right-click the Data Connections node and select the Create New SQL Server Database menu option. Within the resulting dialog box, connect to the SQL Server installation on your local machine and specify AutoLot as the database name (Windows Authentication should be fine—see Figure 22-3).

Figure 22-3. Creating a new SQL Server 2005 Express database using Visual Studio 2008

CHAPTER 22 ADO.NET PART I: THE CONNECTED LAYER

745

Note Rather than specifying the name of your machine (such as INTERUBER in Figure 22-3), you can simply enter (local)\SQLEXPRESS in the Server name text box.

At this point, the AutoLot database is completely devoid of any database objects (tables, stored procedures, etc.). To insert the Inventory table, simply right-click the Tables node and select Add New Table (see Figure 22-4).

Figure 22-4. Adding the Inventory table

Using the table editor, add four data columns (CarID, Make, Color, and PetName). Ensure that the CarID column has been set to the Primary Key (via right-clicking the CarID row and selecting Set Primary Key). Figure 22-5 shows the final table settings.

Figure 22-5. Designing the Inventory table

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

Save (and then close) your new table and be sure you name this new database object as Inventory. At this point, you should see the Inventory table under the Tables node of the Server Explorer. Right-click the Inventory table icon and select Show Table Data. Enter a handful of new automobiles of your choosing (to make it interesting, be sure to have some cars that have identical colors and makes). Figure 22-6 shows one possible list of inventory.

Figure 22-6. Populating the Inventory table

Authoring the GetPetName() Stored Procedure

Later in this chapter in the section “Executing a Stored Procedure,” we will examine how to make use of ADO.NET to invoke stored procedures. As you may already know, stored procedures are routines stored within a particular database that operate often on table data to yield a return value. We will add a single stored procedure that will return an automobile’s pet name based on the supplied CarID value. To do so, simply right-click the Stored Procedures node of the AutoLot database within the Server Explorer and select Add New Stored Procedure. Enter the following within the resulting editor:

CREATE PROCEDURE GetPetName @carID int,

@petName char(10) output AS

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

When you save your procedure, it will automatically be named GetPetName, based on your CREATE PROCEDURE statement. Once you are done, you should see your new stored procedure within the Server Explorer (see Figure 22-7).

Note Stored procedures are not required to return data using output parameters as shown here; however, doing so will set the stage for talking about the Direction property of the SqlParameter later in this chapter in the section “Executing a Stored Procedure.”

CHAPTER 22 ADO.NET PART I: THE CONNECTED LAYER

747

Figure 22-7. The GetPetName stored procedure

Creating the Customers and Orders Tables

Our testing database will have two additional tables. The Customers table (as the name suggests) will contain a list of customers, which will be represented by three columns (CustID [which should be set as the primary key], FirstName, and LastName). Taking the same steps you took to create the Inventory table, create the Customers table using the following schema (see Figure 22-8).

Figure 22-8. Designing the Customers table

Once you have saved your table, add a handful of customer records (see Figure 22-9).

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

Figure 22-9. Populating the Customers table

Our final table, Orders, will be used to represent the automobile a given customer is interested in purchasing by mapping OrderID values to CarID/CustID values. Figure 22-10 shows the structure of our final table (again note that OrderID is the primary key).

Figure 22-10. Designing the Orders table

Now, add data to your Orders table. Assuming that the OrderID value begins at 1000, select a unique CarID for each CustID value (see Figure 22-11).

Figure 22-11. Populating the Orders table

Given the entries used in this text, we can see that Dave Brenner (CustID = 1) is interested in the red Volkswagen (CarID = 2), while Pat Walton (CustID = 3) has her eye on the tan Volkswagen (CarID = 8).

CHAPTER 22 ADO.NET PART I: THE CONNECTED LAYER

749

Visually Creating Table Relationships

The final task is to establish parent/child table relationships between the Customers, Orders, and Inventory tables. Doing so using Visual Studio 2008 is quite simple, as we can elect to insert a new database diagram by right-clicking the Database Diagrams node of the AutoLot database in the Server Explorer. Once you do so, be sure to select each of the tables from the resulting dialog box before clicking the Add button.

To establish the relationships between the tables, begin by clicking the CarID key of the Inventory table and (while holding down the mouse button) drag to the CarID field of the Orders table. Once you release the mouse, accept all defaults from the resulting dialog boxes.

Now, repeat the same process to map the CustID key of the Customers table to the CustID field of the Orders table. Once you are finished, you should find the class dialog box shown in Figure 22-12 (note that I enabled the display of the table relationships by right-clicking the designer and selecting Show Relationship Labels).

Figure 22-12. The interconnected Orders, Inventory, and Customers tables

With this, the AutoLot database is complete! While it is a far cry from a real-world corporate database, it will most certainly serve our purposes over the remainder of this book. Now that we have a database to test with, let’s dive into the details of the ADO.NET data provider factory model.

The ADO.NET Data Provider Factory Model

The .NET data provider factory pattern allows us to build a single code base using generalized data access types. Furthermore, using application configuration files (and the <connectionStrings> subelement), we are able to obtain providers and connection strings declaratively without the need to recompile or redeploy the assembly.

To understand the data provider factory implementation, recall from Table 22-1 that the objects within a data provider each derive from the same base classes defined within the System.Data. Common namespace:

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

DbCommand: Abstract base class for all command objects

DbConnection: Abstract base class for all connection objects

DbDataAdapter: Abstract base class for all data adapter objects

DbDataReader: Abstract base class for all data reader objects

DbParameter: Abstract base class for all parameter objects

DbTransaction: Abstract base class for all transaction objects

In addition, each of the Microsoft-supplied data providers contains a class type deriving from System.Data.Common.DbProviderFactory. This base class defines a number of methods that retrieve provider-specific data objects. Here is a snapshot of the relevant members of DbProviderFactory:

public abstract class DbProviderFactory

{

...

public virtual DbCommand CreateCommand();

public virtual DbCommandBuilder CreateCommandBuilder(); public virtual DbConnection CreateConnection();

public virtual DbConnectionStringBuilder CreateConnectionStringBuilder(); public virtual DbDataAdapter CreateDataAdapter();

public virtual DbDataSourceEnumerator CreateDataSourceEnumerator(); public virtual DbParameter CreateParameter();

}

To obtain the DbProviderFactory-derived type for your data provider, the System.Data.Common namespace provides a class type named DbProviderFactories (note the plural in this type’s name). Using the static GetFactory() method, you are able to obtain the specific DbProviderFactory object of the specified data provider, for example:

static void Main(string[] args)

{

// Get the factory for the SQL data provider.

DbProviderFactory sqlFactory = DbProviderFactories.GetFactory("System.Data.SqlClient");

...

// Get the factory for the Oracle data provider.

DbProviderFactory oracleFactory = DbProviderFactories.GetFactory("System.Data.OracleClient");

...

}

Of course, rather than obtaining a factory using a hard-coded string literal, you could read in this information from a client-side *.config file (much like the previous MyConnectionFactory example). You will do so in just a bit. However, in any case, once you have obtained the factory for your data provider, you are able to obtain the associated provider-specific data objects (connections, commands, data readers, etc.).

Registered Data Provider Factories

Before you build a full example of working with ADO.NET data provider factories, it is important to note that the DbProviderFactories type is able to fetch factories for only a subset of all possible data providers. The list of valid provider factories is recorded within the <DbProviderFactories> element within the machine.config file for your .NET 3.5 installation (note that the value of the invariant attribute is identical to the value passed into the DbProviderFactories.GetFactory() method):

Note

CHAPTER 22 ADO.NET PART I: THE CONNECTED LAYER

751

<system.data>

<DbProviderFactories>

<add name="Odbc Data Provider" invariant="System.Data.Odbc" description=".Net Framework Data Provider for Odbc" type="System.Data.Odbc.OdbcFactory,

System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />

<add name="OleDb Data Provider" invariant="System.Data.OleDb" description=".Net Framework Data Provider for OleDb" type="System.Data.OleDb.OleDbFactory,

System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />

<add name="OracleClient Data Provider" invariant="System.Data.OracleClient" description=".Net Framework Data Provider for Oracle" type="System.Data.OracleClient.OracleClientFactory, System.Data.OracleClient, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />

<add name="SqlClient Data Provider" invariant="System.Data.SqlClient" description=".Net Framework Data Provider for SqlServer" type="System.Data.SqlClient.SqlClientFactory, System.Data, Version=2.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />

</DbProviderFactories>

</system.data>

If you wish to leverage a similar data provider factory pattern for DMBSs not accounted for in the machine.config file, it is technically possible to add new invariant values that point to shared assemblies in the GAC. However, you must ensure that the data provider is ADO.NET 2.0 compliant and works with the data provider factory model.

A Complete Data Provider Factory Example

For a complete example, let’s create a new C# Console Application (named DataProviderFactory) that prints out the automobile inventory of the AutoLot database. For this initial example, we will hard-code the data access logic directly within the DataProviderFactory.exe assembly (just to keep things simple for the time being). However, once we begin to dig into the details of the ADO.NET programming model, we will isolate our data logic to a specific .NET code library that will be used throughout the remainder of this text.

First, add a reference to the System.Configuration.dll assembly and import the System. Configuration namespace. Next, insert an App.config file to the current project and define an empty <appSettings> element. Add a new key named provider that maps to the namespace name of the data provider you wish to obtain (System.Data.SqlClient). As well, define a connection string that represents a connection to the AutoLot database:

<?xml version="1.0" encoding="utf-8" ?> <configuration>

<appSettings>

<!-- Which provider? -->

<add key="provider" value="System.Data.SqlClient" />

<!-- Which connection string? -->

<add key="cnStr" value=

"Data Source=(local)\SQLEXPRESS;Initial Catalog=AutoLot;Integrated Security=True"