大家好,欢迎收看由土星卫视直播的大型综艺节目——老周吹逼逼。
今天咱们吹一下 EF Core 有关的话题。先说说模型和数据库是怎么建起来的,说装逼一点,就是我们常说的 “code first”。就是你先创建了数据模型,然后再根据模型来创建数据库。这种做法的一个好处是让面向对象的逻辑更好地表现出来。以前,咱们通常是先创建数据库的。
像 EF 这么嗨的东西,ASP.NET Core 中自然也是少不了的,即 EF Core。
好了,以上就是理论部分,比较乏味,是吧。那好,下面咱们干点正事。
构建模型
建立模型很简单,就是定义一个类(为了好理解,老周暂且不说关系模型)。来,看看,就像下面这个类,假设它表示的是某工厂生产的山寨产品信息。
public class Product { public int ProdID { get; set; } public string ProdName { get; set; } public DateTime FinishDate { get; set; } public double Weight { get; set; } }
有人会问:完事了?嗯,完事了,这就是一个模型了,但还是不能创建数据库的。
继承 DBContext
虽然咱们有了山寨产品的模型类,但你还得实现一个数据上下文。通常呢,数据上下文是映射到某个数据库的。上下文的定义是从 DbContext 类派生出一个类,然后,把它与模型类关联起来。
public class MyDBContext : DbContext { public DbSet<Product> Products { get; set; } }
DbSet 会映射到数据库中的一个表。
为了实现依赖注入,以及能够在 Startup 类中进行配置,你可以在自己实现的 DBContext 子类中公开构造函数,并且接收一个 DbContextOptions<TContext> 类型的参数注入,TContext 就是咱们自己定义的从 DBContext 类派生的类。
public class MyDBContext : DbContext { public MyDBContext(DbContextOptions<MyDBContext> options) : base(options) { // 暂无其他代码 } public DbSet<Product> Products { get; set; } }
注册服务
有了模型和数据上下文,接下来咱们要在 Startup 类中注册一下相关的服务,并且配置一下像连接字符串之类的参数。
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddDbContext<MyDBContext>(option => { option.UseSqlServer("server=(localdb)\MSSQLLocalDB;database=DemoDB"); }); }
创建数据库“迁移”
创建迁移的好处是灵活,如果你的模型后面修改了(比如添加了一个属性),那么你可以在原有的迁移基础上再添加新的迁移,这些数据迁移会不断叠加,所以,你不需要删除过去的迁移版本,因为后面添加的不会重复,只会包含更新数据模型的代码。
创建迁移有多种方式:1、dotnet 命令行;2、VS 中的 nuget 控制台;3、直接用代码。
dotnet cli 即使用 dotnet 命令行工具来对数据模型进行迁移,其命令为 dotnet ef <...>。这里老周演示的是用 VS 中的 nuget 控制台来处理,dotnet cli 的方法类似,你可以输入 dotnet ef --help 来查看帮助。
在 VS 中,打开 【工具】-【NuGet 包管理器】-【程序包管理器】菜单项,随后就能打开控制台窗口。你可以输入以下命令查看帮助文档。
get-help about_EntityFrameworkCore
你要是觉得名字太长了,可以这样输入
get-help about_*core
星号是通配符,它会查找所有以 about_ 开头,以 Core 结尾的说明文档。
好,下面咱们为前面已定义好的 MyDBContext 生成数据迁移代码,使用的命令是 Add-Migration。用法如下。
Add-Migration [-Name] <String> [-OutputDir <String>] [-Context <String>] [-Project <String>] [-StartupProject <String>]
其实后面还有个参数列表的,但用不上,就不列出来了。注意,只有位于第一个位置的 -Name 参数名可以省略,后面的都不能省略参数名。即对于迁移点的命名,你可以输入
Add-Migration -Name "demo001"
也可以输入
Add-Migration "demo001"
-OutputDir 指的是生成的代码放在哪个目录下面,默认叫 Migrations。注意它是相对于项目目录的路径。-Context 指定的是你自己定义的 DBContext 的子类的名称,包含命名空间名称,如果是当前项目,可以不写。
-Project 和 -StartupProject 一般不用刻意指定,如果解决方案中有多个项目,可以指定一下,生成的迁移属于哪个项目。-StartupProject 可以不指定,让它选择与解决方案配置一致的启动项目。
好,下面咱们为刚刚定义的 MyDBContext 生成数据迁移。输入
add-migration "demo001" -Context "MyDBContext" -OutputDir "CustMigrations"
执行后就呵呵了,出现一个警告和一个异常。
警告信息只是 SDK 与运行时版本没统一而已,这个可以不鸟它,不影响命令执行。最大的问题是发生异常,这会导致命令不能执行。发生异常是因为我们上面定义的那个 Product 类,没有声明主键。
于是,我们就让 ProdID 作为主键,方法有两种。第一,通过“数据批注”,就像这样。
public class Product { [Key] public int ProdID { get; set; } public string ProdName { get; set; } public DateTime FinishDate { get; set; } public double Weight { get; set; } }
如果你认为用特性来批注很难看,那就用第二种方法,在继承 DBContext 的类中重写 OnModelCreating 方法。
public class MyDBContext : DbContext { …… protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Product>().HasKey(p => p.ProdID); } }
现在再执行一次 Add-Migration 命令,就顺利创建数据迁移了。
假如现在我觉得模型要修改,新增一个 Remark 属性。
public class Product { public int ProdID { get; set; } public string ProdName { get; set; } public DateTime FinishDate { get; set; } public double Weight { get; set; } public string Remark { get; set; } }
此时,你不用删除前面创建的迁移,你只需要再加一个迁移即可,它会自动累积的。
add-migration "demo002" -Context "MyDBContext" -OutputDir "CustMigrations"
你能看到,demo002 迁移生成的代码,仅仅是添加了 Remark 列。
public partial class demo002 : Migration { protected override void Up(MigrationBuilder migrationBuilder) { migrationBuilder.AddColumn<string>( name: "Remark", table: "Products", nullable: true); } protected override void Down(MigrationBuilder migrationBuilder) { migrationBuilder.DropColumn( name: "Remark", table: "Products"); } }
所以,看得出来,它不会重复生成表结构的。Up 方法表示的是当前的状态,Down 方法是在执行 Remove-Migration 时进行回退,回退时删除 Remark 列。
创建数据库
有了上面的步聚,现在可以创建数据库了。这里老周以 SQLLocalDB 为例,在 CMD 中启动默认的 MSSQLLocalDB 实例。
sqllocaldb start mssqllocaldb
回到 VS 中,执行 Update-Database 命令。
update-database
无参数的情况下,执行所有迁移中的内容,为了使创建的数据库结构完整,应该执行所有迁移。
SQLLocalDB 创建的数据库默认存放在你的用户目录下,即 C:\Users\Your name\ 下面,路径变量是 %userprofile%。
当你想删除数据库时,可以输入以下命令。
drop-database -Context "MyDBContext"
这时候,它会问你,真的要删库跑路吗?
此时你心意已决,删库跑路,输入 Y 或 A,确认。
测试数据库
好了,现在,模型也建好了,数据库也有了,可以来测一下了。
先创建个控制器。
public class DemoController : Controller { MyDBContext _dbcontext; public DemoController(MyDBContext context) { _dbcontext = context; } [HttpGet] public ActionResult Products() { return View(_dbcontext.Products.ToList()); } [HttpPost] public ActionResult Products(Product p) { if (ModelState.IsValid) { _dbcontext.Products.Add(p); _dbcontext.SaveChanges(); } return View(_dbcontext.Products.ToList()); } }
db context 可以在构造函数能过依赖注入来获取,因为前面我们已经在 Startup.ConfigureServices 方法中注册了相关服务。添加新记录时直接把方法参数接收到的 Product 实例 Add 到 DbSet 中即可,但要记得调用 SaveChanges 方法,因为调用方法后数据才会真正写入数据库。
控制器中包含了两个 Products 的 action 方法,使用以下路由规则,可以匹配出两个方法。
app.UseMvc(route => { route.MapRoute("test", "{controller=Demo}/{action=Products}"); });
解决方法就是,无参数的 Products 方法以 GET 方式访问,而带参数的 Products 方法以 POST 方式访问。
创建一个与 Products 方法同名的视图。在视图中用 @model 指令定义 Model 的类型为 List<Product>,因为上面控制器中,调用 View 方法时,传递给视图的是 List<Product> 类型的 Model。
视图代码如下。
@using Web7362 @model List<Product> @addTagHelper *,Microsoft.AspNetCore.Mvc.TagHelpers <html> <body> <div> <form method="post"> <table> <tr> <td> 产品名称: </td> <td><input type="text" name="ProdName" /></td> </tr> <tr> <td>完成日期:</td> <td><input name="FinishDate" type="date"/></td> </tr> <tr> <td>产品重量:</td> <td> <input name="Weight"/> </td> </tr> <tr> <td>产品备注:</td> <td><input name="Remark" type="text" /></td> </tr> <tr> <td colspan="2"> <input type="submit" value="新增" /> </td> </tr> </table> </form> </div> <div> <table border="1"> @foreach(var p in Model) { <tr> <td>@p.ProdID</td> <td>@p.ProdName</td> <td>@p.FinishDate</td> <td>@p.Weight</td> <td>@p.Remark</td> </tr> } </table> </div> </body> </html>
第一个 div 中的 form 用于提交新的山寨产品记录,第二个 div 用来显示产品列表。
当提交时,如何把 form 中输入的内容传递给 Product 新对象,你可能会想到使用 asp-for 标签帮助器。但此处不能使用 asp-for 帮助器,因为 Model 的类型是 List<Product> ,不是 Product 类型。
那咋办呢,可以利用 input 元素的 name 值,将 name 值设置为与 Product 类的各属性名称相同的值即可。
<input type="text" name="ProdName" /> <input name="FinishDate" type="date"/> <input name="Weight"/> <input name="Remark" type="text" />
这样设置后,在提交时 Model Binder 就可以自动识别并填充 Product 实例的各个属性了。
你也会问了,为啥没有为 ProdID 属性弄个 input 元素?因为这个属性是主键,其值由数据库生成,不必手动输入。
来来来,看看效果。
IDesignTimeDbContextFactory<out TContext> 接口
这个接口有两种情况下,你可以考虑使用。
1、默认项目模板生成的 Main 方法被你修改了。准确地说,是你删除了 CreateWebHostBuilder 方法。默认生成的 Main 是这样的。
public static void Main(string[] args) { CreateWebHostBuilder(args).Build().Run(); } public static IWebHostBuilder CreateWebHostBuilder(string[] args) => WebHost.CreateDefaultBuilder(args) .UseStartup<Startup>();
然后,你嫌它生成的代码不好看,也觉得日志太多影响性能,所以改为这样。
public static void Main(string[] args) { var host = new WebHostBuilder() .UseContentRoot(Directory.GetCurrentDirectory()) .UseStartup<Startup>() .UseKestrel() .UseUrls("http://localhost:7676") .UseEnvironment(EnvironmentName.Development) .UseSetting(WebHostDefaults.ApplicationKey, "大飞侠充值系统") .Build(); host.Run(); }
这样一来,你想执行 Add-Migration 命令,就会收到这条错误。
2、设计时需要。有时候,你用来开发测试的数据库服务器和正式投入使用的不是同一个服务器。这时候,你可以实现 IDesignTimeDbContextFactory<out TContext> 接口,创建用于测试的数据上下文(尤其是连接字符串)。
下面用另一个示例来演示一下。
先创建一个模型类。
public class Charge { public int ID { get; set; } public DateTime Time { get; set; } public decimal Money { get; set; } public string PhoneNo { get; set; } }
然后是实现数据上下文。
public class DemoDBContext : DbContext { public DemoDBContext(DbContextOptions<DemoDBContext> options) : base(options) { } public DbSet<Charge> Charges { get; set; } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Charge>().HasKey(o => o.ID); } }
由于默认的 Main 函数被修改了,执行 Add-Migration 命令,会发生错误。
其实,错误信息中已经告诉你解决方法了,就是实现 IDesignTimeDbContextFactory<out TContext> 接口。所以,就实现一下呗。
public class CustDesigntimeContext : IDesignTimeDbContextFactory<DemoDBContext> { public DemoDBContext CreateDbContext(string[] args) { var optionsBuilder = new DbContextOptionsBuilder<DemoDBContext>(); // 设置连接字符串 optionsBuilder.UseSqlServer("server=(localdb)\mssqllocaldb;database=test_db"); // 创建上下文实例 return new DemoDBContext(optionsBuilder.Options); } }
现在,再执行 Add-Migration 命令就正常了。
add-migration "check01" -outputdir "MgChecks" -context "DemoDBContext"
然后可以创建数据库。
update-database
接下来用 Web API 来测一下。先在 Startup.ConfigureServices 方法中注册一下相关服务。
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddDbContext<DemoDBContext>(opt => { opt.UseSqlServer("server=(localdb)\mssqllocaldb;database=test_db"); }); }
实现 IDesignTimeDbContextFactory 接口只用于设计阶段,不用于应用程序运行阶段,所以,相关的配置还是要做的。
定义控制器。
[Route("charger/[action]")] public class ChargerController : Controller { private readonly DemoDBContext _dbcontext; public ChargerController(DemoDBContext cxt) { _dbcontext = cxt; // 初始化一些数据 if (!_dbcontext.Charges.Any()) { Charge c1 = new Charge { PhoneNo = "13325236411", Money = 50.00M, Time = new DateTime(2018, 10, 9, 20, 16, 0) }; Charge c2 = new Charge { PhoneNo = "15900254200", Money = 100.00M, Time = new DateTime(2018, 6, 22, 19, 0, 0) }; Charge c3 = new Charge { PhoneNo = "13500001122", Money = 30.1M, Time = new DateTime(2018, 10, 13, 15, 20, 10) }; _dbcontext.Charges.AddRange(c1, c2, c3); _dbcontext.SaveChanges(); } } public ActionResult Index() { return Json(_dbcontext.Charges); } }
运行结果如下。
好了,今天的内容就到这里了,文中示例的源代码可以拼命点 这里 下载。