Подтвердить что ты не робот

Entity Framework: настаивает на добавлении нового объекта во многих-ко-многим вместо повторного использования существующего FK

У меня есть много разных отношений, кратко Cases -----< CaseSubjectRelationships >------ CaseSubjects

Более полно: Случаи ( ID, CaseTypeID,.......)
CaseSubjects ( ID, DisplayName, CRMSPIN)
CaseSubjectsRelationships ( CaseID, SubjectID, PrimarySubject, RelationToCase,...)

В моей таблице ссылок "многие-ко-многим" есть дополнительные свойства, относящиеся к предметной ассоциации с конкретным случаем - например, дата начала, дата окончания, отношение свободного текста к случаю (наблюдатель, создатель и т.д.)

Создана модель данных Entity Framework - версия ASP.NET 4.0

У меня есть служба WCF с методом CreateNewCase, который принимает в качестве своего параметра объект Case (объект, созданный Entity Framework) - его задача состоит в том, чтобы сохранить регистр в базе данных.

Служба WCF вызывается сторонним инструментом. Здесь отправлен 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.

Я вижу, что в принципе это должно сработать - вы увидите, что я сделал то же самое для Case Type - я выбрал идентификатор, отправленный в 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
4b9b3361

Ответ 1

ОК: Таким образом, разрешение этого было комбинацией того, что сказал @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

Ответ 2

Вы не указали, с какой контекстной моделью вы работаете, поэтому я предполагаю, что вы используете значение по умолчанию (то есть у вас нет явных файлов .tt для генерации ваших сущностей).

Итак, в основном, это то, что, как я думаю, происходит.
В вашем коде, когда вы извлекаете что-то из контекста:

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

этот ct находится в контексте. Аргумент метода, который вы десериализовали из службы (c), не находится в контексте. Вы можете рассматривать этот контекст как объект отслеживания объекта и его выборки, который гарантирует, что все, что привязано к нему, может знать о любых изменениях, если оно новое, удалено и т.д.

Итак, когда вы дойдете до части:

 ' 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, и он уже прикреплен - "эй, нет необходимости идти в базу данных, у меня уже есть это!" (пытается быть умным и избегать удара db), и он вернет ваш собственный объект.

Наконец, когда вы сохраните все, он будет сохранен, но ваш прикрепленный c и все его дочерние объекты, помеченные как "новые", будут вставлены вместо обновления.

Самое простое исправление - сначала запросить все, что вам нужно из контекста, и только затем начать назначать его свойствам вашего объекта. Кроме того, посмотрите UpdateCurrentValues ​​, также может быть полезно...