最近在学习EntityFramework Core的相关东西,其实数据库访问的方式有很多种比如有上古神器ADO.NET ,还有从java泊来的NHibernate等,当然还包括之前的Entity Framework,还有比较轻量级的Dapper等。关于数据访问,当然可以用手写SQL,然后用ADO.NET那一套来处理,但是都2020年了,这么搞显然不够高级,而且不够效率,最重要的是容易996。但是据我所知,并不是所有的公司都会去用ORM,就拿我曾经待过的还算比较大的一家公司来说,一般开发人员是接触不到DB的,对数据库的一些操作操作,有专门的DBA来管理,比如DBA会对某张表默认生成CRUD的存储过程,去调用就可以了,对于查询,一般的也是开发人员用ADO.NET 自己写SQL语句,最多就是把ADO.NET封装一下,提供给一般的开发人员使用。甚至一般的LINQ在有些公司都是禁止使用的。

     确实,自己写SQL语句比较直接,有一种一切都在掌控中的感觉,用LINQ或者其他一些ORM,小白通常不知道它在背后会给你生产什么奇怪的SQL语句。并且如果对LINQ或者一些ORM的用法不熟悉,搞不好还会产生比较严重的性能问题。但是这种一刀切,不去了解然后明令禁止的做法也不可取,我们应该去了解其背后的逻辑避免不必要的性能损失,同时利用好其优点,这样才能给我们的开发带来效率,这样才能避免996进ICU😂。

    以前在做开发的时候,被告知数据库建库不要去建外键,同时对一些数据进行适当的冗余。这样能提高效率,现在来看,对于外键的问题,我觉得还是需要的,毕竟能够保证数据的完整性。

    回到EF,EF有三种模式一种是Code-first,代码优先,就是先建模,然后通过EF生成对应的数据库表以及表和表之间的关联等等(数据库还需要自己创建,表可以通过EF生成),一种是Database-first,就是先有数据,然后根据数据库生成相关类以及访问的脚手架代码。还有一种是对于已经存在的数据,手动编写映射脚本来对应DB。

Code-First遇到的问题

    我最近学习EntityFramework Core看的是这本Pro Entity Framework Core 2 for ASP.NET Core MVC,不得不说,这个作者真是高产,写了不知道有多少本ASP.NET Core MVC的书了,微软发布一个新版本他就更新一个版本,当然说到书上来,这本书还是不错,通俗易懂。

    但是跟大部分的教材一样,有一个小问题:

     一般的教材这样,教你都是快速入门,怎样简单怎样方便怎么来,完全不会考虑实际项目中的项目结构,代码效率之类的,这样无可厚非。但是实际开发中,可能会遇到很多问题。比如,我在使用代码优先生成数据库就遇到问题了。我的项目结构是这样的:

    

      数据库相关的基础设置类,都定义在了Odyssey.Infrastructure里了。启动项目是Odyssey.API,是一个WebAPI项目。在我的项目中,DbContext如下,就是本博客的结构,基本为Moonglade的原始结构。

 public class OdysseyBlogContext : DbContext,IUnitOfWork
    {
        private readonly IMediator _mediator;
        public OdysseyBlogContext(DbContextOptions<OdysseyBlogContext> options, IMediator mediator) : base(options)
        {
            _mediator = mediator ?? throw new ArgumentNullException(nameof(mediator));
        }

        public DbSet<Post> Posts { get; set; }

        public DbSet<PostPublishEntity> PostPublishes { get; set; }

        public DbSet<PostExtensionEntity> PostExtensions { get; set; }

        public DbSet<TagEntity> Tags { get; set; }
        public DbSet<PostTagEntity> PostTags { get; set; }

        public DbSet<CategoryEntity> Categories { get; set; }
        public DbSet<PostCategoryEntity> PostCategories { get; set; }

        public DbSet<CommentEntity> Comments { get; set; }

        public DbSet<CommentReplyEntity> CommentReplies { get; set; }

        public DbSet<CustomPageEntity> CustomPages { get; set; }

        public DbSet<FriendLinkEntity> FriendLinks { get; set; }

        public DbSet<PingbackHistoryEntity> PingbackHistories { get; set; }

        public async Task<bool> SaveEntitiesAsync(CancellationToken cancellationToken = default(CancellationToken))
        {
            await _mediator.DispatchDomainEventsAsync(this);
            var result = await base.SaveChangesAsync(cancellationToken);
            return true;
        }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            modelBuilder.ApplyConfiguration(new CategoryEntityConfiguration());
            modelBuilder.ApplyConfiguration(new CommentEntityConfiguration());
            modelBuilder.ApplyConfiguration(new CommentReplyEntityConfiguration());
            modelBuilder.ApplyConfiguration(new PostCategoryEntityConfiguration());
            modelBuilder.ApplyConfiguration(new PostEntityConfiguration());
            modelBuilder.ApplyConfiguration(new PostExtensionEntityConfiguration());
            modelBuilder.ApplyConfiguration(new PostPublishEntityConfiguration());
            modelBuilder.ApplyConfiguration(new TagEntityConfiguration());
            modelBuilder.ApplyConfiguration(new PostTagEntityConfiguration());
            modelBuilder.ApplyConfiguration(new CustomPageEntityConfiguration());
            modelBuilder.ApplyConfiguration(new FriendLinkEntityConfiguration());
            modelBuilder.ApplyConfiguration(new PingbackHistoryEntityConfiguration());
        }
    }

      这个DbContext实体映射是放在一个名为“Odyssey.Infrastructure” 类库里的。在WebAPI里面,指定好字符串:

  public static IServiceCollection AddCustomDbContext(this IServiceCollection services, IConfiguration configuration)
        {
            services.AddEntityFrameworkSqlServer()
                   .AddDbContext<OdysseyBlogContext>(options =>
                   {
                       options.UseSqlServer(configuration[$"ConnectionStrings:{Constants.DbConnectionName}"],
                           sqlServerOptionsAction: sqlOptions =>
                           {
                               sqlOptions.MigrationsAssembly(typeof(OdysseyBlogContext).GetTypeInfo().Assembly.GetName().Name);
                               sqlOptions.EnableRetryOnFailure(maxRetryCount: 10, maxRetryDelay: TimeSpan.FromSeconds(30), errorNumbersToAdd: null);
                           });
                   },
                       ServiceLifetime.Scoped  //Showing explicitly that the DbContext is shared across the HTTP request scope (graph of objects started in the HTTP request)
                   );
 
            return services;
        }

    然后进行注入:

 // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddControllers();
            services.AddMemoryCache();
            services.AddCustomDbContext(_configuration);
            ...........
        }

         下面,启用命令行,对于项目,创建migration,就是根据模型生成创建表的SQL脚本。于是按照上述教程里,切换到WebAPI项目目录,调用dotnet ef migrations Add Initial,不出意外会看到如下错误:

          

        sqlOptions.MigrationsAssembly(typeof(Startup).GetTypeInfo().Assembly.GetName().Name);  这个地方,无论是改成” Odyssey.API”项目,还是” Odyssey.Infrastructure”都会报上述错误。

原因在于跟教程里把启动项目,跟DbContext放在同一个项目里不同,一般的开发里,通常都是启动项目跟DbContext是不同项目里的,对着启动项目进行生成Migrations就回报上述错误。

那现在,如果直接对包含DbContext的” Odyssey.Infrastructure”项目来生成Migration呢?

        将命令行启动目录切换到 Odyssey.Infrastructure,然后执行上述操作。

      

      报如上错误,因为Infrastructure项目是一个.NET Stand库,似乎上述命令不支持,没办法,难道就没办法了吗?

解决办法

    进入了死胡同,如是想办法怎么解决。后来发现,使用ef,除了在命令行使用dotnet ef命令之外,还可以使用NugGet内嵌在Visual Studio里的程序包管理控制台命令:

   

        打开后,可以在下面看到命令行,现在我们在命令行下,输入:Add-Migration Initial

    如果报错,可能没有安装以下包:

    成功之后,切换到WebAPI项目,再次输入Add-Migration Initial 创建迁移。

     在这个里面创建迁移跟在dotnet ef里面的命令不一样,要用Add-Migration 命令。对启动项目来创建迁移,报错,提示在启动项目里找不到包含DbContext的程序集。

     于是,我们把默认项目切换到包含DbContext的项目 Odyssey.Infrastructure,再次输入命令:

       提示无法创建DbContext,还给了个链接。仔细想一下,这是个类库,我没有地方往里面注入连接字符串,他当然不知道该往哪个服务器上的什么数据库里面创建迁移了。错误里面给了一个链接,我们点进去可能有线索。

     “如果无法从应用程序服务提供程序获得 DbContext,则工具会在项目中查找派生 DbContext 类型。 然后,它们尝试使用不带参数的构造函数创建实例。 如果 DbContext 是使用OnConfiguring方法配置的,则这可能是默认构造函数。 还可以通过实现 IDesignTimeDbContextFactory<TContext> 接口告诉工具如何创建 DbContext:如果实现此接口的类在与派生 DbContext 相同的项目中或在应用程序的启动项目中找到,则这些工具将绕过其他创建 DbContext 的方法,而改用设计时工厂”

     文章里还给了个例子:

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
using Microsoft.EntityFrameworkCore.Infrastructure;

namespace MyProject
{
    public class BloggingContextFactory : IDesignTimeDbContextFactory<BloggingContext>
    {
        public BloggingContext CreateDbContext(string[] args)
        {
            var optionsBuilder = new DbContextOptionsBuilder<BloggingContext>();
            optionsBuilder.UseSqlite("Data Source=blog.db");

            return new BloggingContext(optionsBuilder.Options);
        }
    }
}

        得到启发,于是,我在OdysseyDbContext的那个.cs文件里,添加一个默认的工厂类,以返回DbContext,这个类仅用于在生成迁移的时候使用。   

 public class OdysseyBlogContextDesignFactory : IDesignTimeDbContextFactory<OdysseyBlogContext>
    {
        public OdysseyBlogContext CreateDbContext(string[] args)
        {
            var optionsBuilder = new DbContextOptionsBuilder<OdysseyBlogContext>()
                .UseSqlServer("Server=DESKTOP-CXXXXXXX\\SQLEXPRESS;Database=OdysseyDatabase; Trusted_Connection=True; MultipleActiveResultSets=true");
 
            return new OdysseyBlogContext(optionsBuilder.Options, new NoMediator());
        }

        class NoMediator : IMediator
        {
            public Task Publish<TNotification>(TNotification notification, CancellationToken cancellationToken = default(CancellationToken)) where TNotification : INotification
            {
                return Task.CompletedTask;
            }

            public Task Publish(object notification, CancellationToken cancellationToken = default)
            {
                return Task.CompletedTask;
            }

            public Task<TResponse> Send<TResponse>(IRequest<TResponse> request, CancellationToken cancellationToken = default(CancellationToken))
            {
                return Task.FromResult<TResponse>(default(TResponse));
            }

            public Task Send(IRequest request, CancellationToken cancellationToken = default(CancellationToken))
            {
                return Task.CompletedTask;
            }

            public Task<object> Send(object request, CancellationToken cancellationToken = default)
            {
                return Task.FromResult<object>(default(object));
            }
        }
    }

     可以看到,我在这里面指定了字符串,因为我在DbContext的构造函数里使用了MediatorR,所以这里也创建了一个。

     编写完成之后,再次将Odyssey.Infrastructure作为默认项目,然后创建迁移。

     可以看到,能够创建成功迁移了,接下来将迁移同步到数据库。

发现报错,因为这个库是.NET Standard库,需要指定初始启动项目,使用-StartupProject,或者简写-s参数即可。

 至此问题解决。

总结

   总结一下dotnet ef相关命令:

   在cmd里,如果使用dotnet ef命令,如果提示如下:

    

   表示cmd里还没有安装ef工具,解决方法,安装ef 工具:

dotnet tool install --global dotnet-ef --version 3.0.0-*

   下面是一些ef的常用命令,migrations迁移表示操作数据库的脚本,这些脚本还没有执行:

dotnet ef migrations add Initial //创建迁移   
dotnet ef migrations remove//删除最近的一个迁移
dotnet ef migrations remove //移除最近的一个迁移
dotnet ef migrations remove –force //强制删除迁移
dotnet ef database update //根据最新的迁移生成数据库
dotnet ef database drop --force   //删除数据库

一般每次对db进行修改,会新建一个迁移,然后更新数据库,比如我要对数据库添加索引,在对模型映射添加索引信息之后,创建一个名为Index的migration,然后更新到数据库:

dotnet ef migrations add Indexes //添加了索引
dotnet ef database update//更新数据库

    在某些情况下,如果应用里不止一个数据库,生成的时候,就需要指定DbContext:

dotnet ef dbcontext list //列出所有dbcontext

指定DbContext:

dotnet ef migrations add Initial --context EFCustomerContext
dotnet ef migrations add Current --context EFDatabaseContext

dotnet ef database update --context EFDatabaseContext
dotnet ef database update --context EFCustomerContext

根据数据库,生成代码:

dotnet ef dbcontext scaffold "Server=(localdb)\MSSQLLocalDB;Database=OdysseyDb" "Microsoft.EntityFrameworkCore.SqlServer" --output-dir "Models/Scaffold" --context ScaffoldContext --force --no-build

以上是在cmd里面用dotnet命令还产生迁移脚本或数据库。还可以在NugGet内嵌在Visual Studio里的程序包管理控制台命令里,用另外一套命令来生成。

Add-Migration Initial  //创建迁移,Initial这个只是个名称,可以自己取
Add-Migration Identity -Context AppIdentityDbContext //多个DbContext,通过-Context参数指定哪个DBContext
Remove-Migration //删除最近的迁移
Update-Database -Migration Initial //将迁移作用到数据库。
Update-Database -Migration:0 //恢复数据库到初始状态。
Update-Database -StartupProject Service\API\Odyssey.API//指定起始项目
Update-database -context AppIdentityDbContext //多个DbContext,通过-Context参数指定哪个DBContext

     总之,不论是在命令行里,还是在程序包管理控制台里,使用命令都能根据模型生成数据库,或者根据数据库生成代码,能敲代码就不要拖控件点鼠标,这样显得逼格极高😂。

     这篇文章主要介绍了如何在代码优先的情况下,针对包含DbContext的类库,使用EntityFramework Core生成迁移和数据库对象,希望对您有帮助。