ADO.NET入门:Connection/Command/DataReader

更多
2019年08月11日 23点18分 作者:叶飞 修改

ADO全称:ActiveX Data Objects,是微软退出的一系列可操作数据库的对象。其中基于.NET平台的,又被称之为ADO.NET。


添加引用

在项目中使用的NuGet添加可连接SQL Server的:

连接不同的数据库,需要不同的package(dll类库):

(演示:略)


Connection对象

操作数据库,首先需要打开数据库连接:

    string connectionString = @"Data Source=(localdb)\MSSQLLocalDB;Initial Catalog=17bang;Integrated Security=True;";
    using (DbConnection connection = new SqlConnection(connectionString))
    {
        connection.Open();  //需要显式的Open()
                   //进行其他操作
    }


连接字符串

上述代码中connectionString被称之为“连接字符串”,它主要包含了:

  • Data Source:数据库实例名称
  • Initial Catalog:数据库名称
  • Integrated Security:数据库登录方式

因为我们使用的localDB,所以不需要用户名和密码。

通常我们不会手写连接字符串(error prone),而是从VS已连接的数据库属性中查找并复制粘贴演示:略)


继承层次

SqlConnection继承自DbConnection (演示:F12查看,略)

不同的数据库,有DbConnection不同的实现。

@想一想@

  • 为什么要抽象出一个DbConnection?
  • 为什么我们能使用using?

生成connection对象之后,要忘了显式的Open()

但因为using会调用Dispose(),SqlConnection的Dispose()中已调用Close(),所以可以省略Open()调用。


性能

通常,打开一个数据库连接是比较消耗资源的。(一般来说,I/O操作都是比较耗性能的)

所以,管理好DbConnection对象是提高代码性能的重要组成部分。

一般有两种性能策略:
  1. 尽可能晚的打开,尽可能早的关闭,以提高并发;
  2. 在一次连接中做尽可能多的(和数据库相关的)工作,以减少使用连接的数量。就类似于“好不容易连接一次,一次多弄点……”

常见错误:

  1. 过早的打开数据库,然后在连接中做一些和数据库操作无关的事情
        //错!
        using (DbConnection connection = new SqlConnection(connectionString))
        {
            connection.Open();
            //模拟和数据库无关的其他操作
            System.Threading.Thread.Sleep(1000);
        }
    
        //对
        //模拟和数据库无关的其他操作
        System.Threading.Thread.Sleep(1000);
        using (DbConnection connection = new SqlConnection(connectionString))
        {
            connection.Open();
        }
  2. 忘记/或者因异常等原因无法关闭数据库,所以推荐使用using
        DbConnection connection = new SqlConnection(connectionString);
        connection.Open();
        //其他事情
        throw new Exception();
        connection.Close();
  3. 在循环中打开/关闭数据库连接
        //方案1. 错
        for (int i = 0; i < 1000; i++)
        {
            using (DbConnection connection = new SqlConnection(connectionString))
            {
                connection.Open();
            }
        }
    
        //方案2. 正确
        using (DbConnection connection = new SqlConnection(connectionString))
        {
            connection.Open();
            for (int i = 0; i < 1000; i++)
            {
    
            }
        }

连接池(pool)

SqlClient中内置了一个连接池:连接用完之后不会立即销毁,而是放回池中,以便下次再用(复习:字符串池和线程池)

所以,上述常见错误3,假设打开一次连接耗时1秒,其他操作耗时0.1秒:

  • 如果没有连接池,错误方式耗时1000*1+1000*0.1=1100秒
  • 如果有连接池,忽略连接池取出存入的开销(比较小),错误方式也只耗时 1*1+1000*0.1=101,和正确方式没有区别

但是,要因为连接池就随便乱来,-_-||,连接池的资源也有耗尽的时候的……


常见面试题:比较以下三种清理connection方式的区别:

connection.Close()
关闭连接。但连接仍然存在,且连接上的各种配置仍然存在,可以再次Open()
connection.Dispose()
会自动调用Close()关闭连接,且释放连接上的配置,也就无法再次Open()
connection=null
使得connection变量指向的连接对象能被垃圾回收,连接会在垃圾回收时才会被Dispose()

演示:connection.State


Command对象

基类是DbCommand,可以直接new一个SqlCommand子类出来:

SqlCommand command = new SqlCommand();

Command对象中必须要有依赖两个属性:

  • connection对象:指定Command运行在哪个Connection上
  • sql语句:指定Command的运行内容

因为历史原因(当时对多态/解耦等意识没有现在这样强烈),只有当connection对象是SqlConnection时,才能直接使用构造函数:

    DbCommand getUserByName = new SqlCommand(
        $"SELECT Count([Id]) FROM [Student] WHERE [Name] = N'{UserName}'",
        connection);

如果是DbConnection变量,就只能用属性赋值:

    DbCommand enroll = new SqlCommand(
        $"UPDATE Student SET Enroll = '{DateTime.Now}' WHERE Id = 1"
    );
    command.Connection = connection;

Command执行,需要调用ExcuteX()方法,具体包括:


ExecuteNonQuery()

用于执行非查询的SQL语句,增删改都可以:

    int rows = command.ExecuteNonQuery();

返回的是:受影响的函数。


ExecuteScalar()

用于执行查询(SELECT)的SQL语句,但只取返回结果集中第一行第一列的值,常用于取SELECT COUNT(*)等聚合函数的值:

    object id = (int)getUserByName.ExecuteScalar();

返回的值会被装箱成object(复习)

@想一想@:如何区分(复习:SQL中的EXISTS)

  • 没有返回行
  • 返回一行,列值为NULL

为此,ADO.NET引入了DBNull:代表从数据库取到了值,但是值为NULL。比较:

    object result = new SqlCommand(
        queryString, (SqlConnection)connection).ExecuteScalar();

    Console.WriteLine(result == null);
    Console.WriteLine(result == DBNull.Value);



ExecuteReader()

一样是执行查询语句时使用,但它会返回一个


DbDataReader对象

DataReader对象是一个集合(演示:实现IEnumerable)

    DbDataReader reader = command.ExecuteReader();
    if (reader.HasRows)
    {
        while (reader.Read())
        {
            Console.WriteLine($"Id={reader[0]},Name={reader["Name"]},Enroll={reader[2]},Score={reader["Score"]}");
        }
    }
上述代码,通过循环调用Read()方法,读取全部结果集。具体来说,主要是使用reader的:
  • HasRows属性,判断command的执行结果有无行数据
  • Read()方法
    1. 从结果集中取出第n行数据填充到reader之中
    2. n=n+1
    3. 其返回值为true,表示可以之后还有“行数据”;为false,已是最后一行
  • 索引器获取列值。索引值可以是列名,也可以是列的序号
注意
  • SELECT查询的结果,实际上存储在客户端的网络缓冲区中,每一次Read()会再次连接数据库进行查询

  • 但是,在调用Read()时,还是需要保持connection的Open状态:这被称之为“连接式”查询


作业

将之前ASP.NET项目中以下Repository方法用ADO.NET实现:

  • 注册/登录
  • 内容:
  1. 发布/修改
  2. 单页呈现
  3. 列表页呈现(包括:过滤/分页)
  • 批量标记Message为已读
  • ……


源栈培训 C# ADO.NET
赞: 178 踩: 0

打赏
已收到打赏的 帮帮币

你的 打赏 非常重要!
为了保证文章的质量,每一篇文章的发布,都已经消耗了作者 1 枚 帮帮币
没有“帮帮币”,作者无法发布新的文章。

全系列阅读
评论 / 0

源栈培训:ASP.NET全栈开发


数据持久化

如何通过C#进行数据库的读取,包含ADO.NET和Entity Framework相关知识……

ASP.NET

综合之前所学,连接前端和数据库,包括RazorPage、MVC和其他(如WebForm/WebApi等)技术……

锋利的C#

从入门的变量赋值、分支循环、到面向对象,以及更先进的语言特性,如:泛型、Lambda、Linq、异步方法等…………

项目管理相关

需求发布、开发规划、部署、测试,源代码版本管理(git)等……

前端基础

HTML、CSS和JavaScript,以及类库Bootstrap和JQuery

数据库和SQL

建库建表、增删改查、索引并发、函数和存储过程等……

全部
关键字



帮助

反馈