ASP.NET MVC со свойствами навигации EF 4.1

После нескольких дней изучения EF, чтобы понять (вроде ..), как это работает, я наконец понял, что у меня может быть большая проблема.

Представьте, что у меня есть две сущности: Pais и UF. Отношения между ними Pais (0..1) ... (*) UF. Скриншот: https://i.imgur.com/rSOFU.jpg.

Сказал, что у меня есть контроллер с именем UFController, и у него есть действия для Edit и Create, которые вполне подходят. Мои представления используют помощник EditorFor (или аналогичный) для входных данных, поэтому, когда я отправлю форму, контроллер получит объект UF, заполненный всеми данными (автоматически) со ссылкой на почти пустой Pais. Мой код просмотра (его часть):

@* UF attributes *@
@Html.EditorFor(m => m.Sigla)
@Html.EditorFor(m => m.Descricao)
@Html.EditorFor(m => m.CodigoIBGE)
@Html.EditorFor(m => m.CodigoGIA)
@* Pais primary key ("ID") *@
@Html.EditorFor(m => m.Pais.Codigo) // Pais id

Код действия контроллера Edit:

[HttpPost]
public ActionResult Edit(UF uf)
{
    try
    {
        if (ModelState.IsValid)
        {
            db.UFs.Attach(uf);
            db.ObjectStateManager.ChangeObjectState(uf, EntityState.Modified);
            db.SaveChanges();

            return this.ClosePage(); // An extension. Just ignore it.
        }
    }
    catch (Exception e)
    {
        this.ModelState.AddModelError("Model", e.Message.ToString());
    }

    return View(uf);
}

Когда я отправляю форму, это действие получает как uf:

{TOTALWeb.UF}
    base {System.Data.Objects.DataClasses.EntityObject}: {TOTALWeb.UF}
    (...)
    CodigoGIA: 0
    CodigoIBGE: 0
    Descricao: "Foobar 2001"
    ID: 936
    Pais: {TOTALWeb.Pais}
    PaisReference: {System.Data.Objects.DataClasses.EntityReference<TOTALWeb.Pais>}

И uf.Pais:

{TOTALWeb.Pais}
    base {System.Data.Objects.DataClasses.EntityObject}: {TOTALWeb.Pais}
    Codigo: 0
    CodigoBACEN: null
    CodigoGIA: null
    CodigoIBGE: null
    Descricao: null
    UF: {System.Data.Objects.DataClasses.EntityCollection<TOTALWeb.UF>}

Исходная информация (та, что есть в базе) - uf.Pais.Codigo == 716. Итак, сейчас я получаю обновленную информацию. Проблема в том, что контроллер не загружает FK в базу данных.

Я не хочу устанавливать EntityState с uf.Pais на Modified, потому что сама сущность не была изменена (я не менял информацию из этой записи), но была изменена связь.

Другими словами, я пытаюсь изменить значение FK, указав uf.Pais на другой экземпляр Pais. Afaik, невозможно изменить состояние отношения на Modified (выбросить исключение), поэтому я ищу альтернативные решения.

Я прочитал кучу тем, которые нашел в Google, по поводу такого рода проблем, но до сих пор не нашел простого и элегантного решения. Последние, которые я прочитал здесь, в stackoverflow:

Несколько дней назад я задал вопрос о подобной проблеме (Entity Framework 4.1 - EntityState по умолчанию для FK?). В то время я не понимал, как работает EF, поэтому теперь некоторые вещи кажутся мне ясными (поэтому я открываю новый вопрос).

Для действия Create я тестировал это решение (предложенное Ладиславом по моему другому вопросу), но оно генерирует дополнительный выбор (который в конечном итоге может быть медленным для нас):

// Here UF.Pais is null
db.UFs.AddObject(uf);
// Create dummy Pais
var pais = new Pais { Id = "Codigo" };
// Make context aware of Pais
db.Pais.Attach(pais); // <- Executing a SELECT on the database, which -can- be slow.
// Now make the relation
uf.Pais = pais;
db.SaveChanges();

Я могу воспроизвести это для Edit (я думаю), но мне не нужен этот дополнительный SELECT.

Итак, в резюме: я пытаюсь использовать свойства навигации для отправки данных в контроллер и сохранения их непосредственно в базе данных, используя быстрый и простой способ (без особых проблем с сущностью - они просты, но у нас есть огромные возможности). и очень сложные с большим количеством FK!). Мой вопрос: есть решение, которое не требует выполнения другого запроса в базе данных (простого)?

Спасибо,

Рикардо

PS: извините за ошибки в английском и за любые недоразумения.

Обновление 1: с помощью решения BennyM (вроде ..)

Я протестировал следующий код, но он не работает. Это вызывает исключение: «Объект с таким же ключом уже существует в ObjectStateManager. ObjectStateManager не может отслеживать несколько объектов с одним и тем же ключом». Наверное, потому, что Пайс уже в контексте?

Я использую класс Entities (созданный EF) в качестве контекста. Кроме того, я не знаю, что это за метод Entry, и я не знаю, где он. Ради "развлечения" я проверил это:

// Attach the Pais referenced on editedUF, since editedUF has the new Pais ID, not the old one.
Pais existingPais = new Pais { Codigo = editedUF.Pais.Codigo };
db.Paises.Attach(existingPais);

// Attach the edited UF.
db.UFs.Attach(editedUF);

// Set the correct Pais reference (ignoring the current almost-null one).
editedUF.Pais = existingPais;

// Change the object state to modified.
db.ObjectStateManager.ChangeObjectState(editedUF, EntityState.Modified);

// Save changes.
db.SaveChanges();

return this.ClosePage();

Исключение выдается, когда я пытаюсь прикрепить editedUF к текущему контексту. Я работаю над этой идеей прямо сейчас, пытаюсь найти другие решения. Кроме того, вы правы, BennyM, присоединение Pais к контексту не генерирует дополнительный SELECT. Я не знаю, что произошло в тот раз, на самом деле это ничего не меняет с базой данных.

Тем не менее, это ручное решение: я должен делать это для каждого FK. Этого я пытаюсь избежать. Видите ли, некоторые программисты, даже если вы объясните 100 раз, не вспомнят, что нужно делать это с каждым FK. В конце концов, это вернется ко мне, поэтому я стараюсь избегать всего, что может привести к ошибкам (базы данных или кода), чтобы каждый мог работать без стресса. :)


person Ricardo    schedule 09.08.2011    source источник


Ответы (2)


Я отвечаю на свой вопрос, потому что нашел простое решение (по крайней мере, в моем случае). В моем сценарии для ввода данных используется множество представлений (что означает, что у меня много сущностей). Мне нужно было простое и удобное решение, поэтому я удалил весь EDMX-файл Entities (Ctrl + A, Удалить!).

Затем я решил снова добавить сущности Pais и UF, но установив флажок для отображения атрибута FK. Сначала я, хотя они не могут работать вместе, но они могут, но вы должны быть немного осторожны с тем, как его использовать. Теперь они связаны со свойствами навигации и открытым FK.

Причина, по которой я не мог добавить атрибут FK, в том, что я делал это вручную. Используя «Обновить модель из базы данных», снова проверив правильный вариант, все работало безупречно.

В моем режиме редактирования я устанавливаю идентификатор Pais в атрибут FK, а не Pais.Codigo. Причина, по которой я это делаю, заключается в том, что атрибут FK является скалярным свойством, и тогда я могу обнаруживать изменения.

Это текущий код представления для входа Pais (это не совсем то, но похоже на это):

@Html.EditorFor(m => m.PaisCodigo)

Кстати, PaisCodigo - это ФК. Да, это может немного сбить с толку с Pais.Codigo, но мы не определили никаких правил именования (пока). Будем признательны за любые предложения по этой идее.

Последний код действия Edit выглядит следующим образом (я удалил обработку ошибок, чтобы это выглядело просто!):

[HttpPost]
public ActionResult Edit(UF editedUF)
{
    if (ModelState.IsValid)
    {
        // Attach the edited UF into the context and change the state to Modified.
        db.UFs.Attach(editedUF);
        db.ObjectStateManager.ChangeObjectState(editedUF, EntityState.Modified);

        // Save changes.
        db.SaveChanges();

        // Call an extension (it's a redirect action to another page, just ignore it).
        return this.ClosePage();
    }
}

Вот что я получаю, когда отправляю форму для editedUF:

{TOTALWeb.UF}
base {System.Data.Objects.DataClasses.EntityObject}: {TOTALWeb.UF}
(...)
CodigoGIA: 0
CodigoIBGE: 0
CodigoPais: 0 <-- new Pais ID!
Descricao: "Foobar 2000"
ID: 902
Pais: {TOTALWeb.Pais}
PaisReference: {System.Data.Objects.DataClasses.EntityReference<TOTALWeb.Pais>}
Sigla: "RI"
Usuarios: {System.Data.Objects.DataClasses.EntityCollection<TOTALWeb.Usuario>}

Как видите, CodigoPais указывает на новый Pais ID.

О свойстве навигации editedUF.Pais есть небольшая деталь. Перед присоединением к контексту он должен быть нулевым. Но, эй, после добавления, вот что происходит:

{TOTALWeb.Pais}
base {System.Data.Objects.DataClasses.EntityObject}: {TOTALWeb.Pais}
(...)
Codigo: 0
CodigoBACEN: 1058
CodigoGIA: 0
CodigoIBGE: null
Descricao: "Brasil"
UFs: {System.Data.Objects.DataClasses.EntityCollection<TOTALWeb.UF>}

Итак, он заполнен. Стоимость этого должна составлять один запрос, но я не мог зафиксировать его на мониторе.

Другими словами, просто откройте FK, измените его с помощью View и используйте свойство навигации, чтобы сделать код немного более понятным. Вот и все! :)

Всем спасибо,

Рикардо

PS: я использую dotConnect для Oracle в качестве основы для EF 4.1. Мы не используем SQL Server (по крайней мере, пока). «Монитор», о котором я говорил ранее, был dbMonitor от devArt, поэтому я могу видеть все запросы, отправленные в базу данных Oracle. И снова приношу свои извинения за ошибки в английском!

person Ricardo    schedule 11.08.2011
comment
Вы не используете EF 4.1. Я бы посоветовал перейти на поколение POCO, если вы используете EF 4.0, кстати. - person BennyM; 12.08.2011
comment
@BennyM Как видите, я новичок в этом, но почему вы предлагаете перейти на POCO? Кодирование всего с нуля могло быть для меня излишним (я как раз пытался избежать этого, когда начал использовать EF). - person Ricardo; 12.08.2011
comment
Как и в стратегии генерации кода по умолчанию, доступен генератор кода POCO (и даже dbcontext, как в коде сначала). visualstudiogallery.msdn.microsoft.com/ - person BennyM; 12.08.2011
comment
@BennyM Итак, одна из величайших черт POCO - это постоянное игнорирование. Думаю, это улучшит производительность (особенно в логической части). И я где-то читал, что тоже легче поддерживать. В вашей ссылке есть ссылка в блоге (см. Описание), объясняющая, как работает генератор, и еще одна ссылка, указывающая на рекомендуемый способ генерации кода POCO (что-то связанное с DbContext): Первое руководство по модели и базе данных EF 4.1. Я собираюсь протестировать POCO и посмотреть, как он работает. Есть ли преимущества создания кода с нуля? Спасибо! - person Ricardo; 12.08.2011
comment
У вас уже есть edmx, так что вы можете продолжать так делать. Сделать это самостоятельно (сначала dbcontext с кодом) более естественно для разработчиков. - person BennyM; 12.08.2011
comment
Ах, правда. Я перенес генерацию кода в генератор DbContext, и после переноса некоторых функций он выглядит нормально (хотя не тестировал). Теперь это выглядит проще, и классы намного понятнее, чем раньше. Большое спасибо! - person Ricardo; 12.08.2011

Если вы включите внешние ключи в свою модель. Итак, добавьте свойство PaisId к объекту UF, вы можете напрямую установить его, и оно обновит ассоциацию.

using (var db = new Context())
{
   db.UFs.Attach(editedUF);
   editedUF.PaisId = theForeignKey;
   db.Entry(editedUF).State = EntityState.Modified;
   db.SaveChanges();
}

Также я протестировал подход, который вы уже упомянули в своем вопросе, и я не получаю этого дополнительного выбора при прикреплении объекта. Так что это тоже работает:

using (var db = new Context())
{
   ExistingPais pais = new Pais{Id =theId};
   db.Pais.Attach(pais);
   db.UF.Attach(editedUF);
   editedUF.Pais = pais;
   db.Entry(editedUF).State = EntityState.Modified;
   db.SaveChanges();
}

Предположим, ваш код выглядит так:

public class  Pais
{
    public int Id { get; set; }
    public virtual ICollection<UF> UFs { get; set; }
} 

public class UF
{
  public int Id { get; set; }
  public virtual Pais Pais { get; set; }
  public int PaisId { get; set; }
}  
person BennyM    schedule 10.08.2011
comment
Я не уверен, насколько точно ваш Context класс. Мой db - это Entities класс, созданный EF. Кроме того, и Pais, и UF были сгенерированы на основе кода моей базы данных. Я отредактировал свой вопрос, чтобы показать, что я пробовал. - person Ricardo; 11.08.2011
comment
Кстати, у меня здесь нет Entry метода. Странно, поскольку я видел это на многих примерах. - person Ricardo; 11.08.2011
comment
Не думайте, что вы сначала используете код или класс dbcontext, а скорее entity framework 4.0 с objectcontext. - person BennyM; 12.08.2011
comment
Это я уже знал. Примеры / идеи Code First были для меня просто информационным ресурсом, а не тем, как я работал. Думаю, мне следовало прояснить это в вопросе. Простите за это. - person Ricardo; 12.08.2011