.NET基礎拾遺(6)ADO.NET與數據庫開發基礎

Index :html

(1)類型語法、內存管理和垃圾回收基礎程序員

(2)面向對象的實現和異常的處理面試

(3)字符串、集合與流sql

(4)委託、事件、反射與特性數據庫

(5)多線程開發基礎編程

(6)ADO.NET與數據庫開發基礎緩存

(7)WebService的開發與應用基礎安全

1、ADO.NET和數據庫程序基礎

1.1 安身立命之基本:SQL

  SQL語句時操做關係型數據庫的基礎,在開發數據訪問層、調試系統等工做中十分經常使用,掌握SQL對於每個程序員(不管是.NET、Java仍是C++等)都很是重要。這裏挑選了一個常見的面試題目,來熱熱身。服務器

  常見場景:經過SQL實現單錶行列轉換多線程

  行列轉換時數據庫系統中常常遇到的一個需求,在數據庫設計時,爲了適合數據的累積存儲,每每採用直接記錄的方式,而在展現數據時,則但願整理全部記錄而且轉置顯示。下圖是一個行列轉換的示意圖:

  ①好了,廢話很少說,先創建一張表DeptMaterialDetails:

CREATE TABLE [dbo].[DeptMaterialDetails](
    [Id] [int] IDENTITY(1,1) NOT NULL,
    [DeptName] [varchar](50) NULL,
    [MaterialName] [varchar](100) NULL,
    [Number] [int] NULL,
 CONSTRAINT [PK_DeptMaterialDetails] PRIMARY KEY CLUSTERED 
(
    [Id] ASC
)WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
) ON [PRIMARY]
View Code

  ②填充一些測試數據進該表:

      

  ③分析需求,能夠發現但願作的是找出具備相同部門的記錄,並根據其材料的值累加。通過一番折騰,能夠寫出以下SQL語句:

    select DeptName as '部門',
    SUM(case MaterialName when '材料1' then Number else 0 end) as '材料1消耗',
    SUM(case MaterialName when '材料2' then Number else 0 end) as '材料2消耗',
    SUM(case MaterialName when '材料3' then Number else 0 end) as '材料3消耗'
    from DeptMaterialDetails
    group by DeptName

  執行效果以下圖所示,是否是已經完成要求了:

  

  But,根據上述SQL語句,獲得的結果永遠只有3種材料的消耗量,若是新增了材料4,那麼是否是須要改SQL語句?這時候是否是又想起了在實際開發中時常提到的可擴展性?

  ④咱們能夠根據須要動態拼裝一個SQL語句,即動態地根據實際材料數目來獲得最後的查詢語句:

--申明一個字符串用於動態拼接
declare @sql varchar(8000)
--拼接SQL語句
set @sql = 'select DeptName as "部門"'
--動態地得到材料名,爲每一個材料構建一個列
select @sql = @sql + ',SUM(case MaterialName when '''+temp.Item+''' then Number else 0 end) as ['+temp.Item+'消耗]'
from (select distinct MaterialName as Item from DeptMaterialDetails) as temp
--最終拼上數據源和分組依據
select @sql = @sql + ' from DeptMaterialDetails group by DeptName'
--執行SQL語句
exec(@sql)

  執行結果和第一種方式相同,可是須要注意的是:

動態SQL命令的執行效率每每不高,由於動態拼接的緣由,致使數據庫(查詢優化器)可能沒法對這樣的命令進行優化。此外,這樣的SQL命令還受限於字符串的長度(須要事先肯定其長度限制),而動態SQL命令的長度每每是根據實際表的內容而改變,所以這類動態SQL命令沒法保證100%正常運行

1.2 ADO.NET支持哪幾種數據源?

  ADO.NET支持的數據源不少,從類別上來劃分的話能夠大體分爲四類。ADO.NET也正是經過以下所示這四個命名空間來實現對這些數據源的支持的:

  ① System.Data.SqlClient

  這也許是.NET程序員最經常使用的了,由於MSSQL你懂的!固然,這不是鏈接MSSQL的惟一方案,經過OLEDB或者ODBC均可以訪問,可是SqlClient下的組件直接針對MSSQL,所以ADO.NET實際上是爲其專門作了一些優化工做,所以使用MSSQL應該首選 System.Data.SqlClient 命名空間。

  ② System.Data.OracleClient

  顧名思義,這個命名空間針對Oracle數據庫產品,而且還得搭配Oracle數據庫的客戶端組件(Oracle.DataAccess.dll)一塊兒使用。

  ③ System.Data.OleDb

  該命名空間下的組件主要針對OLEDB(Microsoft提供的通向不一樣數據源的低級API)的標準接口,它還能夠鏈接其餘非SQL數據類型的數據源。OLEDB是一種標準的接口,實現了不一樣數據源統一接口的功能。

  ④ System.Data.Odbc

  該命名空間下的組件針對ODBC標準接口。

關於ODBC:開放數據庫互連(Open Database Connectivity,ODBC)是微軟公司開放服務結構(WOSA,Windows Open Services Architecture)中有關數據庫的一個組成部分,它創建了一組規範,並提供了一組對數據庫訪問的標準API(應用程序編程接口)。這些API利用SQL來完成其大部分任務。ODBC自己也提供了對SQL語言的支持,用戶能夠直接將SQL語句送給ODBC。

  整體來講,ADO.NET爲咱們屏蔽了全部的數據庫訪問層次,提供了統一的API給咱們,使咱們無需考慮底層的數據源是具體的DataBase仍是另外一種標準接口。下圖直觀地展現了ADO.NET與可能的數據源的鏈接:

2、ADO.NET和數據庫的鏈接

2.1 簡述數據庫鏈接池的機制

  數據庫鏈接通常都被認爲是一個性能成本相對較大的動做,因此針對數據庫鏈接以及讀寫的優化每每是系統優化的關鍵點。數據庫鏈接池就是一個很是重要的優化機制。

  (1)數據庫鏈接池的基本概念

  數據庫鏈接池,顧名思義就是一個存儲數據庫鏈接的緩衝池,因爲鏈接和斷開一個數據庫的開銷很大(想一想經典的TCP三次握手和四次揮手),反覆鏈接和斷開數據庫對於系統的性能影響將會很是嚴重。而在.NET程序中,有時候是沒法預測下一次數據庫訪問的需求什麼時候到來,因此一般的作法就是在使用完一個鏈接後就當即關閉它,這就須要ADO.NET的內部機制來維護這個訪問池。

  下圖展現了數據庫鏈接池的機制,在該機制中,當一個用戶新申請了一個數據庫鏈接時,當數據庫池內鏈接匹配的狀況下,用戶會從鏈接池中直接得到一個被保持的鏈接。在用戶使用完調用Close關閉鏈接時,鏈接池會將該鏈接返回到活動鏈接池中,而不是真正關閉鏈接。鏈接回到了活動連接池中後,便可在下一個Open調用中重複使用。

  默認狀況下,數據庫鏈接時處於啓用狀態的。咱們也能夠經過數據庫鏈接字符串設置關閉數據庫鏈接池,以下面的代碼所示:

    using (SqlConnection connection = new SqlConnection("Server=127.0.0.1;Initial Catalog=TestDB;Integrated Security=SSPI;Pooling=false"))
    {
        connection.Open();
        // 執行你想要執行的數據庫操做
    }

  其中參數Pooling=false就表明了關閉鏈接池。固然,咱們還能夠設置鏈接池中的最大和最小鏈接數,參數分別對應Max Pool Size和Min Pool Size。

  (2)數據庫鏈接的複用

   因爲數據源和鏈接參數選擇的不一樣,每一個數據庫的鏈接並非徹底通用的。所以,ADO.NET選擇經過鏈接字符串來區分。一旦用戶使用某個鏈接字符串來申請數據庫鏈接,ADO.NET將判斷鏈接池中是否存在擁有相同鏈接字符串的鏈接,若是有則直接分配,沒有則新建鏈接。

  咱們能夠看看下面一段代碼,三個不一樣的鏈接中,第三個複用第一個鏈接,第二個則沒法複用第一個鏈接:

    using (SqlConnection connection = new SqlConnection("Server=127.0.0.1;Initial Catalog=TestDB;Integrated Security=SSPI"))
    {
        // 假設這是啓動後第一個數據庫鏈接請求,一個新鏈接將被創建
        connection.Open();
    }

    using (SqlConnection connection = new SqlConnection("Server=127.0.0.1;Initial Catalog=TestDB1;Integrated Security=SSPI"))
    {
        // 因爲和上一個鏈接的字符串不一樣,所以沒法複用第一個鏈接
        connection.Open();
    }

    using (SqlConnection connection = new SqlConnection("Server=127.0.0.1;Initial Catalog=TestDB;Integrated Security=SSPI"))
    {
        // 鏈接字符串和第一個鏈接相同,保存在鏈接池中的第一個鏈接被複用
        connection.Open();
    }

  (3)不一樣數據源的鏈接池機制

  事實上,ADO.NET組件自己並不直接包含鏈接池,而針對不一樣類別機制的數據源指定不一樣的鏈接池方案。對於SqlClient、OracleClient命名空間下的組件,使用的鏈接池是由託管代碼直接編寫的,能夠理解爲鏈接池直接在.NET框架中運行。而對於OLEDB和ODBC的數據源來講,鏈接池的實現徹底依靠OLEDB和ODBC提供商實現,ADO.NET只與其約定相應規範。

2.2 如何提升鏈接池內鏈接的重用率

  因爲只有相同鏈接字符串才能共享鏈接,所以常常致使鏈接池失效的問題,因此須要提升鏈接池內鏈接的重用率。

  (1)鏈接池重用率低下的緣由

  因爲數據庫鏈接池僅按照數據庫鏈接字符串來判斷鏈接是否可重用,因此鏈接字符串內的任何改動都會致使鏈接失效。就係統內部而言,數據庫鏈接字符串中最常被修改的兩個屬性就是數據庫名和用戶名/密碼。

  所以,對於多數據庫的系統來講,只有同一數據庫的鏈接纔會被共用,以下圖所示:

  而對多用戶的系統而言,只有同一用戶的申請才能共用數據庫鏈接,以下圖所示:

  (2)如何提升數據庫鏈接池重用率

  這裏提供一種可以有效提升數據庫鏈接池重用率的方式,可是也會帶來一點小安全隱患,在進行設計時須要權衡利弊關係,並根據實際狀況來指定措施。

  ① 創建跳板數據庫

  在數據庫內創建一個全部權限用戶都能訪問的跳板數據庫,在進行數據庫鏈接時先鏈接到該數據庫,而後再使用 use databasename 這樣的SQL語句來選擇須要訪問的數據庫,這樣就可以避免由於訪問的數據庫不一致而致使鏈接字符串不一致的狀況。

  下面的示例代碼演示了這一作法:

    // 假設這裏使用Entry數據做爲跳板數據庫,而後再使用databaseName指定的數據庫
    using (SqlConnection connection = new SqlConnection("Server=192.168.80.100;Uid=public;Pwd=public;Database=Entry"))
    {
        connection.Open();
        SqlCommand command = connection.CreateCommand();
        command.CommandText = string.Format("USE {0}", databaseName);
        command.ExecuteNonQuery();
    }

  ② 不使用數據庫用戶系統來管理系統權限

  這樣作的結果就是永遠使用管理員的帳號來鏈接數據庫,而在作具體工做時再根據用戶的實際權限,使用代碼來限定操做。帶來的好處就是:數據庫看鏈接字符串不會由於實際用戶的不一樣而不一樣。固然,永遠使用管理員帳號來鏈接也會相應帶來安全隱患!

  下圖展現了採用了這種方案後數據庫鏈接池的使用狀況:

3、使用ADO.NET讀寫數據庫

3.1 ADO.NET支持訪問數據庫的方式有哪些?

  對於關係型數據庫,ADO.NET支持兩種訪問模式,一種是鏈接式的訪問模式,而另一種則是離線式的訪問模式。

  (1)鏈接式的訪問

  鏈接式的訪問是指讀取數據時保持和數據庫的鏈接,而且在使用時獨佔整個鏈接,逐步讀取數據。這種模式比較適合從數據量龐大的數據庫中查詢數據,而且不能肯定讀取數量的狀況。使用XXXCommand和XXXDataReader對象來讀取數據就是一個典型的鏈接式數據訪問,這種模式的缺點就是:數據庫鏈接被長時間地保持在打開的狀態。

  下面的一段示例代碼展現了這一讀取模式的典型使用,首先是數據訪問層的靜態方法,該方法返回一個指定SQL命令返回的SqlDataReader獨享,該對象唄關閉時會自動關閉依賴的數據庫鏈接。

    /// <summary>
    /// 數據訪問層類型
    /// </summary>
    public class DataHelper
    {
        private static readonly String conn_String = "Server=localhost;Integrated Security=true;database=TestDB";

        /// <summary>
        /// 使用給定的sql來訪問數據庫
        /// 返回SqlDataReader對象,提供鏈接式訪問
        /// </summary>
        /// <param name="sql">SQL命令</param>
        /// <returns>SqlDataReader對象</returns>
        public static SqlDataReader GetReader(String sql)
        {
            SqlConnection con = new SqlConnection(conn_String);
            try
            {
                // 打開鏈接,執行查詢
                // 而且返回SqlDataReader
                con.Open();
                using (SqlCommand cmd = con.CreateCommand())
                {
                    cmd.CommandText = sql;
                    SqlDataReader dr = cmd.ExecuteReader
                                    (CommandBehavior.CloseConnection);
                    return dr;
                }
            }
            // 鏈接數據庫隨時可能發生異常
            catch (Exception ex)
            {
                if (con.State != ConnectionState.Closed)
                {
                    con.Close();
                }
                return null;
            }
        }
    }
View Code

  其次是調用該方法的入口,使用者將會獲得一個鏈接着數據庫的SqlDataReader對象,該對象自己並不包含任何數據,使用者能夠經過該對象讀取數據庫中的數據。但因爲是鏈接方式,讀取只能是順序地逐條讀取。

    /// <summary>
    /// 使用數據庫訪問層
    /// 鏈接式讀取數據
    /// </summary>
    class Program
    {
        // SQL命令
        private static readonly String sql = "select * from dbo.DeptMaterialDetails";

        static void Main(string[] args)
        {
            // 使用鏈接式方法讀取數據源
            using (SqlDataReader reader = DataHelper.GetReader(sql))
            {
                // 獲得列數
                int colcount = reader.FieldCount;
                // 打印列名
                for (int i = 0; i < colcount; i++)
                {
                    Console.Write("{0}  ", reader.GetName(i));
                }
                Console.WriteLine();
                // 順序讀取每一行,並打印
                while (reader.Read())
                {
                    for (int i = 0; i < colcount; i++)
                    {
                        Console.Write("{0}\t", reader[i].ToString());
                    }
                    Console.WriteLine();
                }
                reader.Close();
            }

            Console.ReadKey();
        }
    }
View Code

  下圖是這個示例的執行結果,從數據庫中讀取了指定表的內容:

  

  (2)脫機式的訪問

  脫機式的訪問並非指不鏈接數據庫,而是指通常在讀取實際數據時鏈接就已經斷開了。脫機式訪問方式在鏈接至數據庫後,會根據SQL命令批量讀入全部記錄,這樣就能直接斷開數據庫鏈接以供其餘線程使用,讀入的記錄將暫時存放在內存之中。脫機式訪問的優勢就在於不會長期佔用數據庫鏈接資源,而這樣作的代價就是將消耗內存來存儲數據,在大數據量查詢的狀況下該方式並不適用。例如,使用XXXDataAdapter和DataSet對象就是最經常使用的脫機式訪問方式。

  下面的實例代碼對上面的鏈接式作了一些修改,藉助SqlDataAdapter和DataSet來實現脫機式訪問:

    /// <summary>
    /// 數據訪問層類型
    /// </summary>
    public class DataHelper
    {
        private static readonly String conn_String = "Server=localhost;Integrated Security=true;database=TestDB";

        /// <summary>
        /// 使用給定的sql來訪問數據庫
        /// 返回DataSet對象
        /// </summary>
        /// <param name="sql">SQL命令</param>
        /// <returns>DataSet對象</returns>
        public static DataSet GetDataSet(String sql)
        {
            SqlConnection con = new SqlConnection(conn_String);
            DataSet ds = new DataSet();
            try
            {
                // 打開鏈接,執行查詢
                // 而且返回DataSet
                con.Open();
                using (SqlDataAdapter sd = new SqlDataAdapter(sql, con))
                {
                    // 這裏數據將被批量讀入
                    sd.Fill(ds);
                }
                return ds;
            }
            // 鏈接數據庫隨時可能發生異常
            catch (Exception ex)
            {
                if (con.State != ConnectionState.Closed)
                {
                    con.Close();
                }
                return ds;
            }
        }
    }

    /// <summary>
    /// 使用數據庫訪問層
    /// 脫機式讀取數據
    /// </summary>
    class Program
    {
        //SQL命令
        private static readonly String sql = "select * from dbo.DeptMaterialDetails";

        static void Main(string[] args)
        {
            DataSet ds = DataHelper.GetDataSet(sql);
            // 打印結果,這裏假設只對DataSet中的第一個表感興趣
            DataTable dt = ds.Tables[0];
            // 打印列名
            foreach (DataColumn column in dt.Columns)
            {
                Console.Write("{0}  ", column.ColumnName);
            }
            Console.WriteLine();
            // 打印表內容
            foreach (DataRow row in dt.Rows)
            {
                for (int i = 0; i < dt.Columns.Count; i++)
                {
                    Console.Write("{0}  ", row[i].ToString());
                }
                Console.WriteLine();
            }

            Console.ReadKey();
        }
    }
View Code

  因爲數據訪問類的處理至關趕忙,調用者輕鬆就能得到包含數據源的DataSet對象,這時任何操做都已經和數據源沒有聯繫了。

3.2 簡述SqlDataAdapter的基本工做機制

  ADO.NET提供的XXXDataAdapter類型都使用了很是一致的機制,而且向使用者提供了統一的接口。一個SqlDataAdapter對象,在數據庫操做中充當了中間適配的角色,它組織起數據緩存對數據庫的全部操做,進行統一執行。一個SqlDataAdapter對象內實際包含四個負責具體操做的SqlCommand對象,它們分別負責查詢、更新、插入和刪除操做。下圖展現了SqlDataAdapter的工做機制:

  如上圖所示,實際上進行數據操做的是包含在SqlDataAdapter內的四個SqlCommand對象,而當SqlDataAdapter的Update方法被調用時,它會根據DataSet獨享的更新狀況而調用插入、刪除和更新等命令。

3.3 如何實現批量更新的功能?

  (1)批量更新的概念

  使用XXXDataAdapter更新數據,因爲每一行都須要都須要一個從程序集到數據庫的往返,在大批量更新的狀況下,效率是很是低的。能夠考慮使用一次發送多條更新命令的處理方式,這就須要用到UpdateBatchSize屬性。在.NET 2.0以後,SqlClient和OracleClient都支持這個屬性,這裏以SQL Server數據源爲例,介紹一下UpdateBatchSize的基本使用。

UpdateBatchSize的值一共有三種:

  ① =0,DbDataAdapter將使用服務器能處理的最大批處理大小;

  ② =1,禁用批量更新;

  ③ >1,使用UpdateBatchSize操做批處理一次性發送的量;

  當批量更新被容許時,SqlDataAdapter的Update方法將每次發送多條更新命令到數據庫,從而提升性能。

  But,使用批量更新並不意味着SQL的合併或優化。事實上,批量的意義在於把多個發往數據庫服務器的SQL語句放在一個請求中發送。例如,將UpdateBatchSize設置爲20時,本來每一個更新行發送一次更新命令將變爲每20個更新行發送一次更新命令,而每一個命令中包含了20個更新一行的命令。下圖展現了這一區別:

  (2)批量更新的使用

  下面的示例代碼展現瞭如何使用UpdateBatchSize屬性來設置批量更新,這裏更改了DataHelper的Update方法,在內部設置了UpdateBatchSize屬性。

    public class DataHelper
    {
        private static readonly string conn_string = "Server=localhost;Integrated Security=true;database=TestDB";
        //選擇、更新、刪除和插入的SQL命令
        static readonly string SQL_SELECT = "SELECT * FROM DeptMaterialDetails";
        static readonly string SQL_UPDATE = "UPDATE DeptMaterialDetails SET Department=@Department,Item=@Item,Number=@Number where Id=@Id";
        static readonly string SQL_DELETE = "DELETE FROM DeptMaterialDetails where Id=@Id";
        static readonly string SQL_INSERT = "Insert INTO DeptMaterialDetails (Department,Item,Number) VALUES (@Department,@Item,@Number)";

        /// <summary>
        /// 獲得SqlDataAdapter,私有方法
        /// </summary>
        /// <param name="con"></param>
        /// <returns></returns>
        private static SqlDataAdapter GetDataAdapter(SqlConnection con)
        {
            SqlDataAdapter sda = new SqlDataAdapter();
            sda.SelectCommand = new SqlCommand(SQL_SELECT, con);
            sda.UpdateCommand = new SqlCommand(SQL_UPDATE, con);
            sda.DeleteCommand = new SqlCommand(SQL_DELETE, con);
            sda.InsertCommand = new SqlCommand(SQL_INSERT, con);
            sda.UpdateCommand.Parameters.AddRange(GetUpdatePars());
            sda.InsertCommand.Parameters.AddRange(GetInsertPars());
            sda.DeleteCommand.Parameters.AddRange(GetDeletePars());
            return sda;
        }

        // 三個SqlCommand的參數
        private static SqlParameter[] GetInsertPars()
        {
            SqlParameter[] pars = new SqlParameter[3];
            pars[0] = new SqlParameter("@Department", SqlDbType.VarChar, 50, "Department");
            pars[1] = new SqlParameter("@Item", SqlDbType.VarChar, 50, "Item");
            pars[2] = new SqlParameter("@Number", SqlDbType.Int, 4, "Number");
            return pars;
        }

        private static SqlParameter[] GetUpdatePars()
        {
            SqlParameter[] pars = new SqlParameter[4];
            pars[0] = new SqlParameter("@Id", SqlDbType.VarChar, 50, "Id");
            pars[1] = new SqlParameter("@Department", SqlDbType.VarChar, 50, "Department");
            pars[2] = new SqlParameter("@Item", SqlDbType.VarChar, 50, "Item");
            pars[3] = new SqlParameter("@Number", SqlDbType.Int, 4, "Number");
            return pars;
        }

        private static SqlParameter[] GetDeletePars()
        {
            SqlParameter[] pars = new SqlParameter[1];
            pars[0] = new SqlParameter("@Id", SqlDbType.VarChar, 50, "Id");
            return pars;
        }

        /// <summary>
        /// 更新數據庫,使用批量更新
        /// </summary>
        /// <param name="ds">數據集</param>
        public static void Update(DataSet ds)
        {
            using (SqlConnection connection = new SqlConnection(conn_string))
            {
                connection.Open();
                using (SqlDataAdapter adapater = GetDataAdapter(connection))
                {
                    // 設置批量更新
                    adapater.UpdateBatchSize = 0;
                    adapater.Update(ds);
                }
            }
        }
    }
View Code

PS:近年來比較流行的輕量級ORM例如Dapper一類的這裏就不做介紹了,後續我會實踐一下寫一個初探系列的文章。另外,數據庫中的事務及其隔離級別一類的介紹也會在後續詳細閱讀《MSSQL技術內幕》後寫一個讀書筆記,到時分享給各位園友。

參考資料

(1)朱毅,《進入IT企業必讀的200個.NET面試題》

(2)張子陽,《.NET之美:.NET關鍵技術深刻解析》

(3)王濤,《你必須知道的.NET》

(4)百度百科,ODBC

 

相關文章
相關標籤/搜索