.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

 

相关文章
相关标签/搜索