在开发带数据库的.NET系统中我使用过各种方式,包括直接使用ADO.NET、使用基于ADO.NET封装的各类工具(其中有自己封装的)、还有各类ORM类库,比如NHibernate、MyBatisNet、Linq to SQL、Entity Framwrok等,在上面的工具或类库中,MyBatisNet一段时间曾是我的最爱:由于它基于XML的配置可以灵活适应一些特殊的场景,不过有时候在面对中小型项目时又觉得MyBatisNet有些大材小用,此外还有一个原因是MyBatisNet这个基于Java的MyBatis改造而来的项目最近几乎没有更新了。
很早就听说过Dapper这个类库了,只不过一直没有尝试使用,但是很早就知道它是国外大型IT问答社区StackOverFlow最早开发并开源的。最近用了一下,感觉确实很方便。Dapper的源代码放在github上托管,并且可以用NuGet方式添加到项目中,只不过我现在开发的桌面软件有一部分用户还在使用WindowsXP系统,因此不能使用高于.NET Framrwork4.5以上版本开发且开发工具是Visual Studio 2015,这也限制了我不能使用最新版本的Dapper,于是我选择了Dapper 1.50.2这个版本。
我们可以在Visual Studio 2015中直接使用NuGet来添加,具体办法就是“工具”-“NuGet包管理器”-“管理解决方案的BuGet程序包”,如下图所示:
在.NET数据库访问方面的Dapper类库介绍
然后在弹出的窗口中搜索“Dapper”,如下图所示:
在.NET数据库访问方面的Dapper类库介绍
在上述界面中可以选择安装到当前解决方案的那些项目中,并且还可以指定Dapper的版本。

本文的描述都是针对Dapper 1.50.2版本。

扩展方法介绍
在介绍Dapper之前首先要介绍一下.NE中的扩展方法。扩展方法是.NET3.5添加的一个特性,使用扩展方法可以让你为现有的类扩展方法而无需创建新的派生类。下面以一个例子来说明:
在我解析某个XML文件节点时,需要读取某个节点的属性,但是这个节点并不是一直有这个属性值,如下:
<SubNetwork name="SF-SubNetWork" type="">
为了避免name属性不存在时抛出异常,我必须先进行判断,如下:

string name=string.Empty;
if (subNetworkNode.Attributes["name"] != null)
{
    name=subNetworkNode.Attributes["name"].Value;
}

如果一个XML节点里有几个可能不存在的属性时,就需要处处这样判断了,于是我对代码进行了改进,针对此类情况定义了扩展方法,方法如下:

public static class ExtendMethodClass
{
    /// <summary>
    /// 获取指定属性的值,如果没有设置指定属性名,则返回空字符串
    /// </summary>
    /// <param name="attributes">XML节点的属性集合</param>
    /// <param name="attributeName">属性名</param>
    /// <returns></returns>
    public static string GetAttributeValue(this XmlAttributeCollection attributes,string attributeName)
    {
        if (string.IsNullOrEmpty(attributeName))
        {
            throw new ArgumentNullException("attributeName", "不能为空");
        }

        if (attributes == null||attributes[attributeName]==null)
        {
            return string.Empty;
        }

        return attributes[attributeName].Value;
    }
}

这样一来,原来的代码就可以写成如下了:

string name = subNetworkNode.Attributes.GetAttributeValue("name");

初一看,就像是XmlAttributeCollection这类原来就有GetAttributeValue(string attributeName)这样一个方法,其实这个方式是我们自己扩展的。
定义扩展方法有几点:
1、定义扩展方法的类必须用static修饰,即必须为静态类。
2、定义的扩展方法必须用static修饰,即必须为静态方法,同时方法的第一个参数前必须加this修饰,this后必须是类名,表示为this后的类添加扩展方法,如本例中this XmlAttributeCollection attributes表示为XmlAttributeCollection这个类添加扩展方法,如果需要在方法体内访问XmlAttributeCollection这个类的实例,通过后面的attributes参数即可(注意这个参数的名称可以随便取)。

Dapper介绍
通过上面的介绍,大家可以初步了解扩展方法是怎么回事。其实Dapper主要也是用了扩展方法为IDbConnection和IDataReader添加扩展方法,比如在SqlMapper.cs中有如下代码为IDbConnection添加扩展方法(节选):

// <summary>
/// Execute parameterized SQL.
/// </summary>
/// <param name="cnn">The connection to query on.</param>
/// <param name="sql">The SQL to execute for this query.</param>
/// <param name="param">The parameters to use for this query.</param>
/// <param name="transaction">The transaction to use for this query.</param>
/// <param name="commandTimeout">Number of seconds before command execution timeout.</param>
/// <param name="commandType">Is it a stored proc or a batch?</param>
/// <returns>The number of rows affected.</returns>
public static int Execute(this IDbConnection cnn, string sql, object param = null, IDbTransaction transaction = null, int? commandTimeout = null, CommandType? commandType = null)
{
    var command = new CommandDefinition(sql, param, transaction, commandTimeout, commandType, CommandFlags.Buffered);
    return ExecuteImpl(cnn, ref command);
}

/// <summary>
/// Execute parameterized SQL.
/// </summary>
/// <param name="cnn">The connection to execute on.</param>
/// <param name="command">The command to execute on this connection.</param>
/// <returns>The number of rows affected.</returns>
public static int Execute(this IDbConnection cnn, CommandDefinition command) => ExecuteImpl(cnn, ref command);

在SqlMapper.IDataReader.cs为IDataReader添加扩展方法的代码(节选):

/// <summary>
/// Parses a data reader to a sequence of data of the supplied type. Used for deserializing a reader without a connection, etc.
/// </summary>
/// <typeparam name="T">The type to parse from the <paramref name="reader"/>.</typeparam>
/// <param name="reader">The data reader to parse results from.</param>
public static IEnumerable<T> Parse<T>(this IDataReader reader)
{
    if (reader.Read())
    {
        var deser = GetDeserializer(typeof(T), reader, 0, -1, false);
        do
        {
            yield return (T)deser(reader);
        } while (reader.Read());
    }
}

/// <summary>
/// Parses a data reader to a sequence of data of the supplied type (as object). Used for deserializing a reader without a connection, etc.
/// </summary>
/// <param name="reader">The data reader to parse results from.</param>
/// <param name="type">The type to parse from the <paramref name="reader"/>.</param>
public static IEnumerable<object> Parse(this IDataReader reader, Type type)
{
    if (reader.Read())
    {
        var deser = GetDeserializer(type, reader, 0, -1, false);
        do
        {
            yield return deser(reader);
        } while (reader.Read());
    }
}

在本人2011年7月25日写的一篇名为《利用ADO.NET的体系架构打造通用的数据库访问通用类》的博客当中介绍了ADO.NET的体系架构,如下图:
在.NET数据库访问方面的Dapper类库介绍
就是首先定义了一系列的借口,如IDbConnection之类的,任何基于数据库访问只要实现了接口的定义,就都能在.NET访问,包括了微软自己对SQL Server和Access等数据库的实现以及MySQL和Oracle针对这个定义的第三方实现(其实JDBC也是这个道理,只不过是基于Java实现罢了)。因为包括SQL Server/MySQL/Oracle/PostgreSQL/SQLite在内的数据库都实现了IDbConnection的定义,而Dapper又是基于IDbConnection的扩展,因此使用Dapper理论上可以访问任何支持ADO.NET访问的数据库(前提是需要相关的数据库驱动,dll形式)。
在使用Dapper的实际开发中,用得较多的还是针对IDbConnection的扩展方法,主要有:
int Execute():相当于Command.ExecuteNonQuery(),指定增加、删除、修改SQL语句,返回受影响的行数。
object ExecuteScalar():相当于Command. ExecuteScalar(),返回结果集第一行第一列,用于聚合函数等。
T ExecuteScalar<T>():相当于Command. ExecuteScalar(),返回结果集第一行第一列,不过返回的结果指定了具体类型。
IDataReader ExecuteReader():相当于Command. ExecuteReader()。
IEnumerable<dynamic> Query()
IEnumerable<T> Query<T>()
IEnumerable<object> Query()
IEnumerable<dynamic> Query()
dynamic QueryFirst()
dynamic QueryFirstOrDefault()
dynamic QuerySingle()
dynamic QuerySingleOrDefault()
IEnumerable<T> Query<T>()
T QueryFirst<T>()
T QueryFirstOrDefault<T>()
T QuerySingle<T>()
T QuerySingleOrDefault<T>()
IEnumerable<object> Query()
object QueryFirst()
object QueryFirstOrDefault()
object QuerySingle()
object QuerySingleOrDefault()
对于上面各种类型的Query和返回结果,就是分几种情况:返回一个实现IEnumerable接口的结果集,返回单个结果,返回单个结果或在没有找到匹配结果下返回默认值(引用类型、数值类型、枚举、日期等的默认值)

基本用法
使用了Dapper之后,在插入或者查询时默认是按照数据库字段名与类属性名不区分大小写的情况下对应。
加入有在SQL Server中有如下表:

CREATE TABLE IF NOT EXISTS tblBay (
    Id                   integer              NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
    Name                 nvarchar(50)         not null,
    Desc                 nvarchar(100)        not null
    )

同时有如下类定义:

public class Bay
{
    public int ID { get; set; }
    public string Name { get; set; }
    public string Desc { get; set; }
}

那么插入可以这么写:

string connectionString = ".";//将这里改成你自己的数据库连接字符串
string sqlInsert = "INSERT INTO tblBay(Name,Desc)VALUES(@Name,@Desc)";
SqlConnection connection = new SqlConnection(connectionString);
Bay bay = new Bay { Name = "test", Desc = "desc" };
connection.Execute(sqlInsert, bay);

查询可以这么写:

string connectionString = ".";//将这里改成你自己的数据库连接字符串
string sqlQuery = "select * from tblBay where Id=@Id";
int id = 1;
SqlConnection connection = new SqlConnection(connectionString);
IEnumerable<Bay> bayList = connection. QueryFirstOrDefault<Bay>(sqlQuery,new { @Id = id });

字段与属性不一致情况下关联
但是在某些情况下,比如使用MySQL数据库时我们可能会在由多个单词构成的字段名之间以下划线分割,如”user_id”、”user_name”等,而定义实体类时我们又将实体类的属性定义为UserId、UserName,那么就需要为他们之间建立关联,比较简单的一种方式就是在select的时候使用as。
假定在MySQL中存在如下表:

CREATE TABLE IF NOT EXISTS tblperson (
    user_id               integer              NOT NULL PRIMARY KEY AUTOINCREMENT UNIQUE,
    user_name            nvarchar(50)         not null,
    email                 nvarchar(100)        not null
    )

而对应的实体类为:

public class Person
{
    public int UserId { get; set; }
    public string UserName { get; set; }
    public string Email { get; set; }
}

那么插入可以这么写:

string connectionString = ".";//将这里改成你自己的数据库连接字符串
string sqlInsert = "INSERT INTO tblperson(user_name,email)VALUES(@Name,@Email)";
SqlConnection connection = new SqlConnection(connectionString);
Person person = new Person { UserName = "test", Email = "email@email.com" };
DynamicParameters parameters = = new DynamicParameters();
parameters.Add("@Name", person.UserName);
parameters.Add("@Email", person.Email);
connection.Execute(sqlInsert, parameters);

查询可以这么写:

string connectionString = ".";//将这里改成你自己的数据库连接字符串
string sqlQuery = "select user_id as userId,user_name as username,email from tblperson where user_id=@UserId";
int userId = 1;
SqlConnection connection = new SqlConnection(connectionString);
DynamicParameters parameters = = new DynamicParameters();
parameters.Add("@UserId ", userId);
IEnumerable<Person> bayList = connection. QueryFirstOrDefault<Person>(sqlQuery, parameters);

也就是数据库字段名与实体类属性名如果忽略大小写的情况下是一致的,则我们无需单独处理它们之间的映射关系,如果数据库字段名与实体类属性在忽略大小写的情况下仍然不一致,那么我们需要手动处理映射:在INSERT、DELETE、UPDATE时可以通过DynamicParameters来处理;在SELECT时可以通过在SQL语句中使用AS来处理。
有关Dapper的更进一步用法可以查看Dapper的用户手册或直接查看源代码。

周金桥
2018/04/22