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

Pro CSharp And The .NET 2.0 Platform (2005) [eng]

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

764 CHAPTER 22 DATABASE ACCESS WITH ADO.NET

Table 22-4. Additional ADO.NET-centric Namespaces

Namespace

Meaning in Life

Microsoft.SqlServer.Server

This new .NET 2.0 namespace provides types that allow you to

 

author stored procedures via managed languages for SQL Server

 

2005.

System.Data

This namespace defines the core ADO.NET types used by all data

 

providers.

System.Data.Common

This namespace contains types shared between data providers,

 

including the .NET 2.0 data provider factory types.

System.Data.Design

This new .NET 2.0 namespace contains various types used to

 

construct a design-time appearance for custom data

 

components.

System.Data.Sql

This new .NET 2.0 namespace contains types that allow you to

 

discover Microsoft SQL Server instances installed on the current

 

local network.

System.Data.SqlTypes

This namespace contains native data types used by Microsoft

 

SQL Server. Although you are always free to use the corresponding

 

CLR data types, the SqlTypes are optimized to work with SQL

 

Server.

 

 

Do understand that this chapter will not examine each and every type within each and every ADO.NET namespace (that task would require a large book in and of itself). However, it is quite important for you to understand the types within the System.Data namespace.

The System.Data Types

Of all the ADO.NET namespaces, System.Data is the lowest common denominator. You simply cannot build ADO.NET applications without specifying this namespace in your data access applications. This namespace contains types that are shared among all ADO.NET data providers, regardless of the underlying data store. In addition to a number of database-centric exceptions (NoNullAllowedException,

RowNotInTableException, MissingPrimaryKeyException, and the like), System.Data contains types that represent various database primitives (tables, rows, columns, constraints, etc.), as well as the common interfaces implemented by data provider objects. Table 22-5 lists some of the core types to be aware of.

Table 22-5. Core Members of the System.Data Namespace

Type

Meaning in Life

Constraint

Represents a constraint for a given DataColumn object

DataColumn

Represents a single column within a DataTable object

DataRelation

Represents a parent/child relationship between two DataTable objects

DataRow

Represents a single row within a DataTable object

DataSet

Represents an in-memory cache of data consisting of any number of

 

interrelated DataTable objects

DataTable

Represents a tabular block of in-memory data

DataTableReader

Allows you to treat a DataTable as a fire-hose cursor (forward only, read-only

 

data access); new in .NET 2.0

CHAPTER 22 DATABASE ACCESS WITH ADO.NET

765

Type

Meaning in Life

DataView

Represents a customized view of a DataTable for sorting, filtering, searching,

 

editing, and navigation

IDataAdapter

Defines the core behavior of a data adapter object

IDataParameter

Defines the core behavior of a parameter object

IDataReader

Defines the core behavior of a data reader object

IDbCommand

Defines the core behavior of a command object

IDbDataAdapter

Extends IDataAdapter to provide additional functionality of a data adapter

 

object

IDbTransaction

Defines the core behavior of a transaction object

 

 

Later in this chapter, you will get to know the role of the DataSet and its related cohorts (DataTable, DataRelation, DataRow, etc.). However, your next task is to examine the core interfaces of System.Data at a high level, to better understand the common functionality offered by any data provider. You will learn specific details throughout this chapter, so for the time being let’s simply focus on the overall behavior of each interface type.

The Role of the IDbConnection Interface

First up is the IDbConnection type, which is implemented by a data provider’s connection object. This interface defines a set of members used to configure a connection to a specific data store, and it also allows you to obtain the data provider’s transactional object. Here is the formal definition of

IDbConnection:

public interface IDbConnection : IDisposable

{

string ConnectionString { get; set; } int ConnectionTimeout { get; } string Database { get; } ConnectionState State { get; } IDbTransaction BeginTransaction();

IDbTransaction BeginTransaction(IsolationLevel il); void ChangeDatabase(string databaseName);

void Close();

IDbCommand CreateCommand(); void Open();

}

The Role of the IDbTransaction Interface

As you can see, the overloaded BeginTransaction() method defined by IDbConnection provides access to the provider’s transaction object. Using the members defined by IDbTransaction, you are able to programmatically interact with a transactional session and the underlying data store:

public interface IDbTransaction : IDisposable

{

IDbConnection Connection { get; } IsolationLevel IsolationLevel { get; } void Commit();

void Rollback();

}

766 CHAPTER 22 DATABASE ACCESS WITH ADO.NET

The Role of the IDbCommand Interface

Next, we have the IDbCommand interface, which will be implemented by a data provider’s command object. Like other data access object models, command objects allow programmatic manipulation of SQL statements, stored procedures, and parameterized queries. In addition, command objects provide access to the data provider’s data reader type via the overloaded ExecuteReader() method:

public interface IDbCommand : IDisposable

{

string CommandText { get; set; } int CommandTimeout { get; set; }

CommandType CommandType { get; set; } IDbConnection Connection { get; set; } IDataParameterCollection Parameters { get; } IDbTransaction Transaction { get; set; } UpdateRowSource UpdatedRowSource { get; set; } void Cancel();

IDbDataParameter CreateParameter(); int ExecuteNonQuery();

IDataReader ExecuteReader();

IDataReader ExecuteReader(CommandBehavior behavior); object ExecuteScalar();

void Prepare();

}

The Role of the IDbDataParameter and IDataParameter Interfaces

Notice that the Parameters property of IDbCommand returns a strongly typed collection that implements IDataParameterCollection. This interface provides access to a set of IDbDataParameter-compliant class types (e.g., parameter objects):

public interface IDbDataParameter : IDataParameter

{

byte Precision { get; set; } byte Scale { get; set; } int Size { get; set; }

}

IDbDataParameter extends the IDataParameter interface to obtain the following additional behaviors:

public interface IDataParameter

{

DbType DbType { get; set; } ParameterDirection Direction { get; set; } bool IsNullable { get; }

string ParameterName { get; set; } string SourceColumn { get; set; }

DataRowVersion SourceVersion { get; set; } object Value { get; set; }

}

As you will see, the functionality of the IDbDataParameter and IDataParameter interfaces allows you to represent parameters within a SQL command (including stored procedures) via specific ADO.NET parameter objects rather than hard-coded string literals.

CHAPTER 22 DATABASE ACCESS WITH ADO.NET

767

The Role of the IDbDataAdapter and IDataAdapter Interfaces

Data adapters are used to push and pull DataSets to and from a given data store. Given this, the IDbDataAdapter interface defines a set of properties that are used to maintain the SQL statements for the related select, insert, update, and delete operations:

public interface IDbDataAdapter : IDataAdapter

{

IDbCommand DeleteCommand { get; set; } IDbCommand InsertCommand { get; set; } IDbCommand SelectCommand { get; set; } IDbCommand UpdateCommand { get; set; }

}

In addition to these four properties, an ADO.NET data adapter also picks up the behavior defined in the base interface, IDataAdapter. This interface defines the key function of a data adapter type: the ability to transfer DataSets between the caller and underlying data store using the Fill() and Update() methods.

As well, the IDataAdapter interface allows you to map database column names to more userfriendly display names via the TableMappings property:

public interface IDataAdapter

{

MissingMappingAction MissingMappingAction { get; set; } MissingSchemaAction MissingSchemaAction { get; set; } ITableMappingCollection TableMappings { get; }

int Fill(System.Data.DataSet dataSet);

DataTable[] FillSchema(DataSet dataSet, SchemaType schemaType); IDataParameter[] GetFillParameters();

int Update(DataSet dataSet);

}

The Role of the IDataReader and IDataRecord Interfaces

The next key interface to be aware of is IDataReader, which represents the common behaviors supported by a given data reader object. When you obtain an IDataReader-compatible type from an ADO.NET data provider, you are able to iterate over the result set in a forward-only, read-only manner.

public interface IDataReader : IDisposable, IDataRecord

{

int Depth { get; } bool IsClosed { get; }

int RecordsAffected { get; } void Close();

DataTable GetSchemaTable(); bool NextResult();

bool Read();

}

Finally, as you can see, IDataReader extends IDataRecord, which defines a good number of members that allow you to extract a strongly typed value from the stream, rather than casting the generic System.Object retrieved from the data reader’s overloaded indexer method. Here is a partial listing of the various GetXXX() methods defined by IDataRecord (see the .NET Framework 2.0 SDK documentation for a complete listing):

768CHAPTER 22 DATABASE ACCESS WITH ADO.NET

public interface IDataRecord

{

int FieldCount { get; }

object this[ string name ] { get; } object this[ int i ] { get; }

bool GetBoolean(int i); byte GetByte(int i); char GetChar(int i);

DateTime GetDateTime(int i); Decimal GetDecimal(int i); float GetFloat(int i); short GetInt16(int i);

int GetInt32(int i); long GetInt64(int i);

...

bool IsDBNull(int i);

}

Note The IDataReader.IsDBNull() method can be used to programmatically discover if a specified field is set to null before obtaining a value from the data reader (to avoid triggering a runtime exception).

Abstracting Data Providers Using Interfaces

At this point, you should have a better idea of the common functionality found among all .NET data providers. Recall that even though the exact names of the implementing types will differ among data providers, you are able to program against these types in a similar manner—that’s the beauty of interface-based polymorphism. Therefore, if you define a method that takes an IDbConnection parameter, you can pass in any ADO.NET connection object:

public static void OpenConnection(IDbConnection cn)

{

// Open the incoming connection for the caller. cn.Open();

}

The same holds true for a member return value. For example, consider the following simple C# program, which allows the caller to obtain a specific connection object using the value of a custom enumeration (assume you have “used” System.Data):

namespace ConnectionApp

{

enum DataProvider

{ SqlServer, OleDb, Odbc, Oracle }

class Program

{

static void Main(string[] args)

{

// Get a specific connection.

IDbConnection myCn = GetConnection(DataProvider.SqlServer);

// Assume we wish to connect to the SQL Server Pubs database. myCn.ConnectionString =

"Data Source=localhost;uid=sa;pwd=;Initial Catalog=Pubs";

CHAPTER 22 DATABASE ACCESS WITH ADO.NET

769

//Now open connection via our helper function.

OpenConnection(myCn);

//Use connection and close when finished.

...

myCn.Close();

}

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 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 hard-code the types of System.Data.SqlClient, you will obviously need to edit, recompile, and redeploy the assembly.

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 11 that custom data can be programmatically obtained using types within the System.Configuration namespace. For example, assume you have specified the connection string and data provider values within a configuration file as so:

<configuration>

<appSettings>

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

"Data Source=localhost;uid=sa;pwd=;Initial Catalog=Pubs"/> </appSettings>

</configuration>

770 CHAPTER 22 DATABASE ACCESS WITH ADO.NET

With this, you could update Main() to programmatically read these values. By doing so, you essentially build a data provider factory. Here are the relevant updates:

static void Main(string[] args)

{

// Read the provider key.

string dpStr = ConfigurationManager.AppSettings["provider"]; DataProvider dp = (DataProvider)Enum.Parse(typeof(DataProvider), dpStr);

// Read the cnStr.

string cnStr = ConfigurationManager.AppSettings["cnStr"];

// Get a specific connection.

IDbConnection myCn = GetConnection(dp); myCn.ConnectionString = cnStr;

...

}

Note The ConfigurationManager type is new to .NET 2.0. Be sure to set a reference to the System.Configuration.dll assembly and “use” the System.Configuration namespace.

If the previous example were reworked into a .NET code library (rather than a console application), you would be able to build any number of clients that could obtain specific connections using various layers of abstraction. However, to make a worthwhile data provider factory library, you would also have to account for command objects, data readers, data adapters, and other data-centric types. While building such a code library would not necessarily be difficult, it would require a good amount of code. Thankfully, as of .NET 2.0, the kind folks in Redmond have built this very thing into the base class libraries.

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

The .NET 2.0 Provider Factory Model

Under .NET 2.0, we are now offered a data provider factory pattern that allows us to build a single code base using generalized data access types. Furthermore, using application configuration files (and the spiffy new <connectionStrings> section), we are able to obtain providers and connection strings declaratively without the need to recompile or redeploy the client software.

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:

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

CHAPTER 22 DATABASE ACCESS WITH ADO.NET

771

In addition, as of .NET 2.0, each of the Microsoft-supplied data providers now provides a specific class 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 (which is to say, singular) DbProviderFactory 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");

...

}

As you might be thinking, 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, etc.).

Registered Data Provider Factories

Before you look at a full example of working with ADO.NET data provider factories, it is important to point out that the DbProviderFactories type (as of .NET 2.0) 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 2.0 installation (note that the value of the invariant attribute is identical to the value passed into the DbProviderFactories. GetFactory() method):

<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"

Note

772 CHAPTER 22 DATABASE ACCESS WITH ADO.NET

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, note that the Mono distribution of .NET (see Chapter 1) provides a similar data factory that accounts for numerous open source and commercial data providers.

A Complete Data Provider Factory Example

For a complete example, let’s build a console application (named DataProviderFactory) that prints out the first and last names of individuals in the Authors table of a database named Pubs residing within Microsoft SQL Server (as you may know, Pubs is a sample database modeling a fictitious book publishing company).

First, add a reference to the System.Configuration.dll assembly and insert an app.config file to the current project and define an <appSettings> element. Remember that the format of the “official” provider value is the full namespace name for the data provider, rather than the string name of the ad hoc DataProvider enumeration used in the MyConnectionFactory example:

<configuration>

<appSettings>

<!-- Which provider? -->

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

<!-- Which connection string? -->

<add key="cnStr" value=

"Data Source=localhost;uid=sa;pwd=;Initial Catalog=Pubs"/> </appSettings>

</configuration>

Now that you have a proper *.config file, you can read in the provider and cnStr values using the

ConfigurationManager.AppSettings() method. The provider value will be passed to DbProviderFactories. GetFactory() to obtain the data provider–specific factory type. The cnStr value will be used to set the ConnectionString property of the DbConnection-derived type. Assuming you have “used” the System.Data and System.Data.Common namespaces, update your Main() method as follows:

static void Main(string[] args)

{

Console.WriteLine("***** Fun with Data Provider Factories *****\n");

// Get Connection string/provider from *.config. string dp =

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

CHAPTER 22 DATABASE ACCESS WITH ADO.NET

773

ConfigurationManager.AppSettings["cnStr"];

// Make the factory provider.

DbProviderFactory df = DbProviderFactories.GetFactory(dp);

//Now make connection object.

DbConnection cn = df.CreateConnection();

Console.WriteLine("Your connection object is a: {0}", cn.GetType().FullName); cn.ConnectionString = cnStr;

cn.Open();

//Make command object.

DbCommand cmd = df.CreateCommand();

Console.WriteLine("Your command object is a: {0}", cmd.GetType().FullName); cmd.Connection = cn;

cmd.CommandText = "Select * From Authors";

// Print out data with data reader.

DbDataReader dr = cmd.ExecuteReader(CommandBehavior.CloseConnection);

Console.WriteLine("Your data reader object is a: {0}", dr.GetType().FullName);

Console.WriteLine("\n***** Authors in Pubs *****");

while (dr.Read())

Console.WriteLine("-> {0}, {1}", dr["au_lname"], dr["au_fname"]); dr.Close();

}

Notice that for diagnostic purposes, you are printing out the fully qualified name of the underlying connection, command, and data reader using reflection services. If you run this application, you will find that the Microsoft SQL Server provider has been used to read data from the Authors table of the Pubs database (see Figure 22-2).

Figure 22-2. Obtaining the SQL Server data provider via the .NET 2.0 data provider factory

Now, if you change the *.config file to specify System.Data.OleDb as the data provider (and update your connection string) as follows:

<configuration>

<appSettings>

<!-- Which provider? -->

<add key="provider" value="System.Data.OleDb" /> <!-- Which connection string? -->

<add key="cnStr" value=