Entity Framework связывает с несколькими (отдельными) ключами в представлении - PullRequest
15 голосов
/ 26 января 2012

У меня проблемы с настройкой модели Entity Framework 4.

Объект контакта предоставляется в базе данных как обновляемое представление. Также из-за истории базы данных это представление «Контакт» имеет два разных ключа, один из устаревшей системы. Поэтому некоторые другие таблицы ссылаются на контакт с «ContactID», в то время как другие старые таблицы ссылаются на контакт с «LegacyContactID».

Так как это представление, в базе данных нет внешних ключей, и я пытаюсь вручную добавить ассоциации в конструкторе. Но беглые ассоциации, по-видимому, не позволяют определить, на какое поле ссылаются.

Как мне построить эту модель?

public class vwContact
{
  public int KeyField { get; set; }
  public string LegacyKeyField { get; set; }
}

public class SomeObject
{
  public virtual vwContact Contact { get; set; }
  public int ContactId { get; set; } //references vwContact.KeyField
}

public class LegacyObject
{
  public virtual vwContact Contact { get; set; }
  public string ContactId { get; set; } //references vwContact.LegacyKeyField
}

ModelCreatingFunction(modelBuilder)
{
  // can't set both of these, right?
  modelBuilder.Entity<vwContact>().HasKey(x => x.KeyField);
  modelBuilder.Entity<vwContact>().HasKey(x => x.LegacyKeyField);

  modelBuilder.Entity<LegacyObject>().HasRequired(x => x.Contact).??? 
  //is there some way to say which key field this reference is referencing?
}

Ответы [ 6 ]

4 голосов
/ 16 февраля 2012

РЕДАКТИРОВАТЬ 2: "Новые вещи появились на свет, человек" - Его Dudeness

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

Я оставил предложенный код ниже, потому что я все еще рекомендую использовать базовый класс для вашей управляемости на C #, но ниже кода я обновил свой ответ и предоставил другие варианты обхода.

К сожалению, раскрытая правда заключается в том, что вы не можете выполнить то, что ищете, не изменив SQL из-за ограничений на код EF 4.1+.


Базовый класс контактов

public abstract class BaseContact
{
   // Include all properties here except for the keys
   // public string Name { get; set; }
}

Классы сущностей

Настройте это через свободный API, если хотите, но для простоты я использовал аннотации данных

public class Contact : BaseContact
{
   [Key]
   public int KeyField { get; set; }
   public string LegacyKeyField { get; set; }
}

public class LegacyContact : BaseContact
{
   public int KeyField { get; set; }
   [Key]
   public string LegacyKeyField { get; set; }    
}

Использование сущностей

  1. Классы, которые ссылаются или управляют контактными объектами, должны ссылаться на базовый класс так же, как интерфейс:

    public class SomeCustomObject
    {
       public BaseContact Contact { get; set; }
    }
    
  2. Если позже вам нужно программно определить, с каким типом вы работаете, используйте typeof() и соответственно управляйте сущностью.

    var co = new SomeCustomObject(); // assume its loaded with data
    if(co.Contact == typeof(LegacyContact)
        // manipulate accordingly.
    

Новые параметры и обходные пути

  1. Как я уже говорил в комментарии ранее, вы не сможете сопоставить их с одним представлением / таблицей, так что у вас есть пара вариантов:

    а. сопоставьте ваши объекты с их базовыми таблицами и измените методы get / read для репозиториев и классов обслуживания, извлекаемых из объединенного представления -или-

    б. создайте второй вид и отобразите каждый объект на соответствующий вид.

    с. сопоставить одну сущность с базовой таблицей, а другую - с представлением.

Резюме

Сначала попробуйте (B) , создав отдельное представление, поскольку оно требует наименьшего количества изменений как кода, так и схемы БД (вы не работаете с базовыми таблицами и не влияете на хранимые процедуры). Это также гарантирует, что ваши EOC C # POCO будут функционировать одинаково (один для вида и один для таблицы может вызвать причуды). Ответ Мигеля, приведенный ниже, кажется примерно таким же, поэтому я бы начал здесь, если это возможно.

Параметр (C) кажется наихудшим, потому что ваши объекты POCO могут вести себя непредвиденными причудами при сопоставлении с различными частями SQL (таблицами и представлениями), вызывая проблемы кодирования в будущем.

Опция (A) , хотя она наилучшим образом соответствует намерениям EF (сущности, сопоставленные с таблицами), это означает, что для получения объединенного представления необходимо изменить службы / репозитории C # для работы с сущностями EF для добавления , Обновить, Удалить операции, но указать Pull / Read-like методы, чтобы получить данные из объединенных представлений. Вероятно, это ваш лучший выбор, но он требует больше работы, чем (B) , и может также повлиять на схему в долгосрочной перспективе. Чем больше сложность, тем больше риск.

0 голосов
/ 22 февраля 2012

Иногда имеет смысл отобразить его с другого конца отношения, в вашем случае:

modelBuilder.Entity<LegacyObject>().HasRequired(x => x.Contact).WithMany().HasForeignKey(u => u.LegacyKeyField);

, однако для этого потребуется, чтобы u.LegacyKeyField был отмечен как первичный ключ.

И тогда я дам два моих цента:

, если Legacy db использует LegacyKeyField, то, возможно, legacy db будет только для чтения.В этом случае мы можем создать два отдельных контекста Legacy и Non-legacy и отобразить их соответствующим образом.Это может стать немного грязным, так как вам придется помнить, какой объект происходит из какого контекста.Но с другой стороны, ничто не мешает вам добавить один и тот же первый код EF-объекта в 2 разных контекста

Другое решение - использовать представления с добавленным ContactId для всех других унаследованных таблиц и отобразить их в один контекст.Это будет снижать производительность ради наличия более чистых объектов контекста, но этому можно противодействовать на стороне sql: индексированные представления, материализованные представления, хранимые процессы и т. Д. Так, чем LEGACY_OBJECT становится VW_LEGACY OBJECT с перенесенным CONTACT.ContactId, тогда:

modelBuilder.Entity<LegacyObject>().ToTable("VW_LEGACY_OBJECT"); 
modelBuilder.Entity<LegacyObject>().HasRequired(x => x.Contact).WithMany().HasForeignKey(u => u.ContactId);

Лично я бы пошел на создание "представлений картографирования" с CustomerId для устаревших таблиц, так как он более чист с точки зрения уровня C #, и вы можете сделать эти представления похожими на реальные таблицы.Также сложно предложить решение, не зная, в каком именно сценарии у вас возникли проблемы: запросы, загрузка, сохранение и т. Д.

0 голосов
/ 22 февраля 2012

Я думаю, что это возможно при использовании методов расширения, но не напрямую через EF, как @Matthew Walton упоминал в своем редактировании выше.

Однако с помощью методов расширения вы можете указать, что делать за кулисами, и просто вызвать это.

public class LegacyObject
{
    public virtual vwContact Contact { get; set; }
    public string ContactId { get; set; } //references vwContact.LegacyKeyField
}

public class LegacyObjectExtensions
{
    public static vwContact Contacts(this LegacyObject legacyObject)
    {
        var dbContext = new LegacyDbContext();
        var contacts = from o in legacyObject
                       join c in dbContext.vwContact
                           on o.ContactId == c.LegacyKeyField
                       select c;

        return contacts;
    }
}

и

public class SomeObject
{
    public virtual vwContact Contact { get; set; }
    public int ContactId { get; set; } //references vwContact.KeyField
}

public class SomeObjectExtensions
{
    public static vwContact Contacts(this SomeObject someObject)
    {
        var dbContext = new LegacyDbContext();
        var contacts = from o in someObject
                       join c in dbContext.vwContact
                           on o.ContactId == c.KeyField
                       select c;

        return contacts;
    }
}

Тогда для использования вы можете просто сделать так:

var legacyContacts = legacyObject.Contacts();
var someContacts = someObject.Contacts();
0 голосов
/ 19 февраля 2012

Я перепробовал все, что вы можете себе представить, и обнаружил, что большинство решений не будут работать в этой версии EF ... возможно, в будущих версиях он поддерживает ссылку на другую сущность с помощью уникального поля, но это не так сейчас. Я также нашел два решения, которые работают, но они являются скорее обходным путем, чем решения.

Я пробовал все следующие вещи, которые не работали:

  • Отображение двух сущностей в одну таблицу: это не разрешено в EF4.
  • Наследование от базы, у которой нет ключевых определений: все корневые классы должны иметь ключи, чтобы наследуемые классы разделяли этот общий ключ ... именно так работает наследование в EF4.
  • Наследование от базового класса, который определяет все поля, включая ключи, а затем использует modelBuilder, чтобы сказать, какие базовые свойства являются ключами производных типов: это не работает, потому что методы HasKey, Property и другие, которые принимают члены как параметры, должны ссылаться на членов самого класса ... ссылка на свойства базового класса не допускается. Это не может быть сделано: modelBuilder.HasKey<MyClass>(x => x.BaseKeyField)

Две вещи, которые я сделал, сработали:

  • Без изменений БД: Отобразить в таблицу, которая является источником рассматриваемого представления ... то есть, если vwContact является представлением таблицы Contacts, вы можете сопоставьте класс с Contacts и используйте его, установив ключ на KeyField, а другой класс сопоставьте с представлением vwContacts, с ключом LegacyKeyField. В классе Contacts должен существовать LegacyKeyField, и вам придется управлять этим вручную при использовании класса Contacts. Кроме того, при использовании класса vwContacts вам придется вручную управлять KeyField, если только это не поле автоинкремента в БД, в этом случае вы должны удалить свойство из класса vwContacts.

  • Изменение базы данных: Создайте другое представление, как vwContacts, скажем vwContactsLegacy, и сопоставьте его с классом, ключом которого является LegacyKeyField, и map vwContacts к исходному виду, используя KeyField в качестве ключа. Также применимы все ограничения из первого случая: vwContacts должен иметь LegacyKeyField, управляемый вручную. И vwContactsLegacy должен иметь KetField, если он не является идентификатором автоинкремента, в противном случае он не должен быть определен.

Существуют некоторые ограничения:

Как я уже сказал, эти решения являются обходными путями ... не реальными решениями, есть некоторые серьезные последствия, которые могут даже сделать их нежелательными:

  • EF не знает, что вы отображаете два класса на одно и то же. Поэтому, когда вы обновляете одну вещь, другая может быть изменена или нет, это зависит от того, кэшированы ли объекты или нет. Кроме того, вы можете иметь два объекта одновременно, которые представляют одну и ту же вещь в резервном хранилище, например, вы загружаете vwContact и vwContactLegacy, изменяете оба, а затем пытаетесь сохранить оба ... вам придется позаботиться об этом сами.

  • Вам придется управлять одной из клавиш вручную. Если вы используете класс vwContacts, KeyFieldLegacy находится там, и вы должны заполнить его. Если вы хотите создать vwContacts и ассоциировать с LegacyObject, то вам нужно создать ссылку вручную, потому что LegacyObject принимает vwContactsLegacy, а не vwContacts ... вам нужно будет создать ссылку, установив ContactId поле.

Я надеюсь, что это скорее справка , чем разочарование , EF - мощная игрушка, но она далека от совершенства ... хотя я думаю, что она получит намного лучше в следующих версиях.

0 голосов
/ 15 февраля 2012

Вы должны быть в состоянии сделать это с двумя разными объектами, чтобы представить представление Контакт.

public class vwContact
{
  public int KeyField { get; set; }
  public string LegacyKeyField { get; set; }
}

public class vwLegacyContact
{
  public int KeyField { get; set; }
  public string LegacyKeyField { get; set; }
}

public class SomeObject
{
  public virtual vwContact Contact { get; set; }
  public int ContactId { get; set; } //references vwContact.KeyField
}

public class LegacyObject
{
  public virtual vwLegacyContact Contact { get; set; }
  public string ContactId { get; set; } //references vwLegacyContact.LegacyKeyField
}

ModelCreatingFunction(modelBuilder)
{
  // can't set both of these, right?
  modelBuilder.Entity<vwContact>().HasKey(x => x.KeyField);
  modelBuilder.Entity<vwLegacyContact>().HasKey(x => x.LegacyKeyField);

  // The rest of your configuration
}
0 голосов
/ 15 февраля 2012

Редактировать Я не уверен, что это действительно возможно, и вот почему:

Предполагается, что внешний ключ ссылается на первичный ключ.У вас есть два поля, которые оба выступают в качестве первичных ключей vwContact, но в зависимости от того, какой объект вы запрашиваете, это другое поле, которое является первичным ключом.Вы можете иметь только один первичный ключ одновременно, и, хотя у вас может быть составной первичный ключ, вы не можете выполнять первичные ключевые операции только с половиной его - у вас должен быть составной внешний ключ, с которым можно ссылаться на него.

Вот почему у Entity Framework нет способа указать столбец сопоставления на целевой стороне, потому что имеет для использования первичного ключа.

Теперь вы можете наложить слойеще несколько объектов поверх сущностей EF для ручного поиска и имитации свойств навигации, но я не думаю, что вы действительно можете заставить EF делать то, что вы хотите, потому что сам SQL не будет делать то, что вы хотите - правилоодин первичный ключ на таблицу, и он не подлежит обсуждению.

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

Оригинальный ответ, который не сработает

Итак, vwContact содержит ключ KeyField, на который ссылаются многие SomeObject s, и другой ключ LegacyKeyField, на который ссылаются многие LegacyObject s.

I думаю вот каквы должны подойти к этому:

Предоставить vwContact свойства навигации для SomeObject и LegacyObject коллекций:

public virtual ICollection<SomeObject> SomeObjects { get; set; }
public virtual ICollection<LegacyObject> LegacyObjects { get; set; }

Дать эти свойства навигации внешним ключам для использования:

modelBuilder.Entity<vwContact>()
    .HasMany(c => c.SomeObjects)
    .WithRequired(s => s.Contact)
    .HasForeignKey(c => c.KeyField);
modelBuilder.Entity<vwContact>()
    .HasMany(c => c.LegacyObjects)
    .WithRequired(l => l.Contact)
    .HasForeignKey(c => c.LegacyKeyField);

Беда в том, что я думаю, вы уже пробовали это, и это не сработало, и в этом случае я не могу предложить вам больше, так как я не сделал огромное количество такого рода вещей (наша база данных намного ближе к тем вещам, которые ожидает EF, поэтому нам пришлось сделать относительно минимальные переопределения сопоставления, обычно с отношениями «многие ко многим».

Что касается ваших двух вызовов HasKey onvwContact, они не могут быть окончательным ключом для объекта, так что этоr составной ключ, который содержит оба из них, или выберите один, или есть другое поле, которое вы не упомянули, это первичный ключ real .Отсюда невозможно сказать, какой правильный вариант есть.

...