轻松访问数据






4.81/5 (25投票s)
运行时数据访问器生成
引言
在开始讨论主题之前,让我们创建一些典型数据访问方法的示例。
首先,我们需要几个存储过程。
这是我们的第一个存储过程
CREATE Procedure GetPersonListByName(
    @firstName  varchar(50),
    @lastName   varchar(50),
    @pageNumber int,
    @pageSize   int)
AS
  -- stored procedure implementation
此存储过程接收筛选器和分页参数,并从Person表中返回recordset。
实现存储过程调用的方法可以如下所示
public List<Person> GetPersonListByName(
    string firstName,
    string lastName,
    int    pageNumber,
    int    pageSize)
{
    // method implementation.
}
第二个示例将通过 ID 返回单个Person记录。
存储过程
CREATE Procedure GetPersonByID(@id int)
AS
  -- stored procedure implementation
数据访问方法
public Person GetPersonByID(int id)
{
    // method implementation.
}
最后一个示例将通过ID从数据库中删除记录。
存储过程
CREATE Procedure DeletePersonByID(@id int)
AS
  -- stored procedure implementation
数据访问方法
public void DeletePersonByID(int id)
{
    // method implementation.
}
那么,让我们比较存储过程和 C# 方法签名,看看我们能说些什么。
- 存储过程和方法名称匹配。
- 顺序、方法参数类型和名称对应于存储过程参数。
- 方法的返回值可以让我们大致了解我们应该使用哪个Execute方法,以及如果需要,使用什么对象类型来映射 recordset 中的数据。
如上所示,方法定义包含了我们实现方法体所需的所有信息。实际上,通过定义方法签名,我们完成了数据访问器开发中最具智慧的部分。其余的工作绝对是机械的。老实说,我厌倦了仅仅成为一个一遍又一遍地编写相同数据访问代码的编码机器,尤其是考虑到这个过程可以自动化。
本文展示了如何避免数据访问开发的实现步骤,以及如何将这个例程过程简化为方法声明。
抽象类
不幸的是,主流的 .NET 语言仍然没有像某些函数式或混合式语言那样的编译时转换系统。我们今天拥有的只是预编译时和运行时代码生成。
本文重点介绍运行时代码生成及其在Business Logic Toolkit for .NET中的支持。
让我们退一步,将前面示例中的方法放在一个类中。理想情况下,这个数据访问器类可能看起来像下面这样
using System;
using System.Collections.Generic;
public class PersonAccessor
{
    public List<Person> GetPersonListByName(
        string firstName, string lastName, int pageNumber, int pageSize);
    public Person GetPersonByID   (int id);
    public void   DeletePersonByID(int id);
}
这个示例的坏消息是,我们不能使用这样的语法,因为编译器期望方法体已经实现。
好消息是我们可以使用abstract类和方法,它们为我们提供了非常相似的、可编译的源代码。
using System;
using System.Collections.Generic;
public abstract class PersonAccessor
{
    public abstract List<Person> GetPersonListByName(
        string firstName, string lastName, int pageNumber, int pageSize);
    public abstract Person GetPersonByID   (int id);
    public abstract void   DeletePersonByID(int id);
}
此代码 100% 有效,我们的下一步是使其可运行。
抽象数据访问器
Business Logic Toolkit 提供了DataAccessor类,它用作开发数据访问器类的基类。如果我们将DataAccessor添加到我们先前的示例中,它将如下所示
using System;
using System.Collections.Generic;
using BLToolkit.DataAccess;
public abstract class PersonAccessor : DataAccessor<Person,PersonAccessor>
{
    public abstract List<Person> GetPersonListByName(
        string firstName, string lastName, int pageNumber, int pageSize);
    public abstract Person GetPersonByID   (int id);
    public abstract void   DeletePersonByID(int id);
}
就是这样!现在这个类是完整且功能齐全的。
下面的代码演示了如何使用它
using System;
using System.Collections.Generic;
namespace DataAccess
{
    class Program
    {
        static void Main(string[] args)
        {
            PersonAccessor pa = PersonAccessor.CreateInstance();
            List<Person> list = pa.GetPersonListByName("Crazy", "Frog", 0, 20);
            foreach (Person p in list)
                Console.Write("{0} {1}", p.FirstName, p.LastName);
        }
    }
}
这里唯一的魔法是CreateInstance方法。首先,此方法创建一个继承自PersonAccessor类的新类,然后根据每个方法声明生成abstract方法体。如果我们手动编写这些方法,可能会得到类似这样的结果
using System;
using System.Collections.Generic;
using BLToolkit.Data;
namespace Example.BLToolkitExtension
{
    public sealed class PersonAccessor : Example.PersonAccessor
    {
        public override List<Person> GetPersonListByName(
            string firstName,
            string lastName,
            int    pageNumber,
            int    pageSize)
        {
            using (DbManager db = GetDbManager())
            {
                return db
                    .SetSpCommand("GetPersonListByName",
                        db.Parameter("@firstName",  firstName),
                        db.Parameter("@lastName",   lastName),
                        db.Parameter("@pageNumber", pageNumber),
                        db.Parameter("@pageSize",   pageSize))
                    .ExecuteList<Person>();
            }
        }
        public override Person GetPersonByID(int id)
        {
            using (DbManager db = GetDbManager())
            {
                return db
                    .SetSpCommand("GetPersonByID", db.Parameter("@id", id))
                    .ExecuteObject<Person>();
            }
        }
        public override void DeletePersonByID(int id)
        {
            using (DbManager db = GetDbManager())
            {
                db
                    .SetSpCommand("DeletePersonByID", db.Parameter("@id", id))
                    .ExecuteNonQuery();
            }
        }
    }
}
(DbManager类是另一个用于“低级”数据库访问的BLToolkit类)。
方法声明的每个部分都很重要。方法的返回值指定以下一种Execute方法
| 返回类型 | 执行方法 | 
| IDataReader 接口 | ExecuteReader | 
| DataSet的子类 | ExecuteDataSet | 
| DataTable的子类 | ExecuteDataTable | 
| 实现 IList接口的类 | ExecuteList或ExecuteScalarList | 
| 实现 IDictionary接口的类 | ExecuteDictionary或ExecuteScalarDictionary | 
| void | ExecuteNonQuery | 
| string、byte[]或值类型 | ExecuteScalar | 
| 其他任何情况 | ExecuteObject | 
方法名称明确定义了操作名称,该操作名称将被转换为存储过程名称。
方法参数的类型、顺序和名称被映射到命令参数。此规则的例外情况是
- DbManager类型的参数。在这种情况下,生成器使用提供的- DbManager来调用命令。
- 带有属性的参数:FormatAttribute、DestinationAttribute。
生成过程控制
上面的PersonAccessor类是一个非常简单的例子,当然,它看起来太理想以至于不真实。在实际应用中,我们需要更多的灵活性和对生成代码的更多控制。除了DataAccessor的虚拟成员之外,BLToolkit还包含了一系列用于控制DataAccessor生成的属性。
CreateDbManager 方法
protected virtual DbManager CreateDbManager()
{
    return new DbManager();
}
默认情况下,此方法创建一个新的DbManager实例,该实例使用默认的数据库配置。您可以通过覆盖此方法来更改此行为。例如
public abstract class OracleDataAccessor<T,A> : DataAccessor<T,A>
    where A : DataAccessor<T,A>
{
    protected override BLToolkit.Data.DbManager CreateDbManager()
    {
        return new DbManager("Oracle", "Production");
    }
} 
此代码将使用 Oracle 数据提供程序和 Production 配置。
GetDefaultSpName 方法
protected virtual string GetDefaultSpName(string typeName, string actionName)
{
    return typeName == null?
        actionName:
        string.Format("{0}_{1}", typeName, actionName);
}
正如我提到的,方法名称明确定义了所谓的“操作名称”。最终的存储过程名称由GetDefaultSpName方法创建。默认实现使用以下命名约定
- 如果提供了类型名称,该方法将通过连接类型和操作名称来构建存储过程名称。因此,如果类型名称是“Person”,操作名称是“GetAll”,则结果的存储过程名称将是“Person_GetAll”。
- 如果未提供类型名称,则存储过程名称将等于操作名称。
您可以轻松地更改此行为。例如,对于命名约定“p_Person_GetAll”,该方法的实现可以是以下方式
public abstract class MyBaseDataAccessor<T,A> : DataAccessor<T,A>
    where A : DataAccessor<T,A>
{
    protected override string GetDefaultSpName(string typeName, string actionName)
    {
        return string.Format("p_{0}_{1}", typeName, actionName);
    }
}
GetTableName 方法
protected virtual string GetTableName(Type type)
{
    // ...
    return type.Name;
}
默认情况下,表名是关联的对象类型名称(我们在示例中为Person)。有两种方法可以将对象类型与访问器关联。通过提供泛型参数
public abstract class PersonAccessor : DataAccessor<Person>
{
}
并通过ObjectType属性
[ObjectType(typeof(Person))]
public abstract class PersonAccessor : DataAccessor
{
}
如果您希望在应用程序中使用不同的表名和类型名称,可以覆盖GetTableName方法
public abstract class OracleDataAccessor<T,A> : DataAccessor<T,A>
    where A : DataAccessor<T,A>
{
    protected override string GetTableName(Type type)
    {
        return base.GetTableName(type).ToUpper();
    }
}
TableNameAttribute
此外,您可以通过用TableNameAttribute属性装饰该对象来更改特定对象类型的表名
[TableName("PERSON")]
public class Person
{
    public int    ID;
    public string FirstName;
    public string LastName;
}
ActionNameAttribute
此属性允许更改操作名称。
public abstract class PersonAccessor : DataAccessor<Person, PersonAccessor>
{
    [ActionName("GetByID")]
    protected abstract IDataReader GetByIDInternal(DbManager db, int id);
    public Person GetByID(int id)
    {
        using (DbManager   db = GetDbManager())
        using (IDataReader rd = GetByIDInternal(db, id))
        {
            Person p = new Person();
            // do something complicated.
            return p;
        }
    }
}
ActionSprocNameAttribute
此属性将操作名称与存储过程名称关联
[ActionSprocName("Insert", "sp_Person_Insert")]
public abstract class PersonAccessor : DataAccessor<Person, PersonAccessor>
{
    public abstract void Insert(Person p);
}
当您需要重新分配基类中定义的方法的存储过程名称时,此属性可能会很有用。
SprocNameAttribute
为方法分配与默认存储过程名称不同的名称的常规方法是使用SprocName属性。
public abstract class PersonAccessor : DataAccessor<Person, PersonAccessor>
{
    [SprocName("sp_Person_Insert")]
    public abstract void Insert(Person p);
}
DestinationAttribute
默认情况下,DataAccessor生成器使用方法的返回值来确定使用哪个Execute方法来执行当前操作。DestinationAttribute指示目标对象是带有此属性的参数
public abstract class PersonAccessor : DataAccessor<Person, PersonAccessor>
{
    public abstract void GetAll([Destination] List<Person> list);
}
Direction Attributes
DataAccessor生成器可以将提供的业务对象映射到存储过程参数。Direction属性允许更精确地控制此过程。
public abstract class PersonAccessor : DataAccessor<Person, PersonAccessor>
{
    public abstract void Insert(
        [Direction.Output("ID"), Direction.Ignore("LastName")] Person person);
}
此外,BLToolkit 还提供了另外两个方向属性:Direction.InputOutputAttribute和Direction.ReturnValueAttribute。
DiscoverParametersAttribute
通常,BLToolkit 希望方法参数名称与存储过程参数名称匹配。在这种情况下,参数的顺序并不重要。此属性强制 BLToolkit 从存储过程中检索参数信息,并按顺序分配方法参数。参数名称将被忽略。
FormatAttribute
此属性指示指定的参数应用于构造存储过程名称或 SQL 语句
public abstract class PersonAccessor : DataAccessor<Person, PersonAccessor>
{
    [SqlQuery("SELECT {0} FROM {1} WHERE {2}")]
    public abstract List<string> GetStrings(
        [Format(0)] string fieldName,
        [Format(1)] string tableName,
        [Format(2)] string whereClause);
}
IndexAttribute
如果您希望方法返回一个字典,您必须指定用于构建字典键的字段。Index属性允许您这样做
public abstract class PersonAccessor : DataAccessor<Person, PersonAccessor>
{
    [SqlQuery("SELECT * FROM Person")]
    [Index("ID")]
    public abstract Dictionary<int, Person>           SelectAll1();
    [SqlQuery("SELECT * FROM Person")]
    [Index("@PersonID", "LastName")]
    public abstract Dictionary<CompoundValue, Person> SelectAll2();
}
注意:如果您的键有多个字段,则此键的类型应为CompoundValue。
如果字段名以'@'符号开头,BLToolkit 会从数据源读取字段值,否则从对象属性/字段读取。
ParamNameAttribute
默认情况下,方法参数名称应与存储过程参数名称匹配。此属性显式指定存储过程名称。
public abstract class PersonAccessor : DataAccessor<Person, PersonAccessor>
{
    [ActionName("SelectByName")]
    public abstract Person AnyParamName(
        [ParamName("FirstName")] string name1,
        [ParamName("@LastName")] string name2);
}
ScalarFieldNameAttribute
如果您的方法返回标量值字典,您必须指定用于填充标量列表的字段的名称或索引。Index属性允许您这样做
public abstract class PersonAccessor : DataAccessor<Person, PersonAccessor>
{
    [SqlQuery("SELECT * FROM Person")]
    [Index("@PersonID")]
    [ScalarFieldName("FirstName")]
    public abstract Dictionary<int, string>           SelectAll1();
    [SqlQuery("SELECT * FROM Person")]
    [Index("PersonID", "LastName")]
    [ScalarFieldName("FirstName")]
    public abstract Dictionary<CompoundValue, string> SelectAll2();
}
ScalarSourceAttribute
如果方法返回标量值,则可以使用此属性指定数据库如何返回该值。ScalarSource属性接受ScalarSourceType类型的参数
| ScalarSourceType | 描述 | 
| DataReader | 调用 DbManager.ExecuteReader方法,然后调用IDataReader.GetValue方法读取值。 | 
| OutputParameter | 调用 DbManager.ExecuteNonQuery方法,然后从IDbDataParameter.Value属性读取值。 | 
| 返回值 | 调用 DbManager.ExecuteNonQuery方法,然后从命令参数集合中读取返回值。 | 
| AffectedRows | 调用 DbManager.ExecuteNonQuery方法,然后返回其返回值。 | 
SqlQueryAttribute
此属性允许指定 SQL 语句。
public abstract class PersonAccessor : DataAccessor<Person, PersonAccessor>
{
    [SqlQuery("SELECT * FROM Person WHERE PersonID = @id")]
    public abstract Person GetByID(int @id);
}
结论
我希望这个简短的教程展示了开发数据访问层最简单、最快捷、最少维护的方法之一。此外,您还将获得另一个好处,即令人难以置信的对象映射性能。但这将是以后讨论的主题。
您始终可以在http://www.bltoolkit.net/找到最新版本的 BLToolkit 源代码。
此致,
Igor。

