大家好,歡迎收看由土星衛視直播的大型綜藝節目——老周吹逼逼。
今天咱們吹一下 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); } }
運行結果如下。
好了,今天的內容就到這里了,文中示例的源代碼可以拼命點 這里 下載。