EF6 CodeFisrt支持Oracle

EF6 CodeFisrt支持Oracle


EF说是支持多数据库,但真做起来太多坑了,编程这个词以后要换换,叫填坑好了。这次把我在做EF6 CodeFisrt支持Oracle数据库过程中遇到的坑写下来,给需要的人减少点填坑的痛苦。

先说下使用环境

  • EF6.1.3
  • CodeFirst
  • Oracle版本我用的是11.2
  • Oracle Provider用的是Oracle官方的ODP.NET, Managed Driver

Oracle官方文档

http://docs.oracle.com/cd/E56485_01/win.121/e55744/entityCodeFirst.htm#ODPNT8309

搭建环境

这里只说使用代码的配置方式,App.config或Web.config配置方式参照官方文档做就好。

1. 下载Oracle Provider

在vs的管理解决方案的NuGet程序包中搜索Oracle,找到ODP.NET(这是个简写),有两个,忽略Unmanaged,下载带Managed的。

2. 添加dll引用

在相关项目中添加下面两个dll引用:
Oracle.ManagedDataAccess.dll
Oracle.ManagedDataAccess.EntityFramework.dll

3. SetProvider

定义DbMigrationsConfiguration类

internal sealed class MyMigrationsConfiguration : DbMigrationsConfiguration<MyContext>
{
    public DbConfiguration()
    {
        AutomaticMigrationsEnabled = true;
        AutomaticMigrationDataLossAllowed = true;
    }
    protected override void Seed(T context)
    {
        //种子数据
    }
}

定义DbConfiguration类

public class MyConfiguration : DbConfiguration
{
    public MyConfiguration()
    {
        SetDefaultConnectionFactory(new OracleConnectionFactory());
        SetProviderServices("Oracle.ManagedDataAccess.Client", EFOracleProviderServices.Instance);
        SetProviderFactory("Oracle.ManagedDataAccess.Client", new OracleClientFactory());
    }
}

给你的DbContext类添加Attribute

[DbConfigurationType(typeof(EFConfiguration))]
public class MyContext : DbContext
{
}

4. 创建数据库

使用下面两段代码之一初始化数据库:

Database.SetInitializer(new MigrateDatabaseToLatestVersion<MyContext, MyMigrationsConfiguration>());
using (var ctx = new MyContext())
{
    ctx.Database.Initialize(true);
}

new DbMigrator(new MyMigrationsConfiguration()).Update();

网上的几乎所有的例子都是让人在NuGet命令行中敲命令升级数据库,试问客户现场的生产数据库如何去升级?所以我们使用自动迁移,必须做到在没有开发人员参与下,由实施人员甚至是用户自己去点击个按钮,就自动根据实体去创建或者修改数据库。

  1. 不出意外,这时候肯定会出错,出什么错都有可能,我遇到的错误是:

System.InvalidOperationException: 序列不包含任何匹配元素

通过翻看EF的源码,发现是实体类定义中用了ColumnAttribute.TypeName指定了SQLServer中的类型,如“NVarChar(4000)”,改用StringLength去指定长度,另外发现如果设置为int.MaxValue,它在sqlserver上会是NVarChar(MAX)类型,在Oracle上是NCLOB。

  1. 继续运行,出下面或者类似的错误:

ORA-01918: 用户'SCOTT'不存在解决

解决:在Oracle中添加用户,然后在MyContext类中加入下面代码

protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
    base.OnModelCreating(modelBuilder);
    modelBuilder.HasDefaultSchema("你的Oracle用户名");
}

注意:添加Oracle用户时至少:设置Unlimited Tablespace、Create Session权限和resource角色;

  1. 继续运行,错误:

不支持影响迁移历史记录系统表的位置的自动迁移(例如默认架构更改)。对于影响迁移历史记录系统表的位置的操作,请使用基于代码的迁移。

翻看EF的源码,发现它会判断Schema是否为默认Schema,而默认的Schema定义的是一个常量“dbo”。同时ODP.NET文档中也写了必须是“dbo”Schema。

Code First Automatic Migrations is limited to working with the dbo schema only. Due to this limitation it is recommended to use
code-based migrations, that is, add explicit migrations through the
Add-Migration command.

这么问题就来了,无法自动迁移怎么办,我现在的解决办法是只能使用小写的“dbo”用户,这样很肯定为客户现场的部署带来未知的麻烦。我之前还准备修改EF源码,去掉这个默认Schema的限制,重新编译它,但又当心他们这么做是某些硬性条件导致,所以也就没去尝试,如果有人知道怎么解决这个限制还请告知。
如果你不需要自动迁移,那么可能问题简单很多,你可以自由的使用Oracle用户名。不过有可能你需要给__MigrationHistory表的实体HistoryRow设置Schema,做法很简单,参考https://msdn.microsoft.com/en-us/library/dn456841(v=vs.113).aspx

  1. 使用小写“dbo”做Oracle用户名

Oracle中所有名称都默认是大写的,如果需要区分大小写或者说是按你输入原文做名称,就需要加双引号,sql语句中也是同样。所以建小写“dbo”用户名的时候加英文的双引号就好。
使用小写“dbo”做用户名后,自动迁移就顺利了,当然错误还是会有的,根你软件的复杂情况有关系,比较容易出现的错误如下,都比较好解决:

ORA-00972: 标识符过长

Oracle限制似乎是所有名称30个字符,你可能有表名超过了限制。

ORA-00955: 名称已由现有对象使用

表名等被使用了,可能的原因是__MigrationHistory表中记录的上一次迁移没有某个表,但实际创建成功,出现的可能性很小,我是遇到了。

ORA-02264: 名称已被一现有约束条件占用

这个是因为某个表的同一个列加了多个外键导致,第一个外键约束会创建成功,第二个外键约束会使用相同名称导致重名。

  1. 数据库函数、过程等

我是在Seed方法中用DbContext对象的Database.ExecuteSqlCommand去创建函数、过程、触发器等的sql脚本的,这块出的问题比较单一了,都是sql语句的错,自行解决就好。
ODP.NET是声明不支持表值函数的,我在拦截器中修改sql让它支持,做法如下:
定义拦截器类:

public class OracleInterceptor : IDbCommandInterceptor
{
        public void ScalarExecuting(DbCommand command, DbCommandInterceptionContext<object> interceptionContext)
        {
            NReplace(command, interceptionContext.ObjectContexts.First());
        }
        public void ReaderExecuting(DbCommand command, DbCommandInterceptionContext<DbDataReader> interceptionContext)
        {
            NReplace(command, interceptionContext.ObjectContexts.First());
        }
        private static void NReplace(DbCommand command, ObjectContext ctx)
        {
                if (!command.CommandText.StartsWith("CREATE OR REPLACE"))
                {
                    foreach (var item in ctx.MetadataWorkspace.GetItems<EdmFunction>(DataSpace.SSpace).Where(i => i.NamespaceName == "CodeFirstDatabaseSchema"))
                    {
                        if (item.ReturnParameter == null || item.ReturnParameter.TypeUsage.EdmType.BuiltInTypeKind != BuiltInTypeKind.CollectionType) continue;
                        var strs = new List<string>();
                        var methodName = item.Name;
                        var str = string.Format(@"""dbo"".""{0}""", methodName);
                        var i = 0;
                        while ((i = command.CommandText.IndexOf(str, i)) >= 0)
                        {
                            var j = i + str.Length;
                            var m = 0;
                            for (; j < command.CommandText.Length; j++)
                            {
                                if (command.CommandText[j] == '(')
                                {
                                    m++;
                                }
                                else if (command.CommandText[j] == ')')
                                {
                                    m--;
                                }
                                if (m == 0)
                                {
                                    break;
                                }
                            }
                            strs.Add(command.CommandText.Substring(i, j - i + 1));
                            i = j;
                        }
                        foreach (var s in strs)
                        {
                            command.CommandText = command.CommandText.Replace(s, string.Format("table({0})", s));
                        }
                    }
                }
        }
}

然后在MyConfiguration构造函数中加一行

AddInterceptor(new OracleInterceptor());

5. Linq To Entities

在数据库创建并可以更新成功后,就开始把软件跑起来了,这个时候出的问题最多的就是Linq编译出来的sql执行错误。我遇到的错误有下面几个:

ORA-12704: 字符集不匹配

这个一般是因为,非Unicode字符串被当成Unicode字符串使用,常见于带有单引号字符串的sql中,并且可能存在字符串与字段连接操作,需要将'str'改成N'str'才行。
解决办法是:给DbConfiguration类中添加一个拦截器,拦截器类中用正则去找出字符串,全部替换成带前缀N。

ORA-00932: 数据类型不一致: 应为 NCHAR, 但却获得 NCLOB

这个错误也在拦截器中替换TO_NCLOB为TO_NCHAR。

OUTER APPLY not supported by Oracle

Oracle可能是没有OUTER APPLY这样的写法,但Linq转出来的Sql却总是含有它,导致大量的Linq出错,没办法,只能一点点改了,看官有好办法麻烦告知。
这个错误一般都是linq中有子查询,并且子查询有join或者子查询还有其他子查询,也可能Include方法也会导致这个问题,我现在还是换个写法来解决,如改成left join。

posted @ 2017-06-22 21:09  Rick Carter  阅读(3505)  评论(2编辑  收藏  举报