国产探花免费观看_亚洲丰满少妇自慰呻吟_97日韩有码在线_资源在线日韩欧美_一区二区精品毛片,辰东完美世界有声小说,欢乐颂第一季,yy玄幻小说排行榜完本

首頁 > 學院 > 開發設計 > 正文

EF為什么向我的數據庫再次插入已有對象?(ZT)

2019-11-17 03:11:54
字體:
來源:轉載
供稿:網友

EF為什么向我的數據庫再次插入已有對象?(ZT)

最近做了個多對多對實體對象,結果發現每次只要增加一個子實體,就會自動添加一個父實體進去,而不管該父實體是否已經存在.

找了好久,終于找到這篇文章,照文章內容來看,應該是斷開連接導致的.

原文地址:http://msdn.microsoft.com/zh-cn/magazine/dn166926.aspx

------------------------------------------------------------------------------

在為本期專欄的主題構思的時候,有三位朋友通過 twitter 和郵件問我,實體框架為什么向他們的數據庫再次插入已有對象。

看來,我不用為本期專欄寫什么而頭疼了。

由于實體框架具有狀態管理能力,因此當它處理圖形時,其實體狀態行為并不總是符合你的期望。

我們來看一個典型示例。

假定有兩個類:Screencast 和 Topic 類,且為每個 Screencast 對象分配一個 Topic 對象,如圖 1 所示。

圖 1 Screencast 和 Topic 類

public class Screencast{  public int Id { get; set; }  public string Title { get; set; }  public string Description { get; set; }  public Topic Topic { get; set; }  public int TopicId { get; set; }}public class Topic{  public int Id { get; set; }  public string Name { get; set; }}

如果我想要檢索 Topic 的列表,并將其中一個對象分配給新的 Screencast 對象然后保存(整個操作集都包含在一個上下文中),整個過程不會有任何問題,如下例所示:

        using (var context = new ScreencastContext()){  var dataTopic =     context.Topics.FirstOrDefault(t=>t.Name.Contains("Data"));  context.Screencasts.Add(new Screencast                               {                                 Title="EF101",                                 Description = "Entity Framework 101",                                 Topic = dataTopic                               });  context.SaveChanges();}        

于是,數據庫中就會插入一個 Screencast 對象,并且具有指向所選 Topic 的相應外鍵。

如果你是在客戶端應用程序中工作,或是在上下文跟蹤所有活動的單個工作單元內執行這些步驟,那么上述處理方式可能正是你期望的。

不過,如果您正在處理已斷開連接的數據,那么其處理方式將會迥然不同,結果也可能會讓許多開發者大吃一驚。

在斷開連接的場景中包含圖形的處理方式

我在處理引用列表時通常采用的一種模式是使用獨立的上下文,當保存任何用戶修改時該上下文將不再處于可訪問范圍內。

這對 Web 應用程序和 Web 服務來說是常見的情景,但也可能發生在客戶端應用程序中。

下面的例子使用一個存儲庫來存儲引用數據,通過下面的 GetTopicList 方法來檢索 Topic 的列表:

       public class SimpleRepository{  public List<Topic> GetTopicList()  {    using (var context = new ScreencastContext())    {      return context.Topics.ToList();    }  } ...          }

然后你可以將這些 Topic 對象以列表形式展現在一個 Windows PResentation Foundation (WPF) 表單中,以便讓用戶可以新建 Screencast 對象,例如圖 2 所示的表單。

圖 2 用來輸入新 Screencast 對象的 Windows Presentation Foundation 表單

然后,在客戶端應用程序中(如圖 2 所示的 WPF 表單),將下拉列表中選定的條目賦給新 Screencast 對象的 Topic 屬性,代碼如下:

          private void Save_Click(object sender, RoutedEventArgs e){  repo.SaveNewScreencast(new Screencast                {                  Title = titleTextBox.Text,                  Description = descriptionTextBox.Text,                  Topic = topicListBox.SelectedItem as Topic                });}

此時 Screencast 變量是一個包含了新建的 Screencast 和 Topic 實例的圖形。

將該變量傳遞給存儲庫的 SaveNewScreencast 方法,即可將此圖形添加到新建的上下文實例中并隨即保存到數據庫,如下列代碼所示:

          public void SaveNewScreencast(Screencast screencast){  using (var context = new ScreencastContext())  {    context.Screencasts.Add(screencast);    context.SaveChanges();  }}

對數據庫活動進行分析,我們發現以上代碼不僅向數據庫插入了 Screencast 對象,而且在此之前,還向 Topics 表插入了關于 Data Dev 主題的一行新記錄,即使該主題已經存在:

          exec sp_executesql N'insert [dbo].[Topics]([Name])values (@0)select [Id]from [dbo].[Topics]where @@ROWCOUNT > 0 and [Id] =   scope_identity()',N'@0 nvarchar(max) ',@0=N'Data Dev'

這種行為使許多開發者感到困惑。

發生這種情況的原因是,當你調用 DBSet.Add 方法(即 Screencasts.Add)時,不僅根實體的狀態標記為“Added”,圖形中上下文之前未知的所有實體的狀態也都標記為 Added。

盡管開發者可能注意到 Topic 對象已經有一個 Id 值,但實體框架則以其 EntityState (Added) 狀態為準,無視已有的 Id,仍然為該 Topic 對象創建一條 Insert 數據庫命令。

雖然許多開發者可能會預測到這種行為,但是還有許多人并不了解。

在后一種情況下,如果你沒有對數據庫活動進行分析,可能不會意識到發生了什么,直到下次你(或用戶)在 Topics 列表中發現重復條目才知道出了問題。

注: 如果你對實體框架如何插入新記錄不太了解,可能會對上文所述的 SQL 中的 select 語句感到好奇。

它是用來確保實體框架能夠取回新創建的 Screencast 記錄的 Id 值,以便在 Screencast 實例中設置此值。

當加入整個圖形時,這不僅只是個問題

我們來看看另一種可能發生此問題的場景。

如果不向存儲庫傳遞圖形,而是讓存儲庫方法將新建的 Screencast 和選定的 Topic 同時作為請求參數,會怎么樣?

這樣一來,不再是添加整個圖形,而是添加 Screencast 實體,然后設置其 Topic 導航屬性:

public void SaveNewScreencastWithTopic(Screencast screencast,  Topic topic){  using (var context = new ScreencastContext()) {    context.Screencasts.Add(screencast);    screencast.Topic = topic;    context.SaveChanges();  }}

在本例中,SaveChanges 的行為與已添加圖形的行為沒什么兩樣。

您可能已經熟悉如何使用實體框架的 Attach 方法將未跟蹤的實體附加到上下文。

在本例中,實體的初始狀態是 Unchanged。

但在這里,當我們把 Topic 賦給 Screencast 實例而非上下文時,實體框架會把它看成是未識別的實體,而實體框架對無狀態的未識別實體的默認處理方式是將其標記為 Added。

這樣一來,Topic 將在調用 SaveChanges 時被再次插入數據庫。

我們可以對狀態進行控制,但這需要對實體框架的行為有更深入的理解。

例如,如果你準備將 Topic 直接附加到上下文,而不是附加到狀態為 Added 的 Screencast 對象,那么其 EntityState 狀態的初始值將會是 Unchanged。

此時將 Topic 賦值給 screencast.Topic 將不會引起狀態變化,因為上下文已經意識到 Topic 的存在了。

下面是展示這一邏輯的修改后的代碼:

using (var context = new ScreencastContext()){  context.Screencasts.Add(screencast);  context.Topics.Attach(topic);  screencast.Topic = topic;  context.SaveChanges();}

還有另外一種處理方法:不調用 context.Topics.Attach(topic),而是代之以在此前或此后設置 Topic 的狀態,明確地將其狀態設置為 Unchanged:

context.Entry(topic).State = EntityState.Unchanged

如果在上下文意識到 Topic 的存在之前調用上述代碼,會導致上下文附加該 Topic,并隨即設置其狀態。

盡管上述這些做法是處理該問題的正確模式,但我們不會自然而然地想到這么做。

除非你已經預先了解實體框架的這種處理方式,并知道所需的代碼模式,否則你可能會更傾向于編寫看起來符合正常邏輯的代碼,然后在實際運行中遇到這個問題,只有到這時候你才會開始研究到底出了什么事。

避免麻煩,使用外鍵

但還有一種簡單得多的方法,利用外鍵屬性,可以避免這種迷惑/混淆(原諒我的俏皮話)。

與其設置 Topic 這個導航屬性并且不得不為其狀態操心,不如只設置 TopicId 屬性,因為你確實可以在 Topic 實例中訪問到它的值。

這是我經常給開發者建議的做法。

甚至在 Twitter 上,我也看到這樣的問題: “為什么實體框架會插入已經存在的數據?”而我在回復中經常猜對了: “你是不是在對新建實體設置導航屬性,而沒有用外鍵? J”

因此,讓我們回顧一下 WPF 表單中的 Save_Click 方法,并改為設置 TopicId 屬性而非 Topic 導航屬性:

 repo.SaveNewScreencast(new Screencast               {                 Title = titleTextBox.Text,                 Description = descriptionTextBox.Text,                 TopicId = (int)topicListBox.SelectedValue)               });

此時,發送給存儲庫方法的 Screencast 就不再是圖形,只是單個實體。

實體框架可以用該外鍵屬性來直接設置表的 TopicId。

這樣一來,對實體框架來說,為包含 TopicId 值(在本例中,其值為 2)的 Screencast 實體創建一個 insert 方法就簡單了(而且更快了):

 exec sp_executesql N'insert [dbo].[Screencasts]([Title], [Description], [TopicId])values (@0, @1, @2)select [Id]from [dbo].[Screencasts]where @@ROWCOUNT > 0 and [Id] = scope_identity()',N'@0 nvarchar(max) ,@1 nvarchar(max) ,@2 int',  @0=N'EFFK101',@1=N'Using Foreign Keys When Setting Navigations',@2=2

如果你想把這段構造邏輯限制在存儲庫內,而且不想讓用戶界面開發者操心外鍵的設置,可以把 Topic 的 Id 和 Screencast 指定為存儲庫方法的參數,如下所示:

         public void SaveNewScreencastWithTopicId(Screencast screencast,   int topicId){  using (var context = new ScreencastContext())  {    screencast.TopicId = topicId;    context.Screencasts.Add(screencast);    context.SaveChanges();  }}

我們需要擔心的不止于此,還需要考慮到,開發者可能還會設置 Topic 導航屬性。

換言之,即使我們想用外鍵來避免 EntityState 問題,但萬一 Topic 實例是圖形的一部分怎么辦?例如以下所示 Save_Click 按鈕的另一種代碼實現:

       repo.SaveNewScreencastWithTopicId(new Screencast   {     Title = titleTextBox.Text,      Description = descriptionTextBox.Text,      Topic=topicListBox.SelectedItem as Topic    },  (int) topicListBox.SelectedValue);        

不幸的是,這將讓你回到問題的原點: 實體框架將 Topic 實體看成是圖形,并將該實體與 Screencast 一起添加到上下文中,即使已經設置了 Screencast.TopicId 屬性也是如此。 而且 Topic 實例的 EntityState 再次造成了混淆: 實體框架將插入一條新的 Topic 記錄,并在插入 Screencast 記錄時用該值作為新記錄的 Id。

避免這一問題的最安全方法,是在設置外鍵的值時將 Topic 屬性設置為 null。

如果有其他用戶界面要使用存儲庫方法,而您又無法確保只會用到已有的 Topic,那么你甚至可能想在這種可能的情況下新建一個 Topic 傳遞過去。

圖 3 展示了為完成這一任務而再次修改的存儲庫方法。

圖 3 旨在防止向數據庫意外插入導航屬性的存儲庫方法

    public void SaveNewScreencastWithTopicId(Screencast screencast,
發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
主站蜘蛛池模板: 奉节县| 苍溪县| 四平市| 房产| 阿勒泰市| 米易县| 女性| 樟树市| 阿拉尔市| 隆德县| 阜城县| 盐池县| 望谟县| 磴口县| 垣曲县| 松桃| 女性| 社旗县| 家居| 桂阳县| 巴林左旗| 英山县| 紫阳县| 宝山区| 万宁市| 晋中市| 赞皇县| 嘉兴市| 获嘉县| 岫岩| 静乐县| 云安县| 莆田市| 莱州市| 嫩江县| 拉萨市| 中超| 通渭县| 禄劝| 华容县| 潜山县|