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

У меня есть отношения многие ко многим, кратко
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 вызывается сторонним инструментом. Вот мыло отправлено:

<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

Ответы [ 2 ]

3 голосов
/ 29 августа 2011

Вы не указали, с какой контекстной моделью вы работаете, поэтому я предполагаю, что вы используете значение по умолчанию (т. Е. У вас нет явных файлов .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, и он уже присоединен - ​​"эй, нетнадо зайти в базу, у меня уже есть такая!(пытаясь быть умным и избегать попадания в БД), и он вернет ваш собственный объект.

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

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

1 голос
/ 02 сентября 2011

ОК: Таким образом, разрешение этого вопроса было комбинацией того, что @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
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...