【ASP.NET Core】EF Core 模型與資料庫的建立
大家好,歡迎收看由土星衛視直播的大型綜藝節目——老周吹逼逼。
今天咱們吹一下 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 類派生的類。
publicclass 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); } }
執行結果如下。
好了,今天的內容就到這裡了,文中示例的原始碼可以拼命點 這裡 下載。