ADO.NET 架構
ADO.NET 數據提供程序
數據提供程序是一組用於訪問特定數據庫,執行SQL命令並獲取值的ADO.NE類,就其本質而言,數據提供程序是應用程序和數據元之間的一座橋梁。
數據提供程序包括以下幾個類:
- Connection:建立和數據源的連接
- Command:執行SQL命令和存儲過程
- DataReader:提供對查詢結果的快速只讀,只進的訪問方式,它是保持連接的處理方式
- DataAdapter:從數據源獲得信息填充到DataSet,依照DataSet的修改更新數據源,它是一系列表和關系的集合,它是斷開連接的處理方式
ADO.NET沒有提供通用的數據提供程序,它只為不同數據源和特別設計不同數據提供程序。每個數據提供程序包含為特定 RDBMS(關系型數據庫管理系統)特別實現的 Connection、Command、DataReader 類。
.NET Framework 提供4個提供程序:
- SQL Server:提供對 SQL Server 數據庫的優化訪問
- OLE DB:提供對有 OLE DB 驅動的任意數據源的訪問
- Oracle:提供對 Oracle 數據庫訪問
- ODBC:提供對有 ODBC 驅動的任意數據源訪問
自 .NET 4 開始,Oracle 提供程序被廢棄,雖然仍可以使用它,但微軟推薦使用第三方 ADO.NET 提供程序訪問 Oracle 數據庫,比如 Oracle 發布的 ODP.NET,它為特殊的 Oracle 數據類型【如 LOB(大對象)、時間戳、XML 數據】提供更豐富的支持,此外還有一些特性。
ADO.NET 的標准化
初看起來,ADO.NET 似乎提供了一個松散的模型,它沒有提供能夠和不同類型的數據庫一起工作的通用對象,這樣,如果換了 RDBMS,就需要修改數據訪問代碼以實用不同的類。(不同數據提供程序使用完全不同的底層方法和API,各有各的特色和優化,因此也無法通用)。這個模型的好處和效果也許不是那么明顯,但確實有好處:
- 每個提供程序使用相同的接口和基類,所以基於接口而不是基於提供程序類的編碼仍可以寫出通用的數據訪問代碼。(但要付出一些額外的代價)
- 每個提供程序分別獨立實現,擁有各自的優化
- 自定義提供程序還可以加入其他提供程序沒有的非標准特性(如 SQL Server 執行 XML 查詢的能力)
基本 ADO.NET 類
ADO.NET 有兩種類型的對象:
- 基於連接的對象:如 Connection、Command、DataReader 和 DataAdapter,它們連接到數據庫,執行 SQL 語句,基於連接的對象是針對具體數據源類型的,並且可以在各自的命名空間中(例如SQL Server 提供程序的 System.Data.SqlClient)找到。
- 基於內容的對象:包括 DataSet、DataColumn、DataRow、DataRelation等,它們完全和數據源獨立,出現在 System.Data 命名空間里。
ADO.NET 支持的最重要命名空間:
System.Data | 關鍵數據容器類。包括列、關系、表、數據集、行、視圖和約束建立模型。 |
System.Data.Common | 包括大部分基本的抽象類,這些類實現 System.Data 中的某些接口並定義了 ADO.NET的核心功能。 數據提供程序繼承這些類來創建它們自己的版本。 |
System.Data.OleDb | 包含用於連接 OLE DB提供程序的類。這些類支持大部分 OLE DB 提供程序。 |
System.Data.SqlClient | 包含用於連接微軟 SQL Server 數據庫所需的類,這些類經過優化以便使用 SQL Server 的 TDS 接口。 |
System.Data.OracleClient | 包含用於連接 Oracle 數據庫的類,這些類使用經過優化的 Oracle 調用接口(OCI) |
System.Data.Odbc | 包含連接大部分 ODBC 驅動所需的類。 所有數據源都包含 ODBC 驅動,並可以通過“控制面板”中的“數據源”快捷方式配置。 |
System.Data.SqlTypes | 包含 SQL Server 本地數據類型相對應的類型。 這些類不是必須的,但它們提供了一種使用標准.NET數據類型的選擇,這是自動類型轉換時所必需的。 |
Connection 類
Connection類用於和要交互的數據源建立連接,在執行任何操作前(增、刪、改、查)必須建立連接。
連接字符串
創建 Connection 對象時,必須先提供連接字符串。連接字符串是以分號(;)分隔的一系列 名稱/值 對的選項,這些選項的順序並不重要,大小寫也不重要。
盡管隨着 RDBMS 和提供程序的不同,連接字符串也不同,但基本需要的信息如下:
- 數據庫所在的服務器
- 要使用的數據庫名稱
- 如何通過數據庫驗證
比如下面這個連接字符串使用整合安全(用當前登錄windows用戶身份訪問數據庫)方式連接本機的 Northwind 數據庫:
string connStr = "Data Source=localhost;Initial Catalog=Northwind;Integrated Secutiry=SSPI" // SSPI 等同於 True
如果數據庫不支持整合安全,就必須指定有效的用戶名和密碼,例如:
string connStr = "Data Source=localhost;Initial Catalog=Northwind;user id=sa;password=xxx"
若使用OLE DB提供程序,連接字符串和前面的相似,但需要額外添加一個提供程序設置來標識 OLE DB 驅動
string connStr = "Data Source=localhost;Initial Catalog=Northwind;user id=sa;password=xxx;Provider=MSDAORA"
通過 MSDAORA OLE DB 提供程序訪問 Oracle 數據庫
連接 Access 數據庫
string connStr="Provider=Microsoft.Jet.OLEDB.4.0;Data Source=C:\DataSources\Northwind.mdb"
沒有任何理由硬編碼數據庫連接字符串,web.config 文件的 <connectionStrings> 節便於保存和隨時修改連接字符串:
<connectionStrings>
<add name="Northwind" connectionString="Data Source=localhost;Initial Catalog=Northwind;Integrated Secutiry=SSPI"/>
</connectionStrings>
接着可以使用名稱從 WebConfigurationManager.ConnectionStrings 集合中讀取連接字符串(導入 System.Web.Configuration 命名空間):
string connStr = WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
測試連接
protected void Page_Load(object sender, EventArgs e)
{
string connStr = WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
SqlConnection conn = new SqlConnection(connStr);
try
{
conn.Open();
lblInfo.Text = "<b>Server Version:</b> " + conn.ServerVersion;
lblInfo.Text += "<br /><b>Connection State:</b> " + conn.State.ToString();
}
catch (Exception err)
{
lblInfo.Text = "Error reading the database. " + err.Message;
}
finally {
conn.Close();
lblInfo.Text += "<br /><b>Connection State:</b> " + conn.State.ToString();
}
}
連接時有限的服務器資源,要盡量晚打開而盡早釋放。finally 塊確保了連接能夠被關閉,否則如果發生了異常,連接將一直保持到垃圾回收器釋放 SqlConnection 對象。
另一種方法是把數據訪問代碼放入到 using 塊中。using 語句用來聲明正在短期使用的一個可釋放的對象,最妙的是你不編寫finally塊,即使由於未處理的異常而退出該塊,using 語句也會釋放正在使用的對象。
string connStr = WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
SqlConnection conn = new SqlConnection(connStr);
using (conn)
{
conn.Open();
lblInfo.Text = "<b>Server Version:</b> " + conn.ServerVersion;
lblInfo.Text += "<br /><b>Connection State:</b> " + conn.State.ToString();
}
lblInfo.Text += "<br /><b>Connection State:</b> " + conn.State.ToString();
連接池
雖然請求連接耗時很短,但它們確實需要時間。在 Web 應用程序中,隨着請求的處理連接被不斷的打開和關閉,每個請求都被高效處理。在這樣的環境中,即使建立連接所需要的微小消耗也會顯著影響系統性能。
一個解決辦法是使用連接池。連接池保證已經打開的數據庫連接,這些連接在使用相同數據源的會話間共享,這樣就省了不斷創建和銷毀連接的時間。當客戶用 Open()方法請求打開連接時,連接直接由連接池提供而不是再次創建。當客戶調用 Close()方法或 Dispose()方法釋放連接時,它並沒有被真的釋放而是重新回到池中等待下一次請求。
ADO.NET 並不包含任何連接池的機制,但是大部分數據庫提供程序提供對連接池的實現。
為了使用 SQL Server 或 Oracle 的連接池,連接字符串必須完全匹配。即使只有微小的差異(哪怕只是修改了參數的順序或是多了一個空格),也會在新池中創建新連接,這也是不要硬編碼連接字符串的原因。
SQL Server 和 Oracle 提供程序都是自動使用連接池的。
Max Pool Size | 池中允許最大連接數(默認 100),如已達最大數,所有打開連接請求將排隊等候 |
min Pool Size | 池中最小連接數(默認 0),第一次打開時會建立相應數量的連接,所以第一次請求時會稍微有點延遲 |
Pooling | 為 true(默認值)時,連接從池中獲取 |
Connection Lifetime | 指定以秒為單位的時間間隔,默認是 0 。池中的連接創建時間早於指定生命周期,將被銷毀。 當需要大量回收連接時,該功能很有效 |
下面這個連接字符串設置最小連接池的大小:
string connStr = "Data Source=localhost;Initial Catalog=Northwind;Integrated Security=SSPI;Min Pool Size=10"
Command 類和 DataReader 類
Command 基礎
Command 類可以執行所有類型的 SQL 語句。雖然它也可以執行 數據定義 任務(創建或修改數據庫、表和索引),但一般被用來執行數據操作任務(增刪改查)。
使用命令前,需要設置命令文本(CommandText)、命令類型(CommandType)並把命令綁定到連接(Connection)上,或者作為構造函數的參數傳遞。
SqlCommand cmd = new SqlCommand();
cmd.Connection = conn;
cmd.CommandType = System.Data.CommandType.Text;
cmd.CommandText = "SELECT * FROM Employees";
傳參的構造方法效率更高一些:
SqlCommand cmd = new SqlCommand("SELECT * FROM Employees", conn);
也可以使用存儲過程:
SqlCommand cmd = new SqlCommand("GetEmployees", conn);
cmd.CommandType = System.Data.CommandType.StoredProcedure;
這些只是定義了 Command 對象而沒有真正的執行該對象,Command 對象的方法如下:
ExecuteNonQuery() | 執行非 SELECT 語句,如增、刪、改。返回受影響的行數。 也可以執行數據定義命令(表的創建,索引,約束等) |
ExecuteScalar() | 執行 SELECT 查詢,返回第一行第一列的值,類型為 Object,常用語執行聚合函數 |
ExecuteReader() | 執行 SELECT 查詢,返回一個封裝了只讀,只進游標的 DataReader 對象 |
DataReader 類
允許以只進,只讀流的方式每次讀取一條 SELECT 命令返回的記錄,這種方式有時候成為流水游標。
Read() | 將行游標前進到流的下一行,在讀取第一條記錄前,也必須調用這個方法,該方法返回一個 bool 值指示是否還有下一行 |
GetValue() | 根據序號或字段名得到當前行中該字段值。基於名稱的查詢更易讀,但效率不高。 |
GetValues() | 將當前行中的值保存到數組中,保存的記錄數取決於你傳遞給該方法的數組的大小。 可以使用 DataReader 對象的 FieldCount 屬性確定一行的列數。 |
GetInt32() GetChars() GetDateTime() GetXXX() |
返回指定序號的字段值,返回類型和方法名稱一致,如果類型不一致會得到一個 InvalidCastException 異常 |
NextResult() | 如果有多個記錄集,該方法將游標移動到下一個記錄集。 |
Close() | 關閉 Reader。如果原命令執行一個帶有輸出參數的存儲過程,該參數僅在 Reader 關閉后才刻度。 |
ExecuteReader()方法和 DataReader 對象示例:
protected void Page_Load(object sender, EventArgs e)
{
string connStr = WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
SqlConnection conn = new SqlConnection(connStr);
string sql = "SELECT * FROM Employees"; // 實際開發請查詢僅需要的列
SqlCommand cmd = new SqlCommand(sql, conn);
conn.Open();
SqlDataReader reader = cmd.ExecuteReader();
StringBuilder htmlStr = new StringBuilder();
while (reader.Read())
{
htmlStr.Append("<li>");
htmlStr.Append(reader["TitleOfCourtesy"]);
htmlStr.Append("<b>");
htmlStr.Append(reader.GetString(1));
htmlStr.Append("</b>, ");
htmlStr.Append(reader.GetString(2));
htmlStr.Append(" - employee from ");
htmlStr.Append(reader.GetDateTime(6).ToString("d"));
htmlStr.Append("</li>");
}
reader.Close();
conn.Close();
Label1.Text = htmlStr.ToString();
}
1. 空值
下面是一個可空的整形示例:
// 可空整型可以包含任何 Int32 位數值以及 null
int? nullableInteger = null;
// 指示當前的 System.Nullable<T> 對象是否有值
if (nullableInteger.HasValue)
{
nullableInteger += 1;
}
遺憾的是,DataReader 沒有和 .NET 可空值集成,這是出於歷史原因。空數據類型在 .NET2.0第一次引入,那時 DataReader 模型已經成功建立並且很難改變。DataReader 遇到數據里的空值時,它返回一個常量 DBNull.Value,任何試圖轉換它的數據類型操作會引發異常。
必須使用這樣的代碼對其進行檢測:
if (reader["NumberOfHires"] == DBNull.Value)
{
numberOfHires = null;
}
else
{
numberOfHires = (int?)reader["NumberOfHires"];
}
2. 處理多個結果集
命令會在兩種情況下返回多個結果集:
- 調用存儲過程時,該存儲過程有多個 SELECT 語句
- 直接使用文本命令時,可以把用分號(;)分隔的命令批次執行
參考下面的示例:
protected void Page_Load(object sender, EventArgs e)
{
string connStr = WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
SqlConnection conn = new SqlConnection(connStr);
string sql = @"SELECT TOP 5 EmployeeID,LastName,FirstName FROM Employees;
SELECT TOP 5 CustomerID,CompanyName,ContactName FROM Customers;
SELECT TOP 5 SupplierID,CompanyName,ContactName FROM Suppliers";
SqlCommand cmd = new SqlCommand(sql, conn);
conn.Open();
SqlDataReader reader = cmd.ExecuteReader();
StringBuilder htmlStr = new StringBuilder();
int i = 0;
do
{
htmlStr.Append("<h2>Rowset: ");
htmlStr.Append(i.ToString());
htmlStr.Append("</h2>");
while (reader.Read())
{
htmlStr.Append("<li>");
// 獲得所有的列
for (int field = 0; field < reader.FieldCount; field++)
{
htmlStr.Append(reader.GetName(field).ToString());
htmlStr.Append(": ");
htmlStr.Append(reader.GetValue(field).ToString());
htmlStr.Append(" ");
}
htmlStr.Append("</li>");
}
htmlStr.Append("<br /><br />");
i++;
} while (reader.NextResult()); // 前進到下一個結果集,如果沒有返回 false
reader.Close();
conn.Close();
Label1.Text = htmlStr.ToString();
}