在开发多个数据库驱动的项目过程中,经常需要处理大量数据。为了实现一个快速工作且易于使用的数据访问层,本文将介绍一个类库所提供的功能。
该类库支持两种SQL命令类型:查询(EntityReaderCommand<TEntity>
)和脚本(EntityScriptCommand<TEntity>
)。
使用EntityReaderCommand<TEntity>
,可以通过执行指定的SQL查询从数据库中读取实体。其工作原理类似于IDataReader
的实现,但当IDataReader
每次获取返回一个IDataRecord
时,EntityReaderCommand<TEntity>
返回一个TEntity
类的实例。
以下代码示例将从数据库表"Person"中填充List<Person>
:
var persons = new List<Person>();
var reader = new EntityReaderCommand<Person>(
args => persons.Add(args.Entity),
@"SELECT Id <Id>, FirstName <FirstName>, LastName <LastName> FROM Person");
reader.Execute();
以下代码示例将从数据库中读取一个Rectangle
实例:
var rectangle = new Rectangle();
new EntityReaderCommand<Rectangle>(
(EntityReaderArguments<Rectangle> args) => {
rectangle = args.Entity;
args.Terminate = true;
},
@"SELECT 1 <Location.X>, 2 <Location.Y>, 3 <Width>, 4 <Height>")
.Execute();
以下代码示例使用EntityScriptCommand<TEntity>
执行相同的操作:
var command = new EntityScriptCommand<Rectangle>(new Rectangle(), @"SET <Location.X> = 1 SET <Location.Y> = 2 SET <Width> = 3 SET <Height> = 4");
command.Execute();
var rectangle = command.Entity;
以下代码示例将新的Person
实例插入数据库表"Person"中。注意,插入后Person
对象将获得一个新的Id
。
var person = new Person { FirstName = "Boris", LastName = "Nadezhdin" };
var insert = new EntityScriptCommand<Person>(person, @"SET <Id> = newid() INSERT INTO Person (Id, FirstName, LastName) VALUES (<Id>, <FirstName>, <LastName>)");
insert.Execute();
从上述示例中可以看出,查询语法是SQL客户端原生的,唯一的区别是实体属性和列别名之间的映射。命令支持复合属性(如Rectangle.Location
)。实体及其属性可以是引用类型或值类型。不需要特殊的XML或基于属性的O/R映射,所有映射都直接在查询中指定。
实体及其属性的类型只有两个限制:
在命令执行逻辑中,没有为实体生成代理,因此如果使用EntityReaderCommand<TEntity>
,将读取TEntity
,而不是通用祖先。
对实体和会话范围没有限制,可以在一个会话中从数据库读取实体,并在另一个会话中更新它。
没有实体缓存,因此可以拥有同一数据库实体的多个实例。
有时需要将一些额外的参数传递到命令中。例如,通过Id
查找Person
:
public Person FindOne(Guid id) {
Person person = null;
var reader = new EntityReaderCommand<Person>(
(EntityReaderArguments<Person> args) => {
person = args.Entity;
args.Terminate = true;
},
@"SELECT Id <Id>, FirstName <FirstName>, LastName <LastName> FROM Person WHERE Id = {0}", id);
reader.Execute();
return person;
}
从上面的示例中可以看出,语法类似于String.Format(String, params Object[] args)
,但在这种情况下,id
不是被替换为其字符串表示,而是作为参数传递到命令中。
以下是另一个示例,演示了如何传递参数并在命令执行后获取其修改后的值:
const int one = 1;
var command = new SimpleCommand("SET {0} = {0} + 1", one);
command.Execute();
Assert.AreEqual(one + 1, command.GetArgs()[0]);
每个命令都需要一个打开的数据库会话;如果没有在当前线程中打开会话,命令会打开一个新的会话并在执行后关闭它。
要手动打开新会话,应该创建一个新的SessionScope
实例;要关闭它,请调用SessionScope
实例的Dispose()
。因此,在SessionScope
对象的作用域内的所有命令都将使用由SessionScope
对象创建的一个会话。
如果正在开发一个Web应用程序,并且希望每个Web请求都有一个数据库会话,可以在BeginRequest
事件处理程序中创建一个SessionScope
实例(例如,将其存储在HttpContext
的项目中),并在EndRequest
事件处理程序中释放它。
除了SessionScope
,还有另一个命令执行的作用域:TransactionScope
。它用于事务管理。
要开始一个新事务,应该创建一个新的TransactionScope
实例。要结束它,请调用TransactionScope
实例的Dispose()
。因此,在该TransactionScope
对象的作用域内的所有命令都将在由TransactionScope
对象创建的事务中工作。事务默认标记为回滚。因此,要提交事务,应该调用TransactionScope
实例的Commit()
。注意,在TransactionScope
被处置之前,不会执行实际的提交。
命令作用域的另一个有趣特性是它们是局部于它们被实例化的线程的。因此,不同线程中的命令将始终在不同的数据库会话中工作。
这个类库的对象不与任何具体的数据库客户端耦合。会话和事务管理、命令执行,都只通过IDatabaseProvider
接口与原生数据库客户端一起工作。所有特定于原生数据库客户端的方法都提取在这个接口中。目前,只有一个IDatabaseProvider
的实现,用于Microsoft SQL Server,但实现Oracle、PgSQL或MySQL的实现真的很简单。
具体的IDatabaseProvider
实现和数据库连接字符串可以在应用程序配置文件中声明性地定义,也可以通过Scope.Configuration
以编程方式定义。默认情况下,应用程序配置文件被使用,例如:
<?xml version="1.0"?>
<configuration>
<configSections>
<section name="dataAccess" type="Yap.Data.Client.ConfigurationSectionHandler, Yap.Data.Client" />
</configSections>
<dataAccess providerType="Yap.Data.Client.Sql.SqlProvider, Yap.Data.Client.Sql" providerAssembly="Yap.Data.Client.Sql.dll" connectionString="Data Source=.\SQLEXPRESS; Database=YAP;Integrated Security=True;" />
</configuration>
解决方案是在Visual Studio 2008中创建的,包括三个项目:
主要的对象模型在Yap.Data.Client中定义。Microsoft SQL Server的IDatabaseProvider
实现在Yap.Data.Client.Sql中定义。Yap.Data.UnitTest包含Yap.Data.Client程序集的测试固定装置。Yap.Data.UnitTest使用MSTest,但可以很容易地转换为NUnit。
要运行单元测试,需要创建一个数据库,执行SQL文件夹中的create-script.sql
,并修改app.config
文件中的连接字符串。