.NET 数据持久性:SQL Server 对比 Matisse 对比 FastObjects






4.45/5 (10投票s)
2004 年 5 月 11 日
23分钟阅读

119939

656
一篇包含源代码的文章,探讨 .NET、Matisse 和 FastObjects 的开发速度和性能能力
引言
源代码包含 4 个 C# 项目:FrontHelpers、TestMatisse、TestSQL、TestVersant。对于 TestMatisee,您必须下载 Matisse 数据库和 .NET 绑定。对于 TestSQL,您必须下载并引用 DAAB 版本 2。对于 TestVersant,您必须下载 FastObjects .NET 试用版,使用增强功能进行编译,然后增强项目(说明在文章正文中)。TestVersant 项目不包含 FastObjects.cfg 文件、dat 和 dict 文件以及 ObjecrScopeProvider。您需要根据以下说明自行生成这些文件)。
我做过或见过的几乎所有企业开发都始于数据库的设计和定义。即使系统是使用纯 OO 方法开发的,编码也始于数据库。最近,每当我告诉我的开发人员开发企业时,让他们做到与数据库无关,他们都会感到震惊。我简直不得不从他们的系统中移除数据库,以强制他们进行不依赖数据库的开发。如果您自下而上开发,即从数据库开始,那么业务层中的主要决策总是受到影响。您将拥有一个依赖于专有数据持久化层的系统,而不是一个可以移植到任何数据持久化机制的系统。
尤其是在 Microsoft 世界(但不仅限于此),以数据库为中心的开发是供应商试图推销其产品的一种症状。无论喜欢与否,Microsoft SQL Server 的偏见已内置于 .NET Framework 中。 .NET 的工具、技术、数据绑定机制、序列化能力都指向 SQL Server。在我看来,这是 .NET 核心中嵌入的一个谎言。 .NET 的灵魂是 OO。显而易见,SQL Server 是基于关系范式的。OO 和关系来自两个不同的概念和实现世界。我不知道您怎么样,但我大部分开发时间都花在编写复杂的对象关系映射 (ORM) 代码上(大量的、枯燥乏味的、冗长乏味的代码)。令我欣慰的是,我的圣经(Martin Fowler 的《企业应用架构模式》)中最大一部分都专门用于 ORM 模式。
这个问题之所以更具时效性,主要有两个原因。首先,微软即将推出其最新版代号为“Yukon”的 SQL Server。据我所见,“Yukon”与 SQL Server 2000(和 7)有很大不同;掌握它需要大量的学习曲线。最重要的是,如果 .NET 偏向 SQL Server,“Yukon”将偏向 .NET。事实上,“Yukon”的 IDE 看起来与 Visual Studio .NET IDE 相同——.NET 和 SQL Server 之间的界限似乎变得模糊。另一个重要原因是,SQL Server 作为最合适的 .NET 数据存储库,面临着严重的挑战者。
在本文中,我将简要介绍其中两种替代方案:Matisse(称自己为后关系型数据库)和 Versant 最近发布的适用于 .NET 的 FastObjects(纯粹的对象数据库)。我将通过一个简单的例子来实现这一点。该例子将模拟一个分层、断开连接的企业场景。将有三个项目——每个项目都处理相同的问题,但使用不同的数据存储技术。我添加了简单的度量标准,以便您进行自己的性能测试。我承认该例子并不代表全面的性能和压力测试场景。我的重点将纯粹从开发人员的角度出发。我不想预先判断结果。本质上,我希望您作为开发人员自行判断哪种数据存储技术使用起来最快、最简单、最有效。
示例
我将处理一个 Person
对象,它只有三个属性
-
Firstname
– 文本 -
Lastname
– 文本 -
Birthdate
- 日期
应用程序必须执行以下操作
-
创建
- 注册 UI 上指示的尽可能多的 Person 对象。 -
获取
- 检索数据存储中的所有 Person 对象并将它们绑定到 UI 上的 ListBox。 -
更新
- 更新绑定到 ListBox 的所有 Person 对象并将这些更新持久化到数据存储。
以上每个功能都将以秒、毫秒和纳秒为单位进行测量。测量结果将写入文本文件。
每个示例都将按相同顺序构建
- 创建 Person 业务对象——
Person
对象将被称为PersonBE
。PersonBE
必须具有三个读/写属性:Firstname
、Lastname
和Birthdate
。PersonBE
的ToString()
方法被重写如下public class PersonBE { public override string ToString() {return this.Firstname + "," + this.Lastname;} }
- 创建数据存储。
- 编写数据访问代码——所有数据访问类都将位于
DataAccess
类中。 - 连接 UI 事件。
应用程序将具有标准 UI
UI 具有以下控件
- 创建按钮 – 将生成 NumericUpDown 控件中指示的 Person 对象数量,并将这些新对象持久化到数据存储。
- NumericUpDown – 指示要生成多少新的 Person 对象进行持久化。最小值设置为 30,最大值设置为 30,000。
- 获取按钮 – 检索数据存储中的所有 Person 对象,并将它们绑定到 ListBox。
- 更新按钮 – 遍历绑定到 ListBox 的所有 Person 对象,修改 Firstname 和 Lastname 属性,并将更改持久化到数据存储。
- ListBox – 显示通过单击“获取”按钮检索到的所有 Person 对象的列表。
- RichTextBox – 显示测量特定操作的度量文件。
- CheckBox – 如果选中,则表示必须创建一个新文件,否则使用现有文件。初始化时,CheckBox 标记为选中。
UI 有三个事件处理程序。在所有版本中,事件处理程序或多或少都是相同的。如果存在差异,我将在单独处理每个示例时指出
-
btn_CreateClick
– 触发创建功能。创建与 NumericUpDown 控件值相对应数量的 Person 对象。Person 对象被添加到容器对象中。然后启动计时器,Person 对象列表被传递到数据访问层进行持久化。当数据访问层将所有对象持久化到数据存储后,计时器停止,文件内容(计时器测试结果)然后写入文件。private void btnCreate_Click(object sender, EventArgs e) { list = new ArrayList(); for(int i = 0; i < ud.Value; i++) { PersonBE p = new PersonBE(); p.Firstname = "FN" + i.ToString(); p.Lastname = "LN" + i.ToString(); p.BirthDate = DateTime.Now; list.Add(p); } counter.Start(); DataAccess.insPersons(list); counter.Stop(); s = FileHelper.preCreate(DB, (int)ud.Value); file(); }
-
btnGet_Click
– 触发获取功能。调用数据访问层,该层返回 Person 对象列表。这些对象必须绑定到 ListBox。我正在使用数据绑定,因此我必须检查 ListBox 是否已绑定对象列表。如果存在现有绑定,我必须暂停绑定,绑定新列表,然后恢复绑定;否则我只需绑定新列表。我在调用数据访问层之前启动计数器,并在列表绑定到控件后停止计数器。然后我调用文件函数。private void btnGet_Click(object sender, EventArgs e) { counter.Start(); if(lb.Items.Count == 0) { list = DataAccess.getPersonsList(); lb.DataSource = list; } else { BindingManagerBase manager = this.BindingContext[list]; manager.SuspendBinding(); list = DataAccess.getPersonsList(); lb.DataSource = list; manager.ResumeBinding(); } counter.Stop(); s = FileHelper.preGet(DB, list.Count); file(); }
-
btnUp_Click
触发更新功能。该方法遍历绑定到 ListBox 的 Person 对象列表;它更改每个 Person 对象的 Firstname 和 Lastname 属性。此时,计时器启动,并调用数据访问层以将更改持久化到数据存储。然后停止计时器,并将测试结果写入文件并显示private void btnUp_Click(object sender, EventArgs e) { foreach(PersonBE p in list) { p.Firstname = p.Firstname + "up"; p.Lastname = p.Lastname + "up"; } counter.Start(); DataAccess.upPersons(list); counter.Stop(); s = FileHelper.preUpdate(DB, list.Count); file(); }
UI 有一个私有方法 file()
,它集中了 UI 对 FileHelper 的访问和使用(见下文)。
每个 UI 表单都有三个类级别变量
- Counter – 对 Counter 助手类(见下文)的引用。
- DB – 一个字符串常量,反映所使用的数据存储类型。
- FILE – 一个字符串常量,指示与应用程序关联的文本文件的名称。
辅助项目
该项目由所有三个应用程序共享。您可以在下载的源代码中找到它,名称为 FrontHelpers.proj。它执行两个主要功能:文件 IO 和度量。
指标
有两个类提供指标
-
NanoCounter
– 我直接从最新的(2004 年 4 月)模式和实践指南《提高 .NET 应用程序性能和可伸缩性》中,在标题为《使用 QueryPerformanceCounter 和 QueryPerformanceFrequency 对托管代码进行时间测量》的“操作方法”部分中,获取了这个类。该类以纳秒(即精确到十亿分之一秒)为单位测量托管代码的执行时间。我所做的唯一修改是添加了一个无参数的 Duration 方法。该类具有以下 API- 开始 – 启动性能计数器。
- 停止 – 停止性能计数器。
- Duration (int iteration) – 返回一个双精度浮点数,表示代码块每次迭代的持续时间。
- Duration – 返回一个双精度浮点数,表示代码块执行从开始到停止的总持续时间(以纳秒为单位)。
-
Counter
- 此类初始化计时器。它包含对 Nanocounter 的引用,用于测量纳秒性能。该类具有以下 API- 启动 – 初始化计时器和纳米计数器。
- 停止 – 停止计时器和纳米计数器。
- 结果 – 返回一个格式化字符串,其中包含以秒、毫秒和纳秒为单位的时间持续时间 – 每个测量值单独一行。
文件 I/O
常见的 File IO 功能封装在 FileHelper
类中。此类主要有两种类型的功能:文件创建、读取和写入;以及文件内容的格式化。该类的所有方法都是静态的。
文件 I/O API 如下
-
WriteFile
– 一个创建新文件或追加到现有文件并将文件保存到磁盘的方法。该方法有三个参数-
FileName (string)
- 要创建并写入磁盘的文件名。 -
FileContent (string)
– 要创建和保存的文件内容。 -
NewFile (bool)
– 如果文件是新文件,Filemode 将为 Create,否则 FileMode 为 Append。
-
-
ReadFile
– 此方法读取给定名称文件的内容,并将这些内容作为字符串返回。只有一个参数,即要读取的文件名。
文件格式化 API 包含在应用程序将要测量的功能执行之前格式化信息文本的方法。每个方法都将要格式化的文件名称和操作将测量的记录数作为参数。有三个公共方法对应于三个可测量操作
-
预创建
-
预获取
-
预更新
SQL Server
第一个应用程序使用 SQL Server 2000 作为数据存储。我使用了 Microsoft 推荐的最佳实践,特别是 DAAB(数据访问应用程序块)。SQL Server 版本的源代码可以在 TestSQL.proj 中找到。
- 创建
PersonBE
对象 – 在 SQL 版本中必须添加一个额外的属性,即 ID。这将使应用程序能够识别特定的 Person 对象(此特定演示中我未提供此功能,但在任何普通场景中都是必不可少的)。 - 创建数据存储(请参阅源代码中的 sqltest.sql 脚本)
对于 SQL Server,设置数据库有两个阶段
- 设置数据库表
CREATE DATABASE SQLTest go use SQLTest go CREATE TABLE Person ( ID int IDENTITY(1, 1) NOT NULL, Firstname varchar(30) NOT NULL, Surname varchar(30) NOT NULL, BirthDate smalldatetime NOT NULL, CONSTRAINT Person_PK PRIMARY KEY (ID) ) go
- 编写存储过程(在这个简单实例中有三个)
CREATE PROC getPersons AS SELECT ID, Firstname, Surname, Birthdate FROM Person go CREATE PROC insPerson @first varchar(30), @last varchar(30), @birth smalldatetime AS INSERT INTO Person VALUES(@first, @last, @birth) go CREATE PROC upPerson @id int, @first varchar(30), @last varchar(30), @birth smalldatetime AS UPDATE Person SET Firstname = @first, Surname = @last, Birthdate = @birth WHERE ID = @id GO
- 设置数据库表
- 编写数据访问代码。
SQL 数据访问引用 DAAB 并使用
System.Data
和System.Data.SqlClient
命名空间。该类必须获取 SQL Server 连接字符串的句柄。在此实例中,我使用 SQL 身份验证和“sa”用户硬编码了连接字符串。(使用此示例时,请将“1234”替换为您的 SQL“sa”密码。)有四个方法,都是静态的——其中三个是内部的,一个私有的。API 如下
-
insPersons
– 此方法将一个ArrayList
的人员作为参数。它遍历列表,为每个PersonBE
的每个属性创建一个 ADO.NETSqlParameter
,与insPerson
存储过程参数相对应。对于ArrayList
中的每个PersonBE
,都会调用 DAAB 的ExecuteNonQuery
方法。 -
getPersons
– 此方法返回数据库中所有PersonBE
的ArrayList
。首先调用getPersonsDS
私有方法;此方法调用 DAAB 的ExecuteDataSet
方法,该方法调用getPersons
存储过程并返回一个DataSet
。然后调用方法遍历 DataSet 中的每一行,为每次迭代创建一个新的PersonBE
对象,并将行的列与PersonBE
的属性匹配。每个PersonBE
都添加到返回的ArrayList
中。(这是 ORM 代码)internal static ArrayList getPersons() { ArrayList list = new ArrayList(); DataSet ds = getPersonsDS(); foreach(DataRow row in ds.Tables[0].Rows) { PersonBE p = new PersonBE(); p.ID = (int)row["ID"]; p.Firstname = (string)row["Firstname"]; p.Lastname = (string)row["Surname"]; p.BirthDate = (DateTime)row["BirthDate"]; list.Add(p); } return list; }
-
upPersons
– 此方法将一个ArrayList
的PersonBE
对象作为参数。该方法遍历每个ArrayList
,将每个对象映射到SqParameters
,并在每次迭代中使用 upPerson 存储过程调用 DAAB 的ExecuteNonQuery
。我假设列表中的每个PersonBE
都已被修改。这是在现实世界中无法做出的假设。我必须有一些机制来测试我的 Person 对象是否“脏”,即我是否确实更改了某个属性,以至于我从数据库中提取的数据确实不同,因此需要更新。使用 SQL 并且不使用DataSets
来传输我的数据,我必须进行此测试。
-
- 连接 UI – 在 SQL UI 中,我使用
ArrayList
来包含我下载的和新的 Person 对象。
马蒂斯
要运行 Matisse 示例,您需要下载 Matisse 数据库和 Matisse .NET 数据绑定——两者都可以从 Matisse 网站 获取。[在这个简短的示例中,我无法完全展示 Matisse 和 .NET 绑定的奇妙之处。我强烈建议您下载文档并根据自己的闲暇时间阅读。对于 Matisse 新手,John Sasak 在一系列 5 篇文章中提供了一个具有指导意义的优秀入门教程。] 在这个 Matisse 示例中,我想演示一个如何在断开连接的分层环境中使用 Matisse 的基本框架。Matisse 项目在源代码中作为 TestMatisse.proj。
- 创建
PersonBE
对象 – MatissePersonBE
是所有对象中最简单的。不需要像 SQL 示例那样设置 ID 属性。 - 创建数据存储——Matisse 数据库可以通过多种方式设置:Rational Rose;Matisse SQL (DDL);对象定义语言 (ODL)。我发现 ODL 最适合 .NET 工作。该示例的 ODL 很简单(请参阅源代码中的 MatTest.odl)
interface Person: persistent { attribute String Firstname; attribute String Lastname; attribute Date Birthdate; };
启动 Matisse 企业管理器。点击文件 -> 新建数据库,在对话框中输入“MTest”并点击确定。
一个新的 Matisse 数据库“MTest”已创建。在企业管理器中导航到 MTest 并展开它;右键单击 Schema 节点并选择“导入 ODL 模式...”。在您的磁盘上找到 MTest.odl 并导入它。数据库模式应该会生成一个名为“Person”的类。
下一步是根据数据库模式生成存根类。在 Matisse 世界中,您必须区分连接类和断开连接类:前者(由 Matisse 生成)与 Matisse 数据库建立实时连接;后者将 Matisse 类数据传输到未连接到数据库的层。Matisse 能够生成断开连接类,但我喜欢控制我的断开连接类,所以我自己定义它们。在本例中,我们已经编码了 PersonBE。
通过启动命令控制台并在项目目录中调用 mt_stbgen,在 TestMatisse 命名空间下生成 Matisse .NET 类。
c:\ directory of project\mt_stbgen name of your machine MTest –p “TestMatisse”
Matisse 会将一个 Person.cs 文件写入您的目录。将此文件导入您的项目。(我建议您替换我源代码中的那个。请注意——我已将源代码格式化为区域,以便更易于理解。我不会在本文中详细介绍 Matisse 生成的类。)
下一阶段是编写 Person 类上的一些方法,使其能够与
PersonBE
同步。请记住,Person 通过实时数据库连接与 Matisse 数据库通信。发送到表示层的实际数据包含在 PersonBE 中。当我从数据库检索数据时,我需要一种机制将 Person 属性传递给PersonBE
属性;另一方面,当我想要从前端所做的更改更新数据库时,我需要将PersonBE
的属性转换为 Person 属性。我通过 Matisse 生成的类上的三个方法实现了这种同步机制。- update 方法将
PersonBE
(来自 UI)与Person
对象同步。该方法接受一个PersonBE
并逐步将PersonBE
属性传输到相应的 Person 属性。public void update(PersonBE obj) { this.Firstname = obj.Firstname; this.Lastname = obj.Lastname; this.Birthdate = obj.BirthDate; }
- 将 Person 对象转换为
PersonBE
是通过重写 Person 的ToDataObject
和CopyAttributesToDataObj
方法实现的。ToDataObject
方法创建一个新的PersonBE
对象并将其传递给重写的CopyAttributesToDataObj
方法,该方法将其所有属性值复制到PersonBE
类的相应属性值。然后将此对象发送到 UI 层。public override object ToDataObject() { PersonBE person = new PersonBE(); CopyAttributesToDataObj(person); return person; } public override void CopyAttributesToDataObj(object dataObj) { base.CopyAttributesToDataObj (dataObj); ((PersonBE)dataObj).Firstname = this.Firstname; ((PersonBE)dataObj).Lastname = this.Lastname; ((PersonBE)dataObj).BirthDate = this.Birthdate; }
- update 方法将
- 编写数据访问代码——我在 Matisse 数据访问层做了一些额外的工作。我创建了两个类:
BaseMatisse
和DataAccess
。BaseMatisse
是一个抽象类,充当 Matisse 的一种 DAAB。我在更复杂、更精密的 Matisse 编程中使用了这个类,它几乎满足了我所有的需求。本质上,它是 Matisse 数据连接功能的一个外观模式。特定的数据访问类只是继承了BaseMatisse
功能。在此版本的
BaseMatisse
中,我将 Matisse 版本的连接对象硬编码为一个名为 db 的静态MtDatabase
对象。然后该类有一系列静态方法,用于打开和关闭 Matisse 数据库,开始和提交 Matisse 事务,以及打开和关闭 Matisse 版本读取(有关版本和事务读取和写入的解释,请参阅 Matisse 文档)。您必须将源代码中的“PDEV1”更改为您自己服务器的名称!!using System; using com.matisse.reflect; using com.matisse.Data; using com.matisse; namespace TestMatisse { public abstract class BaseMatisse { protected static MtDatabase db = new MtDatabase("PDEV1", "MTest", new MtPackageObjectFactory("TestMatisse,TestMatisse")); protected static void close() { if(db.IsConnectionOpen()) db.Close(); } protected static void open() { if(!db.IsConnectionOpen()) db.Open(); } protected static void beginTrans() { if(!db.IsConnectionOpen()) open(); if(!db.IsTransactionInProgress()) db.BeginTransaction(); } protected static void commitTrans(bool off) { if(db.IsTransactionInProgress()) db.Commit(); if(off) close(); } protected static void openVersion() { if(!db.IsConnectionOpen()) open(); if(!db.IsVersionAccessInProgress()) db.StartVersionAccess(); } protected static void closeVersion(bool off) { if(db.IsVersionAccessInProgress()) db.EndVersionAccess(); if(off) close(); } } }
现在,继承
BaseMatisse
并为应用程序编写数据访问逻辑变得简单直观。这在DataAccess
类中完成,该类有两个简短且不言自明的方法internal class DataAccess: BaseMatisse { internal static ArrayList getPersons() { ArrayList list = new ArrayList(); openVersion(); foreach(Person person in Person.InstanceEnumerator(db)) { PersonBE pbe = (PersonBE)person.ToDataObject(); list.Add(pbe); } closeVersion(true); return list; } internal static void insPersons(ArrayList persons) { beginTrans(); foreach(PersonBE pbe in persons) { Person person = new Person(db); person.update(pbe); } commitTrans(true); } }
与 SQL 数据访问逻辑不同,这里不依赖 SQL 存储过程。(Matisse 确实有等效于存储过程的功能,但您可以不使用它们。)此外,Matisse 使用相同的方法来更新和插入数据,即
insPerson
方法。此外,请注意getPersons
中从 Person 到PersonBE
的映射,以及insPersons
方法中从PersonBE
到 Person 的相反映射。 - 连接 UI – UI 与 SQL UI 完全相同。
快速对象
大约八个月前,当我发现 Matisse 时,它让我大吃一惊。我当时就意识到,值得花力气学习这种数据存储技术,并尝试将其推荐给我的客户。我花了整整三个月的时间才精通这个产品。两周前我发现了 FastObjects——我只能说我现在爱上了它。不要犹豫——现在就去 www.FastObjects.com 下载它。文档全面易懂;技术简单而强大。我不会再多说什么了。[不幸的是,这篇文章不是关于 FastObjects 的详细教程。也许以后再说。] 我将带您了解我们使用 Matisse 和 SQL Server 所做的事情的步骤,并自行判断。
下载并安装 FastObjects .NET 试用版(2004 年 6 月底到期)。
FastObjects 源代码位于 TestVersant.proj 中。
- 创建 PersonBE 对象 - 编写
PersonBE
对象及其属性。引用 FastObjects.t7.Runtime(您可以在 Program File/FastObjects_.NET_Trial/bin 目录中找到它)。使用FastObjects
命名空间。将您的PersonBE
类标记为[Persistent(Verifiable=true)]
属性。(在断开连接的场景中需要 Verifiable 限定符。)using System; using FastObjects; namespace TestVersant { [Persistent(Verifiable=true)] public class PersonBE { private string first, last; private DateTime birth; public PersonBE(){} public string Firstname { get{return first;} set{first = value;} } public string Lastname { get{return last;} set{last = value;} } public DateTime BirthDate { get{return birth;} set{birth = value;} } public override bool Equals(object obj) { return this.Firstname + ", " + this.Lastname; } } }
在解决方案资源管理器中选择 TestVersant 项目,然后按 F4 获取项目属性。您将看到 FastObjects 生成的以下属性:ConfigFile;Database;Enhancing;Policy File;Project File;Project Folder;Schema。
FastObjects 的工作原理是为您的数据存储库生成一个模式文件和一个数据文件。数据文件包含您的数据,由 Database 属性定义。模式文件在 FastObjects 世界中被称为字典,由 Schema 属性定义。
关键在于 Enhancing 属性。如果您将 Enhancing 属性设置为 True,则在您编译项目时,FastObjects 会检查所有标有 [Persistent] 属性的类,并自动生成 FastObjects 数据库(字典和数据库)。无需 SQL 脚本、ODL、DDL,也无需进入企业管理器——只需将 enhancing 设置为 True 并编译即可。(FastObjects 是如何做到这一点的——必须留待以后讨论。)
继续并按如下方式设置属性值
-
数据库 = “TestVersantBase”
-
增强 = “True”
-
模式 = “TestVersantDict”
编译!
基本上就是这样了。但 FastObjects 可以为您做更多工作。您应该会在 Visual Studio 顶部菜单上看到一个 FastObjects 菜单。选择“启用项目”选项并按照向导操作。FastObjects 现在会在您的项目中生成两个对象:一个 FastObjects.cfg 文件和一个 ObjectScopeProvider1.cs(类)。
-
- 创建数据存储 – 见上文!!!!!
- 编写数据访问代码
FastObjects 数据访问的核心概念简单易懂。通过
DataBase
对象访问数据存储。但是,要使用数据库,需要一个 ObjectScope 对象。ObjectScope 实现了IObjectScope
接口。FastObjects 等同于数据字符串的是数据文件的 URL。在本例中fastobjects://LOCAL/TestVersantBase
通过两行简单的代码,您就可以获取
IObjectInterface
的句柄。FastObjects.Database db = FastObjects.Database.Get( "fastobjects://LOCAL/TestVersantBase" ); IObjectScope scope = db.GetObjectScope();
FastObjects 生成的
ObjectScopeProvider1
类为我们编写了所有这些代码。要开始使用 FastObjects 数据库,您必须通过调用作用域的 Transaction 属性的 Begin 方法来开始一个事务。事务必须提交。
IObjectScope scope = ObjectScopeProvider1.ObjectScope(); scope.Transaction.Begin(); …….. scope.Transaction.Commit();
一旦您打开了作用域的事务,您就可以在数据库上执行一系列操作(您现在拥有一个实时连接)。其中一个选项是遍历数据库中特定类的对象范围。您可以获取一个枚举器来遍历范围,并遍历该范围的每个实例。
IDBObjectEnumerator en = scope.GetExtent( typeof(PersonBE)).GetEnumerator();
因为
ObjectScope
需要连接到数据库,所以它不能发送到应用程序中的其他层。与 Matisse 一样,范围必须有一种机制以断开连接的方式传输其数据。这就是 FastObjectsObjectContainer
发挥作用的地方。我无法开始歌颂ObjectContainer
——它本身就值得一系列文章。ObjectContainer
是一个用于ObjectScope
数据的容器,您希望将其从数据库中分离并发送到 UI 或业务层。ObjectScope
中的数据可以序列化;可以转换为 XML。但更重要的是——ObjectContainer
监视其包含的对象所做的更改。当ObjectContainer
从 UI 返回以持久化更改时——FastObjects 会自动知道哪些对象是“脏”的,因此必须更新,哪些对象是新的,因此必须插入。还有很多——但这都是下次再说。FastObjects
DataAccess
类,像 MatisseDataAccess
类一样,只有两个方法:getPersons
和insPersons
。在 Matisse 中,getPersons
返回一个ArrayList
– 在 FastObjects 中,它返回一个ObjectContainer
。在 Matisse 中,insPersons
接受一个ArrayList
作为参数 – 在 FastObjects 中,参数是神奇的ObjectContainer
。insPersons
方法获取IObjectScope
接口的句柄并开始一个事务。然后,在一行代码中,ObjectContainer
中的对象被复制到 ObjectScope;Verfy.All
和Verify.Lock
参数执行 FastObjects 并发检查(不幸的是,这也不是解释其奥秘的地方)。container.CopyTo(scope, Verify.All | Verify.Lock);
然后提交事务,就这样。所有新对象都被插入。所有更改的对象都被更新。
internal static void insPersons(ObjectContainer container) { IObjectScope scope = ObjectScopeProvider1.ObjectScope(); scope.Transaction.Begin(); container.CopyTo(scope, Verify.All | Verify.Lock); scope.Transaction.Commit(); }
getPersons
方法实例化一个新的ObjectContainer
,然后获取IObjectScope
接口的句柄。事务被打开,并获取PersonBE
范围的枚举器。我们将范围及其枚举器复制到ObjectContainer
中——这基本上将所有PersonBE
对象放入ObjectContainer
中。container.CopyFrom(scope, en);
处理完枚举器,提交事务,然后将
ObjectContainer
送去施展它的奇妙。internal static ObjectContainer getPersons() { ObjectContainer container = new ObjectContainer(); IObjectScope scope = ObjectScopeProvider1.ObjectScope(); scope.Transaction.Begin(); IDBObjectEnumerator en = scope.GetExtent( typeof(PersonBE)).GetEnumerator(); container.CopyFrom(scope, en); en.Dispose(); scope.Transaction.Commit(); return container; }
- 连接 UI – FastObjects UI 使用
ObjectContainer
而不是ArrayList
。因此,UI 需要对 FastObjects 的引用,并且必须使用 FastObjects 命名空间。ArrayList
对象被ObjectContainer
对象替换。using FastObjects; public class Form1 : System.Windows.Forms.Form { private ObjectContainer container; ….. } //The ObjectContainer has a getList() methods which returns //an IList interface of the internal collection of objects //in the ObjectContainer. This feature allows me to use data //binding directly on the ObjectContainer. private void btnGet_Click(object sender, EventArgs e) { counter.Start(); container = DataAccess.getPersons(); if(lb.Items.Count == 0) { lb.DataSource = container.GetList(); } else { BindingManagerBase manager = this.BindingContext[ container.GetList()]; manager.SuspendBinding(); lb.DataSource = container.GetList(); manager.ResumeBinding(); } counter.Stop(); s = FileHelper.preGet(DB, container.GetList().Count); file(); }
测试结果
我使用所有三个数据库进行了多次测试。我将在这里给出记录插入的测试结果。测试很简单:向每个数据库插入 30、300 和 3000 个 Person 对象。记下完成操作的毫秒时间,并对每种类型执行五次,即 5 * 30、5 * 300。
这些是我为本文目的获得的结果
1 | 2 | 3 | 4 | 5 | 平均毫秒数 | ||
SQL | 30 | 15.625 | 15.625 | 15.625 | 15.625 | 15.625 | 15.625 |
300 | 296.875 | 250 | 203.125 | 187.5 | 234.375 | 234.375 | |
3000 | 3046.875 | 2953.125 | 2765.625 | 2718.75 | 4203.125 | 3137.5 | |
Matisse | 30 | 15.625 | 31.25 | 15.625 | 15.615 | 46.875 | 24.998 |
300 | 62.5 | 62.5 | 62.5 | 62.5 | 62.5 | 62.5 | |
3000 | 453.125 | 453.125 | 453.125 | 437.5 | 437.5 | 446.875 | |
FastObjects | 30 | 62.5 | 78.125 | 15.625 | 16 | 62.5 | 46.8 |
300 | 93.75 | 109.375 | 93.75 | 109.357 | 62.5 | 93.746 | |
3000 | 578.125 | 437.5 | 453.125 | 453.125 | 484.375 | 481.25 |
总结如下
SQL | 马蒂斯 | FastObjects | |
30 | 15.625 | 24.998 | 46.875 |
300 | 234.375 | 62.5 | 93.746 |
3000 | 3137.5 | 446.875 | 481.25 |
从上面可以看出以下几点
- 对于少量插入,SQL Server 性能更佳。Matisse 性能优于 FastObjects。
- 对于 300 次插入,FastObjects 和 Matisse 都明显优于 SQL Server。Matisse 的性能比 FastObjects 快约 30%。
- 对于 3000 次插入,Matisse 和 FastObjects 的性能比 SQL Server 好约 700% (!!!!)。Matisse 的性能比 FastObjects 好约 2%。
总的来说——其他测试证实了上述发现,即 Matisse 总体性能更优。对于小型任务(即有限记录),SQL Server 性能更好。
我还没有进行大规模的压力测试,涉及对多次读写(例如,30 个并发写入,分别处理 30、300 和 3000 条记录;3000 次写入,分别处理 30、300、3000 条记录等等…)。
但测试中还有一项重要内容要补充。SQL 应用程序的开发时间最长,其次是 Matisse。FastObjects 应用程序的开发时间大约是 Matisse 和 SQL 的四分之一。如果您添加对象(即表)和对象关系(即联结表),SQL 的开发时间将呈指数级增长;Matisse 将逐步增长,而 FastObjects 的开发时间仍将是最小的。
将上市时间、数据模型复杂度、代码可维护性和性能纳入考量(前提是没有进行大规模压力测试),FastObjects 是我毫不犹豫的选择。
最后
如果您对这些产品进行任何高强度压力测试,我将非常感谢您的反馈。FastObjects 已经发布了试用版,但我认为最终版本发布时不会有太大变化。Matisse 已经发布了增量 .NET 绑定版本。我使用 Matisse 已经很长时间了——到目前为止,它已被证明坚固、稳定,并且能够承受我所施加的一切。我还要提出一点:与 SQL Server(更不用说“Yukon”)相比,Matisse 和 FastObjects 的占用空间微不足道。
我还在努力更改此网站上的个人资料。但请在我的网站 www.pdev.co.za 注册,我会随时向您发布有关 Matisse 和 FastObjects 的最新信息。