17 Stimmen

Entity Framework: Besteht auf dem Hinzufügen einer neuen Entität in Many-to-Many statt der Wiederverwendung bestehender FK

Ich habe eine Beziehung von vielen zu vielen, kurz
Cases -----< CaseSubjectRelationships >------ CaseSubjects

Ausführlicher: Fälle( ID , CaseTypeID, .......)
CaseSubjects( ID , DisplayName, CRMSPIN)
CaseSubjectsRelationships( FallID , BetreffID , PrimarySubject, RelationToCase, ...)

In meiner Many-to-many-Verknüpfungstabelle befinden sich zusätzliche Eigenschaften, die sich auf die Verbindung des Betreffs mit dem spezifischen Fall beziehen - z. B. Startdatum, Enddatum, Freitextbeziehung zum Fall (Beobachter, Ersteller usw.)

Es wurde ein Entity Framework-Datenmodell erstellt - ASP.NET Version 4.0

Ich habe einen WCF-Dienst mit einer Methode namens CreateNewCase die als Parameter eine Case Objekt (eine vom Entity Framework erstellte Entität) - seine Aufgabe ist es, den Fall in der Datenbank zu speichern.

Der WCF-Dienst wird von einem Tool eines Drittanbieters aufgerufen. Hier wird das SOAP gesendet:

<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>

Die WCF-Engine deserialisiert dies in eine Case-Entität für mich richtig, und wenn ich im Debugger sehe, ist alles richtig eingerichtet.

Was ich tun möchte, ist nur eine neue CaseSubject wenn es nicht bereits einen Eintrag mit diesem Namen in der Datenbank gibt CRMSPIN angegeben (CRMSPIN ist eine Referenznummer aus einer zentralen Kundendatenbank)

Im folgenden Beispiel möchte ich also sehen, ob ich bereits einen Eintrag in CaseSubjects für jemanden mit CRMSPIN 601, und wenn ich das tue, möchte ich keinen weiteren (doppelten) Eintrag erstellen, sondern den neuen Fall mit dem bestehenden Thema verknüpfen (obwohl natürlich eine neue Zeile in CaseSubjectsRelationships mit den spezifischen "zusätzlichen" Informationen wie Beziehung usw. erstellt werden muss)

Hier ist der .NET-Code, den ich versucht habe, dies zu tun.

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

Wie Sie sehen können, wird stattdessen die For Each Schleife versuche ich, einen Betreff auf der Grundlage der CRMSPIN zu erhalten, und wenn ich etwas erhalte, aktualisiere ich die Entität "CaseSubject". (Ich habe auch versucht csr.SubjectID = s.ID anstatt die gesamte Einheit zu setzen, und ich habe auch versucht, sie beide zu setzen!)

Doch selbst wenn man einen Haltepunkt an der ctx.SaveChanges() Zeile, und wenn man sich anschaut, wie die Fächer eingerichtet sind und im Debugger sieht, dass es gut aussieht, ist es siempre Erstellung einer neuen Zeile in der Tabelle CaseSubjects.

Ich kann sehen, im Prinzip sollte dies funktionieren - Sie werden sehen, ich habe genau das Gleiche für Case Type getan - ich habe den Bezeichner in der XML gesendet ausgewählt, fand die Entität mit diesem Bezeichner über den Kontext, dann änderte der Fall die .CaseType zu der von mir gefundenen Einrichtung. Wenn es speichert, funktioniert es perfekt und wie erwartet und ohne duplizierte Zeilen.

Ich habe nur Schwierigkeiten, die gleiche Theorie auf eine Seite einer Many-to-many-Beziehung anzuwenden.

Hier sind einige (hoffentlich relevante) Auszüge aus der .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>

EDIT: Die Eigenschaftssetzer für die CaseSubject Eigenschaft der CaseSubjectsRelationships Objekt:

/// <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

3voto

veljkoz Punkte 8104

Sie haben nicht angegeben, mit welchem Kontextmodell Sie arbeiten, daher gehe ich davon aus, dass Sie das Standardmodell verwenden (d. h. Sie haben keine expliziten .tt-Dateien, um Ihre Entitäten zu generieren).

Ich denke also, dass dies der Fall ist.
In Ihrem Code, wenn Sie etwas aus dem Kontext holen:

Dim ct = ctx.CaseTypes.SingleOrDefault(Function(x) x.Identifier.ToUpper = c.CaseType.Identifier.ToUpper)

este ct es im Zusammenhang . Das Methodenargument, das Sie vom Dienst deserialisiert haben (das c ) ist nicht im Zusammenhang . Man kann den Kontext als "Objektverfolgung und -abruf" betrachten, der sicherstellt, dass alles, was mit ihm verbunden ist, über alle Änderungen, ob neu, gelöscht usw., Bescheid weiß.

Also, wenn Sie zu dem Teil kommen:

 ' Set the case type based on that found in database: '
  c.CaseType = ct

In dem Moment, in dem Sie etwas, das angehängt ist, etwas nicht Angehängtes zuweisen, wird das nicht angehängte Objekt ebenfalls in den Kontext gezogen - es kann keine "teilweise" angehängten Entitäten geben - wenn es angehängt ist, muss alles, auf das es verweist, ebenfalls angehängt sein. Dies ist also der Moment, in dem die c wird (implizit) in den Kontext "gezogen". Wenn es in den Kontext eintritt, wird es als "neu" markiert, da es noch nichts darüber weiß (es hat kein Wissen über es, keine Informationen zur Änderungsverfolgung...).

Da nun alles über dieses Objekt c im Kontext steht, wenn Sie den Kontext dazu abfragen:

 Dim s As CaseSubject = ctx.CaseSubjects.SingleOrDefault(Function(x) x.CRMSPIN = spin)

wird es feststellen, dass es in der Tat ein Objekt mit dieser CRMSPIN gibt und dass es bereits angehängt ist - "Hey, kein Grund, zur Datenbank zu gehen, ich habe das schon!" (um intelligent zu sein und einen DB-Treffer zu vermeiden), und es wird Ihr eigenes Objekt zurückgeben.

Wenn Sie schließlich alles speichern, wird es zwar gespeichert, aber Ihre angehängten c und alle untergeordneten Objekte, die als "neu" markiert sind, werden eingefügt statt aktualisiert.

Die einfachste Lösung wäre, zunächst alles, was Sie benötigen, aus dem Kontext abzufragen und erst dann damit zu beginnen, es den Eigenschaften Ihres Objekts zuzuweisen. Werfen Sie auch einen Blick auf UpdateCurrentValues kann es auch hilfreich sein...

1voto

bgs264 Punkte 4482

OK: Die Lösung für dieses Problem war eine Kombination aus dem, was @veljkoz in seiner Antwort sagte (die sehr nützlich war, um mir zu helfen, die endgültige Lösung zu finden, aber für sich genommen nicht die vollständige Lösung war)

Durch das Verschieben der For Each Schleife auf das erste, was getan wird, bevor irgendetwas anderes getan wird (wie von @veljkoz angedeutet), das hat die Collection was modified, enumeration may not continue Fehler, den ich erhielt, als ich die csr.CaseSubject = Nothing .

Es stellte sich auch heraus, dass es wichtig ist, keine Entitäten anzuhängen (z.B. keine csr.CaseSubject an eine Entität, sondern nur an Nothing ), sondern stattdessen die .SubjectID Eigentum. Eine Kombination aus all den oben genannten führte mich zu folgendem Code, der perfekt funktioniert und nicht versucht, doppelte Zeilen einzufügen.

+1 an @veljkoz für die Hilfe, aber beachten Sie auch, dass die Lösung das Setzen der Entitätsreferenz auf Nothing und die Verwendung des ID Eigentum.

Vollständiger, funktionierender Code:

 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

CodeJaeger.com

CodeJaeger ist eine Gemeinschaft für Programmierer, die täglich Hilfe erhalten..
Wir haben viele Inhalte, und Sie können auch Ihre eigenen Fragen stellen oder die Fragen anderer Leute lösen.

Powered by:

X