EF4.0 — вставка связанных объектов в базу данных

Я пробую Entity Framework 4.0, и вот самая упрощенная версия случая -

У меня есть следующие две связанные таблицы:

Адрес
Идентификатор
Город

Клиент
Идентификатор
AddressId
Имя

Я загрузил адреса в ComboBox. Все, что я хочу, это ввести имя клиента в TextBox, выбрать адрес из ComboBox в качестве адреса клиента и нажать кнопку «Сохранить». Я хочу, чтобы мой клиент был сохранен в таблице клиентов. Вот что я пробовал -

    /*loads Addresses to the ComboBox*/
    private void LoadData()
    {
        using (CAEntities ctx = ModelAccess.GetContext())
            this.AddressList = ctx.Addresses.ToList();
    }

    /*(tries) to insert the Client to the database*/
    private void Save()
    {
        /*here this.Client is the Client object that is instantiated & initialized   
          by previous code and this.Address is the SelectedItem of the ComboBox*/
        this.Client.Address = this.Address;
        using (CAEntities ctx = ModelAccess.GetContext())
        {
            ctx.Clients.AddObject(this.Client);
            ctx.SaveChanges();
        }
    }

Джули Лерман в Programming Entity Framework говорит, что ...Because the entities are joined, you can add either entity, and it will bring along the rest of the graph... Но то, что у меня есть, это InvalidOperationException, в котором говорится: «Свойство EntityKey может быть установлено только тогда, когда текущее значение свойства равно null».
Если я использую -

this.Client.AddressId = this.Address.Id;  

вместо -

this.Client.Address = this.Address;  

Клиент вставляется в базу данных отлично. Но я думаю, что я также должен иметь возможность напрямую связывать Сущности друг с другом, верно?
Я предположил, что проблема связана с отдельным контекстом, который я создаю. Итак, я попробовал это -

    private void Save()
    {
        this.Client.Address = this.Address;
        using (CAEntities ctx = ModelAccess.GetContext())
        {
            ctx.Addresses.Attach(this.Address);
            ctx.SaveChanges();
        }
    }  

но на этот раз я получаю InvalidOperationException, в котором говорится: «Объект с временным значением EntityKey не может быть присоединен к контексту объекта». Так что же я здесь делаю не так? Заранее спасибо.


person atiyar    schedule 31.08.2012    source источник
comment
потому что то, что вы захватываете с помощью this.Client.Address, является значением из поля со списком, а не объектом адреса. он пытается создать новый адресный объект в базе данных, и вы не можете назначить его клиенту, пока он не будет создан. поэтому this.Client.AddressId работает, вам нужно будет создать экземпляр адреса с чем-то вроде var Address = GetById(this.Client.AddressId, а затем установить Client.Address = Address.   -  person Brian    schedule 31.08.2012
comment
@Brian: я знаю, что то, что ты предлагаешь, работает, но я не понимаю, почему то, что я беру из поля со списком, является значением, а не адресным объектом? я устанавливаю список объектов Address в качестве источника данных, и я беру текущий SelectedItem в качестве this.Address, и я получаю из него свойства Id и City.   -  person atiyar    schedule 31.08.2012
comment
потому что это источник данных. Поле со списком содержит пару ключ-значение, а не объект. Таким образом, назначая эту пару ключ-значение в качестве объекта адреса обратно клиенту, вы сообщаете контексту БД создать новый адрес в БД, но он должен существовать, чтобы сохранить его клиенту. Вот откуда приходит сообщение о временном ключе объекта.   -  person Brian    schedule 31.08.2012
comment
@ Брайан: я действительно не понял тебя. из xaml я установил привязку свойства SelectedItem поля со списком к этому адресу в моей ViewModel. поэтому всякий раз, когда изменяется SelectedItem, я не должен получать адресный объект? SelectedItem, как указано здесь msdn.microsoft.com/en-us/library/ возвращает System.Object, и вы можете привести его к своему пользовательскому объекту, я прав?   -  person atiyar    schedule 31.08.2012
comment
В какой-то степени да, но имейте в виду, что такое поле со списком и что оно собирается вернуть, а также среду, на которой вы его используете (страница просмотра). Он просто вернет пару ключ-значение из поля со списком. Это то, с чем вы должны работать.   -  person Brian    schedule 31.08.2012
comment
@Brian: мы на той же странице, что и в WPF? потому что ты больше похож на asp.net: P   -  person atiyar    schedule 31.08.2012


Ответы (1)


Это должно решить проблему:

    using (CAEntities ctx = ModelAccess.GetContext())
    {
        ctx.Addresses.Attach(this.Address);
        this.Client.Address = this.Address;
        ctx.Clients.AddObject(this.Client);
        ctx.SaveChanges();
    }

Почему это работает?

DbContext отслеживает объекты, которые он извлекает или которые были явно присоединены к нему. В вашем случае вы устанавливали this.Client.Address для объекта, о котором DbContext не знал. В некоторых случаях это приведет к тому, что инфраструктура сущностей вставит как новую строку клиента , так и новую строку адреса, но в вашем случае из-за некоторых семантических деталей, в которых я не разбираюсь, это вызвало неясную исключение.

Прикрепляя this.Address к DbContext, структура сущностей понимает, что адрес уже существует в базе данных, и что вы создаете ассоциацию с существующим объектом при назначении его this.Client.Address.

ИМО, семантика Entity Framework должна быть лучше задокументирована. Есть много подобных случаев, когда возникает вводящее в заблуждение или непонятное исключение. Например, при присоединении объектов важно помнить, что они присоединяются рекурсивно. Если бы вы в новом контексте данных присоединили this.Client, он также рекурсивно присоединил бы this.Client.Address, и любые другие объекты this.Client могли бы ссылаться. По этой причине, если бы вы явно прикрепили this.Client, а затем this.Address, вы получили бы исключение, потому что адрес был неявно присоединен, когда вы прикрепляли this.Client.

person w.brian    schedule 31.08.2012
comment
БОЛЬШОЕ спасибо, Брайан, это действительно решило проблему: D, но не могли бы вы рассказать немного о том, что именно идет не так, когда я устанавливаю this.Client.Address = this.Address; перед прикреплением адреса. - person atiyar; 31.08.2012
comment
я знаю, что ObjectServices отслеживает только EntityObject, о которых ему известно. в моем случае только объект Address. и поскольку объект Client создается в коде, контекст об этом не знает. но когда я назначаю this.Client.Address = this.Address;, я не создаю полный граф объектов, связывающий объекты друг с другом? а затем после этого, когда я присоединяю объект Address, не должен ли контекст также узнавать об объекте Client, поскольку теперь он связан с объектом Address? так что я все еще не получаю это исключение ...temporary EntityKey value.... в любом случае, спасибо еще раз. - person atiyar; 01.09.2012
comment
и я ПОЛНОСТЬЮ согласен с вашим better documentation мнением. я вижу много людей, которые используют EF, но очень немногие из них имеют представление о том, что происходит под капотом. - person atiyar; 01.09.2012
comment
Если вы хотите увидеть, как DbContext видит сущность, передайте сущность методу dataContext.Entry() и проверьте ее EntityState. Если это Detached, то DbContext ничего о нем не знает, даже если он изначально был извлечен предыдущим DbContext. DbContext нужно сообщить, прикрепив объект: Эй, этот объект уже существует в базе данных. Таким образом, при сохранении изменений DbContext знает, что этот объект Address уже существует в базе данных, и нет необходимости создавать новый перед вставкой строки Client. - person w.brian; 01.09.2012