1.概要
1.1 ADO.NET是什么
ADO.NET是一组允许.NET开发人员使用标准的,结构化的,甚至无连接的方式与数据交互的技术。ADO.NET和ADO是两种截然不同的数据访问方式。
1.2 ADO.NET核心组件
System.Data命名空间提供了不同的ADO.NET类,该类库包含两组重要的类:一组负责处理软件内部的实际数据(DataSet),一组负责与外部数据系统通信(Data Provider)。具体架构如下图所示:
DataSet是ADO.NET的非连接(断开)结构的核心组件。DataSet 的设计目的很明确:为了实现独立于任何数据源的数据访问。DataSet 包含一个或多个 DataTable 对象的集合,这些对象由数据行和数据列以及主键、外键、约束和有关 DataTable 对象中数据的关系信息组成。
ADO.NET结构的另一个核心元素是 .NET 数据提供程序(Data Provider)。具体包括:
- Connection 对象提供与数据源的连接。
- Command对象使您能够访问用于返回数据、修改数据、运行存储过程以及发送或检索参数信息的数据库命令。
- DataReader 对象从数据源中提供快速的,只读的数据流。
- DataAdapter 对象提供连接 DataSet 对象和数据源的桥梁。DataAdapter 使用 Command 对象在数据源中执行 SQL 命令,以便将数据加载到 DataSet 中,并使对 DataSet 中数据的更改与数据源保持一致。
1.3 ADO.NET扩展
Entity Framework和LINQ是微软为了提高ADO.NET核心功能而建立的两个新的工具。需要注意的是,它们并不是ADO.NET的基本组成部分。
2 .NET数据提供程序
2.1 什么是.NET数据提供程序
.NET Framework数据提供程序用于连接数据库、执行命令和检索结果。这些结果将被直接处理,放置在DataSet中以便根据需要向用户公开、与多个源中的数据组合,或在层之间进行远程处理。.NET Framework 数据提供程序是轻量的,它在数据源和代码之间创建最小的分层,并在不降低功能性的情况下提高性能。
下表列出了 .NET Framework 中所包含的数据提供程序。
.NET数据提供程序 | 说明 |
---|---|
用于 SQL Server 的数据提供程序 | 提供对 Microsoft SQL Server 7.0 或更高版本中数据的访问。使用 System.Data.SqlClient 命名空间。 |
用于 OLE DB 的数据提供程序 | 提供对使用 OLE DB 公开的数据源中数据的访问。使用 System.Data.OleDb 命名空间。 |
用于 ODBC 的数据提供程序 | 提供对使用 ODBC 公开的数据源中数据的访问。使用 System.Data.Odbc 命名空间。 |
用于 Oracle 的数据提供程序 | 适用于 Oracle 数据源。用于 Oracle 的 .NET Framework 数据提供程序支持 Oracle 客户端软件 8.1.7 和更高版本,并使用 System.Data.OracleClient 命名空间。 |
EntityClient 提供程序 | EntityClient 提供程序 |
2.2 .NET数据提供程序的核心对象
Connection对象、Command对象、DataReader对象以及DataAdapter对象构成了.NET数据提供程序的骨架。
2.3 其他重要的对象
如果说上述四大对象构成了.NET数据提供程序的骨架,那么下面这些对象可以说是.NET数据提供程序的血肉了。
2.3.1 Parameter对象
Parameter对象定义了命令和存储过程的输入、输出和返回值参数。
先看这样一段代码:
strSQL = "SELECT * FROM users WHERE (name = '" + userName + "') and (pw = '"+ passWord +"');"
试想一下,如果用户填入
userName = "' OR '1'='1";
与
passWord = "' OR '1'='1";
该用户竟然成功登陆网站了。 我们将userName和passWord变量带入strSQL变量后,将得到这样的一条SQL语句:
strSQL = "SELECT * FROM users WHERE (name = '' OR '1'='1') and (pw = '' OR '1'='1');"
也就是实际上运行的SQL命令会变成下面这样的
strSQL = "SELECT * FROM users;"
上面的情况,用专业术语来说就是一个简单的SQL注入(SQL injection)。有SQL注入的出现,因此就有参数化查询(Parameterized Query )的出现。 Parameter对象有两个非常重要的属性:DBType和Value。DBType用来设置或获取参数的类型,Value则用来设置或获取参数的值。
现在用Parameter对象来改写简单的登陆验证代码(详见7.2):
strSQL = "SELECT * FROM users WHERE Name = @Name and Password = @Password"; SqlParamter[] paras = new SqlParamter[]{//参数数组 new SqlParamter("@Name",SqlDBType.Varchar,50) new SqlParamter("@Password",SqlDBType.Varchar,50)}; paras[0].value = userName;//绑定用户名 paras[1].value = password;//绑定用户密码
或者,
SqlCommand cmd = new SqlCommand(); // ... cmd.CommandText = "INSERT into AttendanceTrackerDatabase VALUES (@studentName,@studentID,@Date,@class)"; cmd.Parameters.AddWithValue("@studentName", nameTextBox.Text); cmd.Parameters.AddWithValue("@studentID", studentIDTextBox.Text); cmd.Parameters.AddWithValue("@Date", attendanceDate.Value); cmd.Parameters.AddWithValue("@class", classDropDown.Text);
2.3.2 两大得力助手:ConnectionStringBuilder和CommandBuilder
ConnectionStringBuilder:它提供一种用于创建和管理由 Connection 对象使用的连接字符串的内容的简单方法。 所有 ConnectionStringBuilder 对象的基类均为 DbConnectionStringBuilder 类。
CommandBuilder:它自动生成 DataAdapter 的命令属性或从存储过程中派生参数信息,并填充 Command 对象的 Parameters 集合。 所有 CommandBuilder 对象的基类均为 DbCommandBuilder 类。
2.4 理解.NET数据提供程序
2.4.1 用于 SQL Server 的 .NET Framework 数据提供程序 (SqlClient)
用于 SQL Server 的 .NET Framework 数据提供程序 (SqlClient) 使用自己的协议与 SQL Server 进行通信。 它是轻量的且性能良好,因为它进行了优化,可直接访问 SQL Server,而无需添加 OLE DB 或开放式数据库连接 (ODBC) 层。 下图将用于 SQL Server 的 .NET Framework 数据提供程序与用于 OLE DB 的 .NET Framework 数据提供程序进行对比。 用于 OLE DB 的 .NET Framework 数据提供程序通过 OLE DB 服务组件(它提供连接池和事务服务)和用于数据源的 OLE DB 访问接口与 OLE DB 数据源进行通信。
如果你使用SQL Server数据提供程序需要引入:
using System.Data.SqlClient;
3.连接字符串
3.1 什么是连接字符串
连接字符串,是一组被格式化的键值对:它告诉ADO.NET数据源在哪里,需要什么样的数据格式,提供什么样的访问信任级别以及其他任何包括连接的相关信息。
3.2 语法格式
连接字符串由一组元素组成,一个元素包含一个键值对,元素之间由“;”分开。语法如下:
key1=value1;key2=value2;key3=value3...
典型的元素(键值对)应当包含这些信息:数据源是基于文件的还是基于网络的数据库服务器,是否需要账号密码来访问数据源,超时的限制是多少,以及其他相关的配置信息
3.3 几种典型连接字符串
3.3.1 SQL Sever连接字符串
(1)标准的安全连接
Data Source=myServerAddress;Initial Catalog=myDataBase;User Id=myUsername;Password=myPassword;
说明:
Data Source:需要连接的服务器。需要注意的是,如果使用的时Express版本的SQL Server需要在服务器名后加SQLEXPRESS。例如,连接本地的SQL Server 2008 Express版本的数据库服务器,可以写成Data Source = (local)SQLEXPRESS或者.SQLEXPRESS。
Initial Catalog:默认使用的数据库名称。
User ID:数据库服务器账号。
Password:数据库服务器密码。
或者也可以写成这样:
Server=myServerAddress;Database=myDataBase;User ID=myUsername;Password=myPassword;Trusted_Connection=False;
(2)可信连接
Data Source=myServerAddress;Initial Catalog=myDataBase;Integrated Security=SSPI;
说明:
Data Source:与上述相同。
Initial Catalog:与上述相同。
Integrate Security:使用存在的windows安全证书访问数据库。
或者也可以写成这样:
Server=myServerAddress;Database=myDataBase;Trusted_Connection=True;
3.3.2 Access连接字符串
Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:mydatabase.mdb;User Id=admin;Password=;
3.3.3 MySQL连接字符串
Server=myServerAddress;Database=myDataBase;Uid=myUsername;Pwd=myPassword;
3.3.4 DB2连接字符串
Server=myAddress:myPortNumber;Database=myDataBase;UID=myUsername;PWD=myPassword;
3.3.5 Oracle连接字符串
Data Source=TORCL;User Id=myUsername;Password=myPassword;
3.4 如何构造连接字符串
连接字符串本质上就是一个字符串,完全可以用
string connStr = "Data Source=myServerAddress;Initial Catalog=myDataBase;User ID=myUsername;Password=myPassword";
来构造一个连接字符串。
ADO.NET有一个专门的类来处理连接字符串:DbConnectionStringBuilder。DbConnectionStringBuilder类为强类型连接字符串生成基类。以SQL Server为例,可以这样构建一个连接字符串:
SqlClient.SqlConnectionStringBuilder builder = new SqlClient.SqlConnectionStringBuilder(); builder.DataSource = @"(local)SQLEXPRESS"; builder.InitialCatalog = "myDataBase"; builder.IntegratedSecurity = true;
4.Connection对象
4.1 理解Connection对象
Connection对象表示与特定数据源的连接。对于ADO.NET而言,不同的数据源,都对应着不同的Connection对象。具体Connection对象如下表:
名称 | 命名空间 | 描述 |
---|---|---|
SqlConnection | System.Data.SqlClient | 表示与SQL Server的连接对象 |
OleDbConnection | System.Data.OleDb | 表示与OleDb数据源的连接对象 |
OdbcConnection | System.Data.Odbc | 表示与ODBC数据源的连接对象 |
OracleConnection | System.Data.OracleClient | 表示与Orale数据库的连接对象 |
所有连接对象都继承于DbConnection类。DbConnection类的实现结构如下:
public abstract class DbConnection : Component, IDbConnection, IDisposable
DbConnection是抽象基类,并且继承Compoent,IDbConnection,IDisposable类。由于DbConnection类是抽象基类,因此不能实例化。DbConnection类封装了很多重要的方法和属性,下面将详细讲解几个重要的方法和属性。
4.2 必须掌握的几个方法
Open: 使用 ConnectionString 所指定的设置打开数据库连接。
Dispose: 释放由 Component 使用的所有资源。
Close: 关闭与数据库的连接。 此方法是关闭任何已打开连接的首选方法。Close 方法回滚任何挂起的事务。然后将连接释放到连接池,或者在连接池被禁用的情况下关闭连接。
4.3 必须掌握的几个属性
Database: 在连接打开之后获取当前数据库的名称,或者在连接打开之前获取连接字符串中指定的数据库名。
DataSource: 获取要连接的数据库服务器的名称。
ConnectionTimeOut: 获取在建立连接时终止尝试并生成错误之前所等待的时间。
ConnectionString: 获取或设置用于打开连接的字符串。
State: 获取描述连接状态的字符串。
4.4 ConnectionState
State属性描述了与数据源的连接的当前状态。ConnectionState是一个枚举类型。它包括以下成员:
Closed: 连接处于关闭状态。
Open: 连接处于打开状态。
Connecting: 连接对象正在与数据源连接。
Executing: 连接对象正在执行命令。
Fetching: 连接对象正在检索数据。
Broken: 与数据源的连接中断。
4.5 连接SQL Server的SqlConnection对象
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Data; using System.Data.SqlClient; namespace Connection { class Program { static void Main(string[] args) { //构造连接字符串 SqlConnectionStringBuilder connStr = new SqlConnectionStringBuilder(); connStr.DataSource = @".SQLEXPRESS"; connStr.InitialCatalog = "master"; connStr.IntegratedSecurity = true; SqlConnection conn = new SqlConnection();//创建连接对象 conn.ConnectionString = connStr.ConnectionString;//设置连接字符串 conn.Open();//打开连接 if(conn.State == ConnectionState.Open) { Console.WriteLine("Database is linked."); Console.WriteLine(" DataSource:{0}",conn.DataSource); Console.WriteLine("Database:{0}",conn.Database); Console.WriteLine("ConnectionTimeOut:{0}",conn.ConnectionTimeout); } conn.Close();//关闭连接 conn.Dispose();//释放资源 if(conn.State == ConnectionState.Closed) { Console.WriteLine(" Database is closed."); } Console.Read(); } } }
4.6 安全代码
(1)添加try...catch块
应当确保打开连接后,无论是否出现异常,都应关闭连接和释放资源。所以,必须在finially语句块中调用Close方法关闭数据库连接。
SqlConnection conn = new SqlConnection(connStr); try { conn.Open(); } catch(Exception ex) { ;//todo } finially { conn.Close(); }
(2)使用using语句
using语句的作用是确保资源使用后,并很快释放它们。using语句帮助减少意外的运行时错误带来的潜在问题,它整洁地包装了资源的使用。具体来说,它执行以下内容:
- 分配资源。
- 把Statement放进try块。
- 创建资源的Dispose方法,并把它放进finally块。 因此,上面的语句等同于:
using(SqlConnection conn = new SqlConnection(connStr)) { ;//todo }
5.数据库连接池
5.1 什么是连接池
建立一个数据库连接是一件非常耗时(消耗时间)耗力(消耗资源)的事情。之所以会这样,是因为连接到数据库服务器需要经历几个漫长的过程:建立物理通道(例如套接字或命名管道),与服务器进行初次握手,分析连接字符串信息,由服务器对连接进行身份验证,运行检查以便在当前事务中登记等等。
实际上,ADO.NET已经为我们提供了名为连接池的优化方法。连接池就是这样一个容器:它存放了一定数量的与数据库服务器的物理连接。因此,当我们需要连接数据库服务器的时候,只需去池(容器)中取出一条空闲的连接,而不是新建一条连接。这样的话,就可以大大减少连接数据库的开销,从而提高了应用程序的性能。
5.2 连接池的工作原理
5.2.1 创建连接池
需要说明的是,连接池是具有类别区分的。也就是说,同一个时刻同一应用程序域可以有多个不同类型的连接池。连接池的标识区分,是由进程、应用程序域、连接字符串以及windows标识(在使用集成的安全性时)共同组成签名来标识区分的。
但对于同一应用程序域来说,一般只由连接字符串来标识区分。当打开一条连接时,如果该条连接的类型签名与现有的连接池类型不匹配,则创建一个新的连接池。反之,则不创建新的连接池。
//创建连接对象1 using (SqlConnection conn1 = new SqlConnection( "DataSource=(local);Integrated Security=SSPI;Initial Catalog=Northwind")) { conn1.Open(); } //创建连接对象2 using (SqlConnection conn2 = new SqlConnection( "DataSource=(local);Integrated Security=SSPI;Initial Catalog=pubs")) { conn2.Open(); } //创建连接对象3 using (SqlConnection conn3 = new SqlConnection( "DataSource=(local);Integrated Security=SSPI;Initial Catalog=Northwind")) { conn3.Open(); }
上面实例中,创建了三个SqlConnection对象,但是管理时只需要两个连接池。conn1与conn3的连接字符串相同,所以可以共享一个连接池,而conn2与conn1与conn3不同,所以需要创建新的连接池。
5.2.2 分配空闲连接
当用户创建连接请求或者说调用Connection对象的Open时,连接池管理器首先需要根据连接请求的类型签名找到匹配类型的连接池,然后尽力分配一条空闲连接。具体情况如下:
- 如果池中有空闲连接可用,返回该连接。
- 如果池中连接都已用完,创建一个新连接添加到池中。
- 如果池中连接已达到最大连接数,请求进入等待队列直到有空闲连接可用。
5.2.3 移除无效连接
无效连接,即不能正确连接到数据库服务器的连接。对于连接池来说,存储的与数据库服务器的连接的数量是有限的。因此,对于无效连接,如果如不及时移除,将会浪费连接池的空间。如果连接长时间空闲,或检测到与服务器的连接已断开,连接池管理器会将该连接从池中移除。
5.2.4 回收使用完的连接
当我们使用完一条连接时,应当及时关闭或释放连接,以便连接可以返回池中重复利用。我们可以通过Connection对象的Close或Dispose方法,也可以通过C#的using语句来关闭连接。
5.3 说说几个非常重要属性
连接池的行为可以通过连接字符串来控制,主要包括四个重要的属性:
Connection Timeout:连接请求等待超时时间。默认为15秒,单位为秒。
Max Pool Size: 连接池中最大连接数。默认为100。
Min Pool Size: 连接池中最小连接数。默认为0。
Pooling: 是否启用连接池。ADO.NET默认是启用连接池的,因此,你需要手动设置Pooling=false来禁用连接池。
SqlConnectionStringBuilder connStr = new SqlConnectionStringBuilder(); connStr.DataSource = @".SQLEXPRESS"; connStr.InitialCatalog = "master"; connStr.IntegratedSecurity = true; connStr.Pooling = true; //开启连接池 connStr.MinPoolSize = 0; //设置最小连接数为0 connStr.MaxPoolSize = 50; //设置最大连接数为50 connStr.ConnectTimeout = 10; //设置超时时间为10秒 using( SqlConnection conn = new SqlConnection(connStr.ConnectionString)) { ;//todo }
5.4 连接池异常与处理方法
当用户打开一个连接而没有正确或者及时的关闭时,经常会引发“连接泄露”问题。泄露的连接,会一直保持打开状态,直到调用Dispose方法,垃圾回收器(GC)才关闭和释放连接。与ADO不同,ADO.NET需要手动的关闭使用完的连接。一个重要的误区是:当连接对象超出局部作用域范围时,就会关闭连接。实际上,当超出作用域时,释放的只是连接对象而非连接资源。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Data; using System.Data.SqlClient; namespace ConnectionPool { class Program { static void Main(string[] args) { SqlConnectionStringBuilder connStr = new SqlConnectionStringBuilder(); connStr.DataSource = @".SQLEXPRESS"; connStr.InitialCatalog = "master"; connStr.IntegratedSecurity = true; connStr.MaxPoolSize = 5;//设置最大连接池为5 connStr.ConnectTimeout = 1;//设置超时时间为1秒 SqlConnection conn = null; for (int i = 1; i <= 100; ++i) { conn = new SqlConnection(connStr.ConnectionString); try { conn.Open(); Console.WriteLine("Connection{0} is linked",i); } catch(Exception ex) { Console.WriteLine(" 异常信息: {0}",ex.Message); break; } } Console.Read(); } } }
连接池的最大连接数为5,当创建第6条连接时,由于连接池中连接数量已经达到了最大数并且没有空闲的连接,因此需要等待连接直到超时。当超过超时时间时,就出现了连接异常。因此,使用完的连接应当尽快的正确的关闭和释放。
5.5 高效使用连接池的基本原则
用好连接池将会大大提高应用程序的性能。相反,如果使用不当的话,则百害而无一益。一般来说,应当遵循以下原则:
- 在最晚的时刻申请连接,在最早的时刻释放连接。
- 关闭连接时先关闭相关用户定义的事务。
- 确保并维持连接池中至少有一个打开的连接。
- 尽力避免池碎片的产生。主要包括集成安全性产生的池碎片以及使用许多数据库产生的池碎片。
6.Command对象与数据检索
6.1 什么是Command对象
我们知道ADO.NET最主要的目的对外部数据源提供一致的访问。而访问数据源数据,就少不了增删查改等操作。尽管Connection对象已经我们连接好了外部数据源,但它并不提供对外部数据源的任何操作。Command对象封装了所有对外部数据源的操作(包括增、删、查、改等SQL语句与存储过程),并在执行完成后返回合适的结果。与Connection对象一样,对于不同的数据源,ADO.NET提供了不同的Command对象。具体来说,可分为以下Command对象。
.NET数据提供程序 | 对应Command对象 |
---|---|
用于 OLE DB 的 .NET Framework 数据提供程序 | OleDbCommand对象 |
用于 SQL Server 的 .NET Framework 数据提供程序 | SqlCommand对象 |
用于 ODBC 的 .NET Framework 数据提供程序 | OdbcCommand 对象 |
用于 Oracle 的 .NET Framework 数据提供程序 | OracleCommand 对象 |
不管是哪种Command对象,它都继承于DBCommand类。与DBConnection类一样,DBCommand类也是抽象基类,不能被实例化,并期待派生类(对应于特定.NET数据提供程序的Command类)来实现方法。
DBCommand类定义了完善的健全的数据库操作的基本方法和基本属性,它的结构如下:
public abstract class DbCommand : Component, IDbCommand, IDisposable
从上面我们可以知道,它继承了Component类以及IDbCommand接口和IDisposable接口。
6.2 必须掌握的几个属性
CommandText: 获取或设置对数据源执行的文本命令。默认值为空字符串。
CommandType: 命令类型,指示或指定如何解释CommandText属性。CommandType属性的值是一个枚举类型,定义结构如下:
public enum CommandType { Text = 1, //SQL 文本命令。(默认。) StoredProcedure = 4, // 存储过程的名称。 TableDirect = 512 //表的名称。 }
需要特别注意的是,将CommandType 设置为 StoredProcedure 时,应将 CommandText 属性设置为存储过程的名称。 当调用 Execute 方法之一时,该命令将执行此存储过程。
Connection: 设置或获取与数据源的连接。
Parameters: 绑定SQL语句或存储过程的参数。参数化查询中不可或缺的对象,非常重要。
Tranction: 获取或设置在其中执行 .NET Framework 数据提供程序的 Command 对象的事务。
6.3 必须掌握的几个方法
ExecuteNonQuery: 执行不返回数据行的操作,并返回一个int类型的数据。
注意:对于 UPDATE、INSERT 和 DELETE 语句,返回值为该命令所影响的行数。 对于其他所有类型的语句,返回值 为 -1。
ExecuteReader: 执行查询,并返回一个 DataReader 对象。
ExecuteScalar: 执行查询,并返回查询结果集中第一行的第一列(object类型)。如果找不到结果集中第一行的第一列,则返回 null 引用。
6.4 如何创建Command对象
可用通过string字符串来构造一条SQL语句,也可以通过Connection对象指定连接的数据源。将这些信息交给Command对象,一般来说,有两种方法(相对于SQL Server):
(1)通过构造函数。代码如下:
string strSQL = "Select * from tb_SelCustomer"; SqlCommand cmd = new SqlCommand(strSQL, conn);
(2)通过Command对象的属性。代码如下:
SqlCommand cmd = new SqlCommand(); cmd.Connection = conn; cmd.CommandText = strSQL;
6.5 选择合适的执行命令
Command对象提供了丰富的执行命令操作,用户对数据源的操作不外乎CRUD-S(Create、Update、Delete、Select)操作。下面将探讨如何在不同的场景选择合适的执行命令。
(1)场景一:执行CRUD操作,不返回数据行,返回影响的行数(可选)
对数据表的行(记录)进行增加,删除,更新操作或者处理数据定义语句(比如用Create Table来创建表结构)时,实际上数据库是不返回数据行的,仅仅返回一个包含影响行数信息的整数。一般地,在执行非查询操作时,我们需要调用ExcuteNonQuery方法。
在tb_SelCustomer表中插入一行记录,代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Data;//必须引入 using System.Data.SqlClient;//必须引入 namespace Command { class Program { static void Main(string[] args) { string connSQL = @"Data Source=.SQLEXPRESS; Initial Catalog=db_MyDemo; Integrated Security=SSPI";//构造连接字符串 SqlConnectionStringBuilder connStr = new SqlConnectionStringBuilder(connSQL); using(SqlConnection conn = new SqlConnection(connStr.ConnectionString)) { //拼接SQL语句 StringBuilder strSQL = new StringBuilder(); strSQL.Append("insert into tb_SelCustomer "); strSQL.Append("values("); strSQL.Append("'liuhao','0','0','13822223333','liuhaorain@163.com','广东省深圳市宝安区',12.234556,34.222234,'422900','备注信息')"); Console.WriteLine("Output SQL: {0}",strSQL.ToString()); //创建Command对象 SqlCommand cmd = new SqlCommand(); cmd.Connection = conn; cmd.CommandType = CommandType.Text; cmd.CommandText = strSQL.ToString(); try { conn.Open();//一定要注意打开连接 int rows = cmd.ExecuteNonQuery();//执行命令 Console.WriteLine(" Result: {0}行受影响",rows); } catch(Exception ex) { Console.WriteLine(" Error: {0}", ex.Message); } } Console.Read(); } } }
(2)场景二:执行Select操作,返回多个数据
通过执行Select操作返回一行或多行数据时,就需要使用ExcuteReader方法。ExcuteReader方法返回一个DataReader对象。DataReader是一个快速的,轻量级,只读的遍历访问每一行数据的数据流。使用DataReader时,需要注意以下几点:
- DataReader一次遍历一行数据,并返回一个包含列名字集合。
- 第一次调用Read()方法获取第一行数据,并将游标指向下一行数据。当再次调用该方法时候,将读取下一行数据。
- 当检测到不再有数据行时,Read()方法将返回false。
- 通过HasRows属性,知道查询结果中是否有数据行。
- 当使用完DataReader时,一定要注意关闭。SQL Server默认只允许打开一个DataReader。
查询出tb_SelCustomer表中所有的数据。代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Data; using System.Data.SqlClient; namespace Command2 { class Program { static void Main(string[] args) { //构造连接字符串 SqlConnectionStringBuilder strConn = new SqlConnectionStringBuilder(); strConn.DataSource = @"(local)SQLEXPRESS"; strConn.InitialCatalog = "db_MyDemo"; strConn.IntegratedSecurity = true; using (SqlConnection conn = new SqlConnection(strConn.ConnectionString)) { string strSQL = "select * from tb_SelCustomer"; SqlCommand cmd = new SqlCommand(strSQL, conn);//使用构造函数方式创建Command对象 conn.Open();//记得打开连接 try { SqlDataReader reader = cmd.ExecuteReader();//执行ExecuteReader if (reader != null && reader.HasRows) { int rows = 0;//记录行数 Console.WriteLine("**********Records of tb_SelCustomer********** "); while (reader.Read()) { for (int i = 0; i < reader.FieldCount; ++i) { Console.WriteLine("{0}:{1}", reader.GetName(i), reader.GetValue(i)); } ++rows; } Console.WriteLine(" 共{0}行记录", rows); } reader.Close();//关闭DataReader } catch (Exception ex) { Console.WriteLine(" Error: {0}", ex.Message); } } Console.Read(); } } }
(3)场景三:执行Select操作,返回单个值
当在操作数据库时仅仅只需要返回一个值(比如返回行数),使用ExcuteScalar方法就是处理单个数据。ExcuteScalar返回一个System.Object类型的数据,因此在获取数据时需要进行强制类型转换。当没有数据时,ExcuteScalar方法返回System.DBNull。
获取tb_SelCustomer中的行数。代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Data; using System.Data.SqlClient; namespace Command3 { class Program { static void Main(string[] args) { string connSQL = @"Data Source=.SQLEXPRESS; Initial Catalog=db_MyDemo; Integrated Security=SSPI"; using (SqlConnection conn = new SqlConnection(connSQL)) { string strSQL = "select count(*) from tb_SelCustomer"; SqlCommand cmd = new SqlCommand(strSQL, conn);//创建Command对象 try { conn.Open();//一定要注意打开连接 int rows = (int)cmd.ExecuteScalar();//执行命令 Console.WriteLine("执行ExcuteScalar方法:共{0}行记录", rows); } catch (Exception ex) { Console.WriteLine(" Error: {0}", ex.Message); } } Console.Read(); } } }
7. Command对象高级应用
7.1 异步执行命令
异步执行的根本思想是,在执行命令操作时,无需等待命令操作完成,可以并发的处理其他操作。ADO.NET提供了丰富的方法来处理异步操作,BeginExecuteNonQuery和EndExcuteNonQuery就是一对典型的为异步操作服务的方法。BeginExecuteNonQuery方法返回System.IAsyncResult接口对象。可以根据IAsyncResult的IsCompleted属性来轮询(检测)命令是否执行完成。
将在tb_SelCustomer中插入500行数据,并计算执行时间。代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Data;//必须引入 using System.Data.SqlClient;//必须引入 namespace Command { class Program { static void Main(string[] args) { SqlConnectionStringBuilder connStr = new SqlConnectionStringBuilder(); connStr.DataSource = @".SQLEXPRESS"; connStr.IntegratedSecurity = true; connStr.InitialCatalog = "db_MyDemo"; connStr.AsynchronousProcessing = true;//必须显示说明异步操作 StringBuilder strSQL = new StringBuilder(); //插入100个测试客户 for (int i = 1; i <= 500; ++i) { strSQL.Append("insert into tb_SelCustomer "); strSQL.Append("values('"); string name = "测试客户" + i.ToString(); strSQL.Append(name); strSQL.Append("','0','0','13822223333','liuhaorain@163.com','广东省深圳市宝安区',12.234556,34.222234,'422900','备注信息'); "); } using (SqlConnection conn = new SqlConnection(connStr.ConnectionString)) { conn.Open(); SqlCommand cmd = new SqlCommand(strSQL.ToString(), conn); IAsyncResult pending = cmd.BeginExecuteNonQuery();//开始执行异步操作 double time = 0; //检查异步处理状态 while (pending.IsCompleted == false) { System.Threading.Thread.Sleep(1); time++; Console.WriteLine("{0}s", time * 0.001); } if (pending.IsCompleted == true) { Console.WriteLine("Data is inserted completely... Total coast {0}s", time * 0.001); } cmd.EndExecuteNonQuery(pending);//结束异步操作 } Console.Read(); } } }
7.2 使用参数化查询
在ADO.NET中,查询语句是以字符串的形式传递给外部数据库服务器的。这些字符串不仅包含了基本命令关键字,操作符,还包含了限制查询的数值。与其他编程语言不同,.NET是基于强类型来管理查询字符串数据的。通过提供类型检查和验证,命令对象可使用参数来将值传递给 SQL 语句或存储过程。
与命令文本不同,参数输入被视为文本值,而不是可执行代码。这样可帮助抵御“SQL注入”攻击,这种攻击的攻击者会将命令插入SQL语句,从而危及服务器的安全。参数化命令还可提高查询执行性能,因为它们可帮助数据库服务器将传入命令与适当的缓存查询计划进行准确匹配。
对于不同的数据源来说,Parameter对象不同,但都派生自DbParameter对象。下表列举了不同数据源对应的Parameter对象。
数据提供程序 | 对应Paramter对象 | 命名空间 |
---|---|---|
SQLServer 数据源 | 使用SqlParamter对象 | System.Data.SqlClient.SqlParameter |
Ole DB 数据源 | 使用OleDbParameter对象 | System.Data.OleDb.OleDbParameter |
ODBC 数据源 | 使用OdbcParamter对象 | System.Data.Odbc.OdbcParameter |
Oracle数据源 | 使用OracleParameter对象 | System.Data.OracleClient.OracleParameter |
Paramter对象的属性很多,其中常见而且非常重要的主要有以下几个:
DbType: 获取或设置参数的数据类型。
Direction: 获取或设置一个值,该值指示参数是否只可输入、只可输出、双向还是存储过程返回值参数。
IsNullable: 获取或设置一个值,该值指示参数是否可以为空。
ParamteterName: 获取或设置DbParamter的名称。
Size: 获取或设置列中数据的最大大小。
Value: 获取或设置该参数的值。
以SQL Server为例,SqlCommand对象包含一个Paramters集合,Paramters集合中包含了所有所需的SqlParamter对象。当执行命令时,ADO.NET同时将SQL文本,占位符和参数集合传递给数据库。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Data; using System.Data.SqlClient; namespace Command2 { class Program { static void Main(string[] args) { //构造连接字符串 SqlConnectionStringBuilder connStr = new SqlConnectionStringBuilder(); connStr.DataSource = @".SQLEXPRESS"; connStr.IntegratedSecurity = true; connStr.InitialCatalog = "db_MyDemo"; //拼接SQL语句 StringBuilder strSQL = new StringBuilder(); strSQL.Append("Update tb_SelCustomer Set "); strSQL.Append("Phone = @Phone,"); strSQL.Append("Email = @Email,"); strSQL.Append("ContactAddress = @Address "); strSQL.Append("where Name = @Name"); using (SqlConnection conn = new SqlConnection(connStr.ConnectionString)) { SqlCommand cmd = new SqlCommand(strSQL.ToString(), conn); //构造Parameter对象 SqlParameter[] paras = new SqlParameter[]{ new SqlParameter("@Phone", SqlDbType.VarChar, 12), new SqlParameter("@Email", SqlDbType.VarChar, 50), new SqlParameter("@Address", SqlDbType.VarChar, 200), new SqlParameter("@Name", SqlDbType.VarChar, 20) }; //给Parameter对象赋值 paras[0].Value = "18665691100"; paras[1].Value = "test@163.com"; paras[2].Value = "中国深圳市南山区"; paras[3].Value = "测试客户1"; //遍历添加到Parameters集合中 foreach (var item in paras) { cmd.Parameters.Add(item); } try { conn.Open(); cmd.ExecuteNonQuery(); Console.WriteLine("Update Success..."); } catch (Exception ex) { Console.WriteLine("{0}", ex.Message); } } Console.Read(); } } }
7.3 如何获取插入行的ID
很多时候,需要知道插入行的ID是多少,以方便进行利用插入行的ID进行其他操作,比如在页面上的展示等等。OUTPUT关键字返回INSERT操作的一个字段(一般是主键ID)。因此只要结合OUTPUT关键字以及ExecuteScalar方法,就很容易得到插入行的主键。
在tb_SelCustomer中插入一个新的顾客,并返回这个顾客的ID。代码如下:
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Data; using System.Data.SqlClient; namespace Command2 { class Program { static void Main(string[] args) { //构造连接字符串 SqlConnectionStringBuilder connStr = new SqlConnectionStringBuilder(); connStr.DataSource = @".SQLEXPRESS"; connStr.IntegratedSecurity = true; connStr.InitialCatalog = "db_MyDemo"; //拼接SQL语句 StringBuilder strSQL = new StringBuilder(); strSQL.Append("insert tb_SelCustomer(Name) "); strSQL.Append("OUTPUT inserted.ID values(@Name)"); using (SqlConnection conn = new SqlConnection(connStr.ConnectionString)) { SqlCommand cmd = new SqlCommand(strSQL.ToString(), conn); SqlParameter para = new SqlParameter("@Name", SqlDbType.VarChar, 20); para.Value = "Kemi"; cmd.Parameters.Add(para); try { conn.Open(); int insertedID = (int)cmd.ExecuteScalar();//获取单个值 Console.WriteLine("Inserted ID:{0}", insertedID); } catch (Exception ex) { Console.WriteLine("{0}", ex.Message); } } Console.Read(); } } }
8.理解DataAdapter
8.1 认识DataAdapter
前面所讲的对象中,譬如Connection对象,Command对象以及DataReader对象,这些对象均属于Data Provider的一部分,而且都是基于连接的。拥有强大功能的它们,可以很轻松地连接一个特定的数据源,执行SQL语句,检索只读的数据流等等。这些基于连接的对象都对应于特定的数据源。换句话说,对于不同的数据源,我们需要找到对应的数据库提供程序(Data Provider)来匹配它们。ADO.NET提供了基于非连接的核心组件:DataSet。DataSet组件可以在内存中操作以表为中心的数据集合,就好比操作数据库中的表一样。尽管DataSet没有直接连接数据库,但是,ADO.NET早就为DataSet准备了DataApdater。DataApater数据适配器,将从外部数据源检索到的数据合理正确的调配到本地的DataSet集合中。
8.2 DataAdapter的工作原理
DataApapter本质上就是一个数据调配器。当需要查询数据时,它从数据库检索数据,并填充要本地的DataSet或者DataTable中;当需要更新数据库时,它将本地内存的数据路由到数据库,并执行更新命令。下面以Customer表为例,来理解DataAdapter的工作原理。下图详细描述了一个DataAdapter的工作过程。
从上图可以知道,当查询Customer信息时,DataAdapter首先将构造一个SelectCommand实例(本质就一个Command对象),然后检查是否打开连接,如果没有打开连接则打开连接,紧接着调用DataReader接口检索数据,最后根据维护的映射关系,将检索到得数据库填充到本地的DataSet或者DataTable中。同理,当需要更新数据源时,DataAdatper则将本地修改的数据,跟据映射关系,构造InsertCommand,UpdateCommnad,DeleteCommand对象,然后执行相应的命令。
之所以说,DataAdapter是最复杂的ADO.NET组件,是因为它是架构在所有其他DataProvider对象之上的。Connection对象,DataReader对象,Paramter对象以及Command对象,都尽可能的为它服务。
总体来说,DataAdapter主要有三大功能:
数据检索:尽可能用最简单的方法填充数据源到本地DataSet或者DataTable中。细致的说,DataAdapter用一个DataReader实例来检索数据,因此必须提供一个Select查询语句以及一个连接字符串。
数据更新:将本地修改的数据返回给外部的数据源相对来说稍微复杂一点。即使,从数据库查询数据时,仅仅只需要一条基本的Select语句,而更新数据库则需要区分Insert,Update,Delete语句。
表或列名映射:维护本地DataSet表名和列名与外部数据源表名与列名的映射关系。
8.3 DataAdapter的重要成员
作为DataProvider对象成员之一,DataAdapter跟其他数据提供对象具有相似的特征:都是基于连接的,都继承于基类,不同的数据源都对应自己的派生版本。DataAdapter的基类是DBDataAdapter,它的结构如下:
public abstract class DbDataAdapter : DataAdapter, IDbDataAdapter, IDataAdapter, ICloneable
从上面可以看到,DBDataAdapter是一个抽象基类,不能被实例化,并且继承DataAdapter类,IDBDataApdater,IdataAdapter以及Icloneable接口。DataAdapter成员较多,必须掌握的有以下几种:
SelectComand属性:获取或设置用于在数据源选择记录的命令。
UpdateCommand属性:获取或这只用于更新数据源中的记录的命令。
DeleteCommand属性:获取或设置用于从数据源中删除记录的命令。
InsertCommand属性:获取或设置用于将新记录插入数据源中的命令。
Fill方法:填充数据集。
Update方法:更新数据源。
8.4 如何构造一个DataAdapter对象
在讲如何用DataAdapter获取数据之前,先讨论一个问题:如何构造一个DataApdater对象?我们知道DataAdapter是一个类,那么如何创建一个类的对象呢(再次强调,本系列教程讲的比较基础,主要针对初学者)?呵呵,当然是用构造函数啦!因此,我们有必要了解一下DataAdapter的构造函数。通过前面的知识,我们已经很清晰的知道,对于不同的数据源,ADO.NET提供了不同的Data Provider。以SQL Server数据库为例,它拥有的DataAdapter为SqlDataAdapter类,它包括以下构造函数:
public SqlDataAdapter(); public SqlDataAdapter(SqlCommand selectCommand); public SqlDataAdapter(string selectCommandText, SqlConnection selectConnection); public SqlDataAdapter(string selectCommandText, string selectConnectionString);
对于构造函数的理解,我们主要看他包含哪些参数。从上面可以知道,SqlDataAdapter类包含4个显式的构造函数。因此,我们必须用上述4个构造函数中的一种来实例化SqlDataAdapter类(创建SqlDataAdapter对象)。
8.4.1 SqlDataAdapter()
第一个构造函数相对来说比较简单,它不包含参数。
SqlDataAdapter ada = new SqlDataAdapter();
8.4.2 SqlDataAdapter(SqlCommand selectCommand)
第二个构造函数稍微复杂点,它包含了一个SqlCommand类型的参数。
SqlCommand cmd = new SqlCommand(); SqlDataAdapter ada = new SqlDataAdapter(cmd);
8.4.3 SqlDataAdapter(string selectCommandText, SqlConnection selectConnection)
第三个构造函数,它包含了2个参数。第一个参数是字符串类型,它接受一个查询指令。第2个参数是SqlConnection类型,它表示一个连接对象。
string selCmdStr = "select * from tb_SelCustomer"; SqlConnection conn = new SqlConnection(); SqlDataAdapter ada = new SqlDataAdapter(selCmdStr, conn);
8.4.4 SqlDataAdapter(string selectCommandText, string selectConnectionString)
最后一个构造函数,它也包含了2个参数。2个参数均为字符串类型:第一个参数,它接受一个查询指令;第二个参数,它表示一个连接字符串。
string selCmdStr = "select * from tb_SelCustomer"; string connStr = @"Data Source=.SQLEXPRESS; Initial Catalog=db_MyDemo; Integrated Security=SSPI";//构造连接字符串 SqlDataAdapter ada = new SqlDataAdapter(selCmdStr, connStr);
8.5 填充数据到DataSet
DataAdapter对象有一个非常重要的功能就是填充数据到DataSet。SqlDataAdapter类除了有4个构造函数之外,还包含多个Fill()方法(方法重载)。一般来说,用的比较多的是int Fill(DataTable dt), int Fill(DataSet ds),这2个方法分别接受 DataTable 参数和 DataSet 参数。拥有Fill()方法,可以轻松地将外部数据源中的数据填充到DataSet中。
using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Data; using System.Data.SqlClient; namespace DataAdapter2 { class Program { static void Main(string[] args) { string connStr = @"Data Source=.SQLEXPRESS; Initial Catalog=db_MyDemo; Integrated Security=SSPI";//连接字符串 string selCmdStr = "select * from tb_SelCustomer";//查询指令 using (SqlConnection conn = new SqlConnection(connStr)) { SqlDataAdapter ada = new SqlDataAdapter(selCmdStr, conn); DataSet ds = new DataSet(); ada.Fill(ds); if (ds.Tables.Count > 0) { PrintDataTable(ds.Tables[0]); } } Console.Read(); } // 打印出DataTable中的内容 static void PrintDataTable(DataTable dt) { int col = dt.Columns.Count; foreach (DataRow row in dt.Rows) { for (int i = 0; i < col; ++i) { Console.Write("{0} ", row[i]); } } } } }
原文作者 木小楠 博客 https://www.cnblogs.com/liuhaorain