C#开发训练营

第14课:数据库操作基础——ADO.NET

接下来的几课会讨论数据库的操作,并讨论基于数据的应用开发问题;并封装基于SQL Server和MySQL数据库的操作操作组件。本课会回顾如何在C#代码中使用ADO.NET组件操作数据库。

在.NET Framework类库中,操作数据库的基本资源定义在System.Data命名空间;对于一些第三方数据库系统的操作,厂商也提供了对应的.NET Framework开发资源。比如MySQL数据库相关资源就定义在MySql命名空间,需要从MySQL或Oracle网站下载;下载相关资源后,将MySql.Data.dll文件复制到ASP.NET Web项目中的Bin目录即可。

接下来,我们以SQL Server数据库例,讨论如何使用ADO.NET组件操作数据库;后续课程会完成SQL Server和MySQL数据库常用组件的封装。

连接数据库

为方便测试,可以在SQL Server中创建cdb_demo数据库;然后执行如下代码创建user_main数据表。

SQL
use cdb_demo;
go

create table user_main(
userid bigint not null identity(1,1) primary key,
username nvarchar(30) not null unique,
userpwd nvarchar(64) not null check(len(userpwd)=64),
email nvarchar(30) not null,
locked int not null default 1
);

.NET Framework类库中,支持SQL Server数据库的资源定义在System.Data.SqlClient命名空间。

应用中操作数据库时,首先需要连接数据库服务器(或实例),此时使用IDbConnection接口组件,如连接SQL Server数据库时使用SqlConnection类,连接MySQL数据库时使用MySqlConnection类等。

连接数据库的参数使用一个字符串来设置,每个参数由参数名和参数值两部分组成,多个参数由分号(;)分隔。为方便生成数据库连接字符串,一些数据库还提供了连接字符串辅助生成类,如SQL Server数据库就可以使用SqlConnectionStringBuilder类,MySQL数据库使用MySqlConnectionStringBuilder类。

下面的代码(/app_code/data/sqlserver/tSqlHelper.cs),在tSqlHelper类中的GetCnnStr()方法中使用SqlConnectionStringBuilder类来构建连接字符串。

C#
using System.Data.SqlClient;

public static class tSqlHelper
{
    //
    public static string GetCnnStr(string server, string database)
    {
        SqlConnectionStringBuilder sb =
            new SqlConnectionStringBuilder();
        sb.DataSource = server;
        sb.InitialCatalog = database;
        sb.IntegratedSecurity = true;
        sb.Pooling = true;
        sb.AsynchronousProcessing = true;
        return sb.ConnectionString;
    }
    //
    public static string GetCnnStr(string server, string database,
        string uid, string pwd)
    {
        SqlConnectionStringBuilder sb =
            new SqlConnectionStringBuilder();
        sb.DataSource = server;
        sb.InitialCatalog = database;
        sb.IntegratedSecurity = false;
        sb.UserID = uid;
        sb.Password = pwd;
        sb.Pooling = true;
        sb.AsynchronousProcessing = true;
        return sb.ConnectionString;
    }
    //
}

tSqlHelper类中定义了两个版本的GetCnnStr()方法,分别用于连接SQL Server数据库的本机实例和远程实例。其中使用了SqlConnectionStringBuilder类进行操作,通过各种属性设置连接参数,并通过ConnectionString属性返回数据库连接字符串。

  • SqlConnectionStringBuilder类中常用的属性包括:
  • InitialCatalog,指定数据库名称。
  • DataSource,指定服务器或实例地址。
  • UserID,登录用户名,使用Windows身份验证登录时不指定。
  • Password,登录密码,使用Windows身份验证登录时不指定。
  • IntegratedSecurity,是否使用Windows用户登录,即是否使用当前Windows系统登录用户来登录数据库。
  • AsynchronousProcessing,是否开启异步处理。
  • ConnectTimeout,连接超时
  • ConnectionString,使用相关参数创建的数据库连接字符串。

使用数据库时,可以使用using语句结构,在数据库操作完成时,可以自动关闭数据库连接,如下面的代码。

C#
string CnnStr = tSqlHelper.GetCnnStr(@".\MSSQLSERVER1", "cdb_demo");
using(SqlConnection cnn = new SqlConnection(CnnStr))
{
    cnn.Open();
    // 使用数据库...
}

代码中使用的SQL Server服务器为本机,实例名为MSSQLSERVER1,数据库名称是cdb_demo,如果读者使用了不同的数据库服务器、实例和数据库,应修改相关参数。

执行SQL语句

执行SQL语句时,使用IDbCommand接口组件,SQL Server数据库使用的是SqlCommand类,定义在System.Data.SqlClient命名空间,常用的成员包括:

  • CommandText属性,执行的SQL语句、存储过程名或表名。
  • CommandType属性,指定CommandText属性中的内容是什么类型,定义为CommandType枚举类型,枚举值包括Text(SQL语句)、StoredProcedure(存储过程)、TableDirect(表名,只用于OLEDB数据源)。
  • CommandTimeout属性,命令执行的超时时间,默认为30秒。
  • Parameters属性,指定命令中包含的参数数据。
  • Transaction属性,命令关联的事务,SQL Server数据库中定义为SqlTransaction类型。
  • ExecuteScalar()方法,执行命令,并获取标量数据(object类型),方法会返回查询结果中第一行记录第一个字段的值。
  • ExecuteNonQuery()方法,执行命令,返回影响的记录数量(int类型)。此方法有异步版本,其中,BeginExecuteNonQuery()方法返回IAsyncResult对象,EndExecuteNonQuery()方法使用此对象作为参数,并返回操作结果。
  • ExecuteReader()方法,执行命令,并返回查询结果,操作SQL Server数据库时返回SqlDataReader类型。此方法同样包含异步版本,包括BeginExecuteReader()和EndExecuteReader()方法。

首先来看ExecuteNonQuery()方法的应用,如下面的代码,会向user_main表添加一条记录。

C#
using System;
using System.Data.SqlClient;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string CnnStr = 
            tSqlHelper.GetCnnStr( @".\MSSQLSERVER1","cdb_demo");
        using (SqlConnection cnn = new SqlConnection(CnnStr))
        {
            cnn.Open();
            SqlCommand cmd = cnn.CreateCommand();
            cmd.CommandText = 
@"insert into user_main(username,userpwd,email,locked)
values('user01','"+tStr.Sha256("123456")+"','user01@aaa.bbb',0);";
            int result = cmd.ExecuteNonQuery();
            tWeb.WriteLine(result);
        }
    }
}

创建SqlCommand对象时使用了已打开连接的SqlConnection对象中的CreateCommand()方法,然后指定了CommandText属性的SQL语句,最后调用ExecuteNonQuery()方法执行语句。如果页面显示1,表示已成功添加了一条记录。

使用参数

为了防止SQL注入等有不安全操作,对于客户端提交的数据,或者是文本数据,应使用参数进行传递。SQL语句中参数的数据,可以使用SqlCommand对象中的Parameters属性处理,它定义为一个参数集合。

下面的代码,我们使用参数向user_main表添加一条记录。

C#
using System;
using System.Data.SqlClient;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string CnnStr = 
            tSqlHelper.GetCnnStr(@".\MSSQLSERVER1", "cdb_demo");
        using (SqlConnection cnn = new SqlConnection(CnnStr))
        {
            cnn.Open();
            SqlCommand cmd = cnn.CreateCommand();
            cmd.CommandText =
                @"insert into user_main(username,userpwd,email,locked)
values(@username,@userpwd,@email,@locked);";
            cmd.Parameters.AddWithValue("@username", "user02");
            cmd.Parameters.AddWithValue("@userpwd", tStr.Sha256("123456"));
            cmd.Parameters.AddWithValue("@email", "user02@aaa.bbb");
            cmd.Parameters.AddWithValue("@locked", 1);
            int result = cmd.ExecuteNonQuery();
            tWeb.WriteLine(result);
        }
    }
}

成功添加记录后,页面同样会显示1。代码中,如果需要清除SqlCommand中的参数数据,可以使用cmd.Parameters.Clear()方法

异步方法

使用异步语句执行方法时,还需要IAsyncResult对象配合,如下面的代码,同样是向user_main表添加记录。

C#
using System;
using System.Data.SqlClient;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string CnnStr = 
            tSqlHelper.GetCnnStr(@".\MSSQLSERVER1", "cdb_demo");
        using (SqlConnection cnn = new SqlConnection(CnnStr))
        {
            cnn.Open();
            SqlCommand cmd = cnn.CreateCommand();
            cmd.CommandText =
                @"insert into user_main(username,userpwd,email,locked)
values(@username,@userpwd,@email,@locked);";
            cmd.Parameters.AddWithValue("@username", "user03");
          cmd.Parameters.AddWithValue("@userpwd", tStr.Sha256("123456"));
            cmd.Parameters.AddWithValue("@email", "user03@xxx.yyy");
            cmd.Parameters.AddWithValue("@locked", 0);
            IAsyncResult ar = cmd.BeginExecuteNonQuery();
            int result = cmd.EndExecuteNonQuery(ar);
            tWeb.WriteLine(result);
        }
    }
}

读取标量数据

ExecuteScalar()方法用于返回查询结果中的第一条记录中的第一个字段的值;下面的代码,会向user_main表中添加记录,并返回新记录的ID字段值。

C#
using System;
using System.Data.SqlClient;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string CnnStr = 
            tSqlHelper.GetCnnStr("cdb_demo", @".\MSSQLSERVER1");
        using (SqlConnection cnn = new SqlConnection(CnnStr))
        {
            cnn.Open();
            SqlCommand cmd = cnn.CreateCommand();
            cmd.CommandText =
                @"insert into user_main(username,userpwd,email,locked)
output inserted.userid
values(@username,@userpwd,@email,@locked);";
            cmd.Parameters.AddWithValue("@username", "user04");
            cmd.Parameters.AddWithValue("@userpwd", tStr.Sha256("123456"));
            cmd.Parameters.AddWithValue("@email", "user04@xxx.yyy");
            cmd.Parameters.AddWithValue("@locked", 0);
            tWeb.WriteLine(cmd.ExecuteScalar());
        }
    }
}

执行会显示4,也就是新记录中ID字段的数据,在user_main表中,也就是userid字段的数据。代码中使用了SQL Server数据库中的inserted表返回userid字段数据,这是SQL Server 2005以后添加的新特性,可以通过inserted表返回新添加记录的数据,如果返回记录的全部分数据,可以使用inserted.*。

此外,获取新记录ID数据的传统方式是通过@@identity系统变量,稍后,在讨论事务的执行时会演示相关操作。

使用SqlDataReader对象读取查询结果

前面的示例,我们已经向user_main表添加了4条记录,内容如图1。

图1

下面,我们通过select语句查询这些数据,并使用SqlDataReader对象读取数据。SqlDataReader类中定义了一系列成员,方便读取查询结果中字段信息和记录数据,如:

  • FieldCount属性,返回查询结果中的字段数量。
  • GetName()方法,使用整数索引返回相应的字段名。
  • GetFieldType()方法,使用整数索引返回相应字段的类型(Type类型)。
  • HasRows属性,判断对象中是否包含数据记录。
  • Read()方法,读取下一条记录,成功时返回true,否则返回false。
  • Item属性(索引器),使用整数索引访问当前记录的字段数据。
  • GetInt32()方法,使用整数索引返回Int32(int)类型数据。此外,还有一系列的方法可以返回不同类型的字段数据,它些方法的名称使用System命名空间中定义的类型名称,如返回long类型数据使用GetInt64()方法、返回float类型数据使用GetSingle()方法、返回double类型数据使用GetDouble()方法等。
  • GetOrdinal()方法,根据字段名返回索引值。
  • IsDBNull()方法,判断指定字段中的数据是否为数据库中的空值(NULL)。

下面的代码,我们读取user_main表中userid字段为1的记录,并显示在页面中。

C#
using System;
using System.Data.SqlClient;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string CnnStr = 
            tSqlHelper.GetCnnStr(@".\MSSQLSERVER1", "cdb_demo");
        using (SqlConnection cnn = new SqlConnection(CnnStr))
        {
            cnn.Open();
            SqlCommand cmd = cnn.CreateCommand();
            cmd.CommandText = @"select * from user_main where userid=1;";
            IAsyncResult ar = cmd.BeginExecuteReader();
            using (SqlDataReader dr = cmd.EndExecuteReader(ar))
            {
                if (dr.Read())
                {
                    for(int col = 0; col < dr.FieldCount; col++)
                    {
                        tWeb.WriteLine(dr.GetName(col) + 
                            " : " + tStr.Parse(dr[col]));
                    }
                }
            }
        }
    }
}

页面显示结果如图2。

图2

代码中,使用了ExecuteReader()方法的异步版本来获取查询结果,然后,使用dr.Read()方法读取记录,并通过一个for循环访问了所有字段名和对应的数据。

应用中,如果SqlDataReader对象中包含多条记录,可以使用while循环调用Read()方法逐条读取,如下面的代码就会显示user_main表中的所有记录。

C#
using System;
using System.Data.SqlClient;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string CnnStr = 
            tSqlHelper.GetCnnStr(@".\MSSQLSERVER1", "cdb_demo");
        using (SqlConnection cnn = new SqlConnection(CnnStr))
        {
            cnn.Open();
            SqlCommand cmd = cnn.CreateCommand();
            cmd.CommandText = @"select * from user_main;";
            IAsyncResult ar = cmd.BeginExecuteReader();
            using (SqlDataReader dr = cmd.EndExecuteReader(ar))
            {
                while (dr.Read())
                {
                    for(int col = 0; col < dr.FieldCount; col++)
                    {
                        tWeb.WriteLine(dr.GetName(col) + 
                            " : " + tStr.Parse(dr[col]));
                    }
                    tWeb.WriteLine("***************");
                }
            }
        }
    }
}

本例显示的结果中,每条记录会使用15个星号(*)分隔。

适配器与脱机组件

除了使用SqlDataReader读取数据查询结果,还可以使用SqlDataAdapter对象将数据填充到DataSet对象中。

DataSet属于脱机组件(非连接组件),应用时不需要连接数据库,而SqlConnection、SqlCommand、SqlDataReader、SqlDataAdapter等类型属于连接组件,使用时必须保持数据库连接。

DataSet中主要包括两种对象,即DataTable和DataRelation,表示数据表和数据表之间的关系。在DataSet类中,使用Tables属性获取所有表的集合,使用Relations属性获取所有关系的集合。接下来主要关注数据表的使用。

在表中主要包括了列(Column)信息和行(Row)信息,其中,列包括名称、数据类型等信息,每一行都包含了与列相对应的数据。

下面的示例,我们会创建一个DataTable对象,其中包括了三列、两行数据,然后将这些数据绑定到GridView控件中。首先,在Test.aspx文件中创建GridView控件,如下面的代码。

HTML
<%@ Page Language="C#" AutoEventWireup="true" 
    CodeFile="Test.aspx.cs" Inherits="Test" %>
<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml">
<head runat="server">
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
    <title></title>
</head>
<body>
    <form id="form1" runat="server">
        <div>
            <asp:GridView ID="grd1" runat="server"></asp:GridView>
        </div>
    </form>
</body>
</html>

接下来,修改Test.aspx.cs文件内容如下。

C#
using System;
using System.Data;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        DataTable tbl = new DataTable();
        for (int col = 0; col < 3; col++)
            tbl.Columns.Add("f" + col.ToString());
        for (int row = 0; row < 2; row++)
        {
            DataRow r = tbl.NewRow();
            r["f0"] = "user0" + row.ToString();
            r["f1"] = "user0" + row.ToString() + "@aaa.bbb";
            r["f2"] = 0;
            tbl.Rows.Add(r);
        }
        //
        grd1.DataSource = tbl;
        grd1.DataBind();
    }
}

代码中,首先使用DataTable的Columns属性操作列信息,其中的Add()方法用于添加列,方法的参数就是列名(ColumnName);然后,使用DataTable对象的NewRow()方法创建一个与表关联的新行,并指定对应字段的数据,最后,使用DataTable对象的Rows属性中的Add()方法将新行添加到DataTable对象。

GridView对象的DataSource指定数据源,这里就是DataTable对象,DataBind()方法执行数据绑定操作;页面显示结果如图3。

图3

下面的代码,我们使用SqlDataAdapter(数据适配器)读取user_main表的全部数据,并填充到DataSet对象,最后绑定到GridView对象。

C#
using System;
using System.Data;
using System.Data.SqlClient;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string CnnStr =
           tSqlHelper.GetCnnStr(@".\MSSQLSERVER1", "cdb_demo");
        using (SqlConnection cnn = new SqlConnection(CnnStr))
        {
            cnn.Open();
            SqlCommand cmd = cnn.CreateCommand();
            cmd.CommandText = @"select * from user_main;";
            using (SqlDataAdapter ada = new SqlDataAdapter(cmd))
            {
                DataSet ds = new DataSet();
                ada.Fill(ds);
                grd1.DataSource = ds.Tables[0];
                grd1.DataBind();
            }
        }
    }
}

代码中,使用SqlCommand对象作为参数构建了SqlDataAdapter对象,然后,使用SqlDataAdapter对象的Fill()方法将查询结果的数据填充到DataSet对象。DataSet中的Tables属性包含了所有表的集合,填充的数据就位于第一个表,使用ds.Tables[0]获取此表,并将其作为GridView控件的数据源(DataSource),最后调用GridView对象的DataBind()方法绑定数据。页面显示效果如图4。

图4

执行事务

数据库处理技术中,事务(Transaction)的基本特点是,事务中的任务要不全部完成,否则就什么也不做。一个最常见的例子就是转账操作,如执行A账户向B账户转账100元的事务,就必须从A账户扣除100元,然后B账户添加100元,两项操作缺一不可,即转账事务中的两个操作都必须成功执行,否则就不执行任何操作。

C#开发中执行SQL Server事务时使用SqlTransaction类。

下面的代码,我们再向user_main表中添加一条记录,并使用@@identity系统变量返回新记录的ID字段(userid)数据;这两项操作会在一个事务中完成。

C#
using System;
using System.Data.SqlClient;

public partial class Test : System.Web.UI.Page
{
    protected void Page_Load(object sender, EventArgs e)
    {
        string CnnStr =
            tSqlHelper.GetCnnStr("cdb_demo", @".\MSSQLSERVER1");
        using (SqlConnection cnn = new SqlConnection(CnnStr))
        {
            cnn.Open();
            SqlCommand cmd = cnn.CreateCommand();
            using (SqlTransaction tran = cnn.BeginTransaction())
            {
                cmd.Transaction = tran;
                cmd.CommandText =
                    @"insert into user_main(username,userpwd,email,locked)
values(@username,@userpwd,@email,@locked);";
                cmd.Parameters.AddWithValue("@username", "user05");
          cmd.Parameters.AddWithValue("@userpwd", tStr.Sha256("123456"));
          cmd.Parameters.AddWithValue("@email", "user05@xxx.yyy");
                cmd.Parameters.AddWithValue("@locked", 0);
                if (cmd.ExecuteNonQuery() == 1)
                {
                    cmd.CommandText = "select @@identity;";
                    object result = cmd.ExecuteScalar();
                    tran.Commit();
                    tWeb.WriteLine(result);
                }
            }
        }
    }
}

代码中,使用SqlConnection对象的BeginTransaction()方法构造SqlTransaction对象,并使用SqlCommand对象的Transaction属性将命令执行对象与事务对象关联;然后执行insert语句添加一条新的记录,成功执行后,SqlCommand对象的ExeucteNonQuery()方法会返回1,此时,调用select @@identity语句返回新记录的ID字段数据,并保存到result对象中;在使用事务的Commit()方法提交操作后,在页面中显示新记录的ID字段数据。