Связи Entity Framework с несколькими (отдельными) ключами в представлении

У меня возникли проблемы с настройкой модели 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 ответов


EDIT 2:"новые вещи вышли на свет, человек" - Его Dudeness

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

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

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


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

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

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

настройте это через fluent 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. как я предложил в комментарии раньше, вы не будете в любом случае, вы можете сопоставить их с одним представлением/таблицей, поэтому у вас есть несколько вариантов:

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

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

    Си. карте один объект к его базовой таблицы и один для представления.

резюме

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

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

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


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

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

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

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

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

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

и vwContact содержит ключ KeyField на который ссылаются многие SomeObjectS и еще один ключ LegacyKeyField на который ссылаются многие LegacyObjects.

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

дать 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 on vwContact, они не могут быть окончательным ключ объект, поэтому это либо составной ключ, который имеет оба из них, либо выберите один, или есть другое поле, которое вы не упомянули, которое является реальные первичный ключ. Отсюда на самом деле невозможно сказать, какой правильный вариант есть.


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

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
}

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

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

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

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

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

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

есть некоторые ограничения:

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

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

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


Я думаю, что это может быть возможно с помощью методов расширения, хотя и не напрямую через 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();

иногда имеет смысл сопоставить его с другого конца отношений, в вашем случае:

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

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

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

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

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

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

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