65.9K
CodeProject 正在变化。 阅读更多。
Home

封装ADO.NET核心组件 - C#类库

2012 年 6 月 7 日

CPOL

9分钟阅读

viewsIcon

34009

downloadIcon

423

本文将解释我创建的类库以及如何在您的代码中使用它。该类库旨在通过封装ADO.NET的核心组件来简化数据访问和/或操作的编码。

引言  

ADO.NET提供了许多用于访问或操作数据库中数据的类。使用这些类有时会变得重复。想象一下您有5个执行检索操作的不同方法。每个方法很可能包含以下步骤:   

  1. 创建并打开连接对象(System.Data.SqlClient.SqlConnectionSystem.Data.OracleClient.OracleConnection)   
  2. 创建命令对象(System.Data.SqlClient.SqlCommand 或 System.Data.OracleClient.OracleCommand)   
  3. 创建并迭代数据读取器对象以从数据库中获取记录(System.Data.SqlClient.SqlDataReader 或 System.Data.OracleClient.OracleDataReader) 

使用我创建的类库,所有这些重复的代码都得到了处理。类库的核心是OpenEm.QLite.Database 类,它隐藏了ADO.NET核心组件的初始化,同时还通过事件参数属性或回调参数公开它们,以便开发人员仍然可以访问它们的属性。   

背景        

你们中的许多人可能已经在项目中使用对象关系映射(ORM)工具。我也是。但是,我创建这个类库的主要原因是因为最近我遇到了一个情况,我需要一些直接、让我完全控制编写SQL语句并且不隐藏检索到的数据库记录和CLR对象之间映射的东西。  

值得了解        

在我们进入“操作方法”部分之前,最好先了解以下主题: 

  1. Generics 
  2. DbProviderFactories 
  3. 自定义配置节 
  4. 委托和Lambda表达式
  5. yield 关键字 
  6. 扩展方法  

虽然其中一些主题并未直接用于本文的示例代码中,但了解它们将让您更好地理解该库的工作原理。  

使用类库  

该类库只是一个名为OpenEm.QLite的单个DLL文件。您可以从上面的链接下载此文件。将此程序集的引用添加到您的项目。完成此操作后,将OpenEm.QLite命名空间添加到您的代码中:     

using OpenEm.QLite;          

OpenEm.QLite程序集中命名空间的完整列表如下:  

  • OpenEm.QLite  
  • OpenEm.QLite.Configuration  
  • OpenEm.QLite.Extensions     

OpenEm.QLite命名空间包含Database类,这是该库最重要的组件。大部分编码将围绕此封装了ADO.NET核心组件的类进行。

使用OpenEm.QLite.Database类非常简单。我们可以将其用法分为3个简单步骤: 

  1. 配置(可选)   
  2. 连接  
  3. 执行  

本文的下一节将详细讨论这3个步骤。从现在开始,为了简洁起见,我将把OpenEm.QLite.Database称为Database。 

配置数据库组件  

配置Database组件不是强制性的。但是,我总是选择设置配置,这样我就可以轻松更改全局属性的值,而无需修改我的代码。  

要设置配置,您必须在.config文件中添加OpenEm.QLite.Configuration.DatabaseConfigurationSection节。下面提供了一个示例:   

<configSections>
  <sectionGroup name="openEm">
    <section name="qLite" 
             type="OpenEm.QLite.Configuration.DatabaseConfigurationSection, OpenEm.QLite" />
  </sectionGroup>
</configSections> 

接下来,您必须添加自定义配置节。请注意,您可以为不同的数据库添加不同的配置。 请看下面的示例:  

<openEm>
    <qLite default="main">
      <databases>
        <add name="main"
             connectionStringName="Main"
             alwaysDisconnectAfterExecution="true"
             commandTimeout="60"
             commandType="StoredProcedure" />
        <add name="admin"
             connectionStringName="Admin"
             alwaysDisconnectAfterExecution="false"
             commandType="Text" />
      </databases>
    </qLite>
</openEm>  

让我们讨论上面的XML:  

  1. <openEm> 是一个可选的节组。  
  2. <qLite> 是强制性的。您可以向此元素添加一个default属性,以指定哪个配置是默认数据库配置。在上面的示例中,main是默认配置。 
  3. 以下是您可以定义的配置属性: 
    • name - 这是强制性的,并且必须是唯一的。它被OpenEm.QLite.Configuration.DatabaseConfigurationCollection用作键。 
    • connectionStringName - 该值必须是您.config文件中指定连接字符串的名称。这是强制性的。这意味着您必须将连接字符串放在<connectionStrings>集合中。 
    • alwaysDisconnectAfterExecution - 这是可选的。如果设置为true,则在每次执行与CRUD相关的Database方法后关闭连接。默认情况下,它设置为false。 
    • commandTimeout - 这是可选的。如果未指定,则使用底层CommandTimeout的默认值。 
    • commandType - 这是可选的。如果未指定,则使用底层CommandType的默认值。  
再次强调,您必须将连接字符串放在<connectionStrings>集合中。您还必须指定providerName。该类库使用System.Data.Common.DbProviderFactories,它需要一个提供程序名称来获取适当的System.Data.Common.DbProviderFactory。当您调用Database.Connect()方法时,底层System.Data.Common.DbProviderFactory会为您创建适当的连接对象。

最后,要在代码中使用配置,您必须调用Database.Configure()方法。它接受一个字符串类型的参数,该参数必须是您在.config文件中指定的配置节的名称。 

Database.Configure("openEm/qLite");   

您只需在项目中调用此方法一次。如果您正在构建Web应用程序,可以在Global.asaxApplication_Start()事件处理程序中调用此方法。 

连接到数据库 

使用Database类连接到数据库有3种方法。如果您正在使用配置,只需执行以下操作: 

using (var db = Database.Connect())
{
    // some code here
}    

在上面的示例中,默认配置的connectionStringNameDatabase.Connect()方法内部用于为您创建和打开连接对象。如果您没有指定默认配置,它将从<databases>集合的第一个元素获取connectionStringName。  

连接到数据库的另一种方法是将您首选配置的名称传递给Database.Connect()方法(您可以参考上一节的示例XML配置):  

using (var db = Database.Connect("admin"))
{
    // some code here
}    

最后,如果您不希望使用任何配置设置,可以使用上一示例中Database.Connect()方法的重载,但这次,将连接字符串的名称作为方法参数传递。请看下面的示例: 

<connectionStrings>
    <add name="Developer" 
         connectionString="Data Source=.;Initial Catalog=DevDB;Integrated Security=SSPI" 
         providerName="System.Data.SqlClient" />
</connectionStrings>  
using (var db = Database.Connect("Developer"))
{
    // some code here
}    

请注意,Database类实现了System.IDisposable接口。建议在初始化Database对象时使用using关键字,以确保在using块结束时自动释放它。 另外,您可以调用实例方法Database.Disconnect()来关闭连接。 

执行CRUD操作 

在这个类库中,我花费了更多的时间和精力来设计和编写用于检索操作的方法。 但是,我确实创建了一个用于创建、更新和删除操作的Database.ExecuteNonQuery()方法。此方法只是调用底层命令对象的ExecuteNonQuery() 方法。  

创建、更新和删除操作  

下面的示例演示了如何在数据库中添加记录。如果您想使用Database.ExecuteNonQuery() 方法来更新或删除记录,只需将更新或删除的SQL语句传递给该方法。 

var person = new Person // this is just a sample object
{
    ID = 0,
    FirstName = "John",
    LastName = "Doe"
    // some more properties
};
 
var sql = "INSERT INTO [Person] ([FirstName], [LastName]) VALUES (@FirstName, @LastName)" +
" SET @ID = @@IDENTITY";
Parameterize param = (parameters) =>
{
    parameters.Add("@FirstName", person.FirstName);
    parameters.Add("@LastName", person.LastName);
    parameters.Add("@ID", ParameterDirection.Output, DbType.Int32);
};
CommandCallback onClosing = (command) => person.ID = Convert.ToInt32(command.Parameters["@ID"]);
 
using (var db = Database.Connect())
{
    var result = db.ExecuteNonQuery(sql, param, onClosing);
}      

让我解释一下上面示例中使用的3个参数

  1. sql - 此参数必须是您要执行的SQL语句或存储过程的名称。 
  2. param - 这是一个OpenEm.QLite.Parameterize委托,它接受一个OpenEm.QLite.CommandParameters对象作为参数。 OpenEm.QLite.CommandParameters对象是您添加SQL参数的地方。
  3. onClosing - 这是一个OpenEm.QLite.CommandCallback委托,它接受一个System.Data.Common.DbCommand对象作为参数。此回调在命令对象关闭或释放之前调用,是您检索输出或返回参数值的最佳位置。  

检索操作   

对于数据检索,您有更多选择。我创建了3种不同的方法来帮助您:

  • GetAll<T>() - 顾名思义,它检索所有数据并返回一个System.Collections.Generic.IEnumerable<T> 对象。您无法通过此方法添加SQL参数来过滤查询。  
  • Get<T>() - 此方法让您可以选择添加参数来过滤查询或检索输出或返回参数。它返回一个System.Collections.Generic.IEnumerable<T>对象。 
  • GetOne<T>() - 此方法类似于Get<T>(),但它只返回一个T实例。 

上述每个方法都有多个重载,并且每个重载都有自己的方式将检索到的数据库记录映射到CLR对象。以下是所有3个“get”方法的不同映射方式:   

  • OpenEm.QLite.Mapping<T> - 此委托接受一个System.Data.Common.DbDataReader对象作为参数,您可以使用它来读取检索到的值。您不需要迭代读取器对象。这已经在内部完成。下面提供了一个如何使用此委托的示例: 
var sql = "SELECT [ID], [FirstName], [MiddleName], [LastName] FROM [Person]";
Mapping<Person> mapping = (reader) =>
{
    return new Person
    {
        ID = reader.GetInt32("ID"),
        FirstName = reader.GetString("FirstName"),
        LastName = reader.GetString("LastName")
        // some more properties
    };
}; 
 
using (var db = Database.Connect())
{
    var people = db.GetAll(sql, mapping).ToList(); 
}   
  • OpenEm.QLite.IMapper<T> - 此接口有一个Map()方法,也接受一个System.Data.Common.DbDataReader对象作为参数。其概念与OpenEm.QLite.Mapping<T>委托非常相似。如果您想实现除映射本身之外的更多逻辑,可以使用此选项。下面提供了一个示例: 
public class PersonMapper : IMapper<Person>
{ 
    public int CountOfMothers { get; private set; } 
 
    public Person Map(DbDataReader reader)
    { 
        var personType = reader.GetString("Type");
        if (peronType == "Mother")
            CountOfMothers += 1; 
        return new Person
        {
            ID = reader.GetInt32("ID"),
            FirstName = reader.GetString("FirstName"),
            LastName = reader.GetString("LastName"),
            Type = personType
            // some more properties
        }; 
    }
}  
var sql = "SELECT [ID], [FirstName], [MiddleName], [LastName] FROM [Person]"; 
var mapper = new PersonMapper();
 
using (var db = Database.Connect())
{
    var people = db.GetAll(sql, mapper).ToList();
    var countOfMothers = mapper.CountOfMothers;
}     
  • 通过反射进行映射 - 如果您不想控制映射过程,则有不需要OpenEm.QLite.Mapping<T>委托或OpenEm.QLite.IMapper<T>接口参数的“get”方法重载。这些方法在内部使用反射将数据库记录映射到CLR对象。但是,我还需要提到的是,当使用反射时,只映射具有匹配实体属性的检索到的列。建议仅在非常简单的映射场景中使用此方法。对于复杂或不那么简单的映射场景,建议您使用上面提到的其他技术。 

最后,您可以实现OpenEm.QLite.ICollectionMapper<T>接口。它继承了OpenEm.QLite.IMapper<T>接口,该接口有一个Map()方法。OpenEm.QLite.ICollectionMapper<T>只能与返回System.Collections.Generic.IEnumerable<T> 对象的“get”方法一起使用。它让您可以控制迭代System.Data.Common.DbDataReader 对象。请看下面的示例:  

public class OrdersMapper : ICollectionMapper<Order>
{
    public IEnumerable<Order> Map(DbDataReader reader)
    { 
        var order = default(Order);
        var dictionary = new Dictionary<Guid, Order>(); 
        while (reader.Read())
        {
            var id = reader.GetGuid("ID");
            if (dictionary.ContainsKey(id))
                order = dictionary[id];
            else
            {
                order = MapOrder(reader);
                order.ID = id;
                dictionary.Add(id, order);
            }
            var orderItem = MapOrderItem(reader);
            order.OrderItems.Add(orderItem);
        }
        return dictionary.Values.ToList();
    }
    private Order MapOrder(DbDataReader reader)
    { 
        // do individual mapping here
        // this method is not an ICollectionMapper<T> member
    }
    private OrderItem MapOrderItem(DbDataReader reader)
    {
        // do individual mapping here
        // this method is not an ICollectionMapper<T> member
    }
}  
var sql = "SELECT [o].[ID], [o].[Number], [o].[DateOrdered], [oi].[ID] AS [OrderItemID]," +
" [oi].[OrderID], [oi].[Item], [oi].[Description] FROM [Order] [o]" +
" INNER JOIN [OrderItem] [oi] ON [o].[ID] = [oi].[OrderID]";
ICollectionMapper<Order> collectionMapper = new OrdersMapper(); 
 
using (var db = Database.Connect())
{
    var orders = db.GetAll(sql, collectionMapper);
} 

请注意,不接受OpenEm.QLite.ICollectionMapper<T>接口作为参数且不返回System.Collections.Generic.IEnumerable<T>对象的“get”方法在内部使用yield关键字,这意味着执行是延迟的。使用这些方法时要小心。下面的示例将抛出异常:  

var sql = "SELECT [ID], [FirstName], [MiddleName], [LastName] FROM [Person]";
Mapping<Person> mapping = (reader) =>
{
    return new Person
    {
        ID = reader.GetInt32("ID"),
        FirstName = reader.GetString("FirstName"),
        LastName = reader.GetString("LastName")
        // some more properties
    };
}; 
 
using (var db = Database.Connect())
{
    var collection = db.GetAll(sql, mapping); 
    var count = collection.Count(); // the DbDataReader object will be closed here 
    var people = collection.ToList(); // exception is thrown here
}     

更多方法重载   

Database类具有更多与CRUD相关的方法重载,可以在OpenEm.QLite.Extensions命名空间中找到。这些方法是扩展方法。我将它们隔离在另一个命名空间中的原因是我觉得这些方法很少使用。我编写它们的原因是我觉得拥有它们会很好。

如果您希望使用扩展方法,只需将OpenEm.QLite.Extensions命名空间添加到您的代码中:  

using OpenEm.QLite.Extensions;  

结论 

我只想说这是我在此网站上的第一篇文章。我通常不写博客或在线文章。在过去的6个月里,我一直在新加坡工作,远离朋友和家人,空闲时间没有太多事情可做。撰写这篇文章让我在异乡“独处”的时光变得非常富有成效和愉快。我享受了每天撰写这篇文章和创建类库的过程。我知道它不是最激动人心的文章之一(我知道这一点,因为作为作者,我在校对时睡着了),但我希望您觉得它有帮助和信息量。如有问题或意见,请随时与我联系。 

历史     

2012-06-07 : 首次发布

© . All rights reserved.