最近在学习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生成迁移和数据库对象,希望对您有帮助。