DataObjects.Net - RAD的ORM框架 - 简介






4.85/5 (15投票s)
x-tensive公司ORM框架DataObjects.Net简介
引言
持久化数据经历了漫长的发展历程,从最初的文件、带索引的数据(如Berkeley DB或DBase)、支持事务、存储过程和参照完整性的SQL服务器,到最后的纯面向对象数据库。XML文件在存储少量信息或进行信息交换方面变得非常流行。实际上,没有万能的解决方案或通用的持久化数据方案。但是,对于所谓的业务应用程序,SQL和关系数据库仍然是首选。
从开发人员的角度来看,纯面向对象数据库很诱人。代码看起来很棒,而且没有被数据库的东西所杂乱。但是对于利益相关者来说,使用OO数据库存在许多缺点。首先是(通常)高昂的许可成本,最后是IT部门完全不知道如何维护(备份、转换等)这样的数据库。他们认为成熟稳定的数据库系统已经存在。有些数据库(如PostgreSQL)是免费和开源的。有些数据库(如Microsoft Azure云服务)则不需要硬件或维护。
在这种冲突中,对象关系映射(ORM)框架应运而生。ORM框架使关系数据库看起来像面向对象(或对象关系)数据库。然而,数据仍然存储在关系数据库(甚至更简单的数据库)中。
为什么要使用ORM框架?
首先,关系数据库(因此也包括ORM)的理想项目是复杂的业务应用程序,它们有大量的逻辑(所谓的业务逻辑)。典型的业务应用程序领域包括会计和企业资源规划(ERP)。Web内容管理系统(CMS)、组织者和业务规划师也属于此类应用程序领域。任何具有复杂数据(称为实体)之间关系和逻辑的都适合这个概念。如果您需要存储非结构化或相互之间关系不大的数据(如日志文件),那么使用关系数据库或ORM可能不是最佳选择。
如果您熟悉SQL编程,您会知道在开发这类应用程序时,您总是要做两遍工作。首先,您通过创建大量表和设置它们之间的外键来定义数据库模型。然后,您编写代码来访问这些表,放置查询并更改值。您的逻辑实体分散在多个表中,您必须手动连接它们并(几乎)手动维护一致性。这是自20世纪90年代中期以来所谓的客户端/服务器应用程序的构建块。它在Microsoft .NET Framework中以ADO.NET的形式实现,并处理像DataSet
、DataTable
、DataRow
和DataView
这样的类。
对象关系映射(ORM)的出现是为了通过对象访问数据库。它充当编程语言或环境与底层数据库之间的一个层。由于这种抽象,业务应用程序的编程速度更快,代码也更易于维护。ORM框架实现了多种方法来加速开发过程并提高可维护性。总的来说,它们允许以(几乎)自然的方式通过反映实体的对象来访问数据库。许多ORM允许针对数据库运行LINQ查询来获取实体。还有一些ORM可以完全将业务逻辑与底层数据库解耦。
两个最知名的ORM框架是NHibernate(.NET)和ADO.NET Entity Framework。它们都只实现了部分LINQ,并且在速度方面并非最快的。在这篇文章中,我想介绍x-tensive公司的DataObjects.Net。DataObjects.net是一个具有非常高抽象级别、近乎完整的LINQ实现和良好性能的ORM。
为什么DataObjects.Net值得一试?
首先,我尝试它是因为它具有自动的模式生成和维护功能。我编写SQL多年了,对这些表、视图和存储过程的声明感到厌倦。
在使用过程中,我意识到这个框架几乎解决了ORM带来的所有问题。我可以透明地使用会话和事务,我的实体看起来就像真正的C#/VB对象。X-tensive公司在最大程度地减少通信(与SQL服务器的往返)方面付出了很多努力。对于复杂的业务应用程序,DataObjects.Net甚至比直接使用SQL服务器或任何轻量级ORM框架都要快,这得益于他们的优化。
可以从以下链接找到与其他ORM框架的功能和性能比较:ormbattle.net。
DataObjects.Net是如何工作的?
DataObjects.Net使用面向切面的编程(AOP)技术,将持久化和业务逻辑注入到普通的C#/VB类中。方面可以看作是在其他函数开始或结束时调用的某种函数或委托。它们不需要在源代码中调用,而是在编译程序集后在中间语言(IL)中注入。DataObjects.Net使用著名的PostSharp框架(一个面向切面的编程框架)来注入映射代码。模型以简单的类和一些属性的形式声明,如下面的示例所示。这使得DataObjects.Net非常易于使用,并将所有映射内容与用户隔离开来。
下载和安装DataObjects.Net
在您试用该框架之前,您必须从x-tensive主页下载并安装它。在这篇文章中,我使用的是Visual Studio 2010的4.3版本。
幕后:DataObjects.Net和Visual Studio
您可能会问,PostSharp和DataObjects.Net是如何集成到Visual Studio解决方案或C#项目中的。由于PostSharp需要操作已编译和链接的程序集,因此在编译后必须有一个特殊的构建步骤。如果您在Visual Studio解决方案中打开.csproj
文件,您会发现一个额外的“import project
”行
<Import Project="$(MSBuildToolsPath)\Microsoft.CSharp.targets" />
<Import Project="$(DataObjectsDotNetPath)\Common\DataObjects.Net.targets" />
这个包含行在幕后完成了魔术。顺便说一句:如果您在项目资源管理器窗口中右键单击您的项目,会找到“卸载项目”。在卸载项目后,您会找到“编辑项目”来编辑您的项目文件。编辑后,您可以重新加载项目。
您可以在手册的“在您的项目和解决方案中使用DataObjects.Net”部分找到更多信息。
构建块
任何ORM框架都将一些类或对象映射到数据库中的某些数据。然而,每个ORM框架都有自己的实现方式。在接下来的章节中,我将解释DataObjects.Net应用程序的构建块,并概述其背后的思想。
域和数据存储
DataObjects.Net需要访问某种数据存储,通常是SQL服务器。数据库和C#对象之间的粘合剂是所谓的域(Domain)。域这个术语与互联网域无关,而是源自所谓的领域驱动设计(DDD)。在技术上,域是连接到数据库的连接器。一个域需要两样东西:首先,到数据库或数据存储的总连接字符串。所有数据将存储在那里。其次,定义了持久化类的程序集和命名空间。这些类被称为“模型”(Model)。所需的表、索引、主键和外键都是从这个“模型”生成的。DataObjects.Net域可以在App.config文件或代码中进行配置。以下是如何在代码中完成此操作的示例:
var config = new DomainConfiguration("sqlserver://\\SQLExpress/DO40-Tests");
config.UpgradeMode = DomainUpgradeMode.Recreate; // Drops all tables and recreates them.
// Database is empty after that
config.Types.Register(typeof(SimpleEntity).Assembly, typeof(SimpleEntity).Namespace);
var domain = Domain.Build(config);
我们有一个到名为“DO40-Tests
”的Microsoft SQL Server Express数据库的连接字符串。请记住,DataObjects.Net **可以生成表等**,但**它不能自己生成任何数据库**。您必须创建“DO40-Tests
”数据库,并确保DataObjects.Net可以在给定的连接字符串下访问它。之后,DataObjects.Net将完成剩余的工作。
模型
持久化数据以C#类的形式声明和访问。持久化类型必须派生自Entity
类或Entity
的某个子类。持久化字段声明为带有[Field
]属性的属性。在继承层次结构中,第一个或根类必须带有[HierarchyRoot
]属性,并且至少有一个[Field
]属性必须声明为[Key
]属性才能成为主键。这里有一个简单的实体,带有一个int
键和一个string
数据(payload)字段:
[Serializable]
[HierarchyRoot]
public class SimpleEntity : Entity
{
[Field, Key]
public int Id { get; private set; }
[Field(Length = 100)]
public string Data { get; set; }
}
这些SimpleEntity
对象之间没有任何关系。HierarchyRoot
属性将此类标识为层次结构的基础。此处,“层次结构”是指面向对象意义上的继承基类。下一个示例展示了类如何从这个基类派生:
[Serializable]
[HierarchyRoot]
public class BaseEntity : Entity
{
[Field, Key]
public int Id { get; private set; }
[Field(Length = 100)]
public string BaseEntityData { get; set; }
}
[Serializable]
public class DerivedEntity : BaseEntity
{
[Field(Length = 100)]
public string DerivedEntityData { get; set; }
}
如上所示,派生类以常规的C#方式继承自基类。它继承了基类的所有属性和主键。您可以推测,所有的类型转换(base <-> derived
)都可以以常规的C#方式进行。使用这些持久化对象几乎与使用普通对象一样。在下一节中,将模拟一个典型的父/子或有时称为主/明细关系:
[Serializable]
[HierarchyRoot]
public class ParentEntity : Entity
{
[Field, Key]
public int Id { get; private set; }
[Field]
public EntitySet<ChildEntity> Childs { get; private set; }
}
[Serializable]
[HierarchyRoot]
public class ChildEntity : Entity
{
public ChildEntity( ParentEntity parent )
{
Parent = parent;
}
[Field, Key]
public long Id { get; private set; }
[Field]
[Association(PairTo = "Childs", OnTargetRemove = OnRemoveAction.Cascade)]
public ParentEntity Parent { get; private set; }
}
ParentEntity
类几乎是直观的。在ChildEntity
中,我们看到一个ParentEntity
的'Parent
'字段,用于存储到父对象的引用。我们还看到了形式为Association
属性的业务逻辑。在这种情况下,关联将字段'Parent
'与EntitiySet
'Childs
'配对。任何具有特定父对象的ChildEntity
都自动包含在Childs EntitySet
中,反之亦然。通过“OnTargetRemove = OnRemoveAction.Cascade
”操作,在删除父对象时会自动删除子对象。解释DataObjects.Net的所有业务规则特性远远超出了本介绍的范围。但请记住,这些功能都可以通过DataObjects.Net实现。这两个类都用HierarchyRoot
属性标记,因为它们都是基类(继承层次结构的根)。
为了提高查询性能,还可以为某些字段指定索引。它们使用Index
属性声明,如下面的示例所示。
[Serializable]
[HierarchyRoot]
[Index("Data")]
public class IndexedDataEntity : Entity
{
[Field]
[Key]
public int Id { get; private set; }
[Field]
public int Data { get; set; }
}
基本操作
设置好域和声明模型后,就可以执行一些操作了。尽管DataObjects.Net允许以非常自然的方式访问数据,但它完全支持会话和事务。
会话和事务,持久化对象的创建和销毁
下一个示例展示了如何在代码中以编程方式使用会话和事务。特别是对于事务,也可以使用属性以声明式的方式完成。
using (Session.Open(domain))
{
using (var transactionScope = Transaction.Open())
{
new SimpleEntity { Data = "SimpleEntity 1" };
new SimpleEntity { Data = "SimpleEntity 2" };
transactionScope.Complete();
}
}
使用IDisposable
模式(using
)可以确保在发生异常时会话和事务被关闭。在这个示例中,SimpleEntity
对象确实被持久化了,而没有为它们提供任何事务或会话。为了实现这一点,框架使用一个隐藏的(线程static
)当前会话和当前事务。当前会话和当前事务由Open()
方法自动设置。在下一个示例中,将创建ParentEntity
和ChildEntity
对象。由于ChildEntity
类的构造函数,不可能在没有父对象的情况下创建ChildEntity
对象。DataObjects.Net支持普通C#类的构造函数语义。
using (Session.Open(domain))
{
using (TransactionScope transactionScope = Transaction.Open())
{
var parent = new ParentEntity();
new ChildEntity(parent);
new ChildEntity(parent);
new ChildEntity(parent);
transactionScope.Complete();
}
}
如上所示,使用DataObjects.Net创建持久化对象非常简单。删除或移除这样的对象也没有问题。只需在对象上调用Remove()
成员函数即可。我们应该记住,这些新创建的对象将永久存在,或者至少直到调用Remove()
方法。但是,当我们的局部变量超出范围或我们的应用程序终止时,我们如何再次获取这些对象呢?答案很简单:我们使用LINQ(语言集成查询)从数据库中获取这些对象。这将在下一节中展示。
使用LINQ查询持久化对象
每个DataObjects.Net查询的起点是Query.All<>
。下一个查询以任意顺序返回所有DerivedEntity
对象。
using (Session.Open(domain))
{
var linqQuery = from entity in Query.All<DerivedEntity>() select entity;
foreach (var entity in linqQuery)
{
// Do some work with the objects
}
}
一旦我们获得一个对象,我们就可以以常规的C#方式使用其中的引用和集合。在下一个示例中,我们遍历ParentEntity
对象中的Child
集合。
using (Session.Open(domain))
{
var linqQuery = from entity in Query.All<ParentEntity>() select entity;
foreach (var parentEntity in linqQuery)
{
foreach( var childEntity in parentEntity.Childs )
{
// Do some work with the childs and parents
}
}
}
DataObjects.Net拥有近乎完整的LINQ支持。下一个示例中的查询更为复杂。它对查询施加了一个条件(where
)并对结果进行排序(orderby
)。
using (Session.Open(domain))
{
var linqQuery = from entity in Query.All<IndexedDataEntity>()
where entity.Data < 1000
orderby entity.Data
select entity;
foreach (var entity in linqQuery)
{
// Do some work with the selected objects
}
}
事实上,LINQ查询可以复杂得多。但这远远超出了本介绍的范围。
结论
我们在一个新项目中使用了DataObjects.Net 4.0和PostgreSQL,该项目包含约60个持久化类和目前1000万个实体。起初,我们在PostgreSQL服务器的性能方面遇到了一些问题。但在配置了足够的缓存大小(默认值非常低)和一些小的优化之后,它的速度非常快,几乎在所有情况下都与Microsoft SQL服务器相当。我们正在使用PostgreSQL 9.0 Beta4 64位版本。我对数据库应用程序开发的速度和简便性印象深刻。总而言之:DataObjects.net可以成为加快开发过程的好选择,尤其对于新项目而言。