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

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

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

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

Perhaps the most fundamental difference between classic ADO and ADO.NET is that ADO.NET is a managed library of code, therefore it plays by the same rules as any managed library. The types that make up ADO.NET use the CLR memory management protocol, adhere to the same type system (classes, interfaces, enums, structures, and delegates), and can be accessed by any .NET language.

From a programmatic point of view, the bulk of ADO.NET is represented by a core assembly named System.Data.dll. Within this binary, you will find a good number of namespaces (see Figure 22-1), many of which represent the types of a particular ADO.NET data provider (defined shortly).

Figure 22-1. System.Data.dll is the core ADO.NET assembly.

As it turns out, most Visual Studio 2008 project templates automatically reference this key data access library. However, you will need to update your code files to import the namespaces you wish to use; for example:

using System;

// Bring in some ADO.NET namespaces! using System.Data;

using System.Data.SqlClient;

namespace MyApp

{

class Program

{

static void Main(string[] args)

{

}

}

}

Do understand that there are other ADO.NET-centric assemblies beyond System.Data.dll (such as System.Data.OracleClient.dll) that you may need to manually reference in your current project using the Add Reference dialog box.

Note With the release of .NET 3.5, ADO.NET has received many additional assemblies/namespaces that facilitate ADO.NET/LINQ integration. Chapter 24 will examine LINQ-centric aspects of ADO.NET.

CHAPTER 22 ADO.NET PART I: THE CONNECTED LAYER

733

The Two Faces of ADO.NET

The ADO.NET libraries can be used in two conceptually unique manners: connected or disconnected. When you are making use of the connected layer, your code base will explicitly connect to and disconnect from the underlying data store. When you are using ADO.NET in this manner, you typically interact with the data store using connection objects, command objects, and data reader objects.

The disconnected layer, which is the subject of Chapter 23, allows you to manipulate a set of DataTable objects (contained within a DataSet) that functions as a client-side copy of the external data. When you obtain a DataSet using a related data adapter object, the connection is automatically opened and closed on your behalf. As you would guess, this approach helps quickly free up connections for other callers and goes a long way to increasing the scalability of your systems.

Once a caller receives a DataSet, it is able to traverse and manipulate the contents without incurring the cost of network traffic. As well, if the caller wishes to submit the changes back to the data store, the data adapter (in conjunction with a set of SQL statements) is used once again to update the data source, at which point the connection is closed immediately.

Understanding ADO.NET Data Providers

Unlike classic ADO, ADO.NET does not provide a single set of types that communicate with multiple database management systems (DBMSs). Rather, ADO.NET supports multiple data providers, each of which is optimized to interact with a specific DBMS. The first benefit of this approach is that a specific data provider can be programmed to access any unique features of a particular DBMS. Another benefit is that a specific data provider is able to directly connect to the underlying engine of the DBMS in question without an intermediate mapping layer standing between the tiers.

Simply put, a data provider is a set of types defined in a given namespace that understand how to communicate with a specific data source. Regardless of which data provider you make use of, each defines a set of class types that provide core functionality. Table 22-1 documents some of the core common objects, their base class (all defined in the System.Data.Common namespace), and the data-centric interfaces (each defined in the System.Data namespace) they implement.

Table 22-1. Core Objects of an ADO.NET Data Provider

Object

Base Class

Implemented Interfaces

Meaning in Life

Connection

DbConnection

IDbConnection

Provides the ability to connect to

 

 

 

and disconnect from the data store.

 

 

 

Connection objects also provide

 

 

 

access to a related transaction

 

 

 

object.

Command

DbCommand

IDbCommand

Represents a SQL query or a stored

 

 

 

procedure. Command objects also

 

 

 

provide access to the provider’s data

 

 

 

reader object.

DataReader

DbDataReader

IDataReader,

Provides forward-only, read-only

 

 

IDataRecord

access to data using a server-side

 

 

 

cursor.

Continued

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

Table 22-1. Continued

Object

Base Class

Implemented Interfaces

Meaning in Life

DataAdapter

DbDataAdapter

IDataAdapter,

Transfers DataSets between the

 

 

IDbDataAdapter

caller and the data store. Data

 

 

 

adapters contain a connection and

 

 

 

a set of four internal command

 

 

 

objects used to select, insert,

 

 

 

update, and delete information

 

 

 

from the data store.

Parameter

DbParameter

IDataParameter,

Represents a named parameter

 

 

IDbDataParameter

within a parameterized query.

Transaction

DbTransaction

IDbTransaction

Encapsulates a database

 

 

 

transaction.

 

 

 

 

Although the specific names of these core objects will differ among data providers (e.g.,

SqlConnection versus OracleConnection versus OdbcConnection versus MySqlConnection), each object derives from the same base class (DbConnection in the case of connection objects) that implements identical interfaces (such as IDbConnection). Given this, you are correct to assume that once you learn how to work with one data provider, the remaining providers are quite straightforward.

Note Understand that under ADO.NET, when speaking of a “connection object,” one is really referring to a specific DbConnection-derived type; there is no class literally named “Connection”. The same idea holds true for a “command object,” “data adapter object,” and so forth. As a naming convention, the objects in a specific data provider are prefixed with the name of the related DBMS (for example, SqlConnection, OracleConnection,

SqlDataReader, etc.).

Figure 22-2 illustrates the big picture behind ADO.NET data providers. Note that in the diagram, the “Client Assembly” can literally be any type of .NET application: console program, Windows Forms application, ASP.NET web page, WCF service, a .NET code library, and so on.

 

 

 

 

.NET Platform Data Provider

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Connection Object

 

DataAdapter Object

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Transaction

 

 

 

Select Command

 

Client

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Assembly

 

 

 

 

 

 

 

 

Insert Command

 

 

 

 

 

Connection Object

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Parameter Collection

 

 

 

 

 

 

 

 

 

 

 

 

 

Update Command

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

DataReader Object

Delete Command

Database

Figure 22-2. ADO.NET data providers provide access to a given DBMS.

CHAPTER 22 ADO.NET PART I: THE CONNECTED LAYER

735

Now, to be sure, a data provider will supply you with other types beyond the objects shown in Figure 22-2. However, these core objects define a common baseline across all data providers.

The Microsoft-Supplied ADO.NET Data Providers

Microsoft’s .NET distribution ships with numerous data providers, including a provider for Oracle, SQL Server, and OLE DB/ODBC-style connectivity. Table 22-2 documents the namespace and containing assembly for each Microsoft ADO.NET data provider.

Table 22-2. Microsoft ADO.NET Data Providers

Data Provider

Namespace

Assembly

OLE DB

System.Data.OleDb

System.Data.dll

Microsoft SQL Server

System.Data.SqlClient

System.Data.dll

Microsoft SQL Server Mobile

System.Data.SqlServerCe

System.Data.SqlServerCe.dll

ODBC

System.Data.Odbc

System.Data.dll

Oracle

System.Data.OracleClient

System.Data.OracleClient.dll

 

 

 

Note There is no specific data provider that maps directly to the Jet engine (and therefore Microsoft Access). If you wish to interact with an Access data file, you can do so using the OLE DB or ODBC data provider.

The OLE DB data provider, which is composed of the types defined in the System.Data.OleDb namespace, allows you to access data located in any data store that supports the classic COM-based OLE DB protocol. Using this provider, you may communicate with any OLE DB–compliant database simply by tweaking the Provider segment of your connection string.

Be aware, however, that the OLE DB provider interacts with various COM objects behind the scenes, which can affect the performance of your application. By and large, the OLE DB data provider is only useful if you are interacting with a DBMS that does not define a specific .NET data provider. However, given the fact that these days any DBMS worth its salt should have a custom ADO.NET data provider for download, System.Data.OleDb should be considered a legacy namespace that has little use in the .NET 3.5 world (this is even more the case with the advent of the data provider factory model introduced under .NET 2.0).

Note There is one case in which using the types of System.Data.OleDb is necessary; specifically if you need to communicate with Microsoft SQL Server version 6.5 or earlier. The System.Data.SqlClient namespace can only communicate with Microsoft SQL Server version 7.0 or higher.

The Microsoft SQL Server data provider offers direct access to Microsoft SQL Server data stores, and only SQL Server data stores (version 7.0 and greater). The System.Data.SqlClient namespace contains the types used by the SQL Server provider and offers the same basic functionality as the OLE DB provider. The key difference is that the SQL Server provider bypasses the OLE DB layer and thus gives numerous performance benefits. As well, the Microsoft SQL Server data provider allows you to gain access to the unique features of this particular DBMS.

The remaining Microsoft-supplied providers (System.Data.OracleClient, System.Data.Odbc, and System.Data.SqlClientCe) provide access to Oracle databases, interactivity with ODBC connections, and the SQL Server Mobile edition DBMS (commonly used by handheld devices, such as a

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

Pocket PC). The ODBC types defined within the System.Data.Odbc namespace are typically only useful if you need to communicate with a given DBMS for which there is no custom .NET data provider (in that ODBC is a widespread model that provides access to a number of data stores).

Obtaining Third-Party ADO.NET Data Providers

In addition to the data providers that ship from Microsoft, numerous third-party data providers exist for various open source and commercial databases. While you will most likely be able to obtain an ADO.NET data provider directly from the database vendor, you should be aware of the following site (please note that this URL is subject to change): http://www.sqlsummit.com/DataProv.htm.

This website is one of many sites that documents each known ADO.NET data provider and provides links for more information and downloads. Here you will find numerous ADO.NET providers, including SQLite, DB2, MySQL, PostgreSQL, and Sybase (among others).

Given the large number of ADO.NET data providers, the examples in this chapter will make use of the Microsoft SQL Server data provider (System.Data.SqlClient.dll). Recall that this provider allows you to communicate with Microsoft SQL Server version 7.0 and higher, including SQL Server 2005 Express Edition. If you intend to use ADO.NET to interact with another DBMS, you should have no problem doing so once you understand the material presented in the pages that follow.

Additional ADO.NET Namespaces

In addition to the .NET namespaces that define the types of a specific data provider, the .NET base class libraries provide a number of additional ADO.NET-centric namespaces, some of which are shown in Table 22-3.

Table 22-3. Select Additional ADO.NET-Centric Namespaces

Namespace

Meaning in Life

Microsoft.SqlServer.Server

This namespace provides types that facilitate CLR and SQL Server

 

2005 integration services.

System.Data

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

 

providers, including common interfaces and numerous types that

 

represent the disconnected layer (DataSet, DataTable, etc.).

System.Data.Common

This namespace contains types shared between all ADO.NET data

 

providers, including the common abstract base classes.

System.Data.Sql

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

CHAPTER 22 ADO.NET PART I: THE CONNECTED LAYER

737

The Types of the System.Data Namespace

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-4 lists some of the core types to be aware of.

Table 22-4. 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)

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

 

 

A vast majority of the classes within System.Data are used when programming against the disconnected layer of ADO.NET. In the next chapter, you will get to know the details of the DataSet and its related cohorts (DataTable, DataRelation, DataRow, etc.) and how to use them (and a related data adapter) to represent and manipulate client side copies of remote data.

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 transaction object. Here is the formal definition of

IDbConnection:

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

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();

}

Note Like many other types in the .NET base class libraries, the Close() method is functionally equivalent to calling the Dispose() method directly or indirectly within a C# using scope (see Chapter 8).

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();

}

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);

CHAPTER 22 ADO.NET PART I: THE CONNECTED LAYER

739

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.

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 user-friendly display names via the TableMappings property:

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

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 3.5 SDK documentation for a complete listing):

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);

}

CHAPTER 22 ADO.NET PART I: THE CONNECTED LAYER

741

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). Also recall that C# supports nullable data types (see Chapter 4), which are ideal for interacting with data columns that could be empty.

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. For example, 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();

}

Note Interfaces are not strictly required; the same end result could be achieved using abstract base classes (such as DbConnection) as parameters or return values.

The same holds true for member return values. For example, consider the following simple C# Console Application project (named MyConnectionFactory), which allows you to obtain a specific connection object based on the value of a custom enumeration. For diagnostic purposes, we will simply print out the underlying connection object via reflection services:

//Need these to get definitions of common interfaces,

//and various connection objects for our test.

using System; using System.Data;

using System.Data.SqlClient; using System.Data.Odbc; using System.Data.OleDb;

// Need to reference System.Data.OracleClient.dll to nab this namespace! using System.Data.OracleClient;

namespace MyConnectionFactory

{

// A list of possible providers. enum DataProvider

{ SqlServer, OleDb, Odbc, Oracle, None }

class Program

{

static void Main(string[] args)

{

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