1. 腾来网首页
  2. 开发者博客
  3. 程序开发

EF CodeFirst 必须要解决的问题

Entity Framework有三种模式:Model First、DB First和 CodeFirst,这里只谈CodeFirst。实际项目中如果采用了CodeFirst,那么必定会碰见下面这些问题:而且必须解决,否则开发及项目迭代过程中必定会有各类的困惑,以至于放弃使用EF CodeFirst。

以本人对EF CodeFirst 的学习过程,这些问题有:

问题1:数据库的表和模型(Model)的映射匹配冲突了怎么办?比如:先用CodeFirst从无到有创建了数据库,如果后续的开发过程中直接修改了数据库,直接修改项目里的Model匹配数据库的修改就可以了吗?或者修改Model后对应直接修改数据库表就可以了吗?

要回答这个问题,实际做一做也许就能找到客观的答案:

这里使用VS2017 新建一个类库项目和一个控制台项目,类库中添加EntityFramework的引用;

接着在Entity层中新建一个UserEntity模型:

public class UserEntity
{
public Guid Id { get; set; }
public string UserName { get; set; }
public string UserDesc { get; set; }
public DateTime? CreateDate { get; set; }
}
添加一个Entity和数据表的映射关系:如下面的代码所示:这里把User 指向数据表UserT, 并且Id为主键,且GUID类型自动由数据库生成:(EF 创建UserT表创建了DB默认值约束:ALTER TABLE [dbo].[UserT] ADD  DEFAULT (newsequentialid()) FOR [Id])

public class UserMap : EntityTypeConfiguration
{
public UserMap()
{
this.ToTable(“UserT”);
this.HasKey(t => t.Id);
this.Property(t => t.Id).HasDatabaseGeneratedOption(DatabaseGeneratedOption.Identity);

    }
}

定义一个上下文的引用,派生至System.Data.Entity.DbContext, 用于数据库读写:

public class SqlServerDbContext : DbContext
{
public SqlServerDbContext() : base(“name=EFCodeFirst”)
{
Database.SetInitializer(new CreateDatabaseIfNotExists());
}

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        dynamic user = Activator.CreateInstance(typeof(UserMap));
        modelBuilder.Configurations.Add(user);

        base.OnModelCreating(modelBuilder);
    }
}

回到Console项目中:添加App.config

添加数据库连接字符串:


在main函数执行添加User操作:

static void Main(string[] args)
{
using (SqlServerDbContext context = new SqlServerDbContext())
{
context.Set().Add(new UserEntity() { UserName = “U001”, UserDesc = “add user” });
context.SaveChanges();
}
Console.WriteLine(“OL”);
Console.Read();
}
执行测试结果:

回到问题:直接修改EF 自动生成的db表,先做如下实验:

实验1:新增无相关性的表,例如新建T1表;此时我们的EF测试程序中没有添加T1表的模型及映射,自然不会有冲突;

实验2:在UserT表新增列 CreateBy:然后运行程序,新增User没有问题;

实验3:改变程序中UserEntity的属性UserName 成UserDisplayName, 数据库UserT表的栏位对应修改一致;此时再次测试

看到上图提示很明显了,上下文的模型已经在数据库创建后发生更改了;要快速解决这个问题可以这样:

 public SqlServerDbContext() : base(“name=EFCodeFirst”)
        {
            Database.SetInitializer(null);
        }

原理是:EF中DbContext首次加载时,OnModelCreating会检查__MigrationHistory表,如果DbContext与模型不匹配则会报错:

可以用Sql Profiler检测下sql的执行,搜索_MigrationHistory,可以看下图所示的sql执行:

Database.SetInitializer(null);设置null后将不在检测__MigrationHistory,首次执行EF时也不会自动生成数据库,且还会报错:基础提供程序在 Open 上失败;SqlException: 无法打开登录所请求的数据库 。登录失败。
用户 ‘sa’ 登录失败。

这里建议是当数据库生成后把__MigrationHistory的检测设置为null,可以优化EF首次执行的加载效率;

当然,你还可以换其他的Database.SetInitializer策略:

比如:DropCreateDatabaseIfModelChanges 、DropCreateDatabaseAlways等,但如果是正式环境,这样做会有没顶之灾。

话题再转回来:如果不设置null,解决模型变更后的异常:方法可以这样:

简单来说就是:enable-migration_> add-migration_>update-database

详细来看下过程:

设置Entity层为启动项目(重要,否则报异常):VS2017中菜单:“工具”——>”Nuget包管理器”——>”程序包管理器控制台”

此时再来看下数据库结构和__MigrationHistory表中的变化

这里有个一个缺陷:UserName改为UserDisplayName后 EF的处理是把UserName栏位删除,然后添加UserDisplayName栏位:

public partial class user : DbMigration
{
public override void Up()
{
AddColumn(“dbo.UserT”, “UserLoginAccount”, c => c.String());
AddColumn(“dbo.UserT”, “UserDisplayName”, c => c.String());
DropColumn(“dbo.UserT”, “UserName”);
}

    public override void Down()
    {
        AddColumn("dbo.UserT", "UserName", c => c.String());
        DropColumn("dbo.UserT", "UserDisplayName");
        DropColumn("dbo.UserT", "UserLoginAccount");
    }
}

这样的处理显然是不可接受的,所以在实际开发项目中应该避免这样的操作,如果一定需要改列名,可以尝试保留原来的列,新增一列处理,最大限度保留原有数据;

回答第一个问题:1:数据库的表和模型(Model)的映射匹配冲突了怎么办?比如:先用CodeFirst从无到有创建了数据库,如果后续的开发过程中直接修改了数据库,直接修改项目里的Model匹配数据库的修改就可以了吗?或者修改Model后对应直接修改数据库表就可以了吗?

答:如果EF Database初始化策略为null:Database.SetInitializer(null);

这样做是可以的。即使不是为null 的策略,如果数据库的修改同模型的匹配不会发生冲突,例如数据建立唯一约束,建立索引,新增栏位等操作也不会引起EF 报上下文数据创建后的模型改变异常;其他情形则需要通过EF 数据迁移来完成;

问题2:项目上线正式环境后,如果程序调整,正式环境数据库如何升级;

这里有两种方式:方式一:执行update-database -Verbos 则可以生成此次更新的数据库脚本,把它记录下来放在正式环境中执行就可以了;如下图所示:

实际开发中可能有多次迭代更新,这样就需要记录每次update 的升级脚本,如果每次更新是不同的开发人员来完成,容易造成混乱,不方便管理;

方式二:自动迁移更新数据库:

internal sealed class Configuration : DbMigrationsConfiguration
{
public Configuration()
{
AutomaticMigrationsEnabled = true;
AutomaticMigrationDataLossAllowed = false;
ContextKey = “MyCodeFirst.Entity.SqlServerDbContext”;
}

    protected override void Seed(MyCodeFirst.Entity.SqlServerDbContext context)
    {
        //  This method will be called after migrating to the latest version.
        //  You can use the DbSet<T>.AddOrUpdate() helper extension method 
        //  to avoid creating duplicate seed data.
    }
}

在SqlServerDbContext的构造函数中使用MigrateDatabaseToLatestVersion 方式:

Database.SetInitializer(new MigrateDatabaseToLatestVersion());

这样EF框架自动帮我们做好了数据库的升级;

问题3:实际开发中,数据库都有专人负责设计,数据库在前,此时CodeFirst模式如何使用?

CodeFirst 代码优先:模型是先行设计的,数据库通过模型及模型到数据库表的映射来完成的。比较适合领域驱动设计的开发方式。如果是数据设计已经存在,EF也可以使用CodeFirst模式。

如下图示:

以这种方式添加的Model实际使用时会提示xxx对象已经存在:

通常的原因是EF会查询_MigrationHistory, 比对Model 不存在于数据库中则会生成Create Table 的语句;

处理的方式是不生成Create Table脚本并能在_MigrationHistory里自动迁移就可以了。做法是:

如下图所示:先add-migration xxx

EF自动生成迁移历史后,打开最后生成的迁移文件,注释掉其中的CreateTable,

然后:update-database 

再次执行程序,则不会报对象已经存在的异常了。注释的CreateTable最好取消注释,防止切换数据库连接自动迁移不能生成Create Table脚本。

个人的困惑:觉得使用EF CodeFirst就要通过手工编写模型,后生成DB。但手工编码不直观,而且工作量大,有点费力。如果是借助一些工具来辅助设计,通过工具来生成模型,又有些像Model First。即使自己编写程序来自动生成代码也是要先有可以参照的数据源,比如先有数据库,从数据库读取表的栏位字段及类型等信息后程序批量生成模型,这样做就有些像DB First。且EF发展后面只有CodeFirst。也许是没有真正领会到CodeFirst的精髓!

总结一下:选择EF CodeFirst不管通过何种方式来生成模型,需要保证数据库的表实体和程序模型的对应关系,碰见CodeFirst提示的冲突时,可以通过设置自动迁移或手动迁移完成。知道EF CodeFirst运行原理后可以就开发过程中碰见的各类问题灵活处理。在团队开发过程中,异常或冲突或许会经常发生,要制定一些规则或做好技术防范:比如专人负责迁移,做好备份或做好源代码管控,万一出现错误还可以迁移到上一步等。

发布者:腾来分享,转转请注明出处:http://tenglai.net/blog/coding/2672.html

发表评论

电子邮件地址不会被公开。

联系我们

18702030367

在线咨询:点击这里给我发消息

邮件:Jason@tenglai.cn

工作时间:周一至周五,9:30-18:30,节假日休息

QR code