實體框架:堅持在多對多中添加新實體,而不是重複使用現有的 FK
我有一個多對多的關係,簡單地說
Cases -----< CaseSubjectRelationships >------ CaseSubjects更全面:案例(ID,CaseTypeID,…….)
CaseSubjects(ID,DisplayName,CRMSPIN)
CaseSubjectsRelationships(CaseID,SubjectID,PrimarySubject,RelationToCase,…)
在我的多對多連結表中,還有與主題與特定案例關聯相關的附加屬性——例如,開始日期、結束日期、與案例的自由文本關係(觀察者、創建者等)
已創建實體框架數據模型 - ASP.NET 4.0 版
我有一個 WCF 服務,該服務帶有一個名為的方法,該方法接受一個對象(由實體框架創建的實體)
CreateNewCase作為其參數- 它的工作是將案例保存到數據庫中。CaseWCF 服務由第三方工具呼叫。這是發送的 SOAP:
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"> <s:Body> <CreateNewCase xmlns="http://tempuri.org/"> <c xmlns:a="http://schemas.datacontract.org/2004/07/CAMSModel"> <a:CaseSubjectsRelationships> <a:CaseSubjectsRelationship> <a:CaseSubject> <a:CRMSPIN>601</a:CRMSPIN> <a:DisplayName>Fred Flintstone</a:DisplayName> </a:CaseSubject> <a:PrimarySubject>true</a:PrimarySubject> <a:RelationToCase>Interested</a:RelationToCase> <a:StartDate>2011-07-12T00:00:00</a:StartDate> </a:CaseSubjectsRelationship> <a:CaseSubjectsRelationship> <a:CaseSubject> <a:CRMSPIN>602</a:CRMSPIN> <a:DisplayName>Barney Rubble</a:DisplayName> </a:CaseSubject> <a:RelationToCase>Observer</a:RelationToCase> <a:StartDate>2011-07-12T00:00:00</a:StartDate> </a:CaseSubjectsRelationship> </a:CaseSubjectsRelationships> <a:CaseType> <a:Identifier>Change of Occupier</a:Identifier> </a:CaseType> <a:Description>Case description</a:Description> <a:Priority>5</a:Priority> <a:QueueIdentifier>Queue One</a:QueueIdentifier> <a:Title>Case title</a:Title> </c> </CreateNewCase> </s:Body> </s:Envelope>WCF 引擎正確地為我將其反序列化為 Case 實體,當我查看調試器時,一切都已正確設置。
我想要做的只是創建一個新的
CaseSubject,如果數據庫中還沒有CRMSPIN指定的條目(CRMSPIN 是來自中央客戶數據庫的參考號)因此,在下面的範例中,我想查看我是否已經
CaseSubjects為具有 CRMSPIN 601 的人輸入了一個條目,如果有,我不想創建另一個(重複)條目,而是將新案例連結到現有的主題(儘管顯然需要在 CaseSubjectsRelationships 中創建具有特定“附加”資訊(例如關係等)的新行)這是我嘗試執行此操作的 .NET 程式碼。
Public Class CamsService Implements ICamsService Public Function CreateNewCase(c As CAMSModel.Case) As String Implements ICamsService.CreateNewCase Using ctx As New CAMSEntities ' Find the case type ' Dim ct = ctx.CaseTypes.SingleOrDefault(Function(x) x.Identifier.ToUpper = c.CaseType.Identifier.ToUpper) ' Give an error if no such case type ' If ct Is Nothing Then Throw New CaseTypeInvalidException(String.Format("The case type {0} is not valid.", c.CaseType.Identifier.ToString)) End If ' Set the case type based on that found in database: ' c.CaseType = ct For Each csr In c.CaseSubjectsRelationships Dim spin As String = csr.CaseSubject.CRMSPIN Dim s As CaseSubject = ctx.CaseSubjects.SingleOrDefault(Function(x) x.CRMSPIN = spin) If Not s Is Nothing Then ' The subject has been found based on CRMSPIN so set the subject in the relationship ' csr.CaseSubject = s End If Next c.CreationChannel = "Web service" c.CreationDate = Now.Date ' Save it ' ctx.AddToCases(c) ctx.SaveChanges() End Using ' Return the case reference ' Return c.ID.ToString End Function End Class如您所見,
For Each我嘗試根據 CRMSPIN 獲取主題,而不是循環,如果我得到一些東西,則更新“CaseSubject”實體。(我也嘗試過csr.SubjectID = s.ID設置整個實體,而不是設置它們!)。但是,即使
ctx.SaveChanges()在行上放置斷點並查看主題的設置方式並在調試器中看到它看起來不錯,它總是在 CaseSubjects 表中創建一個新行。我可以看到原則上這應該可以工作 - 你會看到我對案例類型做了完全相同的事情 - 我選擇了在 XML 中發送的標識符,通過上下文找到具有該標識符的實體,然後將案例更改
.CaseType為我找到的實體。當它保存時,它可以完美地按預期工作,並且沒有重複的行。我只是在嘗試將相同的理論應用於多對多關係的一側時遇到了麻煩。
以下是 .edmx 的一些(希望是相關的)摘錄
<EntitySet Name="Cases" EntityType="CAMSModel.Store.Cases" store:Type="Tables" Schema="dbo" /> <EntitySet Name="CaseSubjects" EntityType="CAMSModel.Store.CaseSubjects" store:Type="Tables" Schema="dbo" /> <EntitySet Name="CaseSubjectsRelationships" EntityType="CAMSModel.Store.CaseSubjectsRelationships" store:Type="Tables" Schema="dbo" /> <AssociationSet Name="FK_CaseSubjectsRelationships_Cases" Association="CAMSModel.Store.FK_CaseSubjectsRelationships_Cases"> <End Role="Cases" EntitySet="Cases" /> <End Role="CaseSubjectsRelationships" EntitySet="CaseSubjectsRelationships" /> </AssociationSet> <AssociationSet Name="FK_CaseSubjectsRelationships_CaseSubjects" Association="CAMSModel.Store.FK_CaseSubjectsRelationships_CaseSubjects"> <End Role="CaseSubjects" EntitySet="CaseSubjects" /> <End Role="CaseSubjectsRelationships" EntitySet="CaseSubjectsRelationships" /> </AssociationSet>編輯:對象
CaseSubject屬性的屬性設置器CaseSubjectsRelationships:/// <summary> /// No Metadata Documentation available. /// </summary> <XmlIgnoreAttribute()> <SoapIgnoreAttribute()> <DataMemberAttribute()> <EdmRelationshipNavigationPropertyAttribute("CAMSModel", "FK_CaseSubjectsRelationships_CaseSubjects", "CaseSubject")> Public Property CaseSubject() As CaseSubject Get Return CType(Me, IEntityWithRelationships).RelationshipManager.GetRelatedReference(Of CaseSubject)("CAMSModel.FK_CaseSubjectsRelationships_CaseSubjects", "CaseSubject").Value End Get Set CType(Me, IEntityWithRelationships).RelationshipManager.GetRelatedReference(Of CaseSubject)("CAMSModel.FK_CaseSubjectsRelationships_CaseSubjects", "CaseSubject").Value = value End Set End Property
好的:所以對此的解決方案是@veljkoz 在他的回答中所說的組合(這對幫助我達成最終解決方案非常有用,但它本身並不是完整的解決方案)
通過將
For Each循環移動到其他任何事情之前完成的第一件事(正如@veljkoz 所暗示的那樣),這消除了Collection was modified, enumeration may not continue我在設置時遇到的錯誤csr.CaseSubject = Nothing。事實證明,不附加實體也很重要(例如,不設置
csr.CaseSubject到實體,而只設置到Nothing)而是使用.SubjectID屬性。以上所有內容的組合使我得到了以下程式碼,它完美地工作並且不會嘗試插入重複的行。+1 到@veljkoz 以獲得幫助,但還請注意,解決方案包括設置實體引用
Nothing和使用該ID屬性。完整的工作程式碼:
Public Function CreateNewCase(c As CAMSModel.Case) As String Implements ICamsService.CreateNewCase Using ctx As New CAMSEntities ' Subjects first, otherwise when you try to set csr.CaseSubject = Nothing you get an exception ' For Each csr In c.CaseSubjectsRelationships Dim spin As String = csr.CaseSubject.CRMSPIN Dim s As CaseSubject = ctx.CaseSubjects.SingleOrDefault(Function(x) x.CRMSPIN = spin) If Not s Is Nothing Then ' The subject has been found based on CRMSPIN so set the subject in the relationship ' csr.CaseSubject = Nothing csr.SubjectID = s.ID End If Next ' Find the case type ' Dim ct = ctx.CaseTypes.SingleOrDefault(Function(x) x.Identifier.ToUpper = c.CaseType.Identifier.ToUpper) ' Give an error if no such case type ' If ct Is Nothing Then Throw New CaseTypeInvalidException(String.Format("The case type {0} is not valid.", c.CaseType.Identifier.ToString)) End If ' Set the case type based on that found in database: ' c.CaseType = ct c.CreationChannel = "Web service" c.CreationDate = Now.Date ' Save it ' ctx.AddToCases(c) ctx.SaveChanges() End Using ' Return the case reference ' Return c.ID.ToString End Function
您沒有指定正在使用的上下文模型,所以我假設您使用的是預設值(即,您沒有一些明確的 .tt 文件來生成實體)。
所以,基本上,這就是我認為正在發生的事情。
在您的程式碼中,當您從上下文中獲取某些內容時:
Dim ct = ctx.CaseTypes.SingleOrDefault(Function(x) x.Identifier.ToUpper = c.CaseType.Identifier.ToUpper)這
ct是在上下文中。c您從 service (the )反序列化的方法參數不在 context 中。您可以將上下文視為“對象跟踪和獲取”實體,以確保附加到它的所有內容都可以知道任何更改,如果它是新的、已刪除的等。因此,當您到達該部分時:
' Set the case type based on that found in database: ' c.CaseType = ct在您分配附加到未附加的東西的那一刻,未附加的對像也將被拉入上下文 - 不能有“部分”附加的實體 - 如果它已附加,則它引用的所有內容也必須附加。所以,這是
c被“拖”到上下文中的時刻(隱式地)。當它進入上下文時,它將被標記為“新”,因為它還不知道任何關於它的資訊(它不知道它,沒有更改跟踪資訊……)。因此,既然關於該對象的所有內容
c都在上下文中,那麼當您查詢上下文時:Dim s As CaseSubject = ctx.CaseSubjects.SingleOrDefault(Function(x) x.CRMSPIN = spin)它會發現確實有一個帶有該 CRMSPIN 的對象並且它已經附加了 - “嘿,不需要去數據庫,我已經有了這個!” (盡量聰明並避免數據庫命中),它將返回您自己的對象。
最後,當您保存所有內容時,它將被保存,但您的附加
c對象及其所有標記為“新”的子對象將被插入而不是更新。最簡單的解決方法是首先從上下文中查詢您需要的所有內容,然後才開始將其分配給對象的屬性。另外,看看UpdateCurrentValues,它也可能會有所幫助……