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

使用 oSo.SimpleSql 快速轻松地访问数据

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.95/5 (79投票s)

2009年6月8日

公共领域

14分钟阅读

viewsIcon

186217

downloadIcon

1542

一个使用泛型、Lambda 表达式、流畅接口和 SqlProviderFactory 的数据访问框架。

引言

几年来,我一直在使用 CodeProject 来提升我的 .NET 知识,现在是时候回馈了!这是我的第一篇文章。我过去曾计划撰写一系列文章,但已经被这个网站上许多优秀的帖子抢先了。虽然数据访问框架绝对不是什么新鲜事物,但我认为我的 oSo.SimpleSql(发音为 Oh So Simple SQL)框架有几处添加,是其他框架所没有的。我广泛使用了 C# 2.0 和新的 3.5 特性,如泛型、Lambda 表达式、扩展方法,以及一些有用的模式,如生成器模式和流畅接口。希望 CodeProject 社区中的一些人能在自己的项目中发现这些代码或相关的概念很有用。

所有数据访问框架都试图解决一个共同的问题。它们都试图尽可能轻松无痛地从数据存储(99% 的情况是数据库)中检索和存入数据。许多框架还试图将每个数据库的特性隐藏在抽象层后面,理论上,这允许您轻松地将数据存储替换为另一个(例如,从 SQL Server 切换到 MySQL)。这种抽象通常用于将 SQL 结果转换为代表您的业务域的对象。我试图用 SimpleSql 同时涵盖简单数据访问和简单的对象关系映射。

背景

我为什么要创建 SimpleSql?市面上有海量的框架,如微软的 DAAB、LINQ to SQL、NHibernate,以及冉冉升起之星 SubSonic。真的还需要另一个吗?我曾使用过一个内部数据访问框架/层一段时间,它只适用于 SQL Server。我曾计划更新这个框架,使其更具数据库无关性,并更紧密地映射我所处理的项目类型。如今,我处理的许多项目都有已经创建了大量数据库结构的数据库管理员。出于安全原因,他们喜欢严格使用存储过程和锁定的视图来访问数据。我想要一种能够轻松集成到现有代码和数据库结构中,同时让新开发人员在任何使用该框架的项目中都能快速上手的东西。我开始进行一些小的修改,直到它演变成一次全面的重写。因此,SimpleSql 诞生了。

但是,您会说,那些其他框架呢?嗯,我很快就排除了 LINQ to SQL,原因有很多。LINQ to SQL 实际上应该称为 LINQ to SQL Server,因为它开箱即用只访问一个数据库。此外,微软似乎正在放弃对它的支持,转而支持下一个新的 Redmond 技术,即实体框架。NHibernate 是一个非常强大的 ORM 框架,几乎可以做任何事情。这是它最大的优点和缺点。因为它非常强大,所以学习曲线有点陡峭。我想要一个数据访问框架,让从未见过该框架的开发人员能够非常快速地掌握。

SubSonic 是当前热门的新型对象关系映射器 (ORM)。它比许多其他 ORM 工具要简单得多。但是,与其他 ORM 一样,它试图通过代码模拟 SQL,同时勉强支持存储过程。如果让 SubSonic 控制您的数据库架构和相关类,它会表现得最好。对于新项目来说,这可能是一个不错的选择。但是,如果您已经有了很多现有结构,并且/或者数据库管理员规定了特定的数据库架构和安全性,那么 SubSonic 可能就不那么适合了。SubSonic 还向您的纯 C# 对象 (POCO) 添加了“Save”等方法,使其能够知道如何将自身保存到数据库。我不喜欢这种模式。它使得在不同上下文和域中使用相同的对象变得困难。

但是,我不想过于轻视 SubSonic 或其他任何框架。例如,我认为 SubSonic 非常酷,并且一些非常聪明的人投入了大量精力来开发它。我计划模仿它在 Visual Studio 中自动生成代码的流畅性,用于自动生成我的 SimpleSql 代码!最终,这只是一个有趣的练习,以提高我的 .NET 3.5 技能!

SimpleSql 的一些优势

  • 使开发人员不必编写那么多冗长的 ADO.NET 代码。
  • 它是数据库无关的。
  • 它可以创建事务,而无需开发人员回退到 ADO.NET。它也适用于 TransactionScope 和常规事务。
  • 它支持输出参数和多个结果集,并且可以返回强类型对象和集合。
  • 非常易于使用。

致谢

我要感谢 CodeProject 上的许多人,他们加速了我新数据访问框架的开发。为了减少最初的反射调用次数,Herbrandson 的动态代码生成文章派上了用场。我修改了他的代码,在所有可能的地方都使用了泛型。我还使用了 Andrew Chan 的高性能反射 ORM 文章中的许多辅助方法,并对它们进行了修改以满足我项目的需求。sklivvz 的 SixPack CodeProject 文章中的一些概念也加强了我的一些设计决策。有关所有这些文章的链接,请参阅页面底部的 参考文献

说够了,代码在哪?

基本用法

您会看到许多框架中都有“StoredProcedure”这个方法名,我也没有偏离这个先例 :)。确实没有比这更好的词了。

using oSo

List<Employee> employees = SimpleSql
                            .StoredProcedure("GetAllEmployees")
                               .FetchAll<Employee>();

感受 ORM 的力量!

这将调用一个不带参数的存储过程,并自动填充数据库返回的每个 `employee` 对象中 `public` 属性。SimpleSql 的对象关系映射 (ORM) 功能使其能够轻松地返回填充了数据库数据的对象和列表。稍后我们将讨论更复杂的场景。您也可以使用内联查询进行相同的调用,如下所示:

using oSo

List<Employee> employees = SimpleSql
                            .Query("Select * From Employees")
                               .FetchAll<Employee>();

如果没有指定参数,StoredProcedure 调用将毫无意义。这是一个带有两个输入参数的示例:

using oSo

Employee emp = SimpleSql
                .StoredProcedure("pGetEmployee")
                    .AddParameter("@EmployeeID", 1)
                    .AddParameter("@EmployeeLastName", "Smith")
                        .Fetch<Employee>();

*注意:如果单个 `Fetch<T>()` 调用没有返回任何记录,则返回 `null`。如果 `FetchAll<T>` 调用没有返回任何记录,则返回一个计数为零的空列表。

保存我!

您可以使用上面我演示的 `AddParameter` 方法来保存和检索任何数据库的数据。但是,如果您使用的是 SQL Server,它允许 SimpleSql 发现存储过程中将要使用的参数。这允许您以一种非常简单且 ORM 的方式保存您的对象。像插入和更新这样不需要返回任何内容的操作,可以使用 `Execute` 方法重载之一来处理。

using oSo
//Assume that we already got an Employee object named emp from somewhere
SimpleSql.StoredProcedure("pInsertEmployee").Execute(emp);

那个对象关系不太对

SimpleSql 通过匹配 `DbDataReader` 字段名称与您对象中的 `public` 属性来填充对象。如果名称匹配,属性将被设置为数据库数据。但是,当您有一个对象,其属性与 `DbDataReader` 字段名称不匹配时会发生什么?SimpleSql 通过 `Column` 属性提供了一种简单的方法来处理这种情况。

using oSo.Attributes;

public class Employee{
  
  public String EmployeeFirstName { get; set; }
  
  [Column("EmployeeAge")]
  public Int32 Age { get; set; }  
  
}

现在,当 SimpleSql 获取记录时,它会将 `DbDataReader` 字段与 `EmployeeAge` 匹配,以填充 `Age` 属性。由于 `EmployeeFirstName` 没有 `Column` 属性,它将使用该名称来匹配数据库字段。如果属性名称不匹配且未指定 `Column` 属性,SimpleSql 将简单地忽略该属性。

流畅接口、生成器模式和代码,哦我的!

流畅接口是 Eric Evans 和 Martin Fowler 创造的一个术语。如果您接触 .NET 有一段时间了,无疑已经在代码中多次做过类似的事情:

//Contrived example for demonstration purposes
String myString = "A SENTENCE I WANT TO LOWERCASE THAT ALSO CONTAINS AN ' APOSTROPHE.";
myString = myString.Replace("'", "").ToLower().Replace("a ", "A ");

.NET 的 `String` 类使用 生成器模式 来实现连续调用返回 `String` 的方法。流畅接口更进一步,试图使方法调用链更像 领域特定语言,读起来更像一个句子。然而,流畅接口更难构建,需要仔细考虑才能正确命名方法。这是 `QueryBuilder` 类。它是 SimpleSql 的核心类之一。

namespace oSo
{
    /// <summary>
    /// The QueryBuilder class uses the Builder Pattern
    /// with the StoredProcedure and Query Objects.
    /// This is what makes the fluent interface in SimpleSql possible.
    /// </summary>
    /// <typeparam name="T">Any class that inherits
    /// from the BaseQuery class</typeparam>
    /// 
    public class QueryBuilder<T> where T : BaseQuery
    {
        protected internal T query = default(T);

        /// <summary>
        /// Public constructor that sets the query object
        /// used (either StoredProcedure or Query)
        /// </summary>
        /// <param name="obj">The BaseQuery
        ///   or child of BaseQuery object being stored</param>
        /// 
        public QueryBuilder(T obj)
        {
            query = obj;
        }

        /// <summary>
        /// This method is used to map the fields of an object
        ///         to the fields in a DbDataReader in Fetches and FetchAlls
        /// </summary>
        /// <param name="objectRecordMapper">The Func delegate to execute</param>
        /// <returns>this</returns>
        /// 
        public QueryBuilder<T> Map(Func<DbDataReader, object> objectRecordMapper)
        {
            query.Map(objectRecordMapper);
            return this;
        }


        /// <summary>
        /// Set the time the query will wait for the command to complete
        /// </summary>
        /// <param name="timeout">time to wait</param>
        /// <returns>this</returns>
        /// 
        public QueryBuilder<T> CommandTimeout(int timeout)
        {
            query.CommandTimeout = timeout;
            return this;
        }

        /// <summary>
        /// Use this in an existing transaction. Ideally for use
        /// with the SimpleSql.CreateTransaction() method
        /// </summary>
        /// <param name="transaction">the
        ///   existing transaction to use</param>
        /// <returns>this</returns>
        /// 
        public QueryBuilder<T> WithTransaction(DbTransaction transaction)
        {
            query.WithTransaction(transaction);
            return this;
        }

        #region Action Endpoints

        /// <summary>
        /// This will fetch a single record from the DB and map it to a concrete object
        /// </summary>
        /// <typeparam name="O">The type of object
        ///           to map this query to</typeparam>
        /// <returns>The object specified by the type parameter</returns>
        /// 
        public O Fetch<O>()
        {
            return query.Fetch<O>();
        }

        /// <summary>
        /// This will map each record in the DB resultset to a concrete object
        /// </summary>
        /// <typeparam name="O">The type of object
        ///            to map this query to</typeparam>
        /// <returns>A list of strongly type objects specified
        ///            by the type parameter</returns>
        /// 
        public List<O> FetchAll<O>()
        {
            return query.FetchAll<O>();
        }

        /// <summary>
        /// Executes a query where you don't expect any results back
        /// </summary>
        /// 
        public void Execute()
        {
            query.Execute();
        }

        /// <summary>
        /// Executes a query that returns a single value
        /// </summary>
        /// <typeparam name="O">The type of value
        ///           that you want returned</typeparam>
        /// <returns>The type specified by the type parameter</returns>
        /// 
        public O ExecuteScalar<O>()
        {
            return query.ExecuteScalar<O>();
        }

        /// <summary>
        /// Fetches multiple resultSets
        /// </summary>
        /// <returns>A MultiResult object containing the collection of results</returns>
        /// 
        public MultiResult FetchMultiple()
        {
            return query.FetchMultiple();
        }

        /// <summary>
        /// Gives you access to the underlying DbDataReader
        /// to handle the records the way you want
        /// </summary>
        /// <returns>DbDataReader object</returns>
        /// 
        public DbDataReader FetchReader()
        {
            return query.FetchReader();
        }

        /// <summary>
        /// Fetches a DataSet
        /// </summary>
        /// <returns>returns a DataSet</returns>
        /// 
        public DataSet FetchDataSet()
        {
            return query.FetchDataSet();
        }
        
        #endregion

    }
}

这段代码中有一些内容,所以让我们更仔细地看看 `QueryBuilder`。SimpleSql 中使用的两个主要类,即 `StoredProcedure` 和 `Query`,都继承自一个名为 `BaseQuery` 的基类。

//Code snippet from QueryBuilder class
public class QueryBuilder<T> where T : BaseQuery
{
    protected internal T query = default(T);

    /// <summary>
    /// Public constructor that sets the query object
    /// used (either StoredProcedure or Query)
    /// </summary>
    /// <param name="obj">The BaseQuery or child
    /// of BaseQuery object being stored</param>
    /// 
    public QueryBuilder(T obj)
    {
        query = obj;
    }

我在这里使用了泛型,以确保 `QueryBuilder` 内部使用的类可以是任何类型,只要它在其继承链中的某个点继承自 `BaseQuery` 类。`BaseQuery` 类处理了 `StoredProcedure` 和 `Query` 类的大部分繁重工作,因为它们使用的大多数方法对两者都是通用的。

public QueryBuilder<T> CommandTimeout(int timeout)
{
    query.CommandTimeout = timeout;
    return this;
}

请注意 `CommandTimeout` 等方法。它在设置查询对象的 `CommandTimeout` 属性后,会返回自身。`return this` 使得我们可以链接其他的 `QueryBuilder` 方法。

public O Fetch<O>()
{
    return query.Fetch<O>();
}

您在之前的示例中看到了 `Fetch()` 方法。`Fetch()` 等方法结束了生成器模式。我称它们为操作终结点 (AEP),因为这些方法要么是 void,要么从数据库返回结果。所有 AEP 方法都利用 `System.Data.Common` 命名空间中的类来检索或持久化数据。理论上,这使得通过在 `.config` 文件中更改提供程序名称来替换整个数据库成为可能!

连接字符串在哪里?

由于之前的代码示例中没有指定任何连接字符串信息,SimpleSql 使用 `.config` 文件中指定的第一个连接字符串。您也可以按 `.config` 文件中指定的名称传递连接字符串,或者显式传递所有连接字符串信息。

using oSo
   
int employeeAge = SimpleSql
                    .StoredProcedure("pGetEmployeeAge", 
                                     "WebConfigConnectionName")
                        .AddParameter("@EmployeeID", 2)
                            .ExecuteScalar<int>();

此示例使用泛型从数据库返回单个值,无需强制转换!您将连接字符串参数作为 `StoredProcedure` 或 `Query` 构造函数的参数传递。如果您的连接字符串不在 `.config` 文件中,您也可以直接传递信息。

using oSo

string connString = "Data Source=.\SQLExpress;Initial Catalog=" + 
                    "SimpleDb;User Id=SimpleUser;Password=SimplePassword;";
string provider = "System.Data.SqlClient";

int employeeAge = SimpleSql
                    .Query("Select EmployeeAge From dbo.Employee Where EmployeeID = 2", 
                           connString, provider)
                        .ExecuteScalar<int>();

我能得到一个扩展吗?

我希望所有方法都能通过 `QueryBuilder` 类,这样我就可以链接方法调用。但是,像“AddOutParameter”(添加输出参数)这样的方法只特定于 `StoredProcedures`,在链接 `Query` 方法调用时,它们不应该出现在 IntelliSense 中。扩展方法来帮忙!点击此处了解更多关于这个很棒的 C# 3.5 特性的信息。

public static class QueryBuilderExtensionMethods
{
    public static QueryBuilder<StoredProcedure>
       AddOutParameter(this QueryBuilder<StoredProcedure> qb, 
       string parameterName, object value)
    {
        ((StoredProcedure)qb.query).AddOutputParameter(parameterName, value);
        return qb;
    }
    
    /*
    Additional methods not shown......
    */
}

`QueryBuilderExtensionMethods` 类允许我定义 `QueryBuilder` 方法,这些方法只会在使用 `StoredProcedure` 类时显示在 IntelliSense 下拉菜单中!请注意,`QueryBuilder` 是直接键入到 `StoredProcedure` 的,并且内部的 `Query` 对象被强制转换为 `StoredProcedure`,以确保 `Query` 对象无法看到此方法。返回的对象“qb”与在 `QueryBuilder` 类中返回“`this”是相同的。

您在给我注入什么?

尝试减轻使用内联查询时遭受 SQL 注入攻击的可能性。如果 SQL 查询中的值来自不受信任的源(例如,来自表单的用户、Web 服务等),您应该使用 SQL 参数来防止其中一个值危害您的数据库。请参阅下面的示例:

using oSo
using System.Data;

Employee emp = SimpleSql
                .Query("Select EmployeeFirstName, EmployeeAddress 
			From Employee Where EmployeeID = 
			@EmployeeID AND EmployeeLastName = @TheLastName")
                    .AddParameter("@EmployeeID", 1, DbType.Int32)
                    .AddParameter("@EmployeeLastName", "Goodwrench", DbType.String, 50)
                        .Fetch<Employee>();

更复杂的 ORM 场景

SimpleSql 可以自动设置字符串或简单值类型的 `public` 属性的值。但在现实世界中,我们有包含可以包含其他对象的属性的对象。为了保持 SimpleSql 的“简单”,SimpleSql 通过其 `Map` 方法处理所有复杂的 ORM 场景。这里有两个简单的领域 POCO 对象:

//ComplexEmployee Class makes use of the Address class
public class ComplexEmployee{

    public Int32 EmployeeID { get; set; }
    public String EmployeeFirstName { get; set; }
    public String EmployeeLastName { get; set; }
    //A more complex property
    public Address EmployerAddress { get; set; }
}

//Holds all the address details
public class Address{
    
    public String Address1 { get; set; }
    public String City { get; set; }
    public String State { get; set; }
    public String Zip { get; set; }
}

承诺 Lambda、Lambda、Lambda

现在,让我们使用 SimpleSql 来填充 `ComplexEmployee` 对象。

using oSo

ComplexEmployee ce = 
  SimpleSql
   .StoredProcedure("pGetEmployee")
     .AddParameter("@EmployeeID", 1)
        .Map(reader => {
            return new ComplexEmployee()
            {
                EmployeeID = Convert.ToInt32(reader["EmployeeID"]),
                EmployeeFirstName = Convert.ToString(reader["EmployeeFirstName"]),
                EmployeeLastName = Convert.ToString(reader["EmployeeLastName"]),
                EmployerAddress = new Address()
                {
                    Address1 = Convert.ToString(reader["EmployeeAddress"]),
                    City = Convert.ToString(reader["EmployeeCity"]),
                    State = Convert.ToString(reader["EmployeeState"]),
                    Zip = Convert.ToString(reader["EmployeeZip"])
                }
            };
        }).Fetch<ComplexEmployee>();

正如您在 `QueryBuilder` 代码中之前看到的,`Map()` 方法接受一个 Func 委托作为参数。这允许我们使用 Lambda 表达式来定义数据库数据如何映射到 `ComplexEmployee` 对象。另外,请注意,我还使用了其他 .NET 3.x 的优点,如自动属性和对象初始化。

处理多个结果集和输出参数

您可能不会经常使用这两个功能。但当您确实需要使用它们时,我希望 SimpleSql 能够轻松且强类型地处理这些功能。让我们先看看如何处理多个结果集:

using oSo;
using oSo.Results;

MultiResult results = 
  SimpleSql
    .StoredProcedure("pFetchMultipleEmployeeResults")
          .AddParameter("@EmployeeID", 1)
            .Map(reader => {
                return new Employee(){
                     FirstName = reader["EmployeeFirstName"].ToString()
                     , LastName = reader["EmployeeLastName"].ToString()
                };
            }).Map(reader => {  
                string phoneNum = (Convert.IsDBNull(reader["EmployeePhone"])) ?
                                      "Unlisted" : 
                                      Convert.ToString(reader["EmployeePhone"]);
                return new ComplexEmployee()
                    {
                        EmployeeFirstName = reader["EmployeeFirstName"].ToString()
                         , EmployeeLastName = reader["EmployeeLastName"].ToString()
                         EmployerAddress = new Address()
                        {
                            Address1 = Convert.ToString(reader["EmployeeAddress"]),
                            City = Convert.ToString(reader["EmployeeCity"]),
                            Phone = phoneNum
                        }
                    };
            }).FetchMultiple();

当您调用返回多个结果集的存储过程或查询时,结果会以 `MultiResult` 对象的形式返回。每个 `Map()` 方法处理一个结果集。使用多个 `Map()` 方法来处理多个结果集。请注意,我没有将此类命名为 `ResultsCollection`。我希望确保人们不会产生用 foreach 循环遍历结果的心态。`MultiResult` 对象中包含的每个结果都应按映射的顺序检索,从零开始。

MultiResult 中的每个结果都是强类型的

//From Code above
MultiResult results = SimpleSql.......

//Get first resultset
Employee emp = results.Fetch<Employee>(0);

//Get second resultset
List<ComplexEmployee> cplxEmps = 
            results.FetchAll<ComplexEmployee>(1);

一切都关乎输出

SimpleSql 通过 `Fetch` 方法上的“out”参数来处理输出参数。许多其他 AEP 方法也有一个 out 参数重载。

//Declare a collection that will be filled with any output parameters
OutputParameterCollection opColl = null;
Employee emp = SimpleSql
                .StoredProcedure("GetOneEmployeeWithOutput")
                    .AddParameter("@EmployeeID", 1)
                        .AddOutParameter("@DateHiredOutputParam", DbType.String)
                            .Fetch<Employee>(out opColl);
                            
//I can call the output parameter in the collection by Name
DateTime dateHired = opColl.GetValue("@DateHiredOutputParam").Fetch<DateTime>();

//Or by Index
//DateTime dateHired = opColl.GetValue(0).Fetch<DateTime>();

我需要你提交,否则我将不得不回滚

虽然我不是通过代码处理事务的真正粉丝,但我知道从长远来看这是不可避免的。SimpleSql 可以使用 .NET 2.0 的 `TransactionScope`,或者它可以创建和使用常规事务。

using (SimpleTransaction st = SimpleSql.CreateTransaction())
{
    try
    {
        SimpleSql.StoredProcedure("pInsertEmployee")
                                 .AddParameter("@EmployeeFirstName", "RollBack")
                                 .AddParameter("@EmployeeLastName", "Candidate")
                                 .AddParameter("@EmployeeAddress", "1 Rollback Lane")
                                 .AddParameter("@EmployeeCity", "Los Angeles")
                                 .AddParameter("@EmployeeState", "CA")
                                 .AddParameter("@EmployeeZip", "90245")
                                    .WithTransaction(st.Transaction)
                                        .Execute();

        SimpleSql.StoredProcedure("pInsertEmployee")
                         .AddParameter("@EmployeeFirstName", "Thrown")
                         .AddParameter("@EmployeeLastName", "Out")
                         .AddParameter("@EmployeeAddress", "2 Left Back Way")
                         .AddParameter("@EmployeeCity", "Los Angeles")
                         .AddParameter("@EmployeeState", "CA")
                         .AddParameter("@EmployeeZip", "90047")
                            .WithTransaction(st.Transaction)
                                .Execute();
        st.Commit();
    }
    catch (SimpleSqlException sse)
    {
        st.Rollback();
        Debug.WriteLine(sse.Message);
    }
}

SimpleSql 引入了 `SimpleTransaction` 类,而不是必须回退到 ADO.NET 来创建事务。该类利用 Dispose 模式在事务完成后关闭连接。`CreateTransaction` 方法有多个重载来指定连接字符串信息。另外,请注意,所有参与事务的 `StoreProcedure` 都将共享相同的连接信息。

代码解耦和合适的数据访问层

为了实现清晰的代码分离和单元测试(更不用说良好的架构),我还创建了一个使用 SimpleSql 的数据访问层 (DAL)。这将把所有数据库调用隔离到 DAL,这样您就不会有初级团队成员试图在代码隐藏文件或 *天哪* `.aspx` 页面本身中使用 SimpleSql!DAL 还使您能够通过构造函数指定一次连接字符串,并在每次数据访问方法中使用它。通过构造函数设置连接字符串还允许您使用 依赖注入 框架(如 StructureMap)来设置连接。

//Have your class inherit from SimpleDAL
public class EmployeeDAL : SimpleDAL
{
    /// <summary>
    /// You can explicitly pass in the name of the ConnectionString
    /// from the .config file via the constructor.
    /// This will allow you to use this connection with any method
    /// that uses the SimpleSql methods in this class.
    /// Another benefit is that you can now use a Dependency Injection
    //  framework like StructureMap or Spring.Net etc.
    /// to inject the name of the ConnectionString!
    /// </summary>
    /// <param name="connectConfigName">ConnectionString
    ///           name from the .config file</param>
    /// 
    public EmployeeDAL(string connectConfigName) 
                : base(connectConfigName)
    {
    }

    /// <summary>
    /// A Sample method that gets all employees from the DB
    /// </summary>
    /// <returns>List of employees</returns>
    /// 
    public List<Employee> GetEmployees()
    {
        List<Employee> employees = null;

        try
        {
            //SimpleSql is built into the DAL so you
            //can directly call Query or StoredProcedure
            employees = StoredProcedure("pGetAllEmployees")
                            .FetchAll<Employee>();
        }            
        catch (SimpleSqlException sse)
        {
            throw;
            //I'm just throwing it up the method chain. 
            //Do whatever you want here to handle the error.
        }
        
        return employees;
    }
}

包含在 Zip 下载中

SimpleSql 包含三个项目。

  1. 完整的带注释的 SimpleSql 源代码,包括使用的示例 SQL Server Express 2005 数据库。我还生成了 SQL 脚本,如果您想将数据库安装在另一个版本的 SQL Server 上。
  2. 单元测试使用的是 MSTest,因此不熟悉单元测试的人不必下载和安装 Visual Studio 2008 之外的任何其他工具。但是,如果您有选择,我建议使用 XUnit
  3. 最后但同样重要的是,我包含了一个快速的示例 Web Forms 项目,展示了如何使用 SimpleSql DAL。我想做一个 MVC 项目,但不想让那些没有接触过 ASP.NET MVC 的人因为视图、控制器和路由而陷入困境,只是为了展示如何通过 SimpleSql 将数据从数据库获取到您的 ASPX 页面!

在您的项目中 Yaoxuan SimpleSql

只需在您的类文件顶部引用 `oSo.dll`,并添加 `using oSo` 和 `using oSo.Exceptions` 语句,然后开始使用吧!

关注点

很高兴在这个项目中使用了大量的 C# 3.5 特性。虽然我知道有些人喜欢新的 `var` 关键字,但我并没有那么热衷于让它充斥我的代码。但是,除了代码中的其他有趣之处,我终于找到了我认为非常有用的 `var` 关键字!

当从多结果集返回 `List<T>` 作为结果之一时,我对其进行了强类型化。但是,在结果被强类型化之前,它要么存储在一个对象中(用于单个结果),要么存储在一个 `ArrayList` 中(用于多个结果)。当用户告诉 SimpleSql 实际类型时,它就会被转换为正确的类型。在使用 `var` 关键字之前,我必须像这样编写代码:

//C# 2.0 implementation of the MultiResult FetchAll method
List<T> resultset = new List<T>();
ArrayList results = resultList[index] as ArrayList;
//If this object isn't an ArrayList the value will be null
if (null != results){
    foreach (Object r in (ArrayList)results)
    {
        resultset.Add((T)r);
    }
    }else{
        //Assume its just one object, so add that one object into the list
        resultset.Add((T)results);
    }
    return resultset;

现在,有了 `var` 和扩展方法,代码稍微紧凑一些(如果去掉注释的话)。

//C# 3.5 implementation of the MultiResult FetchAll method
List<T> resultset = new List<T>();
//The new var keyword uses type inference to figure out what the actual type is
var results = resultList[index];
if (results is ArrayList)
{
    //The Cast<T> Extension method save a few lines of looping code
    resultset.InsertRange(0, ((ArrayList)results).Cast<T>());
}
else
{
    resultset.Add((T)results);
}
return resultset;

参考文献

历史

  • 2009年6月7日:初始版本
  • 2009年6月8日:重新措辞以提高清晰度
  • 2009年6月9日:修正了一些拼写错误
  • 2009年6月11日:更新文本以解释当 `Fetch` 或 `FetchAll` 调用数据库未返回任何记录时会发生什么
  • 2009年6月16日:更新源代码以支持参数化查询。还更新了文章以提及这一点并引用 SQL 注入攻击。
© . All rights reserved.