3. 基于 SQL Server Express 2012 的案例研究
3.1. 简介
网上绝大多数关于 Entity Framework 的示例都是基于 SQL Server 的。这很正常,因为它很可能是企业 .NET 领域中使用最广泛的数据库管理系统。我们将遵循这一趋势。随后,这些示例将扩展到第 1.2 节中提到的所有数据库。
3.2. 安装工具
本文将不详细描述工具的安装过程。因为这需要大量屏幕截图,而这些截图很快就会过时。这是一项(诚然并非总是容易)的任务,我们将留给读者自行完成。
我们需要安装以下工具:
- SQL Server Express 2012 数据库管理系统:[http://www.microsoft.com/fr-fr/download/details.aspx?id=29062]。请下载“带工具”版本,该版本包含与数据库管理系统配套的管理工具:
安装完成后,我们启动该数据库管理系统:
![]() |
![]() |
- [1]:从“开始”菜单中启动“SQL Server 配置管理器”;
现在启动 SQL Server 管理工具:
![]() |
- [1]:从“开始”菜单中启动“SQL Server Management Studio”;
- 2:管理工具。
我们将连接到服务器:
![]() |
- 在 [1] 中,打开“对象资源管理器”;
- 在 2 中,输入连接参数:
- 3:(本地) 服务器(注意必须使用括号)指的是安装在该计算机上的服务器,
- [4]:选择 Windows 身份验证。您必须是该计算机的管理员,此连接才能成功,
- [5]:连接;
![]() |
- [6]:已连接;
![]() |
- Windows 身份验证,即刚才所用的方式。拥有相应权限的 Windows 用户即可登录,
- SQL Server 身份验证。用户必须是数据库管理系统中注册的用户之一;
完成上述设置后,我们可以验证服务器属性;
![]() |
- 在 [10] 中,为用户设置密码。在本文档的其余部分中,该密码为 sqlserver2012;
![]() |
- 在 [10] 中,授予其连接权限;
- 在 [11] 中,连接已启用。现在可以确认向导;
- 在 [12] 中,退出服务器。
现在,我们使用 sa/sqlserver2012 登录名重新连接:
![]() |
![]() |
- 在 [6] 中,我们已登录。
现在我们将创建一个演示数据库:
![]() |
![]() |
- 在 [4] 中,数据库已创建;
- 在 [5] 中,在“demo”数据库中创建一个新表;
![]() |
![]() |
![]() |
![]() |
- 在[6]中,我们定义了一个包含两列(ID 和 NAME)的表;
- 在 7 中,我们将 [ID] 列设为主键;
- 在 8 中,主键由一个键表示;
- 在 9 中,保存该表;
- 在 [10] 中,我们为其命名;
- 在 [11] 中,要使该表出现在 [demo] 数据库中,必须刷新数据库;
- 在 [12] 中,[PERSONNES] 表已成功创建。
现在我们已经掌握了使用 SQL Server Management Studio 的基本知识。
3.3. 嵌入式服务器 (localdb)\v11.0
VS Express 2012 自带嵌入式 SQL Server。本文假设 VS Express 2012 已安装 [http://www.microsoft.com/visualstudio/fra/downloads]。启动 VS 2012 [1]:
![]() |
启动 SQL Server 2012 Management Studio 2 并登录 3。
![]() |
- 在 [4] 中,连接到 (localdb)\v11.0 服务器;
- 在 [5] 中,使用 Windows 身份验证;
- 在 [6] 中,连接成功后将显示服务器的数据库。与之前一样,您可以创建一个新数据库。
在 VS 2012 中,我们将不再使用此嵌入式服务器。
3.4. 基于实体创建数据库
Entity Framework 5 的 Code First 模式允许您基于实体创建数据库。接下来我们将对此进行探讨。使用 VS Express 2012,我们创建一个初始的 C# 控制台项目:
![]() |
![]() |
- 在 [1] 中,项目定义;
- 在 2 中,已创建的项目。
所有项目都需要引用 ,即 Entity Framework 5 的 DLL。我们将其添加如下:
![]() |
- 在 [1] 中,NuGet 工具允许您下载依赖项;
![]() |
您可以通过查看已添加引用项的属性来了解更多信息:
![]() |
- 在 [1] 中,DLL 版本。你需要 5 版;
- 在 2 中,其在文件系统中的位置:<solution>\packages\EntityFramework.5.0.0\lib\net45\EntityFramework.dll,其中 <solution> 是 VS 解决方案文件夹。NuGet 添加的所有包都会放入 <solution>/packages 文件夹中;
- 在 3 中,已创建了一个 [packages.config] 文件。其内容如下:
<?xml version="1.0" encoding="utf-8"?>
<packages>
<package id="EntityFramework" version="5.0.0" targetFramework="net45" />
</packages>
它列出了由 NuGet 导入的包。
让我们回到 VS 项目,并在项目中创建一个 [Models] 文件夹:
![]() |
- 在 [1] 中,向项目中添加一个文件夹;
- 在 2 中,该文件夹将被命名为 [Models]。
我们将延续这一做法,将实体定义放置在 [Models] 文件夹中。
为了构建我们的实体,我们将使用 NHibernate 项目中采用的 MySQL 5 数据库定义。让我们回顾一下 EF 实体的作用:
![]() |
实体必须反映数据库表。数据访问层使用这些实体,而不是直接操作表。让我们从 [DOCTORS] 表开始:
3.4.1. [Medecin] 实体
它包含由 [RdvMedecins] 应用程序管理的医生相关信息。
![]() | ![]() |
- ID:医生的ID号——该表的主键
- VERSION:标识表中该行版本的编号。每次对该行进行修改时,该编号会递增 1。
- LAST_NAME:医生的姓
- FIRST_NAME:医生的名字
- TITLE:称谓(Ms.、Mrs.、Mr.)
我们可以从以下 [Doctor] 类开始:
using System;
[Table("MEDECINS", Schema = "dbo")]
namespace RdvMedecins.Entites
{
public class Medecin
{
// data
public int Id { get; set; }
public string Titre { get; set; }
public string Nom { get; set; }
public string Prenom { get; set; }
}
- 第 3 行:[Medecin] 类与数据库中的 [MEDECINS] 表相关联。该表将位于名为“dbo”的架构中。
我们将该类放置在名为 [Entities.cs] 的文件中 [1]。所有实体都将放置在此处。
![]() |
仍在 [Models] 文件夹中,我们创建以下 [Context.cs] 文件:
using System.Data.Entity;
using RdvMedecins.Entites;
namespace RdvMedecins.Models
{
// the context
public class RdvMedecinsContext : DbContext
{
// the doctors
public DbSet<Medecin> Medecins { get; set; }
}
// database initialization
public class RdvMedecinsInitializer : DropCreateDatabaseAlways<RdvMedecinsContext>
{
}
}
- 第 8 行:[RdvMedecinsContext] 类将表示持久化上下文,即由 ORM 管理的实体集合。它必须继承自 [System.Data.Entity.DbContext] 类;
- 第 11 行:[Medecins] 字段代表持久化上下文中的 [Medecin] 实体。其类型为 DbSet<Medecin>。通常 [DbSet] 的数量与数据库中的表数量相同,即每张表对应一个;
- 第 15 行:我们定义了一个 [RdvMedecinsInitializer] 类来初始化已创建的数据库。此处,它继承自 [DropCreateDataBaseAlways] 类,顾名思义,该类会在数据库已存在时将其删除,然后重新创建。这在数据库开发阶段非常有用。 [DropCreateDataBaseAlways] 类的参数是与数据库关联的持久化上下文类型。除了 [DropCreateDataBaseAlways] 之外,还可以使用其他父类作为初始化类:
- [DropCreateDatabaseIfModelChanges]:若实体发生变更则重建数据库,
- [CreateDatabaseIfNotExists]:若数据库不存在则创建;
我们还需要编写一个主程序。它将如下所示 [CreateDB_01.cs]:
using System;
using System.Data.Entity;
using RdvMedecins.Models;
namespace RdvMedecins_01
{
class CreateDB_01
{
static void Main(string[] args)
{
// we create the
Database.SetInitializer(new RdvMedecinsInitializer());
using (var context = new RdvMedecinsContext())
{
context.Database.Initialize(false);
}
}
}
}
- 第 12 行:[System.Data.Entity.DataBase] 是一个提供静态方法的类,用于管理与持久化上下文关联的数据库。静态方法 [SetInitializer] 允许您指定数据库初始化类。这不会触发初始化;
- 第 13 行:要使用持久化上下文,必须对其进行实例化。此处即执行了此操作。使用了 using 语句,以便在语句结束时自动关闭上下文。因此,在第 17 行,上下文被关闭;
- 第 15 行:我们显式地触发了与持久化上下文 [RdvMedecinsContext] 关联的数据库生成。false 参数表示如果该上下文已执行过此操作,则不应再次执行。在此处,我们同样可以将其设置为 true。
在处理数据库时,连接参数通常存储在 [App.config] 文件中。请注意,目前这些参数尚未配置:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
</configSections>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
<entityFramework>
<defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" />
</entityFramework>
</configuration>
当将 Entity Framework 依赖项添加到项目引用时,上述元素会被添加到 [App.config] 中。
请在启动 SQL Server Express 之后运行项目(Ctrl-F5)(这一点很重要):
![]() | ![]() |
执行应能顺利完成且无错误。现在,让我们打开 SQL Server Management Studio 并刷新视图:
![]() |
我们可以看到,一个全名为 [RdvMedecinsContext] 类的数据库已创建,其中包含一个名为 [dbo.MEDECINS](即我们为其命名的名称)的表,其列与 [Medecin] 实体的字段名称相匹配。 如果代码执行成功但上述数据库未出现,请检查嵌入式服务器 (localdb)\v11.0(参见第 19 页)。在 VS 2012 Pro 中,如果执行代码时 SQL Server 未处于活动状态,则会使用此服务器;而在 VS 2012 Express 中则不会。
让我们来查看 [MEDECINS] 表的结构:
- 它采用了 [Medecin] 实体的字段名称;
- [Id] 列是主键。这是 EF 的约定:如果实体 E 有一个 Id 或 Eid 字段(如 MedecinId),则该列即为关联表中的主键;
- 表中的列类型与实体字段的类型一致;
- 对于 Title、Last Name 和 First Name 列,使用了 [nvarchar(max)] 类型。我们可以更具体地指定:标题为 5 个字符,姓氏和名字各为 30 个字符;
- Title、Last Name 和 First Name 列允许为 NULL 值。我们将对此进行修改。
让我们来看看主键 [Id] 的属性:
![]() |
在 [1] 中,我们可以看到主键的类型为 [Identity],这意味着其值由 SQL Server 自动生成。我们将对所有数据库管理系统(DBMS)采用这一策略。
我们将通过使用注解来减少对 EF 约定(conventions)的依赖。位于 [Entities.cs] 中的实体代码如下所示:
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RdvMedecins.Entites
{
[Table("MEDECINS", Schema = "dbo")]
public class Medecin
{
// data
[Key]
[Column("ID")]
public int Id { get; set; }
[Required]
[MaxLength(5)]
[Column("TITRE")]
public string Titre { get; set; }
[Required]
[MaxLength(30)]
[Column("NOM")]
public string Nom { get; set; }
[Required]
[MaxLength(30)]
[Column("PRENOM")]
public string Prenom { get; set; }
[Required]
[Column("VERSION")]
public int Version { get; set; }
}
}
- 第 2 行和第 3 行:这些注解位于 [System.ComponentModel.DataAnnotations] 命名空间(Key、Required、MaxLength)和 [System.ComponentModel.DataAnnotations.Schema] 命名空间(Column)中。 更多注解可参见网址 [http://msdn.microsoft.com/en-us/data/gg193958.aspx];
- 第 11 行:[Key] 用于指定主键;
- 第 12 行:[Column] 设置与该字段对应的列名;
- 第 14 行:[Required] 表示该字段为必填字段(SQL NOT NULL);
- 第 15 行:[MaxLength] 设置字符串的最大长度,[MinLength] 设置其最小长度;
让我们使用这个新的 [Medecin] 实体定义来运行该项目。生成的数据库如下:
![]() |
- 列名已按我们指定的名称命名;
- [Required] 注解已转换为 SQL NOT NULL;
- [MaxLength(N)] 注解已映射为 SQL nvarchar(N) 类型。
在 NHibernate 应用程序中,[VERSION] 列的存在是为了防止对表中同一行进行并发访问。其原理如下:
- 进程 P1 在时间 T1 从 [DOCTORS] 表中读取一行 L。该行的版本为 V1;
- 进程 P2 在时间 T2 从 [DOCTORS] 表中读取同一行 L。由于进程 P1 尚未提交其修改,该行仍为版本 V1;
- 进程 P1 提交对行 L 的修改。此时行 L 的版本号变为 V2 = V1 + 1;
- 进程 P2 将其对行 L 的修改提交。此时 ORM 会抛出异常,因为进程 P2 持有的行 L 版本 V1 与数据库中查找到的版本 V2 不一致。
这被称为乐观并发控制。在 EF 5 中,承担此角色的字段必须具有以下两个属性之一:[Timestamp] 或 [ConcurrencyCheck]。SQL Server 提供 [timestamp] 数据类型。 此类列的值会在插入或修改行时由 SQL Server 自动生成。此类列随后可用于管理并发访问。回到前面的示例,进程 P2 将发现时间戳与它读取的值不同,因为在此期间,进程 P1 所做的修改已经更改了该值。
我们的 [Doctor] 实体演变如下:
using System;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
namespace RdvMedecins.Entites
{
[Table("MEDECINS", Schema = "dbo")]
public class Medecin
{
// data
[Key]
[Column("ID")]
public int Id { get; set; }
[Required]
[MaxLength(5)]
[Column("TITRE")]
public string Titre { get; set; }
[Required]
[MaxLength(30)]
[Column("NOM")]
public string Nom { get; set; }
[Required]
[MaxLength(30)]
[Column("PRENOM")]
public string Prenom { get; set; }
[Column("TIMESTAMP")]
[Timestamp]
public byte[] Timestamp { get; set; }
}
}
- 第 26–28 行:第 27 行中带有 [Timestamp] 属性的新列。字段类型必须为 byte[](第 28 行)。字段名可以是任意名称。我们不设置 [Required] 属性,因为该值并非由应用程序提供,而是由数据库管理系统 (DBMS) 自身提供。
如果使用此新实体运行项目,数据库将按以下方式演变:
![]() |
还有最后一点需要说明。持久化上下文“知道”必须将实体插入数据库,因为此时其主键为空。正是数据库插入操作会为主键赋值。在此,分配给主键 [Id] 的 int 类型并不合适,因为该类型不接受空值。 因此,我们将其类型设为 int?,该类型既支持 int 值,也支持空指针。因此,所使用的 [Medecin] 实体将如下所示:
public class Medecin
{
// data
[Key]
[Column("ID")]
public int? Id { get; set; }
...
我们还需要了解如何在实体中表示表之间的外键关系。
3.4.2. [Creneau] 实体
[CRENEAUX] 表列出了可安排约会的时段:
![]() |
![]() |
- ID:时间段的ID号——该表的主键
- VERSION:标识表中该行版本的编号。每次对该行进行修改时,该编号都会增加 1。
- ID_MEDECIN:用于标识该时段所属医生的ID号——MEDECINS(ID)列的外键。
- START_TIME:该时段的开始时间
- MSTART:该时段的开始分钟
- HFIN:时段结束时间
- MFIN:该时段的结束分钟
例如,[SLOTS] 表(参见上文 [1])的第二行表明,第 2 号时段于上午 8:20 开始,上午 8:40 结束,并归属于第 1 号医生(玛丽·佩利西耶女士)。
基于已知信息,我们可以在 [Entites.cs] 中将 [Creneau] 实体定义如下:
[Table("CRENEAUX", Schema = "dbo")]
public class Creneau
{
// data
[Key]
[Column("ID")]
public int? Id { get; set; }
[Required]
[Column("HDEBUT")]
public int Hdebut { get; set; }
[Required]
[Column("MDEBUT")]
public int Mdebut { get; set; }
[Required]
[Column("HFIN")]
public int Hfin { get; set; }
[Required]
[Column("MFIN")]
public int Mfin { get; set; }
[Required]
public virtual Medecin Medecin { get; set; }
[Column("TIMESTAMP")]
[Timestamp]
public byte[] Timestamp { get; set; }
}
唯一的改动在第 20–21 行。由于 [CRENEAUX] 表对 [MEDECINS] 表具有外键,因此在 [Creneau] 实体中,第 21 行添加了对 [Medecin] 实体的引用以体现这一关系。字段名称无关紧要,关键在于类型。 该属性必须使用 virtual 关键字声明为虚拟属性。这是因为 EF 需要重新定义所有所谓的导航属性——即那些对应外键并允许在表之间进行导航的属性。
要测试新实体,我们需要在 [Context.cs] 中进行一些修改:
using System.Data.Entity;
using RdvMedecins.Entites;
namespace RdvMedecins.Models
{
// the context
public class RdvMedecinsContext : DbContext
{
// entities
public DbSet<Medecin> Medecins { get; set; }
public DbSet<Creneau> Creneaux { get; set; }
}
// database initialization
public class RdvMedecinsInitializer : DropCreateDatabaseIfModelChanges<RdvMedecinsContext>
{
}
}
第 12 行反映了上下文中需要管理的一个额外实体。当我们运行该项目时,会得到以下新的数据库:
![]() |
[CRENEAUX] 表确实已创建,其新特征在于出现了外键 [1] 和 2。它们的名称由实体(Medecin)中对应字段的名称生成,并在末尾添加了 "_Id" 后缀。为了查看该外键的属性,我们尝试对其进行修改 3。
![]() |
上图截图显示,[Medecin_Id] 是 [CRENEAUX] 表中的外键,且它引用了 [MEDECINS] 表中的主键 [ID]。
如果我们为现有数据库创建实体,外键列的名称未必是 [Medecin_Id]。对于其他列,我们看到 [Column] 注解解决了这个问题。奇怪的是,对于外键,情况却更为复杂。我们必须按以下步骤操作:
public class Creneau
{
// data
...
[Required]
[Column("MEDECIN_ID")]
public int MedecinId { get; set; }
[Required]
[ForeignKey("MedecinId")]
public virtual Medecin Medecin { get; set; }
...
}
- 第 5-7 行:我们创建一个外键类型的字段(int)。通过 [Column] 属性,我们指定了在与该实体关联的表中将作为外键的列名;
- 第 9 行:我们在 [Medecin] 类型的字段上添加 [ForeignKey] 注解。该注解的参数是与表中外键列关联的字段名称(而非列名)。
本次运行项目将生成以下表:
![]() |
如上所示,外键列确实采用了我们指定的名称。请注意以下字段:
[Required]
[Column("MEDECIN_ID")]
public int MedecinId { get; set; }
[Required]
[ForeignKey("MedecinId")]
public virtual Medecin Medecin { get; set; }
最终仅生成了一列,即 [MEDECIN_ID] 列。尽管如此,[MedecinId] 字段的存在仍至关重要。当从 [SLOTS] 表读取一行数据时,该字段将获取 [DOCTOR_ID] 列的值,即 [DOCTORS] 表中外键的值。这通常非常有用。
上文中的 [Medecin] 字段反映了连接 [Creneau] 实体与 [Medecin] 实体的多对一关系。多个 [Slot] 对象与同一个 [Doctor] 对象相关联。反向关系——即单个 [Doctor] 对象关联多个 [Slot] 对象——可通过在 [Doctor] 实体中添加一个额外字段来建模:
public class Medecin
{
// data
[Key]
[Column("ID")]
public int? Id { get; set; }
...
public ICollection<Creneau> Creneaux { get; set; }
[Column("TIMESTAMP")]
[Timestamp]
public byte[] Timestamp { get; set; }
在第 8 行,我们添加了 [Slots] 字段,这是一个 [Slot] 对象的集合。该字段将使我们能够访问医生所有可用的时间段。
再次运行项目时,我们会发现 [DOCTORS] 表并未发生变化:
![]() |
没有添加任何列。仅凭 [CRENEAUX] 表与 [MEDECINS] 表之间的外键关系,EF 便足以生成相关字段:
public class Medecin
{
...
public ICollection<Creneau> Creneaux { get; set; }
...
}
public class Creneau
{
...
[Required]
[Column("MEDECIN_ID")]
public int MedecinId { get; set; }
[Required]
[ForeignKey("MedecinId")]
public virtual Medecin Medecin { get; set; }
...
}
我们已经掌握了基础知识。接下来只需创建另外两个实体即可。
3.4.3. [Client] 和 [Appointment] 实体
基于已掌握的知识,我们可以编写 [Client] 和 [Appointment] 实体。[Client] 实体包含由 [DoctorAppointments] 应用程序管理的客户信息。
![]() | ![]() |
- ID:客户的ID号——该表的主键
- VERSION:标识表中该行版本的编号。每次对该行进行修改时,该编号会递增1。
- LAST_NAME:客户的姓
- FIRST NAME:客户的名字
- TITLE:称谓(Ms.、Mrs.、Mr.)
[Client] 实体可能如下所示:
[Table("CLIENTS", Schema = "dbo")]
public class Client
{
// data
[Key]
[Column("ID")]
public int? Id { get; set; }
[Required]
[MaxLength(5)]
[Column("TITRE")]
public string Titre { get; set; }
[Required]
[MaxLength(30)]
[Column("NOM")]
public string Nom { get; set; }
[Required]
[MaxLength(30)]
[Column("PRENOM")]
public string Prenom { get; set; }
// customer rvs
public ICollection<Rv> Rvs { get; set; }
[Column("TIMESTAMP")]
[Timestamp]
public byte[] Timestamp { get; set; }
}
[Client] 类与 [Doctor] 类几乎完全相同。它们可以从同一个父类派生而来。新元素位于第 21 行。它反映了客户可以有多个预约这一事实,并源于 [RVS] 表到 [CLIENTS] 表的外键关系。
[Rv] 实体表示一个预约:
![]() |
- ID:用于唯一标识预约的编号——主键
- DAY:预约日期
- SLOT_ID:预约时段——作为外键关联至[SLOTS]表的[ID]列——同时确定时段及负责医生。
- CLIENT_ID:预约对象的客户ID——作为[CLIENTS]表中[ID]列的外键
[Rv] 实体可能如下所示:
[Table("MEDECINS", Schema = "dbo")]
public class Rv
{
// data
[Key]
[Column("ID")]
public int? Id { get; set; }
[Required]
[Column("JOUR")]
public DateTime Jour { get; set; }
[Column("CLIENT_ID")]
public int ClientId { get; set; }
[ForeignKey("ClientId")]
[Required]
public virtual Client Client { get; set; }
[Column("CRENEAU_ID")]
public int CreneauId { get; set; }
[ForeignKey("CreneauId")]
[Required]
public virtual Creneau Creneau { get; set; }
[Column("TIMESTAMP")]
[Timestamp]
public byte[] Timestamp { get; set; }
}
- 第 5-7 行:主键;
- 第 8-10 行:预约日期;
- 第11-12行:从[RVS]表到[CLIENTS]表的外键;
- 第13–15行:预约客户;
- 第 16–17 行:从 [RVS] 表到 [CRENEAUX] 表的外键;
- 第 18–20 行:预约时段;
- 第 21-23 行:并发访问控制字段。
在第 17 行,我们可以看到一种多对一的关系:一个时间段可以对应多个预约(但不在同一天)。这种反向关系可以在 [Creneau] 实体中体现:
public class Creneau
{
// niche Rvs
public ICollection<Rv> Rvs { get; set; }
...
}
第 4 行:此时间段内已安排的约会集合。
运行项目后,生成的数据库如下:
![]() |
[DOCTORS] 和 [SLOTS] 表没有变化。[CLIENTS] 和 [APPs] 表如下所示:
![]() | ![]() |
这正是预期的结果。我们还有一些细节需要理清:
3.4.4. 设置数据库名称
要设置由 EF 生成的数据库名称,我们将使用 [App.config] 中定义的连接字符串。该配置文件的修改如下:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
</configSections>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
<entityFramework>
<defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" />
</entityFramework>
<!-- connection chain on base -->
<connectionStrings>
<add name="RdvMedecinsContext"
connectionString="Data Source=localhost;Initial Catalog=rdvmedecins-ef;User Id=sa;Password=sqlserver2012;"
providerName="System.Data.SqlClient" />
</connectionStrings>
<!-- the factory provider -->
<system.data>
<DbProviderFactories>
<add name="SqlClient Data Provider"
invariant="System.Data.SqlClient"
description=".Net Framework Data Provider for SqlServer"
type="System.Data.SqlClient.SqlClientFactory, System.Data,
Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
/>
</DbProviderFactories>
</system.data>
</configuration>
- 第 15–19 行:数据库连接字符串;
- 第 16 行:[name] 属性使用用于持久化上下文的 [RdvMedecinsContext] 类的名称。这一点非常重要,请务必记住。可以在上下文构造函数中绕过此限制:
// manufacturer
public RdvMedecinsContext()
: base("monContexte")
{
}
在这种情况下,我们可以将 name 设为 "myContext"。本文档的其余部分将使用此名称。
- 第 17 行:连接字符串。[Data Source]:托管 DBMS 的服务器名称;[Initial Catalog]:数据库名称,本例中为 [rdvmedecins-ef];[User Id]:连接所有者;[Password]:所有者的密码。读者应根据自身环境调整此字符串;
- 第 21–29 行:定义一个 [DbProviderFactory]。我不清楚这是什么。从名称判断,这可能是一个用于生成 [ADO.NET] 层的类,该层将 EF 与 DBMS 隔离开来:
![]() |
实际上,对于 SQL Server 而言,这些代码行是多余的,但我为了兼容其他数据库管理系统不得不添加它们。因此我在此保留以供参考。它们不会引发任何问题。唯一需要注意的是第 27 行中的版本号,它必须与项目引用中列出的 [System.Data] DLL 版本一致:
![]() |
好了,准备就绪。我们运行该项目,得到以下数据库 [rdvmedecins-ef]:
![]() |
这将是我们最终的数据库。剩下的就是向其中填充数据了。
3.4.5. 填充数据库
可以使用数据库初始化类向其中插入数据:
public class RdvMedecinsInitializer : DropCreateDatabaseIfModelChanges<RdvMedecinsContext>
{
// database initialization
public class RdvMedecinsInitializer : DropCreateDatabaseAlways<RdvMedecinsContext>
{
protected override void Seed(RdvMedecinsContext context)
{
base.Seed(context);
// initialize the base
// our customers
Client[] clients ={
new Client { Titre = "Mr", Nom = "Martin", Prenom = "Jules" },
new Client { Titre = "Mme", Nom = "German", Prenom = "Christine" },
new Client { Titre = "Mr", Nom = "Jacquard", Prenom = "Jules" },
new Client { Titre = "Melle", Nom = "Bistrou", Prenom = "Brigitte" }
};
foreach (Client client in clients)
{
context.Clients.Add(client);
}
// the doctors
Medecin[] medecins ={
new Medecin { Titre = "Mme", Nom = "Pelissier", Prenom = "Marie" },
new Medecin { Titre = "Mr", Nom = "Bromard", Prenom = "Jacques" },
new Medecin { Titre = "Mr", Nom = "Jandot", Prenom = "Philippe" },
new Medecin { Titre = "Melle", Nom = "Jacquemot", Prenom = "Justine" }
};
foreach (Medecin medecin in medecins)
{
context.Medecins.Add(medecin);
}
// time slots
Creneau[] creneaux ={
new Creneau{ Hdebut=8,Mdebut=0,Hfin=8,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=8,Mdebut=20,Hfin=8,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=8,Mdebut=40,Hfin=9,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=9,Mdebut=0,Hfin=9,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=9,Mdebut=20,Hfin=9,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=9,Mdebut=40,Hfin=10,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=10,Mdebut=0,Hfin=10,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=10,Mdebut=20,Hfin=10,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=10,Mdebut=40,Hfin=11,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=11,Mdebut=0,Hfin=11,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=11,Mdebut=20,Hfin=11,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=11,Mdebut=40,Hfin=12,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=14,Mdebut=0,Hfin=14,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=14,Mdebut=20,Hfin=14,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=14,Mdebut=40,Hfin=15,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=15,Mdebut=0,Hfin=15,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=15,Mdebut=20,Hfin=15,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=15,Mdebut=40,Hfin=16,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=16,Mdebut=0,Hfin=16,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=16,Mdebut=20,Hfin=16,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=16,Mdebut=40,Hfin=17,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=17,Mdebut=0,Hfin=17,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=17,Mdebut=20,Hfin=17,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=17,Mdebut=40,Hfin=18,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=8,Mdebut=0,Hfin=8,Mfin=20,Medecin=medecins[1]},
new Creneau{ Hdebut=8,Mdebut=20,Hfin=8,Mfin=40,Medecin=medecins[1]},
new Creneau{ Hdebut=8,Mdebut=40,Hfin=9,Mfin=0,Medecin=medecins[1]},
new Creneau{ Hdebut=9,Mdebut=0,Hfin=9,Mfin=20,Medecin=medecins[1]},
new Creneau{ Hdebut=9,Mdebut=20,Hfin=9,Mfin=40,Medecin=medecins[1]},
new Creneau{ Hdebut=9,Mdebut=40,Hfin=10,Mfin=0,Medecin=medecins[1]},
new Creneau{ Hdebut=10,Mdebut=0,Hfin=10,Mfin=20,Medecin=medecins[1]},
new Creneau{ Hdebut=10,Mdebut=20,Hfin=10,Mfin=40,Medecin=medecins[1]},
new Creneau{ Hdebut=10,Mdebut=40,Hfin=11,Mfin=0,Medecin=medecins[1]},
new Creneau{ Hdebut=11,Mdebut=0,Hfin=11,Mfin=20,Medecin=medecins[1]},
new Creneau{ Hdebut=11,Mdebut=20,Hfin=11,Mfin=40,Medecin=medecins[1]},
new Creneau{ Hdebut=11,Mdebut=40,Hfin=12,Mfin=0,Medecin=medecins[1]},
};
foreach (Creneau creneau in creneaux)
{
context.Creneaux.Add(creneau);
}
// dates
context.Rvs.Add(new Rv { Jour = new System.DateTime(2012, 10, 8), Client = clients[0], Creneau = creneaux[0] });
}
}
}
- 第 6 行:初始化在 [Seed] 方法中进行。该方法存在于父类中,此处对其进行了重定义。参数是应用程序的持久化上下文 [RdvMedecinsContext];
- 第 8 行:参数被传递给父类;父类很可能会打开传入的持久化上下文,因为该上下文之后不再需要被打开;
- 第 11–16 行:创建 4 个客户端;
- 第 17–20 行:将这些对象添加到持久化上下文中,更具体地说,是添加到其 doctors 集合中。请注意实现此功能的 [Add] 方法。回顾一下上下文的定义:
public class RdvMedecinsContext : DbContext
{
// entities
public DbSet<Medecin> Medecins { get; set; }
public DbSet<Creneau> Creneaux { get; set; }
public DbSet<Client> Clients { get; set; }
public DbSet<Rv> Rvs { get; set; }
...
据说这些 Clients 已被附加到上下文中,即现在由 EF 管理。此前,它们处于脱离状态。它们作为对象存在,但未被 EF 管理;
- 第 21–27 行:创建 4 位医生;
- 第 28–31 行:将它们添加到持久化上下文中;
- 第 33–70 行:创建时间段。第 34–57 行对应医生 medecins[0],第 58–69 行对应医生 medecins[1]。其余医生没有时间段;
- 第 71–74 行:将这些时间段放入持久化上下文;
- 第 76 行:为第一位客户创建一个使用第一个时间段的预约,并将其放入持久化上下文中。
运行项目后,将得到以下数据库:
![]() | ![]() |
上图显示了已填充数据的 [CLIENTS] 表。
3.4.6. 修改实体
目前,[Doctor] 和 [Client] 类几乎完全相同。事实上,如果移除为 EF 5 持久化管理而添加的字段,它们就完全一致了。我们将让它们继承自 [Person] 类。这样,这两个实体将变为如下形式:
// a person
public abstract class Personne
{
// data
[Key]
[Column("ID")]
public int? Id { get; set; }
[Required]
[MaxLength(5)]
[Column("TITRE")]
public string Titre { get; set; }
[Required]
[MaxLength(30)]
[Column("NOM")]
public string Nom { get; set; }
[Required]
[MaxLength(30)]
[Column("PRENOM")]
public string Prenom { get; set; }
[Column("TIMESTAMP")]
[Timestamp]
public byte[] Timestamp { get; set; }
// signature
public override string ToString()
{
return String.Format("[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
}
// short signature
public string ShortIdentity()
{
...
}
// utility
private string dump(byte[] timestamp)
{
...
}
}
[Table("MEDECINS", Schema = "dbo")]
public class Medecin : Personne
{
// the doctor's time slots
public ICollection<Creneau> Creneaux { get; set; }
// signature
public override string ToString()
{
return String.Format("Medecin {0}", base.ToString());
}
}
[Table("CLIENTS", Schema = "dbo")]
public class Client : Personne
{
// customer rvs
public ICollection<Rv> Rvs { get; set; }
// signature
public override string ToString()
{
return String.Format("Client {0}", base.ToString());
}
}
运行项目时,会生成相同的数据库。EF 5 将继承层次结构中的每个底层类映射到一个单独的表。实际上,EF 5 针对表示 实体继承采用了不同的表生成策略。 本文将不对此进行详细讨论。例如,您可以阅读 [http://www.codeproject.com/Articles/393228/Entity-Framework-Code-First-Inheritance-Table-Per] 上的文章《 Entity Framework Code First 继承:按层次结构建表与按类型建表》。
接下来我们将使用此版本的实体。
3.4.7. 向数据库添加约束
还有一点细节需要说明。用于存储约会的 [RVS] 表如下所示:
![]() |
该表必须具有唯一性约束:对于给定的一天,医生时段只能被预约一次。就表而言,这意味着 (DAY, SLOT_ID) 这对组合必须是唯一的。我不确定该约束是否能直接在代码中表达,无论是通过实体还是上下文。虽然很有可能,但我尚未验证。我们将采取另一种方法。 我们将使用 SQL Server 管理工作室来添加此约束。
使用“SQL Server Management Studio”时,除了执行创建该约束的 SQL 语句外,我尚未找到其他简单的方法来添加此约束:
![]() |
还有其他 SQL Server 管理工具。在此,我们将使用 EMS SQL Manager for SQL Server 免费版工具 [http://www.sqlmanager.net/fr/products/mssql/manager/download]。安装完成后,我们启动该工具:
![]() |
- 在 [1] 中,注册一个数据库;
- 在 2 中,连接到(本地)服务器;
- 在 3 中,使用 SQL Server 身份验证;
- 在 [4] 中,使用用户名 sa;
- 在 [5] 中,密码为 sqlserver2012;
- 在 [6] 中,继续执行下一步;
![]() |
“SQL Manager Lite for SQL Server”允许您在 [RVS] 表上创建唯一约束。
![]() |
我们重新创建被删除的约束:
![]() |
“DDL”选项卡提供了待执行的 SQL 代码:
![]() |
- 在 [6] 中,我们编译该 SQL 语句;
![]() |
“SQL Manager Lite for SQL Server”提供的界面与“SQL Server Management Studio”相似。Oracle、PostgreSQL、Firebird 和 MySQL 数据库也提供了类似的界面。因此,我们将继续使用这一系列数据库管理工具。
要查看表的相关信息,只需双击该表:
![]() |
所选表的相关信息以标签页形式呈现。上图显示的是 [CLIENTS] 表的 [字段] 标签页。[数据] 标签页则显示该表的内容:

3.4.8. 最终数据库
现在我们已经拥有了最终的数据库。我们将导出其 SQL 脚本,以便在必要时可以重新生成该数据库。
![]() |
![]() |
- 在 [4] 中,指定保存 SQL 脚本的文件名;
- 在 [5] 中,指定其编码;
- 在 [6] 中,指定要提取的内容(表、约束、数据);
![]() |
脚本已生成并加载到脚本编辑器中。您可以查看生成的 SQL 代码。我们将使用此脚本重建数据库。
![]() |
![]() |
- 在 [4] 中,登录;
- 在[5]中,运行SQL脚本以创建数据库;
![]() |
- 在 [6] 中,将其保存到“SQL Manager”中;
- 在 7 中,我们连接到刚刚创建的数据库;
![]() |
- 在 8 中,该数据库目前没有表;
- 在 [9a] 中,打开一个 SQL 脚本编辑器;
![]() |
- 在 [9b] 中,打开之前创建的 SQL 脚本;
- 在 [10] 中,执行该脚本;
![]() |
- 在 [11] 中,表已创建;
- 在[12]中,已向其中插入数据;
![]() |
- 在 [14] 中,我们可以看到我们为 [RVS] 表创建的唯一约束。
接下来我们将基于此现有数据库进行操作。如果该数据库被销毁或损坏,我们知道如何重新生成它。
3.5. 使用 Entity Framework 操作数据库
我们将:
- 添加、删除和修改数据库元素;
- 使用 LINQ to Entities 查询数据库;
- 管理对同一数据库元素的并发访问;
- 理解延迟加载和立即加载的概念;
- 发现通过持久化上下文进行的数据库更新是在事务内进行的。
3.5.1. 从持久化上下文中删除项目
我们有一个已填充数据的数据库。我们将清空它。我们在当前项目中创建一个新类 [Erase.cs] [1]:
![]() |
[Erase] 类的定义如下:
using RdvMedecins.Models;
namespace RdvMedecins_01
{
class Erase
{
static void Main(string[] args)
{
using (var context = new RdvMedecinsContext())
{
// empty the current base
// our customers
foreach (var client in context.Clients)
{
context.Clients.Remove(client);
}
// the doctors
foreach (var medecin in context.Medecins)
{
context.Medecins.Remove(medecin);
}
// save the persistence context
context.SaveChanges();
}
}
}
}
- 第 9 行:对持久化上下文的操作必须始终在 [using] 代码块内进行。这确保了当 [using] 代码块结束时,上下文已被关闭;
- 第 13 行:我们遍历客户端上下文 [context.Clients]。数据库中的所有客户端都将被放入持久化上下文中;
- 第 15 行:对于每个客户端,我们执行 [Remove] 操作,将其从上下文中移除。实际上,它们仍然存在于上下文中,但处于“已移除”状态;
- 第 18–21 行:对医生执行相同的操作;
- 第 23 行:我们将持久化上下文保存到数据库中。
将上下文保存到数据库时,上下文中符合以下条件的实体:
- 主键为空的,将执行 SQL INSERT 操作;
- 处于“已删除”状态的,将执行 SQL DELETE 操作;
- 处于“已修改”状态的,将执行 SQL UPDATE 操作;
正如我们稍后将看到的,这些 SQL 操作是在事务内执行的。如果其中任何一个操作失败,之前所做的一切都将被回滚。
让我们将 [Erase] 程序设为项目 [1] 的新起点,然后运行该项目。
![]() |
让我们检查一下数据库。我们会发现所有表都是空的 2。这令人惊讶,因为我们只是要求删除医生和客户。正是通过外键机制,其他表才被级联清空的。
EF 5 提供程序对 [CRENEAUX] 表指向 [MEDECINS] 表的外键定义如下:
![]() |
![]() |
- 在 [4] 中,在 DDL 选项卡中,查看外键约束的 SQL 定义;
- 在 [5] 中,ON DELETE CASCADE 子句确保删除某位医生时,与其关联的时间段也会被删除。
[RVS] 表的外键约束定义方式与此类似:
- 第 1-6 行:删除客户时,也会删除与其关联的预约;
3.5.2. 向持久化上下文中添加项目
既然数据库已清空,我们将重新填充它。我们将程序 [Fill.cs] [1] 添加到项目中。
![]() |
[Fill.cs] 程序如下:
using RdvMedecins.Entites;
using RdvMedecins.Models;
namespace RdvMedecins_01
{
class Fill
{
static void Main(string[] args)
{
using (var context = new RdvMedecinsContext())
{
// empty the current base
foreach (var client in context.Clients)
{
context.Clients.Remove(client);
}
foreach (var medecin in context.Medecins)
{
context.Medecins.Remove(medecin);
}
// reset it
// our customers
Client[] clients ={
new Client { Titre = "Mr", Nom = "Martin", Prenom = "Jules" },
new Client { Titre = "Mme", Nom = "German", Prenom = "Christine" },
new Client { Titre = "Mr", Nom = "Jacquard", Prenom = "Jules" },
new Client { Titre = "Melle", Nom = "Bistrou", Prenom = "Brigitte" }
};
foreach (Client client in clients)
{
context.Clients.Add(client);
}
// the doctors
Medecin[] medecins ={
new Medecin { Titre = "Mme", Nom = "Pelissier", Prenom = "Marie" },
new Medecin { Titre = "Mr", Nom = "Bromard", Prenom = "Jacques" },
new Medecin { Titre = "Mr", Nom = "Jandot", Prenom = "Philippe" },
new Medecin { Titre = "Melle", Nom = "Jacquemot", Prenom = "Justine" }
};
foreach (Medecin medecin in medecins)
{
context.Medecins.Add(medecin);
}
// time slots
Creneau[] creneaux ={
new Creneau{ Hdebut=8,Mdebut=0,Hfin=8,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=8,Mdebut=20,Hfin=8,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=8,Mdebut=40,Hfin=9,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=9,Mdebut=0,Hfin=9,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=9,Mdebut=20,Hfin=9,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=9,Mdebut=40,Hfin=10,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=10,Mdebut=0,Hfin=10,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=10,Mdebut=20,Hfin=10,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=10,Mdebut=40,Hfin=11,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=11,Mdebut=0,Hfin=11,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=11,Mdebut=20,Hfin=11,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=11,Mdebut=40,Hfin=12,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=14,Mdebut=0,Hfin=14,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=14,Mdebut=20,Hfin=14,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=14,Mdebut=40,Hfin=15,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=15,Mdebut=0,Hfin=15,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=15,Mdebut=20,Hfin=15,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=15,Mdebut=40,Hfin=16,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=16,Mdebut=0,Hfin=16,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=16,Mdebut=20,Hfin=16,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=16,Mdebut=40,Hfin=17,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=17,Mdebut=0,Hfin=17,Mfin=20,Medecin=medecins[0]},
new Creneau{ Hdebut=17,Mdebut=20,Hfin=17,Mfin=40,Medecin=medecins[0]},
new Creneau{ Hdebut=17,Mdebut=40,Hfin=18,Mfin=0,Medecin=medecins[0]},
new Creneau{ Hdebut=8,Mdebut=0,Hfin=8,Mfin=20,Medecin=medecins[1]},
new Creneau{ Hdebut=8,Mdebut=20,Hfin=8,Mfin=40,Medecin=medecins[1]},
new Creneau{ Hdebut=8,Mdebut=40,Hfin=9,Mfin=0,Medecin=medecins[1]},
new Creneau{ Hdebut=9,Mdebut=0,Hfin=9,Mfin=20,Medecin=medecins[1]},
new Creneau{ Hdebut=9,Mdebut=20,Hfin=9,Mfin=40,Medecin=medecins[1]},
new Creneau{ Hdebut=9,Mdebut=40,Hfin=10,Mfin=0,Medecin=medecins[1]},
new Creneau{ Hdebut=10,Mdebut=0,Hfin=10,Mfin=20,Medecin=medecins[1]},
new Creneau{ Hdebut=10,Mdebut=20,Hfin=10,Mfin=40,Medecin=medecins[1]},
new Creneau{ Hdebut=10,Mdebut=40,Hfin=11,Mfin=0,Medecin=medecins[1]},
new Creneau{ Hdebut=11,Mdebut=0,Hfin=11,Mfin=20,Medecin=medecins[1]},
new Creneau{ Hdebut=11,Mdebut=20,Hfin=11,Mfin=40,Medecin=medecins[1]},
new Creneau{ Hdebut=11,Mdebut=40,Hfin=12,Mfin=0,Medecin=medecins[1]},
};
foreach (Creneau creneau in creneaux)
{
context.Creneaux.Add(creneau);
}
// dates
context.Rvs.Add(new Rv { Jour = new System.DateTime(2012, 10, 8), Client = clients[0], Creneau = creneaux[0] });
// save the persistence context
context.SaveChanges();
}
}
}
}
- 第 10 行:我们打开持久化上下文;
- 第 13–20 行:将 [CLIENTS] 和 [DOCTORS] 表中的行添加到上下文中,然后从上下文中移除。我们刚才看到,这完全清空了数据库;
- 第 22–88 行:向持久化上下文中添加元素。它们的主键均为 null,因此将被插入数据库;
- 第 90 行:对上下文所做的更改将与数据库同步。数据库将经历一系列 SQL DELETE 操作,随后是一系列 SQL INSERT 操作;
我们将 [Fill] 程序设为项目 [1] 的新起始对象,然后执行它。
![]() |
在2中可以看到,表已填充数据。
3.5.3. 显示数据库内容
现在我们将使用 LINQ to Entities 查询来显示数据库的内容。 LINQ(语言集成查询)于 2007 年随 .NET Framework 3.5 一起推出。它作为 .NET 语言的扩展而存在,这意味着它已集成到语言中,其语法由编译器进行验证。它允许您使用类似于数据库查询所用的 SQL(结构化查询语言)的语法来查询各种集合。LINQ 有不同的版本:
- LINQ to Objects,用于查询内存中的集合;
- LINQ to XML,用于查询 XML;
- LINQ to Entity,用于查询数据库;
LINQ 依赖于对 .NET 语言的众多扩展。这些扩展也可在 LINQ 之外使用。本文将不作详细介绍,仅提供两个参考资料,供读者查阅关于 LINQ 的深入说明:
- 《LINQ in Action》,作者:Fabrice Marguerie、Steve Eichert 和 Jim Wooley,Manning 出版社;
- 《LINQ 口袋参考》,作者:Joseph 和 Ben Albahari,O’Reilly 出版社。
我读过第一本,觉得非常出色。 第二本我还没读过,但LINQ刚发布时,我读过同两位作者写的《C# 3.0速查手册》。我觉得那本书远超我平时读的书的平均水平。看来这两位作者的其他书也都是同样的水准。我们还将使用LINQPad,这是由约瑟夫·阿尔巴哈里编写的一款LINQ学习工具。
我们将展示数据库中的实体。为此,我们将在相关类中添加两个显示方法。让我们先从 [Doctor] 实体开始:
// a doctor
public class Medecin
{
// data
[Key]
[Column("ID")]
public int? Id { get; set; }
[Required]
[MaxLength(5)]
[Column("TITRE")]
public string Titre { get; set; }
[Required]
[MaxLength(30)]
[Column("NOM")]
public string Nom { get; set; }
[Required]
[MaxLength(30)]
[Column("PRENOM")]
public string Prenom { get; set; }
// the doctor's time slots
public ICollection<Creneau> Creneaux { get; set; }
[Column("TIMESTAMP")]
[Timestamp]
public byte[] Timestamp { get; set; }
// signature
public override string ToString()
{
return String.Format("Medecin[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
}
// short signature
public string ShortIdentity()
{
return ToString();
}
// utility
private string dump(byte[] timestamp){
string str = "";
foreach (byte b in timestamp)
{
str += b;
}
return str;
}
}
- 第 27–30 行:该类的 ToString 方法。请注意,它不会显示第 21 行中的集合;
- 第 32–37 行:ShortIdentity 方法,其功能与上述相同。
在此,我们需要解释“延迟加载”和“立即加载”的概念,以评估前两种方法的影响。我们已经看到,一个实体可能依赖于另一个实体。这些依赖关系有两种类型:
- 如上所述的一对多关系,即一名医生关联多个时段;
- 多对一,如下面的 [Slot] 实体所示,一个或多个时段关联到同一位医生;
public class Creneau
{
// data
...
[Required]
[Column("MEDECIN_ID")]
public int MedecinId { get; set; }
[Required]
[ForeignKey("MedecinId")]
public virtual Medecin Medecin { get; set; }
...
}
当依赖项与它们所关联的实体同时加载时,这被称为“立即加载”。否则,则称为“延迟加载”:依赖项仅在首次被引用时才会加载。默认情况下,EF 5 使用延迟加载:依赖项不会与实体同时加载。
让我们来看一下上文中的 [ToString] 方法:
// the doctor's time slots
public ICollection<Creneau> Creneaux { get; set; }
// signature
public override string ToString()
{
return String.Format("Medecin[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
}
// short signature
public string ShortIdentity()
{
return ToString();
}
[ToString] 方法在第 2 行未显示 [Slots] 依赖关系。如果显示了该依赖关系,就会强制在执行前加载所有医生的槽位。正是为了避免这种耗时的加载操作,才未将该依赖关系包含在实体的签名中。一般而言,我们会在每个实体中包含两个签名:
- 一个 ToString 方法,用于显示实体及其任何一对多依赖关系。如前所述,这将触发依赖关系的加载;
- 一个 ShortIdentity 方法,该方法不会引用任何依赖关系。因此,不会加载任何依赖关系;
其他实体的显示方法如下:
[Client] 实体:
public class Client
{
// data
...
// customer rvs
public ICollection<Rv> Rvs { get; set; }
// signature
public override string ToString()
{
return String.Format("Client[{0},{1},{2},{3},{4}]", Id, Titre, Prenom, Nom, dump(Timestamp));
}
// short signature
public string ShortIdentity()
{
return ToString();
}
}
- 第 9–12 行:[ToString] 方法未显示对第 6 行的依赖;
[Creneau] 实体:
public class Creneau
{
...
[Required]
[Column("MEDECIN_ID")]
public int MedecinId { get; set; }
[Required]
[ForeignKey("MedecinId")]
public virtual Medecin Medecin { get; set; }
// niche Rvs
public ICollection<Rv> Rvs { get; set; }
// signature
public override string ToString()
{
return String.Format("Creneau[{0},{1},{2},{3},{4}, {5}]", Id, Hdebut, Mdebut, Hfin, Mfin, Medecin, dump(Timestamp));
}
// short signature
public string ShortIdentity()
{
return String.Format("Creneau[{0},{1},{2},{3},{4}, {5}, {6}]", Id, Hdebut, Mdebut, Hfin, Mfin, Timestamp, MedecinId, dump(Timestamp));
}
}
- 第 16 行:[ToString] 方法引用了第 9 行的依赖项。这将强制加载该依赖项;
- 第 11 行:[Rvs] 依赖项未被引用。它将不会被加载;
- 第 21-22 行:[ShortIdentity] 方法不再引用第 9 行中的 [Medecin] 引用。因此,它将不会被加载。
[Rv] 实体:
public class Rv
{
// data
...
[Column("CLIENT_ID")]
public int ClientId { get; set; }
[ForeignKey("ClientId")]
[Required]
public virtual Client Client { get; set; }
[Column("CRENEAU_ID")]
public int CreneauId { get; set; }
[ForeignKey("CreneauId")]
[Required]
public virtual Creneau Creneau { get; set; }
// signature
public override string ToString()
{
return String.Format("Rv[{0},{1},{2},{3},{4}]", Id, Jour, Client, Creneau, dump(Timestamp));
}
// short signature
public string ShortIdentity()
{
return String.Format("Rv[{0},{1},{2},{3},{4}]", Id, Jour, ClientId, CreneauId, dump(Timestamp));
}
}
- 第 17–20 行:[ToString] 方法引用了第 9 行和第 14 行的依赖项。这将导致这些依赖项被强制加载;
- 第 17–20 行:[ShortIdentity] 方法阻止了这一行为,因此这些依赖项将不会被加载。
综上所述,我们必须注意实体的 [ToString] 方法。如果不注意这一点,当表具有大量依赖项时,显示该表可能会加载数据库的一半内容。
基于上述说明,我们编写以下新代码 [Dump.cs]:
using RdvMedecins.Entites;
using RdvMedecins.Models;
using System;
using System.Linq;
namespace RdvMedecins_01
{
class Dump
{
static void Main(string[] args)
{
// base dump
using (var context = new RdvMedecinsContext())
{
// our customers
Console.WriteLine("Clients--------------------------------------");
var clients = from client in context.Clients select client;
foreach (Client client in clients)
{
Console.WriteLine(client);
}
// the doctors
Console.WriteLine("Médecins--------------------------------------");
var medecins = from medecin in context.Medecins select medecin;
foreach (Medecin medecin in medecins)
{
Console.WriteLine(medecin);
}
// time slots
Console.WriteLine("Créneaux horaires--------------------------------------");
var creneaux = from creneau in context.Creneaux select creneau;
foreach (Creneau creneau in creneaux)
{
Console.WriteLine(creneau);
}
// dates
Console.WriteLine("Rendez-vous--------------------------------------");
var rvs = from rv in context.Rvs select rv;
foreach (Rv rv in rvs)
{
Console.WriteLine(rv);
}
}
}
}
}
我们将解释第 17–21 行,这些行显示了 [Client] 实体。此处的说明同样适用于其他实体。
// our customers
Console.WriteLine("Clients--------------------------------------");
var clients = from client in context.Clients select client;
foreach (Client client in clients)
{
Console.WriteLine(client);
}
- 第 3 行:var 关键字是随 C# 3.0 引入的。它允许您无需指定变量的确切类型。编译器会根据赋值给该变量的表达式的类型来推断变量的类型;
- 第 3 行:赋值给
*clients*变量的表达式是一个 LINQ to Entities 查询。它包含移植到 LINQ 中的 SQL 关键字。此处的语法如下:
from variable in DbSet select variable
一种更通用的 LINQ 语法是
from variable in collection select variable
系统将遍历集合,并对其中每个元素进行变量求值。这仅在第 4–7 行中的 for/each 循环遍历第 3 行中的 [clients] 变量时才会发生。在此之前,[clients] 变量仅仅是一个未求值的查询;
- 第 4 行:对 [clients] 查询进行迭代。这将强制执行该查询。[CLIENTS] 表的行将逐一导入持久化上下文;
- 第 6 行:调用 [Client] 实体的 [ToString] 方法进行显示。此时不会加载任何依赖项;
接下来我们继续分析以下代码行:
- 第 24–28 行:将 [DOCTORS] 表的行引入持久化上下文并显示。未加载任何依赖项;
- 第 31–35 行:将 [SLOTS] 表的行引入持久化上下文并显示。我们看到该实体的 [ToString] 方法会显示 [Doctor] 依赖项。但该依赖项已加载,因此不会重新加载;
- 第 38–42 行:将 [RVS] 表的行引入持久化上下文并显示。我们看到该实体的 [ToString] 方法会显示 [Client] 和 [Slot] 依赖项。但这些依赖项已加载完毕,因此不会进行新的加载。
请注意,显示顺序并非无关紧要。如果我们希望先显示 [Rv] 实体,其 [ToString] 方法将触发与这些预约关联的 [Client] 和 [Creneau] 实体的加载。 其余实体则不会被加载,而会在后续的另一个视图中加载。这会影响性能。前面的代码需要四条 SQL 语句才能显示所有实体。现在假设我们首先查询 [RVS] 预约表。这需要针对 [RVS] 表执行第一条 SQL 查询。 接下来,[Rv] 实体的 [ToString] 方法将触发关联的 [Client] 和 [Slot] 实体的潜在加载。每个实体都需要一个 SQL 查询。假设存在 N2 个客户和 N3 个时间段,且所有这些实体都在 [RVS] 表中被引用,则显示该表将需要 1+N2+N3 个 SQL 查询。 因此,其性能低于我们之前分析的版本。若要显示包含其依赖关系的 [RVS] 表,则需要进行表连接。这可以通过 LINQ 来实现。我们将通过一个示例再次探讨这一点。目前,请记住我们必须关注 LINQ 代码底层的 SQL 查询。
我们将项目配置为运行这段新代码 [1] 和 2,然后执行它:
![]() |
控制台输出如下:
3.5.4. 使用 LINQPad 学习 LINQ
在上文中,我们使用 LINQ to Entities 查询来显示数据库表中的内容。Joseph Albahari 编写了一个程序,旨在帮助您学习 LINQ 的各种形式。现在我们将向您介绍该程序。
LINQPad 可通过以下网址获取 [http://www.linqpad.net/]。安装完成后,我们启动它 [1]:
![]() |
LINQ 初学者可以通过 [示例] 选项卡 2 中的示例入门,该选项卡提供了丰富多样的示例。让我们选择示例 3,它将在新窗口中打开 [4]。该示例的完整代码如下:
// Now for a simple LINQ-to-objects query expression (notice no semicolon):
from word in "The quick brown fox jumps over the lazy dog".Split()
orderby word.Length
select word
// Feel free to edit this... (no-one's watching!) You'll be prompted to save any
// changes to a separate file.
//
// Tip: You can execute part of a query by highlighting it, and then pressing F5.
第 3–5 行是一个 LINQ to Objects 查询的示例。该 LINQ 查询遵循以下语法:
from variable in collection orderby élément1 select élément2
- variable 指代集合中的当前元素。在本例中,该集合是拆分字符串后得到的单词列表;
- 集合将根据 orderby 的 element1 参数进行排序。在本例中,单词集合将按长度排序;
- select 关键字指定了我们要从集合中的当前元素变量中提取的内容。在本例中,这将提取单词本身。
让我们运行这个 LINQ 查询:
![]() |
- 在 [1] 中:按 [F5] 或使用“运行”按钮可执行 LINQ 表达式;
- 在 2 中:显示结果。单词按其长度顺序显示。这个简单的示例展示了 LINQ 的强大功能;
- 在 3 中,您可以下载其他示例,包括来自《LINQ in Action》一书 [4] 的示例;
![]() |
- 在 [5] 中,我们选取了书中的一例;
string[] words = { "hello", "wonderful", "linq", "beautiful", "world" };
// Group words by length
var groups =
from word in words
orderby word ascending
group word by word.Length into lengthGroups
orderby lengthGroups.Key descending
select new { Length = lengthGroups.Key, Words = lengthGroups };
// Print each group out
foreach (var group in groups)
{
Console.WriteLine("Words of length " + group.Length);
foreach (string word in group.Words)
Console.WriteLine(" " + word);
}
- 第 4 行:使用新关键字的新 LINQ 查询;
- 第 5 行:被查询的集合是第 1 行中的单词数组;
- 第 6 行:该集合按单词的字母顺序进行排序;
- 第 7 行:将集合按(按关键词)分组到一个名为 lengthGroups 的新集合中。lengthGroups.Key 代表分组因子(按关键词),此处即单词的长度。lengthGroups 将具有相同分组因子的单词(即长度相同的单词)聚合在一起;
- 第 8 行:lengthGroups 集合按分组键降序排序,即按单词长度由大到小排序;
- 第 9 行:从该集合中创建具有两个字段的新对象(匿名类):
- Length:单词的长度,
- Words:该长度的单词;
在此,我们可以特别看到第 4 行中 var 关键字的优势。由于我们在第 9 行使用了匿名类,因此无法指定 groups 变量的类型。然而,编译器会为该匿名类分配一个内部名称,并用其为 groups 变量进行类型推导。随后,它就能判断 groups 变量是否被正确使用
- 第 12 行:遍历第 4 行中的查询。直到此时,该查询才会被求值。请注意,其执行将生成第 9 行中指定的对象集合;
- 第 14 行:我们显示当前元素的 Length 属性,即单词的长度;
- 第 15–17 行:我们显示 Words 集合中的每个元素,即具有之前显示的长度的单词集合。
执行此查询时,我们在 LINQPad 中得到以下结果:
![]() |
既然我们已经了解了一些 [LINQ to Object] 查询的示例,接下来让我们看看 [LINQ to Entity] 查询,它将使我们能够查询数据库。首先,我们将连接到我们创建并已填充数据的 SQL Server 数据库:
![]() |
- 在 [1] 中,我们添加了一个数据库连接;
- 在 2 中,指定访问数据源的方式。为了访问 SQL Server 数据库,我们将使用 [LINQPad Driver];
- 在 3 中,也可以检索在 .exe 或 .dll 程序集内定义的持久化上下文 [DbContext](选项 3)。遗憾的是,截至今天(2012 年 10 月 8 日),Entity Framework 5 尚未获得支持;
- 在 [4] 中,可以下载除 SQL Server 以外的数据库管理系统(DBMS)的驱动程序;
- 在 [5] 中,我们将下载适用于 MySQL 和 Oracle 数据库管理系统(DBMS)的驱动程序;
![]() |
- 在[6]中,下载的驱动程序;
- 在 7 中,我们将连接到一个 SQL Server 数据库;
![]() |
- 在 8 中,数据库位于本地服务器上;
- 在 9 中,我们使用 sa / sqlserver2012 凭据进行连接;
- 在 [10] 中,连接到我们创建的 [rdvmedecins-ef] 数据库;
- 在 [11] 中,您可以测试连接;
- 在 [12] 中,完成向导;
- 在 [13] 中,连接会显示在 LINQPad 中。
实体是根据 [rdvmedecins-ef] 表创建的。具体如下:
![]() |
- 在 [1] 中,[CLIENTS] 代表 [Client] 实体的集合。每个实体具有:
- 属性 (ID, TITLE, LAST_NAME, FIRST_NAME, TIMESTAMP),
- 以及一个一对多关系 [CLIENTRVS];
- 在2中,[CRENEAUXes]表示[Creneau]实体的集合。每个实体具有:
- 属性 (ID, START_TIME, MIN_TIME, END_TIME, MAX_TIME, DOCTOR_ID, TIMESTAMP),
- 一个一对多关系 [CRENEAURVS],
- 一个多对一关系 [DOCTOR];
- 在 3 中,实体 [MEDECINS] 表示 [Medecin] 实体的集合。每个实体具有:
- 属性 (ID, TITLE, LAST_NAME, FIRST_NAME, TIMESTAMP),
- 一个一对多关系 [DOCTOR-SLOTS];
- 在 [4] 中,实体 [RVS] 代表 [Rv] 实体的集合。每个实体具有:
- 属性 (ID, DAY, CLIENT_ID, SLOT_ID, TIMESTAMP),
- 一个多对一关系 [CLIENT],
- 一个多对一关系 [SLOT]。
请注意,上述属性的名称与我们迄今为止使用的名称不同。这并不重要。我们只是想学习数据库查询的基本原理。
让我们看看如何查询这个实体数据库。例如,我们想要按 TITLE 和 LAST_NAME 排序的医生列表:
![]() |
- 在 [1] 中,我们创建一个新查询;
- 在 2 中,输入查询文本;
![]() |
- 在 3 中,查询结果;
- 在 [4] 中,使用 lambda 表达式的相同查询。使用 lambda 表达式的查询比文本查询更难读,您可能更愿意避免使用它们。然而,它们有时是不可或缺的,因为它们允许实现文本查询无法实现的某些功能。一个 lambda 表达式表示一个具有输入参数 a 和输出参数 b 的函数,形式为 a=>b。上面的 OrderBy 方法接受一个 lambda 函数作为其唯一参数。 这提供了用于对集合进行排序的参数。因此,MEDECINS.OrderBy(m=>m.TITRE) 即按头衔排序的医生列表。该语句应被理解为对集合的管道操作。医生集合作为输入提供给 OrderBy 方法。该方法将依次处理 [Doctor] 实体。 在 lambda 表达式 m=>m.TITLE 中,m 代表 lambda 函数的输入。它可以按需命名。在此,lambda 函数的输入将是一个 [Doctor] 实体。函数 m=>m.TITLE 的含义如下:如果我将 m 称为我的输入(一个 [Doctor] 实体),那么我的输出就是 m.TITLE,即医生的头衔。 MEDECINS.OrderBy(m=>m.TITRE) 则是一个集合,即按头衔排序的医生集合。这个新集合可以作为另一个方法的输入,在本例中是 ThenBy 方法。该方法遵循相同的原理,用于指定对集合进行排序的额外参数。
阅读与我们通常编写的文本代码等效的lambda代码,是学习它的好方法;
![]() |
- 在[5]中,发送到数据库的SQL查询。这里我们同样会仔细阅读这段代码。它使我们能够评估LINQ查询的实际成本。
下面,我们将展示几个 LINQ 查询的示例。对于每个示例,我们将展示显示的结果以及等效的 lambda 表达式和 SQL 代码。要理解这些查询,我们必须回顾连接实体之间的多对一关系。正是通过这些关系,我们才能从一个实体导航到另一个实体。它们被称为导航属性。
![]() |
// 按姓名降序排列、头衔为“Mr”的客户
结果:
![]() |
LINQ | |
Lambda | |
SQL | |
// 包含相关医生的所有时段
结果(部分):
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
// 与该客户和医生相关的所有预约
结果:
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
// 没有预约的医生
结果:
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
此请求没有对应的 LINQ 查询。您必须使用 lambda 表达式。该表达式的含义如下:我获取医生集合(DOCTORS),并仅保留(Where)那些在预约集合(APPOINTMENTS)中无法找到该医生(m)的预约(rv)的医生(m)。
// 佩利西耶女士的时段
(部分)结果:
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
// 佩利西耶女士2012年10月8日的预约次数
结果:
![]() |
LINQ | |
Lambda | |
SQL | |
// 2012年10月8日预约佩利西耶女士的客户名单
结果:
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
// 每位医生负责的时间段数量
结果:
![]() |
LINQ | |
Lambda | ![]() |
SQL | |
3.5.5. 修改与持久化上下文关联的实体
我们已经介绍了持久化上下文中的以下操作:
- 向上下文添加元素 ([dbContext].[DbSet].Add);
- 从上下文中删除一项 ([dbContext].[DbSet].Remove);
- 使用 LINQ 查询查询上下文。
要将上下文与数据库同步,请编写 [dbContext].SaveChanges()。
![]() | ![]() |
[ModifyAttachedEntity] 代码演示了如何修改附加到上下文中的实体:
using System;
using System.Data;
using System.Linq;
using RdvMedecins.Entites;
using RdvMedecins.Models;
namespace RdvMedecins_01
{
class ModifyAttachedEntity
{
static void Main(string[] args)
{
Client client1, client2, client3;
// 1st context
using (var context = new RdvMedecinsContext())
{
// empty the current base
foreach (var client in context.Clients)
{
context.Clients.Remove(client);
}
foreach (var medecin in context.Medecins)
{
context.Medecins.Remove(medecin);
}
// add a customer
client1 = new Client { Nom = "xx", Prenom = "xx", Titre = "xx" };
context.Clients.Add(client1);
// follow-up
Console.WriteLine("client1--avant");
Console.WriteLine(client1);
// save context
context.SaveChanges();
// follow-up
Console.WriteLine("client1--après");
Console.WriteLine(client1);
}
// 2nd context
using (var context = new RdvMedecinsContext())
{
// retrieve client1 from client2
client2 = context.Clients.Find(client1.Id);
// follow-up
Console.WriteLine("client2");
Console.WriteLine(client2);
// modify client2
client2.Nom = "yy";
// save context
context.SaveChanges();
}
// 3rd context
using (var context = new RdvMedecinsContext())
{
// retrieve client2 from client3
client3 = context.Clients.Find(client2.Id);
// follow-up
Console.WriteLine("client3");
Console.WriteLine(client3);
}
}
}
}
- 第 15 行:打开应用程序上下文;
- 第 18–25 行:上下文被清空。更准确地说,所有实体从数据库加载到上下文中,然后被设置为“已删除”状态。请注意,在此阶段,数据库尚未发生变化。只要上下文未与数据库同步,数据库就保持不变。回顾一下,删除 [Doctor] 和 [Client] 实体就足以通过级联删除清空数据库;
- 第 27–28 行:向数据库中添加了一位新客户;
- 第 30–31 行:在将客户保存到数据库之前,先将其显示出来;
- 第 33 行:将上下文与数据库同步。标记为“已删除”的实体将执行 SQL DELETE 操作,而新增的实体将执行 SQL INSERT 操作;
- 第 35–36 行:与数据库同步后显示客户信息;
控制台显示的结果如下:
请注意以下几点:
- 在与数据库同步之前,客户端既没有主键也没有时间戳;
- 同步后,它便拥有了这两者。请注意,主键已配置为由 SQL Server 自动生成。同样,该数据库管理系统也会自动生成时间戳;
- 第 37 行:持久化上下文被关闭。其中包含的实体变为“脱离”状态。它们作为对象存在,但不再是与持久化上下文关联的实体;
- 第 39 行:启动了一个新的空上下文;
- 第 42 行:通过主键直接从数据库检索客户端。随后将其引入上下文。若未找到,Find 方法将返回空指针;
- 第 48–49 行:我们显示该实体;
这将产生以下结果:
- 第 47 行:我们对其进行修改;
- 第 49 行:我们将上下文与数据库同步。EF 会检测到上下文中的某些元素自添加以来已被修改。对于这些元素,它会生成针对数据库的 SQL UPDATE 语句。因此,此处的同步将包含一条 UPDATE 语句;
- 第 50 行:关闭第二个上下文。此前附加到该上下文的 client2 实体现已从上下文中分离;
- 第 52 行:打开第三个空上下文;
- 第 55 行:将数据库中的唯一 client 实体再次加载到该上下文中。我们需要验证在前一个上下文中对其所做的修改是否已反映在数据库中;
- 第 57–58 行:显示该客户端。这将产生以下结果:
该客户的信息确实已在数据库中更新。请注意,其时间戳也已更新。
- 第 59 行:我们关闭上下文。顺便提一下,请注意,与前两次不同,我们无需事先将上下文与数据库同步(SaveChanges),因为上下文并未被修改。
3.5.6. 脱离上下文实体的管理
让我们回到案例研究中那样的应用程序的分层架构:
![]() |
[DAO] 层使用 EF5 ORM 访问数据。我们已经具备了该层的基本构建模块。每个方法都会打开一个持久化上下文,执行必要的操作(插入、更新、删除、查询),然后关闭它。由 [DAO] 层管理的实体将被传递到 ASP.NET Web 层。在此层中,它们处于持久化上下文之外,因此处于脱离状态。 在 Web 层中,用户可以修改这些实体(添加、更新、删除)。当它们返回 [DAO] 层时,仍处于脱离状态。然而,[DAO] 层需要将用户所做的更改反映到数据库中。因此,它必须处理这些脱离的实体。让我们来看三种可能的情况:
添加脱离的实体
这是添加操作的标准流程。只需将脱离上下文的实体添加(Add)到上下文中,并确保其主键为空。
修改脱离上下文的实体
您可以使用以下代码:
- [DbContext].Entry(detached-entity) 方法将该实体添加到上下文中;
- 并将该实体的状态设置为“已修改”,以便其受 SQL UPDATE 语句的影响。
删除脱离上下文的实体
您可以使用以下代码:
- 第 1 行:将主键与脱离实体相同的新实体添加到上下文中;
- 第 2 行:将其删除:
请注意,这需要在数据库中先执行 SELECT 语句,再执行 DELETE 语句,而通常仅执行 DELETE 即可。您也可以参照修改脱离实体的方法,编写如下代码:
由于我无法对数据库中的 SQL 操作实现日志记录,因此无法判断哪种方法更优。
以下是一个示例:
![]() | ![]() |
[ModifyDetachedEntities] 程序的代码如下:
using System;
using System.Data;
using RdvMedecins.Entites;
using RdvMedecins.Models;
namespace RdvMedecins_01
{
class ModifyDetachedEntities
{
static void Main(string[] args)
{
Client client1;
// empty the current base
Erase();
// add a customer
using (var context = new RdvMedecinsContext())
{
// customer creation
client1 = new Client { Titre = "x", Nom = "x", Prenom = "x" };
// add customer to context
context.Clients.Add(client1);
// save the context
context.SaveChanges();
}
// basic view
Dump("1-----------------------------");
// client1 is not in the context - we modify it
client1.Nom = "y";
// new context
using (var context = new RdvMedecinsContext())
{
// here we have an empty context
// we put client1 in the context in a modified state
context.Entry(client1).State = EntityState.Modified;
// save the context
context.SaveChanges();
}
// basic view
Dump("2-----------------------------");
// remove out-of-context entity
using (var context = new RdvMedecinsContext())
{
// here we have a new empty context
// we put client1 in the context in a deleted state
context.Entry(client1).State = EntityState.Deleted;
// save the context
context.SaveChanges();
}
// basic view
Dump("3-----------------------------");
}
static void Erase()
{
// empties base
using (var context = new RdvMedecinsContext())
{
foreach (var client in context.Clients)
{
context.Clients.Remove(client);
}
foreach (var medecin in context.Medecins)
{
context.Medecins.Remove(medecin);
}
// save the context
context.SaveChanges();
}
}
static void Dump(string str)
{
Console.WriteLine(str);
// displays the base
using (var context = new RdvMedecinsContext())
{
foreach (var rv in context.Rvs)
{
Console.WriteLine(rv);
}
foreach (var creneau in context.Creneaux)
{
Console.WriteLine(creneau);
}
foreach (var client in context.Clients)
{
Console.WriteLine(client);
}
foreach (var medecin in context.Medecins)
{
Console.WriteLine(medecin);
}
}
}
}
}
- 第 15 行:清空数据库;
- 第 17–25 行:向数据库中添加一位客户;
- 第27行:显示数据库内容;
- 在第 25 行之后,持久化上下文已不复存在。因此,不再存在任何已附加的实体。client1 实体已转为“已分离”状态;
- 第 29 行:修改了脱离上下文的实体的名称;
- 第 31 行:打开了一个新的空上下文;
- 第 35 行:将脱离上下文的实体 client1 以“已修改”状态放入上下文中;
- 第 37 行:上下文与数据库同步;
- 第 38 行:关闭该上下文;
- 第 40 行:显示数据库;
客户端名称已在数据库中成功更新。请注意,时间戳已更新;
- 第 42 行:打开一个新的空上下文;
- 第 46 行:将脱离数据库的实体 client1 以“已删除”状态放入上下文中;
- 第 48 行:上下文与数据库同步;
- 第 49 行:关闭上下文;
- 第 51 行:显示数据库;
该实体确实已从数据库中删除。
现在,我们将探讨加载实体依赖项的两种模式:延迟加载和立即加载。
3.5.7. 延迟加载与立即加载
让我们重新审视这四个实体的多对一依赖关系模式:
![]() |
在上图中,[Creneau] 实体有一个指向 [Medecin] 实体的导航属性 [Creneau.Medecin]。这被称为依赖关系。我们已经看到,还存在一对多的依赖关系。这里解释的原则同样适用于它们。
默认情况下,EF 5 处于延迟加载模式:当它从数据库将实体引入持久化上下文时,不会同时引入其依赖项。这些依赖项将在首次被使用时加载。这是合乎常理的做法。如果不是这样,根据上述依赖关系,将预约引入上下文将导致:
- 与预约关联的 [Time Slot] 实体;
- 与这些时段关联的 [Doctor] 实体;
- 与预约关联的 [Clients] 实体。
然而,有时我们需要实体及其依赖项。我们将分别演示这两种加载模式。
![]() | ![]() |
[LazyEagerLoading] 的代码如下:
using RdvMedecins.Entites;
using RdvMedecins.Models;
using System;
using System.Linq;
namespace RdvMedecins_01
{
class LazyEagerLoading
{
// entities
static Medecin[] medecins;
static Client[] clients;
static Creneau[] creneaux;
static void Main(string[] args)
{
// initialize the base
InitBase();
Console.WriteLine("Initialisation terminée");
// eager loading
Creneau creneau;
int idCreneau = (int)creneaux[0].Id;
using (var context = new RdvMedecinsContext())
{
// crenel n° 0
creneau = context.Creneaux.Include("Medecin").Single<Creneau>(c => c.Id == idCreneau);
Console.WriteLine(creneau.ShortIdentity());
}
// dependent display
try
{
Console.WriteLine("Médecin={0}", creneau.Medecin);
}
catch (Exception e)
{
Console.WriteLine("L'erreur 1 suivante s'est produite : {0}", e);
}
// lazy loading - default mode
using (var context = new RdvMedecinsContext())
{
// crenel n° 0
creneau = context.Creneaux.Single<Creneau>(c => c.Id == idCreneau);
Console.WriteLine(creneau.ShortIdentity());
}
// dependent display
try
{
Console.WriteLine("Médecin={0}", creneau.Medecin);
}
catch (Exception e)
{
Console.WriteLine("L'erreur 2 suivante s'est produite : {0}", e);
}
}
static void InitBase()
{
// initialize the base
using (var context = new RdvMedecinsContext())
{
// empty the current base
...
// initialize the base
// our customers
clients = new Client[] {
new Client { Titre = "Mr", Nom = "Martin", Prenom = "Jules" },
new Client { Titre = "Mme", Nom = "German", Prenom = "Christine" },
new Client { Titre = "Mr", Nom = "Jacquard", Prenom = "Jules" },
new Client { Titre = "Melle", Nom = "Bistrou", Prenom = "Brigitte" }
};
...
// dates
context.Rvs.Add(new Rv { Jour = new System.DateTime(2012, 10, 8), Client = clients[0], Creneau = creneaux[0] });
// save the persistence context
context.SaveChanges();
}
}
}
}
- 第 18 行:我们从已知的基数开始,即迄今为止使用的那个。执行此操作后,第 11–13 行中的数组将填充为分离的实体;
- 第21–22行:我们关注第一个时间槽及其关联的医生;
- 第 23 行:新建上下文;
- 第 26 行:我们将时间槽及其依赖关系(预加载)放入上下文中。由于这不是默认模式,我们必须显式请求此依赖关系。Include 方法允许我们这样做。其参数是引入上下文的实体中的依赖关系名称。将实体引入上下文的查询使用了 lambda 表达式。 Single 方法允许您指定条件以检索单个实体。在此,我们搜索数据库中主键为 slot #0 的 [Creneau] 实体;
- 第 27 行:我们显示检索到的实体。让我们回顾一下实体中使用的两个写入方法:
// signature
public override string ToString()
{
return String.Format("Creneau[{0},{1},{2},{3},{4}, {5},{6}]", Id, Hdebut, Mdebut, Hfin, Mfin, Medecin, dump(Timestamp));
}
// short signature
public string ShortIdentity()
{
return String.Format("Creneau[{0},{1},{2},{3},{4}, {5}, {6}]", Id, Hdebut, Mdebut, Hfin, Mfin, MedecinId, dump(Timestamp));
}
- 第 2-5 行:[ToString] 方法会显示 [Doctor] 依赖项。如果该依赖项尚未存在于上下文中,系统将从数据库中查找并添加它;
- 第 8-11 行:[ShortIdentity] 方法不显示 [Doctor] 依赖项。因此,如果该依赖项不在上下文中,则不会在数据库中进行查找;
此时,控制台输出如下:
- 第 28 行:上下文已关闭;
- 第 30–37 行:我们尝试写入该实体的 [Doctor] 依赖项。回顾懒加载的工作原理:如果依赖项不存在,则会在首次使用时加载。在此处,该依赖项通常是存在的。输出如下:
- 第 39–44 行:在新的上下文中,系统再次在数据库中搜索槽位 #0 并将其引入上下文。在此处,[Doctor] 依赖关系并未被显式请求。因此它将不会被引入(延迟加载);
- 第43行:插槽的简短标识显示如下:
在此,使用 ShortIdentity 而不是 ToString 来显示实体非常重要。如果使用 ToString,则会显示 [Doctor] 依赖项,并且为此会从数据库中进行查询。但我们并不希望这样。
- 第 44 行:上下文已关闭;
- 第46–53行:我们尝试显示该实体的依赖关系。必须在脱离上下文的情况下进行此操作;否则,系统会从数据库中查找并获取该依赖关系。此时,我们处于脱离上下文的状态。[Creneau] 实体处于脱离状态,且其 [Medecin] 依赖关系缺失(延迟加载)。会发生什么?屏幕显示如下:
EF 发现缺少 [Medecin] 依赖项。它尝试加载该依赖项,但由于上下文已关闭,此操作无法进行。我们将记录此 [System.ObjectDisposedException] 异常,因为这是在未打开的上下文中加载依赖项的典型特征。
现在,让我们来探讨实体的并发访问。
3.5.8. 实体访问中的并发
让我们重新回顾一下 [Client] 实体的定义:
public class Client
{
// data
[Key]
[Column("ID")]
public int? Id { get; set; }
[Required]
[MaxLength(5)]
[Column("TITRE")]
public string Titre { get; set; }
[Required]
[MaxLength(30)]
[Column("NOM")]
public string Nom { get; set; }
[Required]
[MaxLength(30)]
[Column("PRENOM")]
public string Prenom { get; set; }
// customer rvs
public ICollection<Rv> Rvs { get; set; }
[Column("TIMESTAMP")]
[Timestamp]
public byte[] Timestamp { get; set; }
// signature
...
}
我们将重点关注第 23 行中的 [Timestamp] 字段。我们知道该字段的值是由 DBMS 生成的。我们还注意到,第 22 行中的 [Timestamp] 注解会导致 EF 5 使用该注解字段来管理实体访问的并发性。让我们回顾一下什么是并发管理:
- 进程 P1 在时间 T1 从 [DOCTORS] 表中读取一行 L。该行带有时间戳 TS1;
- 进程 P2 在时间 T2 从 [DOCTORS] 表中读取同一行 L。该行具有时间戳 TS1,因为进程 P1 尚未提交其修改;
- 进程 P1 将其对行 L 的修改提交。此时行 L 的时间戳变为 TS2;
- 进程 P2 提交对行 L 的修改。此时 ORM 会抛出异常,因为进程 P2 持有的行 L 的时间戳 TS1 与数据库中查找到的时间戳 TS2 不一致。
这被称为乐观并发控制。在 EF 5 中,承担此角色的字段必须具有以下两种属性之一:[Timestamp] 或 [ConcurrencyCheck]。SQL Server 提供 [timestamp] 数据类型。此类列的值会在插入或修改行时由 SQL Server 自动生成。此类列随后可用于管理并发。
我们将通过两个线程同时修改数据库中同一 [Client] 实体的示例来说明这种并发访问。项目演变过程如下:
![]() | ![]() |
[ConcurrentAccess]程序的代码如下:
using System;
using System.Data;
using System.Linq;
using System.Threading;
using RdvMedecins.Entites;
using RdvMedecins.Models;
namespace RdvMedecins_01
{
// object exchanged with threads
class Data
{
public int Duree { get; set; }
public string Nom { get; set; }
public Client Client { get; set; }
}
// test program
class AccèsConcurrents
{
static void Main(string[] args)
{
Client client1;
using (var context = new RdvMedecinsContext())
{
// main thread
Thread.CurrentThread.Name = "main";
// empty the current base
foreach (var client in context.Clients)
{
context.Clients.Remove(client);
}
foreach (var medecin in context.Medecins)
{
context.Medecins.Remove(medecin);
}
// add a customer
client1 = new Client { Nom = "xx", Prenom = "xx", Titre = "xx" };
context.Clients.Add(client1);
// follow-up
Console.WriteLine("{0} client1--avant sauvegarde du contexte", Thread.CurrentThread.Name);
Console.WriteLine(client1.ShortIdentity());
// backup
context.SaveChanges();
// follow-up
Console.WriteLine("{0} client1--après sauvegarde du contexte", Thread.CurrentThread.Name);
Console.WriteLine(client1.ShortIdentity());
}
// we'll modify client1 with two threads
// thead t1
Thread t1 = new Thread(Modifie);
t1.Name = "t1";
t1.Start(new Data { Duree = 5000, Nom = "yy", Client = client1 });
// thread t2
Thread t2 = new Thread(Modifie);
t2.Name = "t2";
t2.Start(new Data { Duree = 5000, Nom = "zz", Client = client1 });
// we wait for the end of the 2 threads
Console.WriteLine("Thread {0} -- début attente fin des deux threads", Thread.CurrentThread.Name);
t1.Join();
t2.Join();
Console.WriteLine("Thread {0} -- fin attente fin des deux threads", Thread.CurrentThread.Name);
// the modification is displayed - only one was successful
using (var context = new RdvMedecinsContext())
{
// retrieve client1 from client2
Client client2 = context.Clients.Find(client1.Id);
Console.WriteLine("Thread {0} client2", Thread.CurrentThread.Name);
Console.WriteLine("Thread {0} {1}", Thread.CurrentThread.Name, client2.ShortIdentity());
}
}
// thread
static void Modifie(object infos)
{
...
}
- 第 26 行:我们启动一个空上下文;
- 第 29 行:为当前线程命名,以便将其与稍后将创建的两个线程区分开来;
- 第 31–38 行:将 [Doctor] 和 [Client] 实体设置为“已删除”状态;
- 第 40–41 行:将一个客户端添加到上下文中;
- 第 43–44 行:在上下文同步前显示该客户端;
- 第 46 行:与数据库进行上下文同步:处于“已删除”状态的实体将从数据库中移除。放置在上下文中的 [Client] 实体将被插入数据库。它将成为数据库中的唯一元素;
- 第 47–49 行:在上下文同步后显示该客户。此时,屏幕显示如下:
请注意,在上下文同步后,客户端拥有主键和时间戳;
- 第 50 行:上下文被关闭;
- 第 53 行:线程 t1 与第 84 行的 [Modify] 方法相关联。这意味着当它被启动时,将执行 [Modify] 方法;
- 第 54 行:为线程 t1 命名;
- 第 55 行:线程 t1 被启动。参数以第 12–17 行定义的 [Data] 结构形式传递给它:
- Duration:该线程将在执行完成前 X 秒停止,
- Client:指向数据库中待更新的客户端的引用,
- Name:要赋予该客户的名称;
- 第 57–59 行:对第二个线程执行相同操作。最终,两个线程将尝试在数据库中更改同一个客户端的名称;
- 第 60–63 行:启动两个线程后,主线程等待它们执行完毕;
- 第 62 行:等待线程 t1 结束;
- 第 63 行:等待线程 t2 结束;
- 第 64 行:我们无法确定这两个线程将以何种顺序完成。可以确定的是,到第 64 行时,它们已经完成;
- 第 66–72 行:在新的上下文中,我们查询数据库中的客户端以查看其状态。
现在让我们看看两个线程 t1 和 t2 执行什么操作。它们执行以下 [Modify] 方法:
static void Modifie(object infos)
{
// parameter is retrieved
Data data = (Data)infos;
try
{
using (var context = new RdvMedecinsContext())
{
Console.WriteLine("Début Thread {0}", Thread.CurrentThread.Name);
// retrieve client1 from client2
Client client2 = context.Clients.Find(data.Client.Id);
Console.WriteLine("Thread {0} client2", Thread.CurrentThread.Name);
Console.WriteLine("Thread {0} {1}", Thread.CurrentThread.Name, client2.ShortIdentity());
// modify client2
client2.Nom = data.Nom;
// we wait a little
Thread.Sleep(data.Duree);
// save changes
context.SaveChanges();
}
}
catch (Exception e)
{
// exception
Console.WriteLine("Thread {0} {1}", Thread.CurrentThread.Name, e);
}
// end of thread
Console.WriteLine("Fin Thread {0}", Thread.CurrentThread.Name);
}
- 第 4 行:获取线程参数(Duration、Name、Client);
- 第 7 行:新建上下文;
- 第 11 行:将客户端引入上下文;
- 第12–13行:监控以检查客户端的状态;
- 第 15 行:更改其名称;
- 第 17 行:线程暂停数毫秒。这会产生一个有趣的效果。该线程释放了正在执行它的处理器,为另一个线程让出了空间。 在本例中,我们有三个线程:main、t1 和 t2。主线程处于暂停状态,等待线程 t1 和 t2 完成。假设线程 t1 首先获得处理器,它现在将处理器让给线程 t2。这将导致线程 t2 读取与线程 t1 完全相同的数据——同一个客户端,带有相同的时间戳;
- 第 19 行:上下文与数据库同步。 让我们再次假设线程 t1 最先恢复运行。它将保存名为“yy”的客户端。它之所以能做到这一点,是因为其时间戳与数据库中的时间戳相同。由于这次更新,DBMS 将修改时间戳。当线程 t2 随后恢复运行时,它将拥有一个时间戳与当前数据库中不一致的客户端。其更新请求将被拒绝。
屏幕显示如下:
- 第4行:数据库中的客户端;
- 第 9 行:线程 t2 读取的客户端;
- 第 11 行:线程 t1 读取的客户端。因此,两个线程读取的内容相同;
- 第 12 行:线程 t2 率先完成。因此它得以执行更新操作。名称应已更改为“zz”;
- 第 13 行:线程 t1 抛出 [System.Data.OptimisticConcurrencyException]。EF 检测到其时间戳不正确;
- 第 21 行:线程 t1 随后完成;
- 第 22 行:主线程结束等待;
- 第 24 行:主线程显示数据库中的客户信息。确实是线程 t2 获胜。名称为“zz”。请注意,时间戳已发生变化。
现在,让我们考察另一个方面:管理持久化上下文与数据库同步的事务。
3.5.9. 事务内的同步
[CRENEAUX] 表有一个我们手动添加的唯一性约束(参见第 2.2.4 节,第 12 页):
我们将按以下步骤操作:为同一位医生在同一天的同一时间段内同时添加两个预约。让我们看看会发生什么。
项目进展如下:
![]() | ![]() |
[SynchronisationTransaction] 程序的代码如下:
using System;
using System.Linq;
using RdvMedecins.Entites;
using RdvMedecins.Models;
namespace RdvMedecins_01
{
// test program
class SynchronisationTransaction
{
static void Main(string[] args)
{
using (var context = new RdvMedecinsContext())
{
// empty the current base
foreach (var client in context.Clients)
{
context.Clients.Remove(client);
}
foreach (var medecin in context.Medecins)
{
context.Medecins.Remove(medecin);
}
context.SaveChanges();
}
// create a customer
Client client1 = new Client { Nom = "xx", Prenom = "xx", Titre = "xx" };
// we create a doctor
Medecin medecin1 = new Medecin { Nom = "xx", Prenom = "xx", Titre = "xx" };
// we create a niche for this doctor
Creneau creneau1 = new Creneau { Hdebut = 8, Mdebut = 20, Hfin = 8, Mfin = 40, Medecin = medecin1 };
// create two appointments for this doctor and this customer, same day, same time slot
Rv rv1 = new Rv { Client = client1, Creneau = creneau1, Jour = new DateTime(2012, 10, 18) };
Rv rv2 = new Rv { Client = client1, Creneau = creneau1, Jour = new DateTime(2012, 10, 18) };
try
{
// we put it all in the context of persistence
using (var context = new RdvMedecinsContext())
{
context.Clients.Add(client1);
context.Creneaux.Add(creneau1);
context.Medecins.Add(medecin1);
context.Rvs.Add(rv1);
context.Rvs.Add(rv2);
// save the context - you should have an exception
// because the underlying BD has a uniqueness constraint preventing
// to have two RDV on the same day, in the same slot
context.SaveChanges();
}
}
catch (Exception e)
{
Console.WriteLine("Erreur : {0}", e);
}
// if the save occurs in a transaction, then nothing must have been inserted in the database
// because of the previous exception - we check
using (var context = new RdvMedecinsContext())
{
// our customers
Console.WriteLine("Clients--------------------------------------");
var clients = from client in context.Clients select client;
foreach (Client client in clients)
{
Console.WriteLine(client);
}
// the doctors
Console.WriteLine("Médecins--------------------------------------");
var medecins = from medecin in context.Medecins select medecin;
foreach (Medecin medecin in medecins)
{
Console.WriteLine(medecin);
}
// time slots
Console.WriteLine("Créneaux horaires--------------------------------------");
var creneaux = from creneau in context.Creneaux select creneau;
foreach (Creneau creneau in creneaux)
{
Console.WriteLine(creneau);
}
// dates
Console.WriteLine("Rendez-vous--------------------------------------");
var rvs = from rv in context.Rvs select rv;
foreach (Rv rv in rvs)
{
Console.WriteLine(rv);
}
}
}
}
}
- 第 15–27 行:使用持久化上下文清空数据库;
- 第 30 行:创建 [Client] 对象;
- 第 32 行:创建 [Doctor] 对象;
- 第 34 行:创建 [Slot] 对象;
- 第 36 行:创建一个 [Appointment] 对象;
- 第 37 行:创建第二个与前一个完全相同的 [Appointment] 对象;
- 第 41 行:打开一个新的上下文;
- 第 43–47 行:将之前创建的对象附加到新上下文中。请注意,如果考虑依赖关系,我们可以将 Add 操作的数量降至最低。不过,EF 会优化发送到数据库的 SQL INSERT 语句;
- 第 51 行:上下文已与数据库同步。正如注释所示,由于 [RVS] 表上的唯一性约束,两个约会中的一个插入操作必然会失败。但不仅如此,如果同步发生在事务内部,则必须回滚所有操作。因此,不应进行任何插入操作。数据库必须保持为空;
- 第 53 行:关闭上下文;
- 第 61–90 行:显示数据库内容。数据库必须为空。
屏幕显示如下:
- 第 1 行:由于违反了 [RVS] 表上的唯一约束而引发异常;
- 第 9–12 行:数据库确实为空。因此,上下文与数据库的同步是在事务内进行的。
EF 5 无疑还有其他方面值得探索。但我们已掌握足够知识,可以回到多层架构的研究中。在本文开头,读者会发现一些文章和书籍的参考资料,这些资料将有助于他们加深对 EF 5 的理解。
3.6. 基于 EF 5 的多层架构研究
我们回到第2节中描述的案例研究。这是一个结构如下所示的ASP.NET Web应用程序:
![]() |
我们将首先构建 [DAO] 数据访问层。该层将基于 EF5。
3.6.1. 新项目
我们在当前解决方案 [1] 中创建一个新的 VS 2012 控制台项目 [RdvMedecins-SqlServer-02]:
![]() |
我们在其中添加四个文件夹 2,用于组织代码。其中 [Entities] 文件夹是上一项目中 [Entities] 文件夹的副本。复制后会出现错误,因为我们尚未添加正确的引用。 我们需要添加对 Entity Framework 5 的引用。为此,我们将按照第 3.4 节(第 21 页)中所述的方法进行操作。引用列表如下所示 3:
![]() |
至此,项目不应再出现任何编译错误。我们还需从前一个项目中复制 [App.config] 文件,该文件用于配置数据库连接:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
</configSections>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
<entityFramework>
<defaultConnectionFactory type="System.Data.Entity.Infrastructure.SqlConnectionFactory, EntityFramework" />
</entityFramework>
<!-- connection chain on base -->
<connectionStrings>
<add name="monContexte"
connectionString="Data Source=localhost;Initial Catalog=rdvmedecins-ef;User Id=sa;Password=sqlserver2012;"
providerName="System.Data.SqlClient" />
</connectionStrings>
<!-- the factory provider -->
<system.data>
<DbProviderFactories>
<add name="SqlClient Data Provider"
invariant="System.Data.SqlClient"
description=".Net Framework Data Provider for SqlServer"
type="System.Data.SqlClient.SqlClientFactory, System.Data,
Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089"
/>
</DbProviderFactories>
</system.data>
</configuration>
3.6.2. 异常类
我们将使用一个项目专用的异常类。这是 [DAO] 层将抛出的异常类:
![]() |
[DAO] 层将捕获所有向上传播至该层的异常,并将其封装为 [RdvMedecinsException] 类型的异常。该异常定义如下:
using System;
namespace RdvMedecins.Exceptions
{
public class RdvMedecinsException : Exception
{
// properties
public int Code { get; set; }
// manufacturers
public RdvMedecinsException()
: base()
{
}
public RdvMedecinsException(string message)
: base(message)
{
}
public RdvMedecinsException(int code, string message)
: base(message)
{
Code = code;
}
public RdvMedecinsException(int code, string message, Exception ex)
: base(message, ex)
{
Code = code;
}
// identity
public override string ToString()
{
if (InnerException == null)
{
return string.Format("RdvMedecinsException[{0},{1}]", Code, base.Message);
}
else
{
return string.Format("RdvMedecinsException[{0},{1},{2}]", Code, base.Message, base.InnerException.Message);
}
}
}
}
- 第 5 行:该类继承自 [Exception] 类;
- 第 9 行:它向基类添加了一个错误代码;
- 第12–32行:各种构造函数都包含[Code]字段。
该项目的演变过程如下:
![]() |
3.6.3. [DAO] 层
![]() |
[DAO] 层为 [ASP.NET] 层提供了一个接口。要识别这一点,请查看应用程序的网页:
![]() |
- 在上文的 [1] 中,下拉列表已填充了医生列表。[DAO] 层将提供此列表;
- 在2中,[DAO]层将提供;
- 某位医生在指定日期的预约列表,
- 医生可预约时段列表,
- 以及所选医生的其他信息;
![]() |
- 在 3 中,客户下拉列表将由 [DAO] 层提供;
![]() |
- 在[4]中,用户确认预约。[DAO]层必须能够将其添加到数据库中,还必须能够提供所选客户的附加信息;
![]() |
- 在[5]中,用户删除一个预约。DAO层必须支持此操作。
基于上述信息,[DAO]层的[IDao]接口可设计如下:
using System;
using System.Collections.Generic;
using RdvMedecins.Entites;
namespace RdvMedecins.Dao
{
public interface IDao
{
// customer list
List<Client> GetAllClients();
// list of doctors
List<Medecin> GetAllMedecins();
// list of physician slots
List<Creneau> GetCreneauxMedecin(int idMedecin);
// list of RV from a given doctor on a given day
List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour);
// add a RV
int AjouterRv(DateTime jour, int idCreneau, int idClient);
// delete a RV
void SupprimerRv(int idRv);
// find a T entity via its primary key
T Find<T>(int id) where T : class;
}
}
第 10 至 20 行中的方法源自刚才进行的分析。第 22 行中的方法是为了应对我们正在处理延迟加载的情况。如果在 [ASP.NET] 层中,我们需要依赖某个实体,我们将使用此方法从数据库中检索它。
该接口的 [Dao] 实现如下:
using System;
using System.Collections.Generic;
using System.Linq;
using RdvMedecins.Entites;
using RdvMedecins.Exceptions;
using RdvMedecins.Models;
namespace RdvMedecins.Dao
{
public class Dao : IDao
{
//customer list
public List<Client> GetAllClients()
{
// customer list
List<Client> clients = null;
try
{
// opening persistence context
using (var context = new RdvMedecinsContext())
{
// customer list
clients = context.Clients.ToList();
}
}
catch (Exception ex)
{
throw new RdvMedecinsException(1, "GetAllClients", ex);
}
// we return the result
return clients;
}
// list of doctors
public List<Medecin> GetAllMedecins()
{
// list of doctors
List<Medecin> medecins = null;
try
{
// opening persistence context
using (var context = new RdvMedecinsContext())
{
// list of doctors
medecins = context.Medecins.ToList();
}
}
catch (Exception ex)
{
throw new RdvMedecinsException(2, "GetAllMedecins", ex);
}
// we return the result
return medecins;
}
// list of time slots for a given doctor
public List<Creneau> GetCreneauxMedecin(int idMedecin)
{
...
}
// list of a doctor's RV for a given day
public List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour)
{
...
}
// add a RV to the list
public int AjouterRv(DateTime jour, int idCreneau, int idClient)
{
...
}
// delete a RV
public void SupprimerRv(int idRv)
{
...
}
// find a customer
public Client FindClient(int id)
{
...
}
// find a niche
public Creneau FindCreneau(int id)
{
...
}
// find a doctor
public Medecin FindMedecin(int id)
{
....
}
// find an rv
public Rv FindRv(int id){
...
}
}
}
下面我们来解释一下 [GetAllClients] 方法,该方法应返回所有客户的列表:
- 第 18–31 行:客户端搜索在 try/catch 块内进行。后续所有方法也将采用相同的方式;
- 第 21 行:打开一个新的上下文;
- 第 24 行:将 [Client] 实体加载到上下文中并放入列表中。
返回所有医生列表的 [GetAllMedecins] 方法与此类似(第 37–57 行)。
[GetCreneauxMedecin] 方法如下:
// list of time slots for a given doctor
public List<Creneau> GetCreneauxMedecin(int idMedecin)
{
// list of slots
try
{
// opening persistence context
using (var context = new RdvMedecinsContext())
{
// we get the doctor back with his slots
Medecin medecin = context.Medecins.Include("Creneaux").Single(m => m.Id == idMedecin);
// list of doctor's slots
return medecin.Creneaux.ToList<Creneau>();
}
}
catch (Exception ex)
{
throw new RdvMedecinsException(3, "GetCreneauxMedecin", ex);
}
}
- 第 9 行:打开一个新的持久化上下文;
- 第 11 行:根据已知的主键搜索医生。请求包含 [Creneaux] 依赖项——即该医生的时间段集合。如果医生不存在,Single 方法将抛出异常;
- 第 13 行:返回时间段列表。
[GetRvMedecinJour] 方法必须返回医生在指定日期内的预约列表。其代码可能如下所示:
// list of a doctor's RV for a given day
public List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour)
{
// rv list
List<Rv> rvs = null;
try
{
// opening persistence context
using (var context = new RdvMedecinsContext())
{
// we get the doctor back
Medecin medecin = context.Medecins.Find(idMedecin);
if (medecin == null)
{
throw new RdvMedecinsException(10, string.Format("Médecin [{0}] inexistant", idMedecin));
}
// appointment list
rvs = context.Rvs.Where(r => r.Creneau.Medecin.Id == idMedecin && r.Jour == jour).ToList();
}
}
catch (Exception ex)
{
throw new RdvMedecinsException(4, "GetRvMedecinJour", ex);
}
// we return the result
return rvs;
}
- 第 13 行:根据给定的主键检索医生;
- 第 14–17 行:如果不存在,则抛出异常;
- 第 19 行:用于检索该医生预约记录的 LINQ 查询;
[AddAppointment] 方法必须将预约添加到数据库中,并返回已插入项的主键。其代码如下:
// add a RV to the list
public int AjouterRv(DateTime jour, int idCreneau, int idClient)
{
// rdv n° added
int idRv;
try
{
// opening persistence context
using (var context = new RdvMedecinsContext())
{
// we get the slot back
Creneau creneau = context.Creneaux.Find(idCreneau);
if (creneau == null)
{
throw new RdvMedecinsException(5, string.Format("Créneau [{0}] inexistant", idCreneau));
}
// we get the customer back
Client client = context.Clients.Find(idClient);
if (client == null)
{
throw new RdvMedecinsException(6, string.Format("Client [{0}] inexistant", idCreneau));
}
// niche creation
Rv rv = new Rv { Jour = jour, Client = client, Creneau = creneau };
// added in context
context.Rvs.Add(rv);
// save context
context.SaveChanges();
// retrieve the primary key of the added rv
idRv = (int)rv.Id;
}
}
catch (Exception ex)
{
throw new RdvMedecinsException(7, "AjouterRv", ex);
}
// result
return idRv;
}
- 第 12 行:在数据库中搜索预约时段;
- 第 13–16 行:如果未找到,则抛出异常;
- 第 18 行:在数据库中搜索该预约的客户;
- 第19–22行:若未找到,则抛出异常;
- 第 24 行:使用必要信息创建一个 [Rv] 对象;
- 第 26 行:将其添加到持久化上下文中;
- 第 28 行:将持久化上下文与数据库同步。随后,该预约将被保存到数据库中;
- 第 30 行:我们知道在数据库同步后,插入项的主键已可用。我们获取所添加预约的主键;
- 第 31 行:关闭持久化上下文。
[DeleteAppointment] 方法必须删除其接收的主键对应的预约。
// delete a RV
public void SupprimerRv(int idRv)
{
try
{
// opening persistence context
using (var context = new RdvMedecinsContext())
{
// we recover the Rv
Rv rv = context.Rvs.Find(idRv);
if (rv == null)
{
throw new RdvMedecinsException(5, string.Format("Rv [{0}] inexistant", idRv));
}
// deletion Rv
context.Rvs.Remove(rv);
// save context
context.SaveChanges();
}
}
catch (Exception ex)
{
throw new RdvMedecinsException(8, "SupprimerRv", ex);
}
}
- 第 7 行:新建持久化上下文;
- 第 10 行:将待删除的预约传递给上下文;
- 第 11–15 行:如果不存在,则抛出异常;
- 第 16 行:将其从上下文中移除;
- 第 18 行:将上下文与数据库同步;
- 第 19 行:关闭上下文。
[Find<T>] 方法允许您使用主键在数据库中搜索类型为 T 的实体。其代码可能如下所示:
public T Find<T>(int id) where T : class
{
try
{
// opening persistence context
using (var context = new RdvMedecinsContext())
{
return context.Set<T>().Find(id);
}
}
catch (Exception ex)
{
throw new RdvMedecinsException(20, "Find<T>", ex);
}
}
- 第 8 行:Set<T> 方法允许您获取一个 DbSet<T>,您可以对其应用常规方法。
该项目的演变过程如下:
![]() |
3.6.4. 测试 [DAO] 层
我们将为[DAO]层创建一个测试程序。测试架构如下:
![]() |
一个控制台程序会请求 [Spring.net] 实例化 [DAO] 层。完成实例化后,该程序将测试 [DAO] 层接口的各项功能。虽然这里使用的是控制台程序,但更理想的做法是编写一个 NUnit 风格的测试程序。[DAO] 层的测试程序可能如下所示:
using System;
using System.Collections.Generic;
using RdvMedecins.Dao;
using RdvMedecins.Entites;
using RdvMedecins.Exceptions;
using Spring.Context.Support;
namespace RdvMedecins.Tests
{
class Program
{
public static void Main()
{
IDao dao = null;
try
{
// instantiation layer [DAO] via Spring
dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;
// customer display
List<Client> clients = dao.GetAllClients();
DisplayClients("Liste des clients :", clients);
// physician display
List<Medecin> medecins = dao.GetAllMedecins();
DisplayMedecins("Liste des médecins :", medecins);
// list of time slots for doctor no. 0
List<Creneau> creneaux = dao.GetCreneauxMedecin((int)medecins[0].Id);
DisplayCreneaux(string.Format("Liste des créneaux horaires du médecin {0}", medecins[0]), creneaux);
// list of doctor's appointments for a given day
DisplayRvs(string.Format("Liste des RV du médecin {0}, le 23/11/2013 :", medecins[0]), dao.GetRvMedecinJour((int)medecins[0].Id, new DateTime(2013, 11, 23)));
// add a RV to doctor n°1 in slot n° 0
Console.WriteLine(string.Format("Ajout d'un RV au médecin {0} avec client {1} le 23/11/2013", medecins[0], clients[0]));
int idRv1 = dao.AjouterRv(new DateTime(2013, 11, 23), (int)creneaux[0].Id, (int)clients[0].Id);
Console.WriteLine("Rdv ajouté");
DisplayRvs(string.Format("Liste des RV du médecin {0}, le 23/11/2013 :", medecins[0]), dao.GetRvMedecinJour((int)medecins[0].Id, new DateTime(2013, 11, 23)));
// add an appointment in an already occupied slot - must trigger an exception
int idRv2;
Console.WriteLine("Ajout d'un RV dans un créneau déjà occupé");
try
{
idRv2 = dao.AjouterRv(new DateTime(2013, 11, 23), (int)creneaux[0].Id, (int)clients[0].Id);
Console.WriteLine("Rdv ajouté");
DisplayRvs(string.Format("Liste des RV du médecin {0}, le 23/11/2013 :", medecins[0]), dao.GetRvMedecinJour((int)medecins[0].Id, new DateTime(2013, 11, 23)));
}
catch (RdvMedecinsException ex)
{
Console.WriteLine(string.Format("L'erreur suivante s'est produite : {0}", ex));
}
// delete an appointment
Console.WriteLine(string.Format("Suppression du RV n° {0}", idRv1));
dao.SupprimerRv(idRv1);
DisplayRvs(string.Format("Liste des RV du médecin {0}, le 23/11/2013 :", medecins[0]), dao.GetRvMedecinJour((int)medecins[0].Id, new DateTime(2013, 11, 23)));
}
catch (Exception ex)
{
Console.WriteLine(string.Format("L'erreur suivante s'est produite : {0}", ex));
}
//break
Console.ReadLine();
}
// utility methods - display lists
public static void DisplayClients(string Message, List<Client> clients)
{
Console.WriteLine(Message);
foreach (Client c in clients)
{
Console.WriteLine(c.ShortIdentity());
}
}
public static void DisplayMedecins(string Message, List<Medecin> medecins)
{
...
}
public static void DisplayCreneaux(string Message, List<Creneau> creneaux)
{
...
}
public static void DisplayRvs(string Message, List<Rv> rvs)
{
...
}
}
}
- 第 14 行:对 [DAO] 层的引用。为了使测试与 [DAO] 层的实际实现解耦,该引用采用 [IDao] 类型(接口)而非 [Dao] 类型(类);
- 第 18 行:[DAO] 层由 Spring 实例化。我们稍后将讨论实现此功能的必要配置。我们将 Spring 返回的对象引用强制转换为 [IDao] 接口类型的引用;
- 第 21–22 行:显示客户;
- 第 25–26 行:显示医生;
- 第 29–30 行:显示医生 #0 的时段列表;
- 第 33 行:显示医生 #0 在 2013 年 11 月 23 日的预约。此时应无预约;
- 第 37 行:为医生 #0 添加 2013 年 11 月 23 日的预约;
- 第 39 行:显示医生 #0 在 2013 年 11 月 23 日的预约。此时应显示一条;
- 第46行:同一预约被重复添加。此时应抛出异常;
- 第57行:删除之前添加的那个预约;
- 第58行:显示医生#0在2013年11月23日的预约。此时应无预约。
3.6.5. Spring.net 配置
在上述测试程序中,我们简要提到了实例化 [DAO] 层的语句:
dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;
[ContextRegistry] 类是位于 [Spring.Context.Support] 命名空间中的 Spring 类。要使用 Spring,我们需要将它的 DLL 文件添加到项目引用中。具体操作如下:
![]() |
- 在 [1] 中,使用 [NuGet] 工具搜索包;
![]() |
项目引用将按以下方式更改:
![]() |
[Spring.Core] 包依赖于 [Common.Logging] 包。该包也已加载。至此,该项目不应再有任何错误。
不过,这并不意味着它就能正常工作。我们首先需要在 [App.config] 文件中配置 Spring。这是该项目中最棘手的部分。新的 [App.config] 文件如下:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
<section name="entityFramework" type="System.Data.Entity.Internal.ConfigFile.EntityFrameworkSection, EntityFramework, Version=5.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" requirePermission="false" />
<!-- spring -->
<sectionGroup name="spring">
<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
</sectionGroup>
<!-- common logging-->
<section name="logging" type="Common.Logging.ConfigurationSectionHandler, Common.Logging" />
</configSections>
<startup>
<supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5" />
</startup>
<!-- Entity Framework -->
<entityFramework>
<defaultConnectionFactory type="System.Data.Entity.Infrastructure.LocalDbConnectionFactory, EntityFramework">
<parameters>
<parameter value="v11.0" />
</parameters>
</defaultConnectionFactory>
</entityFramework>
<!-- Connection chains -->
<connectionStrings>
<add name="monContexte" connectionString="Data Source=localhost;Initial Catalog=rdvmedecins-ef;User Id=sa;Password=sqlserver2012;" providerName="System.Data.SqlClient" />
</connectionStrings>
<system.data>
<DbProviderFactories>
<add name="SqlClient Data Provider" invariant="System.Data.SqlClient" description=".Net Framework Data Provider for SqlServer" type="System.Data.SqlClient.SqlClientFactory, System.Data, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
</DbProviderFactories>
</system.data>
<!-- spring configuration -->
<spring>
<context>
<resource uri="config://spring/objects" />
</context>
<objects xmlns="http://www.springframework.net">
<object id="rdvmedecinsDao" type="RdvMedecins.Dao.Dao,RdvMedecins-SqlServer-02" />
</objects>
</spring>
<!-- configuration common.logging -->
<logging>
<factoryAdapter type="Common.Logging.Simple.ConsoleOutLoggerFactoryAdapter, Common.Logging">
<arg key="showLogName" value="true" />
<arg key="showDataTime" value="true" />
<arg key="level" value="DEBUG" />
<arg key="dateTimeFormat" value="yyyy/MM/dd HH:mm:ss:fff" />
</factoryAdapter>
</logging>
</configuration>
首先,让我们移除所有已知的内容:Entity Framework、连接字符串、ProviderFactory。文件演变如下:
<?xml version="1.0" encoding="utf-8"?>
<configuration>
<configSections>
<!-- For more information on Entity Framework configuration, visit http://go.microsoft.com/fwlink/?LinkID=237468 -->
<section name="entityFramework" ... />
<!-- spring -->
<sectionGroup name="spring">
<section name="context" type="Spring.Context.Support.ContextHandler, Spring.Core" />
<section name="objects" type="Spring.Context.Support.DefaultSectionHandler, Spring.Core" />
</sectionGroup>
<!-- common logging-->
<sectionGroup name="common">
<section name="logging" type="Common.Logging.ConfigurationSectionHandler, Common.Logging" />
</sectionGroup>
</configSections>
...
<!-- spring configuration -->
<spring>
<context>
<resource uri="config://spring/objects" />
</context>
<objects xmlns="http://www.springframework.net">
<object id="rdvmedecinsDao" type="RdvMedecins.Dao.Dao,RdvMedecins-SqlServer-02" />
</objects>
</spring>
<!-- configuration common.logging -->
<common>
<logging>
<factoryAdapter type="Common.Logging.Simple.ConsoleOutLoggerFactoryAdapter, Common.Logging">
<arg key="showLogName" value="true" />
<arg key="showDataTime" value="true" />
<arg key="level" value="DEBUG" />
<arg key="dateTimeFormat" value="yyyy/MM/dd HH:mm:ss:fff" />
</factoryAdapter>
</logging>
</common>
</configuration>
- 第 3–15 行:定义配置部分;
- 第 8 行:定义将管理 XML 文件中 <spring><context> 部分的类(第 19–21 行);
- 第 9 行:定义将管理 XML 文件中 <spring><objects> 部分的类(第 22–24 行);
- 第 13 行:定义将管理 XML 文件中 <common><logging> 部分的类(第 27–36 行);
- 第 7–14 行:内容稳定。在其他项目中无需更改;
- 第 18–25 行:Spring 配置。除第 22–24 行(定义 Spring 将实例化的对象)外,其余部分保持稳定;
- 第 23 行:对象的定义。id 属性是任意的,它是该对象的标识符。type 属性以“完整类名,包含该类的程序集”的形式指定要实例化的类。此处的类是实现 [DAO] 层的类:[RdvMedecins.Dao.Dao]。要查找其程序集,请查看项目属性:
![]() |
在 [1] 中,需提供该程序集的名称;
- 第 27–36 行:"Common Logging" 配置是稳定的。您可能需要修改第 32 行的日志级别。调试阶段结束后,您可以将级别设置为 INFO。
最终,尽管乍看之下较为复杂,但 Spring 配置文件其实很简单。仅需修改以下内容:
- 第 22–24 行,用于定义待实例化的对象;
- 第 32 行:日志级别。
在测试程序中,实例化 [DAO] 层的语句如下:
dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;
[ContextRegistry] 是一个 Spring 类,它使用 [Web.config] 或 [App.config] 文件中指定的 Spring 配置。在此,它将使用 [App.config] 文件中的以下部分:
<spring>
<context>
<resource uri="config://spring/objects" />
</context>
<objects xmlns="http://www.springframework.net">
<object id="rdvmedecinsDao" type="RdvMedecins.Dao.Dao,RdvMedecins-SqlServer-02" />
</objects>
</spring>
- ContextRegistry.GetContext() 使用第 2–4 行定义的上下文。第 3 行表示 Spring 对象在配置文件的 [spring/objects] 部分中定义。该部分即第 5–7 行;
- ContextRegistry.GetContext().GetObject("rdvmedecinsDao") 使用第 5–7 行中的部分。它返回一个指向具有 id="rdvmedecinsDao" 属性的对象的引用。这是第 6 行中定义的对象。随后,Spring 将使用其无参构造函数实例化由 type 属性定义的类。因此,该构造函数必须存在。 完成上述操作后,生成的对象引用将返回给调用代码。如果代码中再次请求该对象,Spring 仅会返回对首次创建的对象的引用。这就是所谓的单例设计模式。
对象的构造可能更为复杂。您可以使用带参数的构造函数,或在对象创建后指定某些对象字段的初始化。有关此主题的更多信息,请参阅 [http://tahe.developpez.com/dotnet/springioc/] 上的文章《.NET 版 Spring IOC 教程》。
完成上述操作后,我们可以运行应用程序。屏幕显示结果如下:
结果符合预期。现在我们可以认为我们的 [DAO] 层是有效的。本教程到此结束。到目前为止,我们已经涵盖了:
- Entity Framework 5 ORM的基础知识;
- 基于该 ORM 的 [DAO] 层。
让我们回顾本文开头描述的案例研究。我们从一个具有以下架构的现有应用程序开始:
![]() |
我们希望将其改造为如下结构:
![]() |
其中 EF5 已取代 NHibernate。我们刚刚构建了 [DAO2] 层。实际上,它的接口与 [DAO1] 层并不相同,后者的接口更为有限:
public interface IDao
{
// customer list
List<Client> GetAllClients();
// list of doctors
List<Medecin> GetAllMedecins();
// list of physician slots
List<Creneau> GetCreneauxMedecin(int idMedecin);
// list of RV from a given doctor on a given day
List<Rv> GetRvMedecinJour(int idMedecin, DateTime jour);
// add a RV to the list
int AjouterRv(DateTime jour, int idCreneau, int idClient);
// delete a RV
void SupprimerRv(int idRv);
}
[DAO2] 层已向此接口添加了以下方法:
// find a T entity via its primary key
T Find<T>(int id) where T : class;
添加此方法是因为 EF 5 ORM 默认处于延迟加载模式。实体到达 [ASP.NET] 层时不包含其依赖项。上述方法允许我们在需要时检索这些依赖项,而在某些情况下,我们确实需要它们。 NHibernate 默认同样采用延迟加载模式,但我曾将其设置为立即加载模式。此时,实体连同其依赖项一起到达 [ASP.NET] 层。
我们将完成将 ASP.NET/NHibernate 应用程序移植到 ASP.NET/EF 5 应用程序的工作。不过,由于这已不再涉及 EF5,我们将不再对 Web 代码进行评论。我们仅会说明如何设置 Web 应用程序并进行测试。该应用程序可在本教程的网站上获取。
3.6.6. 生成 [DAO] 层 DLL
在以下架构中:
![]() |
[ASP.NET] 层将通过 DLL 形式访问其右侧的各层。因此,我们将构建 [DAO] 层 DLL。
![]() |
- 在 [1] 中,选择测试程序,并在 2 中,将其排除在将要生成的 DLL 之外;
- 在 3 中,于项目属性中指定要创建的程序集为 DLL;
- 在 [4] 中,通过 VS 菜单指定生成 [Release] 程序集,其包含的信息量少于 [Debug] 程序集;
![]() |
- 在 [5] 中,重新生成项目程序集。DLL 将被生成;
- 在 [6] 中,显示所有项目文件;
![]() |
![]() |
- 在9中,将[Release]文件夹中的DLL文件收集到外部的[lib]文件夹中[10]。Web项目将从此处获取其引用。
3.6.7. [ASP.NET] 层
在此我们将说明如何将 [ASP.NET / NHibernate] 应用程序移植为 [ASP.NET / EF 5] 应用程序。我们将使用 Visual Studio Express 2012 for Web 进行操作,该软件可从 [http://www.microsoft.com/visualstudio/fra/downloads] 免费获取。
我们将从使用 VS 2010 创建的现有 Web 项目开始。
![]() |
- 在[1]中,我们打开现有项目:
- 在 2 中,加载的项目包含以下引用 3:
- [NHibernate] 是 NHibernate 框架的 DLL,
- [Spring.Core] 是 Spring.net 框架的 DLL,
- [log4net] 是 log4net 日志记录框架的 DLL。该框架被 Spring.net 所使用,
- [MySql.Data] 是 MySQL 数据库管理系统(DBMS)的 ADO.NET 驱动程序,
- [rdvmedecins] 是使用 NHibernate 构建的 [DAO] 层的 DLL;
- 在 [4] 中,我们更改了项目名称;在 [5] 中,我们删除了之前的引用;
![]() |
- 在 [6] 中,我们向项目添加引用;
- 在 7 中,在向导中,我们使用 [浏览] 选项;
![]() |
完成上述操作后,项目将呈现如下状态:
![]() |
- 在[1]中,管理网页的代码被分置于[Global.asax]和[Default.aspx]这两个文件中。辅助代码已放置在[Entities]文件夹内。最后,应用程序通过[Web.config]文件进行配置;
- 在 2 中,我们生成项目程序集;
- 在 3 中,出现了错误。
让我们来分析这些错误,例如以下这个:
![]()
及其说明:
![]()
[medecin.Id] 的类型是 int?,而 [GetCreneauxMedecin] 方法的类型是 int。因此,需要进行类型转换。该错误在代码中反复出现,因为 ASP.NET/NHibernate 项目中的实体主键类型为 int,而 ASP.NET/EF 5 项目中的实体主键类型为 int?。 我们修正了所有此类错误并重新生成项目。此后便不再出现错误。
在运行项目之前,还有一项细节需要处理:由 Spring 框架实例化 [DAO] 层。这在 [Global.asax] 中完成:
protected void Application_Start(object sender, EventArgs e)
{
// caching of certain database data
try
{
// layer instantiation [dao]
Dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;
...
}
catch (Exception ex)
{...
}
}
在 [DAO] 层测试程序中,它是这样实例化 [DAO] 层的:
dao = ContextRegistry.GetContext().GetObject("rdvmedecinsDao") as IDao;
这两个方法是完全相同的。请注意,此处对 [DAO] 层的实例化依赖于 [App.config] 中指定的配置。随后,我们将 Web 项目的当前 [Web.config] 内容替换为 [DAO] 层项目中的 [App.config] 内容,以确保配置一致。
现在我们可以进行首次运行了。主页已显示 [1]:
![]() |
- 在 2 中,我们输入预约日期并提交;
![]() |
- 在3中,发生错误。
检查页面显示的错误信息时,我们发现报告的异常与延迟加载有关:我们在管理该对象的持久化上下文已关闭的情况下,试图加载该对象的依赖项。该对象现在处于“脱离”状态。此错误的原因在于,NHibernate 处于立即加载模式,而 EF 5 默认工作在延迟加载模式下。在上文中用红色标出的行中:
- rdv 代表一个未加载其依赖项的 [Rv] 对象;
- 为了评估 rdv.Creneau.Id,应用程序尝试加载依赖项 rdv.Creneau。但由于我们已不再处于该上下文中,因此无法加载,从而引发了异常。
这里的解决方案很简单。第 108 行:我们在字典中创建一个条目,其键为预约时段的主键。然而,事实证明 [Rv] 实体封装了关联时段的主键。因此我们写:
dicoRvPris[(int)rdv.CreneauId] = rdv;
我们再次尝试运行代码。这次出现的错误如下:
![]() |
错误信息类似。第 132 行:我们在 ASP.NET 层尝试加载 [Rv] 对象的 [Client] 依赖项,但这超出了上下文范围。我们必须从数据库中检索 [Client] 对象。为了解决此问题,已为 [IDao] 接口增强了以下方法:
// find a T entity via its primary key
T Find<T>(int id) where T : class;
这将使我们能够检索依赖项。因此,上面的错误代码行将重写为如下形式:
Client client = Global.Dao.Find<Client>(agenda.Creneaux[i].Rdv.ClientId);
再次,我们注意到实体内嵌外键的优势。在此,[Rdv] 实体使我们能够访问关联的 [Creneau] 依赖项的外键。完成这两处修正后,应用程序即可正常运行。欢迎读者测试本文网站示例下载中提供的 [RdvMedecins-SqlServer-03] 应用程序。
3.7. 结论
我们已成功移植了一个 ASP.NET / NHibernate 应用程序:
![]() |
并将其转换为 ASP.NET / EF 5 应用程序:
![]() |
尽管这种架构本应让我们能够保持 [ASP.NET] 层完整,但出于两个原因,我们不得不对其进行修改:
- 实体类型并不完全一致。NHibernate 实体的主键类型为
int,而 EF 5 则为int?</span>**<span style="color: #000000">。这导致我们在 Web 代码中引入了类型转换; - 两个 ORM 的实体加载模式不同:NHibernate 采用即时加载(Eager Loading),EF 5 采用延迟加载(Lazy Loading)。这促使我们通过添加一个泛型方法来增强 [DAO] 层接口,以便能够通过主键检索实体。
尽管如此,此次移植过程证明相当简单,这再次印证了——如果还需要证明的话——分层架构以及使用 Spring 或其他依赖注入框架进行依赖注入的合理性。
接下来我们将评估数据库管理系统变更对原有架构的影响。我们将把所有现有项目移植到另外四种数据库管理系统上:
- Oracle Database Express Edition 11g Release 2;
- MySQL 5.5.28;
- PostgreSQL 9.2.1;
- Firebird 2.1。
代码将保持不变。仅以下元素会发生变化:
- 实体中用于控制对实体并发访问的字段定义;
- 配置文件 [App.config] 或 [Web.config];
我们将仅对发生变更的元素进行说明。


















































































































































