【ASP.NET Core】EF Core - “影子屬性”
有朋友說老週近來部落格更新較慢,確實有些慢,因為有些 bug 要研究,另外就是老周把部分內容轉到直播上面,所以寫部落格的內容減少了一點。
老周覺得,視訊直播可能會好一些,雖然我的水平一般,不過直播時,老周可以現場演示,可能會比看部落格效果要好(因為現場演示,有時候會有失誤,沒辦法,水平有限)。還有一個,就是.NET 的資料其實很多,畢竟也發展了十幾年了,有些東西如果別人都寫過了,那我也不好意思重複了。.NET Core 儘管是跨平臺版本,但核心依然是.net 基礎,我們不需要全新去學習,只要掌握一些新的變化就可以了。目前比較期待 .NET Core 3 的正式釋出,等正式上線了,老周再挑一些有意義的內容寫一下。
此外,老周也可能會寫一寫其他方面的部落格,比如 Python、GO、Ruby、Typescript 等。老周並不是只會玩.NET ,只不過老周是主攻 .NET,在接觸 .NET 之前,老周就學過很多東西,比如古老的 QBasic、Pascal ,老周在上初中時就學過。後來向 VB、C、C++ 進攻,順便把 Ruby、Python、PB 也調戲一下,後來有一段時間,Delphi 和 E 語言也挺流行的,所以順便也玩了兩把。
再後來,學過 Java 和 PHP,拋 Java 而投 .NET 是因為 Java 太複雜,效率不高,沒有深度把玩的興趣。現在所謂的 Python 很熱門純屬是商業炒作,Python 又不是什麼新玩意兒,很古老了,當然相對於 C 來說,是新了一點,究其特點,就是一種指令碼語言(雖然有人死要說它不是指令碼語言)。現在網上更有些無知小輩,以為自己會寫幾行 Python 程式碼就到處去蹭熱點,告訴你,老周當年學各種程式語言時,說不定你還沒出生呢。所以,如果你真心喜歡 Python 的話,你用心去學就是了(其實老周也喜歡用 Python 來做圖表),不必理會商業炒作。
記得去年 C 語言也被商業炒作了幾個月,再往前幾年,Javascript 和 Web 前端也被拼命炒作,說得好像 js 是萬能的似的,嚇得老周都不敢寫前端了。最近幾年,IT 界開始懷舊了,各種遠古生物都被挖出來了,可能是現在計算機行業已經沒什麼可以創新的原因吧。現在說得較多的是人工智障,這個可以用,也可以不用,反正不痛不癢,算不上生產力革命(至少其震動效果比不上當年 Office 問世時對企業生產的影響大)。不過,人工智障在某些輔助領域還是有用的,比如現在有些小區的智慧門,應用效果還可以。但是,漏洞也是百出的,總之,人類可以用它來進行輔助,但不能過於依賴它。它不能解決所有問題。
許多科幻小說都會說人類會被機器人消滅。機器人也是人創造出來的,機器不可能比人強,也不可能滅掉人類(除非機器人比奧特曼裡面的超獸還牛逼)。如果人類真的智力在衰減,那麼根源還是在人類自己。說白了就是,只有可能是人類自己滅掉自己。你也不用覺得太恐怖,其實只要你不要太依賴機器就好,不要失去你的本能和思考方式就行。
就像我們碼農,老周也一樣,天天跟計算機打交道,但老週一直堅持:用電腦,但不依賴電腦,多做些機器不能做的事。再加一句:科學只能解決數學和工程問題,而人的問題,需要哲學和美學來解決。
=============================================================================
好了,以上的都是 F 話,下面咱們聊正題。今天咱們耍一下 EF Core 中的影子屬性。這個詞翻譯版本 TMD 多,有翻譯為“卷影屬性”的,現在的文件又改為“陰影屬性”,這不好聽,太有心理陰影了,故而,老周覺得,叫“影子屬性”好一些。
不管叫什麼,你只要知道它是個啥就行。老周喜歡一句話總結,所以,來一句話:
模型類中沒有定義的,但資料表中存在的屬性——即模型類與資料表中沒有對應關係的屬性。
老周就用一個簡單的示例來說明一下吧。這個示例也是老周在視訊直播時用的。
假設,有個模型類,叫 Student。
public class Student { public int Id { get; set; } public string Name { get; set; } public int Age { get; set; } }
但我想要一個屬性,用來記錄資料記錄被寫進資料庫的時間,即還有一個屬性,叫 InsertTime,不過,這個屬性在 Student 類中是沒有定義的,但在資料表中是有這一欄位的。
因此,在從 DbContext 類派生時,需要重寫 OnModelCreating 方法,通過 Model Builder 來定義這個“隱藏”的屬性。
protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Student>().Property<DateTime>("InsertTime"); }
這個 InsertTime 屬性就成了影子屬性了。
下面是 DbContext 派生類的完整程式碼,我放出來是方便你去抄襲的,放心吧,無版權稅的,儘管抄。
public class MyContext : DbContext { public DbSet<Student> Students { get; set; } protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) { optionsBuilder.UseSqlServer(@"server=(localdb)\MSSQLLocalDB;database=TestDb"); } protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.Entity<Student>().Property<DateTime>("InsertTime"); } }
現在,到 Startup 類中,註冊一下服務,讓自定義的資料庫上下文可以進行依賴注入。
public void ConfigureServices(IServiceCollection services) { services.AddMvc(); services.AddDbContext<MyContext>(); }
剛剛在 MyContext 類中已經配置連線字串了,所以註冊服務時就不用指定連線字串了。
按老周前面所寫的博文,接下來是建立資料遷移,不過,這一次老周使用的是直接在執行的時候建立資料庫,方法也是很簡單的,找到 Main 入口點,把裡面的程式碼改一下(專案模板預設生成的程式碼不進行大改)。
public static void Main(string[] args) { var host = CreateWebHostBuilder(args).Build(); using(IServiceScope scope= host.Services.CreateScope()) { MyContext c = scope.ServiceProvider.GetService<MyContext>(); c.Database.EnsureCreated(); } host.Run(); }
也就是在 host 執行之前,建立一個“作用域”級別的服務例項,建立資料庫,這個例項是臨時使用,不遵循服務容器的生命週期規則。EnsureCreated 方法會檢測資料庫是否存在,如果不存在,就建立,然後返回 true;如果資料庫已經存在,不做任何處理並返回 false。
如果你不打算寫入一些初始資料,可以不在乎方法的返回值,如果要寫入初始資料,可以 if 一下,如果方法返回 true,就寫一下資料進去(true 表示新資料庫)。
接下來,咱們用一個 API 控制器來測試一下。
[Route("api/[action]")] public class TestController : Controller { readonly MyContext context; public TestController(MyContext c) => context = c; [HttpPost] public ActionResult AddNew([FromBody]Student stu) { …… } [HttpGet] public JArray GetList() { …… } }
資料庫上下文的例項因為已經註冊到服務容器中,所以通過建構函式可以得到其例項引用。這個控制器有兩個 action,AddNew 方法用來提交資料,以 POST 方式訪問。
[HttpPost] public ActionResult AddNew([FromBody]Student stu) { // 新增實體 context.Students.Add(stu); // 設定影子屬性 context.Entry(stu).Property<DateTime>("InsertTime").CurrentValue = DateTime.Now; context.SaveChanges(); return Ok("操作成功"); }
記得,引數要加上 FromBody 特性,因為它要從 HTTP 訊息正文中提取,上次我直播時就是忘了寫這個,所以提交不到資料。
這裡要注意影子屬性的賦值方法,因為它沒有在 Student 類中公開,你不能通過訪問成員來設定它,只能先通過 CurrentValue 屬性來設定。
然後還有一個 GetList 方法,以 GET 方式來訪問。有來返回所有 Student 資料。此處我用 JArray 以 JSON 陣列格式返回。
[HttpGet] public JArray GetList() { var q = from s in context.Students select new { s.Id, s.Name, s.Age, // 讀取影子屬性值 InsertTime = EF.Property<DateTime>(s, "InsertTime") }; JArray arr = new JArray(); foreach (var s in q) { JObject obj = new JObject(); obj.Add("id", new JValue(s.Id)); obj.Add("name", new JValue(s.Name)); obj.Add("age", new JValue(s.Age)); obj.Add("add_time", new JValue(s.InsertTime)); arr.Add(obj); } return arr; }
讀取影子屬性的方法是用 EF.Property 靜態方法,第一個引數是實體模型類的例項,第二個引數是影子屬性的名字。這個方法只能在 LINQ 樹中使用,如果不在 LINQ 中用會發生異常。這裡還有一個問題,就是在 select 子句中用 new 返回的匿名型別無法建立 JObject 物件,所以只好手動去構建。
現在可以測一下了,首先提交一下資料。
{ "name":"李三跳", "age":52 }
接著獲取一下資料列表。
[ { "id": 1, "name": "什麼鬼", "age": 29, "add_time": "2018-11-26T11:15:38.2012809" }, { "id": 2, "name": "陳大扣", "age": 83, "add_time": "2018-11-26T11:21:07.5374308" }, { "id": 3, "name": "李三跳", "age": 52, "add_time": "2018-11-26T12:41:51.758849" } ]
好了,示例就完成了。
影子屬性的典型用法就像剛剛這個例子這樣,可以用來記錄資料的新增時間或者更新時間,但這種資料,一般不需要在實體模型中公開。