Skip to content

5. Oracle Database Express Edition 11g Release 2 案例研究

5.1. 安装工具

需要安装的工具如下:

  • 数据库管理系统(DBMS):[http://www.oracle.com/technetwork/products/express-edition/downloads/index.html];
  • 管理工具:EMS SQL Manager for Oracle 免费版 [http://www.sqlmanager.net/fr/products/oracle/manager/download];
  • .NET 版 Oracle 客户端:ODAC 11.2 Release 5 (11.2.0.3.20) 及 Visual Studio 版 Oracle 开发工具:[http://www.oracle.com/technetwork/developer-tools/visual-studio/downloads/index.html]。

在以下示例中,用户“system”的密码为“system”

启动 Oracle [1],然后启动 [SQL Manager Lite for Oracle] 工具,我们将使用该工具来管理 DBMS [2]。

  • 在[3]中,我们连接到一个现有的数据库;
  • 在[4]中,我们使用Oracle XE服务进行连接;
  • 在[5]中,我们指定了XE数据库的名称;
  • 在[6]中,我们使用system/system登录;
  • 在 [7] 中,我们完成向导;
  • 在 [8] 中,我们连接到数据库;
  • 在[9]中,我们已连接成功;
  • 由于我们是以具有扩展权限的系统/system用户身份登录的,因此我们可以,例如,管理用户 [10];
  • 在[11]中,创建一个新用户;
  • 在 [12] 中,该用户将被命名为 [RDVMEDECINS-EF];
  • 在 [13] 中,其密码为 rdvmedecins
  • 在 [14] 中,确认用户创建;
  • 在 [15] 中,用户已创建;
  • 在 [16] 中,用户 [RDVMEDECINS-EF] 同时也是一个数据库模式;
  • 在 [17] 中,创建的用户权限不足。我们通过 SQL 脚本为其授予权限;
  • 在 [18] 中,执行该脚本;
  • 在 [19] 中,我们将尝试以 [RDVMEDECINS-EF] 身份登录,以查看其可执行的操作。为此,我们首先在 [EMS Manager] 中注册一个新数据库;
  • 在 [19] 中,我们通过 XE 服务登录;
  • 在 [20] 中,我们以 RDVMEDECINS-EF / rdvmedecins 身份登录;
  • 在 [21] 中,我们分配一个反映登录用户名称的别名;
  • 在 [22] 中,我们使用提供的凭据连接到 Oracle;
  • 在[22]中,我们成功登录;
  • 在 [23] 中,我们尝试在 [RDVMEDECINS-EF] 模式中创建一个表;
  • 在 [24] 中,我们定义了一个表;
  • 在 [25] 中,我们验证了该表的定义;
  • 在 [26] 中,该表已创建。我们将其删除;
  • 在[27]中,该表已被删除。

现在我们已拥有具备足够权限的用户,将创建一个 VS 2012 项目,该项目将根据实体定义在 [RDVMEDECINS-EF] 架构中创建表。

5.2. 根据实体创建数据库

首先,我们将 [RdvMedecins-SqlServer-01] 项目文件夹复制到 [RdvMedecins-Oracle-01] [1]:

  • 在 [2] 中,在 VS 2012 中,我们将 [RdvMedecins-SqlServer-01] 项目从解决方案中移除;
  • 在 [3] 中,该项目已被移除;
  • 在 [4] 中,我们添加另一个项目。该项目取自我们之前创建的 [RdvMedecins-Oracle-01] 文件夹;
  • 在 [5] 中,加载的项目名为 [RdvMedecins-SqlServer-01];
  • 在 [6] 中,我们将它的名称更改为 [RdvMedecins-Oracle-01]
  • 在 [7] 中,我们将另一个项目添加到解决方案中。该项目取自先前从解决方案中移除的 [RdvMedecins-SqlServer-01] 文件夹;
  • 在 [8] 中,[RdvMedecins-SqlServer-01] 项目已重新添加到解决方案中。

[RdvMedecins-Oracle-01] 项目与 [RdvMedecins-SqlServer-01] 项目完全相同。我们需要进行一些修改。在 [App.config] 中,我们将修改连接字符串和 [DbProviderFactory],这些内容必须根据不同的数据库管理系统(DBMS)进行适配。


<!-- connection chain on base -->
  <connectionStrings>
    <add name="monContexte" connectionString="Data Source=(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521)))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=XE)));User Id=RDVMEDECINS-EF;Password=rdvmedecins;" providerName="Oracle.DataAccess.Client" />
  </connectionStrings>
  <!-- the factory provider -->
  <system.data>
    <DbProviderFactories>
      <remove invariant="Oracle.DataAccess.Client" />
      <add name="Oracle Data Provider for .NET" invariant="Oracle.DataAccess.Client" description="Oracle Data Provider for .NET" type="Oracle.DataAccess.Client.OracleClientFactory, Oracle.DataAccess, Version=4.112.3.0, Culture=neutral, PublicKeyToken=89b483f429c47342" />
    </DbProviderFactories>
  </system.data>
  • 第 3 行:用户名和密码;
  • 第 6–11 行:DbProviderFactory。第 9 行引用了一个我们没有的 DLL [Oracle.DataAccess]。我们可以使用 NuGet [1] 获取它:
  • 在 [2] 中,在搜索框中输入关键词“oracle”
  • 在 [3] 中,选择相应的包 [Oracle Data Provider]。这是 Oracle 的 ADO.NET 连接器;
  • 在 [4] 中,引用已添加;
  • 在 [5] 中,于 [App.config] 文件中,必须指定 DLL 的正确版本。您可以在其属性中找到该版本。

在 [Entities.cs] 文件中,您必须调整将要生成的表的架构。所使用的架构即为拥有这些表的用户名。


  [Table("MEDECINS", Schema = "RDVMEDECINS-EF")]
  public class Medecin : Personne
  {...}
 
  [Table("CLIENTS", Schema = "RDVMEDECINS-EF")]
  public class Client : Personne
  {...}
 
  [Table("RVS", Schema = "RDVMEDECINS-EF")]
  public class Rv
  {...}
 
  [Table("CRENEAUX", Schema = "RDVMEDECINS-EF")]
  public class Creneau
  {...}

我们配置项目构建:

  • 在 [1] 中,我们为将要生成的程序集指定了一个不同的名称;
  • 在 [2] 中,我们还指定了一个不同的默认命名空间;
  • 在 [3] 中,我们指定要运行的程序。

目前阶段没有编译错误。让我们运行 [CreateDB_01] 程序。我们得到了以下异常:

Exception non gérée : System.Data.MetadataException: Le schéma spécifié n'est pas valide. Erreurs :
(11,6) : erreur 0040: Le type rowversion n'est pas qualifié avec un espace de noms ou un alias. Seuls les types primitifs peuvent être utilisés sans qualification.
(23,6) : erreur 0040: Le type rowversion n'est pas qualifié avec un espace de noms ou un alias. Seuls les types primitifs peuvent être utilisés sans qualification.
(33,6) : erreur 0040: Le type rowversion n'est pas qualifié avec un espace de noms ou un alias. Seuls les types primitifs peuvent être utilisés sans qualification.
(43,6) : erreur 0040: Le type rowversion n'est pas qualifié avec un espace de noms ou un alias. Seuls les types primitifs peuvent être utilisés sans qualification.
   à System.Data.Metadata.Edm.StoreItemCollection.Loader.ThrowOnNonWarningErrors
()
   ...
   à RdvMedecins_01.CreateDB_01.Main(String[] args) dans d:\data\istia-1213\c#\d
vp\Entity Framework\RdvMedecins\RdvMedecins-Oracle-01\CreateDB_01.cs:ligne 15

我们记得在 MySQL 中也遇到过同样的错误。这与实体中 Timestamp 字段的类型有关。我们进行相同的修改。在实体中,我们将以下三行代码替换为


    [Column("TIMESTAMP")]
    [Timestamp]
    public byte[] Timestamp { get; set; }

如下所示:


    [ConcurrencyCheck]
    [Column("VERSIONING")]
    public int? Versioning { get; set; }

因此,我们将列类型从 byte[] 更改为 int?。需要注意的是,无论是 SQL Server 还是 MySQL,用于管理访问并发性的表列,每次插入或修改行时都会由数据库管理系统(DBMS)赋予一个值。从现在起,我们将使用一个整型实体字段。在数据库管理系统中,我们将通过存储过程,在每次插入或修改行时将该整数递增 1。

我们对所有四个实体都进行了上述更改,然后重新运行应用程序。随后,我们收到以下错误:

1
2
3
4
5
6
Exception non gérée : System.Data.DataException: An exception occurred while initializing the database. See the InnerException for details. ---> System.Data.ProviderIncompatibleException: DeleteDatabase n'est pas pris en charge par le fournisseur.
   à System.Data.Common.DbProviderServices.DbDeleteDatabase(DbConnection connection, Nullable`1 commandTimeout, StoreItemCollection storeItemCollection)
   ...
   à System.Data.Entity.Internal.LazyInternalContext.InitializeDatabase()
   à RdvMedecins_01.CreateDB_01.Main(String[] args) dans d:\data\istia-1213\c#\d
vp\Entity Framework\RdvMedecins\RdvMedecins-Oracle-01\CreateDB_01.cs:ligne 15

第 1 行表明 Oracle ADO.NET 连接器无法删除现有数据库。让我们来回顾一下发生了什么。 [CreateDB_01.cs] 中的代码如下:


using System;
using System.Data.Entity;
using RdvMedecins.Models;
 
namespace RdvMedecins_01
{
  class CreateDB_01
  {
    static void Main(string[] args)
    {
      // create the database
      Database.SetInitializer(new RdvMedecinsInitializer());
      using (var context = new RdvMedecinsContext())
      {
        context.Database.Initialize(false);
      }
    }
  }
}

第 15 行触发了 [RdvMedecinsInitializer] 类的执行(第 12 行)。该类定义如下:


  public class RdvMedecinsInitializer : DropCreateDatabaseAlways<RdvMedecinsContext>

它继承自 [DropCreateDatabaseAlways] 类,该类会尝试先删除数据库,然后重新创建。我们将类定义修改为:


  public class RdvMedecinsInitializer : CreateDatabaseIfNotExists<RdvMedecinsContext>

只有当数据库不存在时,才会创建它。 我们重新运行 [CreateDB_01.cs] 后,不再出现错误。但在 [EMS Manager] 中,我们发现 [RDVMEDECINS-EF] 数据库仍然为空。因为 EF 5 检测到数据库已存在,因此未执行任何操作。它仅在数据库不存在时才会采取行动。由此,我们陷入了一个循环。实际上,连接数据库管理系统 (DBMS) 的连接字符串如下:


  <connectionStrings>
    <add name="monContexte" connectionString="Data Source=(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521)))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=XE)));User Id=RDVMEDECINS-EF;Password=rdvmedecins;" providerName="Oracle.DataAccess.Client" />
</connectionStrings>

第 2 行:连接字符串使用用户名而非数据库名。该用户必须已存在。

因此,我们必须使用 [EMS Manager for Oracle] 工具手动创建 [RDVMEDECINS-EF] 数据库。本文不会详细描述每个步骤,仅介绍最重要的步骤。

Oracle 数据库将如下所示:

这些表拥有与前两个示例中相同的表所具有的主键和外键。其中,外键特别设置了 ON DELETE CASCADE 属性。

序列

此处我们创建了 Oracle 序列。这些序列用于生成连续的数字。共有 5 个序列 [1]。

  • 在[2]中,我们可以看到[SEQUENCE_CLIENTS]序列的属性。它生成以1为增量、从1开始直至一个非常大的值的连续数字。

所有序列均基于相同的模型构建。

  • [SEQUENCE_CLIENTS] 将用于生成 [CLIENTS] 表的主键;
  • [SEQUENCE_DOCTORS] 将用于生成 [DOCTORS] 表的主键;
  • [SEQUENCE_SLOTS] 将用于生成 [SLOTS] 表的主键;
  • [SEQUENCE_RVS] 将用于生成 [RVS] 表的主键;
  • [SEQUENCE_VERSIONS] 将用于生成所有表中 [VERSIONING] 列的值。

触发器

触发器是在表中发生某个事件(插入、更新、删除)之前或之后由数据库管理系统(DBMS)执行的存储过程。我们共有 8 个触发器 [1]:

 

让我们来看一下 [TRIGGER_PK_CLIENTS] 触发器的 DDL 代码,该触发器用于填充 [CLIENTS] 表的主键:

1
2
3
4
5
6
7
8
CREATE TRIGGER "RDVMEDECINS-EF".TRIGGER_PK_CLIENTS
 BEFORE INSERT
 ON "RDVMEDECINS-EF".CLIENTS
REFERENCING NEW AS NEW OLD AS OLD FOR EACH ROW
BEGIN
        SELECT SEQUENCE_CLIENTS.NEXTVAL INTO :new.ID from DUAL;
END;
/
  • 第 1-5 行:在 [CLIENTS] 表的每次 INSERT 操作之前;
  • 第 6 行:[ID] 列将从 [SEQUENCE_CLIENTS] 序列中获取下一个值。因此,主键将由该序列提供连续的值。

触发器 [TRIGGER_PK_MEDECINS、TRIGGER_PK_CRENEAUX、TRIGGER_PK_RVS] 的工作原理与此类似。

让我们来看一下触发器 [TRIGGER_VERSIONS_CLIENTS] 的 DDL 代码,该触发器用于填充 [CLIENTS] 表的 [VERSIONING] 列:

CREATE TRIGGER "RDVMEDECINS-EF".TRIGGER_VERSION_CLIENTS
 BEFORE INSERT OR UPDATE
 OF
  VERSIONING
 ON "RDVMEDECINS-EF".CLIENTS
REFERENCING NEW AS NEW OLD AS OLD FOR EACH ROW
BEGIN
        SELECT SEQUENCE_VERSIONS.NEXTVAL INTO :new.VERSIONING from DUAL;
END;
/
  • 第 1-2 行:在 [CLIENTS] 表上的每次 INSERT 或 UPDATE 操作之前;
  • 第 8 行:[VERSIONING] 列将从 [SEQUENCE_VERSIONS] 序列中获取下一个值。因此,[VERSIONING] 列将拥有由该序列提供的连续值。

触发器 [TRIGGER_VERSION_MEDECINS、TRIGGER_VERSION_CRENEAUX、TRIGGER_VERSION_RVS] 的工作原理类似。这四个 [VERSIONING] 列的值均来自同一序列。

用于生成 Oracle 数据库表 [RDVMEDECINS-EF] 的脚本已放置在 [RdvMedecins / databases / oracle] 文件夹中。读者可以加载并运行该脚本以创建这些表。

完成此操作后,即可运行项目中的各项程序。这些程序生成的结果与 SQL Server 环境下的结果相同,但 [ModifyDetachedEntities] 程序除外——该程序会因与 MySQL 环境相同的理由而崩溃。解决此问题的方法与 MySQL 环境相同。 只需将 [RdvMedecins-MySQL-01] 项目中的 [ModifyDetachedEntities] 程序复制到 [RdvMedecins-Oracle-01] 项目中即可。但这又引发了一个新问题:

1
2
3
4
5
6
7
8
9
1-----------------------------
Client [206,x,x,x,616]
2-----------------------------
Client [206,x,x,y,617]

Exception non gérée : System.Data.Entity.Infrastructure.DbUpdateConcurrencyException: Une instruction de mise à jour, d'insertion ou de suppression dans le magasin a affecté un nombre inattendu de lignes (0). Des entités ont peut-être été modifiées ou supprimées depuis leur chargement. Actualisez les entrées ObjectStateManager. ---> System.Data.OptimisticConcurrencyException: Une instruction de mise à jour, d'insertion ou de suppression dans le magasin a affecté un nombre inattendu de lignes (0). Des entités ont peut-être é modifiées ou supprimées depuis leur chargement. Actualisez les entrées ObjectStateManager.
   ...
   à RdvMedecins_01.ModifyDetachedEntities.Main(String[] args) dans d:\data\istia-1213\c#\dvp\Entity Framework\RdvMedecins\RdvMedecins-Oracle-01\ModifyDetachedE
ntities.cs:ligne 56
  • 第 1–4 行:已成功更新脱离的客户端;
  • 第 6 行:已知的异常。这是在尝试修改实体但未持有正确版本时出现的异常。然而,在此情况下,我们并非要修改实体,而是要删除该实体:

      // remove out-of-context entity
      using (var context = new RdvMedecinsContext())
      {
        // here we have a new empty context
        // put client1 in the context in a deleted state
        context.Entry(client1).State = EntityState.Deleted;
        // save the context
        context.SaveChanges();
}

EF 5 拒绝从数据库中删除 client1,因为 client1(第 6 行)的版本不一致。我们在 MySQL 中从未遇到过这个问题。我们逐渐意识到,针对不同数据库管理系统(DBMS)的 ADO.NET 连接器存在细微差异。我们通过以下方式进行修正:


using (var context = new RdvMedecinsContext())
      {
        // here we have a new empty context
        // put client1 in the context to delete it
        context.Clients.Remove(context.Clients.Find(client1.Id));
        // save the context
        context.SaveChanges();
      }

而且它有效。

5.3. 基于 EF 5 的多层架构

让我们回到第7页第2段中描述的案例研究。

我们将首先构建[DAO]数据访问层。为此,我们将VS 2012控制台项目[RdvMedecins-SqlServer-02]复制为[RdvMedecins-Oracle-02] [1]:

  • 在[2]中,我们删除[RdvMedecins-SqlServer-02]项目;
  • 在 [3] 中,我们将一个现有项目添加到解决方案中。该项目取自刚刚创建的 [RdvMedecins-Oracle-02] 文件夹;
  • 在 [4] 中,新项目的名称与被删除的项目相同。我们将更改其名称;
  • 在 [5] 中,我们已更改了项目名称;
  • 在[6]中,我们修改了其中的一些属性,例如此处的程序集名称;
  • 在[7]中,删除了[Models]文件夹,并用[RdvMedecins-Oracle-01]项目中的[Models]文件夹替换。这是因为这两个项目共享相同的模型。
  • 在 [8] 中,该项目的当前引用;
  • 在 [9] 中,已使用 NuGet 工具添加了 Oracle ADO.NET 连接器。

在 [App.config] 文件中,将 SQL Server 数据库信息替换为 Oracle 数据库信息。这些信息可在 [RdvMedecins-Oracle-01] 项目的 [App.config] 文件中找到:


<!-- connection chain on base -->
  <connectionStrings>
    <add name="monContexte" connectionString="Data Source=(DESCRIPTION=(ADDRESS_LIST=(ADDRESS=(PROTOCOL=TCP)(HOST=localhost)(PORT=1521)))(CONNECT_DATA=(SERVER=DEDICATED)(SERVICE_NAME=XE)));User Id=RDVMEDECINS-EF;Password=rdvmedecins;" providerName="Oracle.DataAccess.Client" />
  </connectionStrings>
  <!-- the factory provider -->
  <system.data>
    <DbProviderFactories>
      <remove invariant="Oracle.DataAccess.Client" />
      <add name="Oracle Data Provider for .NET" invariant="Oracle.DataAccess.Client" description="Oracle Data Provider for .NET" type="Oracle.DataAccess.Client.OracleClientFactory, Oracle.DataAccess, Version=4.112.3.0, Culture=neutral, PublicKeyToken=89b483f429c47342" />
    </DbProviderFactories>
  </system.data>

Spring 管理的对象也会发生变化。目前我们有:


  <!-- 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>

第 7 行引用了 [RdvMedecins-SqlServer-02] 项目的程序集。该程序集现已更名为 [RdvMedecins-Oracle-02]。

完成上述操作后,我们即可运行 [DAO] 层测试。首先,必须确保数据库已填充数据(使用 [RdvMedecins-Oracle-01] 项目中的 [Fill] 程序)。测试程序运行成功。

我们按照处理 [RdvMedecins-SqlServer-02] 项目的方法生成该项目的 DLL,并将所有项目 DLL 集中到 [RdvMedecins-Oracle-02] 目录下新建的 [lib] 文件夹中。这些文件将作为后续 [RdvMedecins-Oracle-03] Web 项目的引用。

  

现在,我们准备开始构建应用程序的 [ASP.NET] 层:

我们将从 [RdvMedecins-SqlServer-03] 项目开始。我们将此项目的文件夹复制为 [RdvMedecins-Oracle-03] [1]:

  • 在 [2] 中,使用 VS 2012 Express for the Web,我们打开 [RdvMedecins-Oracle-03] 文件夹中的解决方案;
  • 在 [3] 中,我们将解决方案名称和项目名称均进行修改;
  • 在 [4] 中,查看项目当前的引用;
  • 在 [5] 中,我们将其删除;
  • 在 [6] 中,用我们刚刚存储在 [RdvMedecins-Oracle-02] 项目内 [lib] 文件夹中的 DLL 引用替换它们。

剩下的就是修改 [Web.config] 文件。我们将该文件的当前内容替换为 [RdvMedecins-Oracle-02] 项目中 [App.config] 文件的内容。完成此操作后,运行 Web 项目。它运行正常。在运行 Web 应用程序之前,切记要先填充数据库。