Связи 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?
}
Ответы
Ответ 1
РЕДАКТИРОВАТЬ 2: "Новые вещи стали известны, человек" - Его Дудность
После нескольких экспериментов и новостей я нашел использование базового класса, а дочерние классы с разными ключами не будут работать сами по себе. При первом использовании кода базовые объекты должны определять ключ, если они явно не отображаются в таблицы.
Я оставил предложенный код ниже, потому что по-прежнему рекомендую использовать базовый класс для управления С#, но я под кодом, я обновил свой ответ и предоставил другие варианты обхода.
К сожалению, раскрытая правда заключается в том, что вы не можете выполнить то, что ищете, не изменяя 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; }
}
Использование объектов
-
Классы, которые ссылаются или манипулируют контактными объектами, должны ссылаться на базовый класс так же, как на интерфейс:
public class SomeCustomObject
{
public BaseContact Contact { get; set; }
}
-
Если позже вам нужно программно определить, с каким типом вы работаете с typeof()
, и соответствующим образом обрабатывать объект.
var co = new SomeCustomObject(); // assume its loaded with data
if(co.Contact == typeof(LegacyContact)
// manipulate accordingly.
Новые параметры и обходные пути
-
Как я уже говорил в комментарии, вы не сможете сопоставить их ни с одним представлением/таблицей, так что у вас есть пара вариантов:
а. сопоставьте свои объекты с базовыми таблицами и измените методы "get/read" в репозиториях и классах обслуживания, которые вытащите из объединенного представления -или -
б. создать второе представление и сопоставить каждый объект с соответствующим видом.
с. сопоставить одну сущность с ее базовой таблицей, а другую - с представлением.
Резюме
Сначала попробуйте (B), создав отдельный просмотр, поскольку он требует наименьшего количества изменений для кода и схемы БД (вы не играете с базовыми таблицами или не влияете на хранимые процедуры). Он также гарантирует, что ваши EF С# POCOs будут функционировать одинаково (один для просмотра, а один для таблицы может вызвать причуды). Ответ Мигеля ниже выглядит примерно таким же, поэтому я бы начал здесь, если это возможно.
Вариант (C) кажется худшим, потому что ваши объекты POCO могут вести себя непредвиденными причудами при сопоставлении с разными фрагментами SQL (таблицы против представлений), вызывая проблемы с кодированием в будущем.
Опция (A), в то время как она лучше всего подходит для намерений EF (объекты, сопоставленные с таблицами), это означает, что для получения объединенного представления вы должны изменить свои сервисы/хранилища С# для работы с объектами EF для Добавлять, обновлять, удалять операции, но рассказывать методам Pull/Read-like для захвата данных из совлокальных представлений. Вероятно, это ваш лучший выбор, но он требует больше работы, чем (B), а также может повлиять на схему в долгосрочной перспективе. Более сложная ситуация равна большему риску.
Ответ 2
Изменить Я не уверен, что это действительно возможно, и вот почему:
Предполагается, что внешний ключ ссылается на первичный ключ. У вас есть два поля, которые действуют как первичные ключи vwContact, но в зависимости от того, какой объект вы запрашиваете у него другое поле, которое является первичным ключом. У вас может быть только один первичный ключ одновременно, и хотя у вас может быть составной первичный ключ, вы не можете делать первичные ключевые вещи только с половиной - вы должны иметь сложный внешний ключ, с которым можно ссылаться.
Вот почему у Entity Framework нет способа указать столбец сопоставления на целевой стороне, потому что он должен использовать первичный ключ.
Теперь вы можете сложить еще несколько объектов поверх объектов EF, чтобы выполнить ручной поиск и имитировать свойства навигации, но я не думаю, что вы действительно можете заставить EF делать то, что вы хотите, потому что сам SQL не будет сделайте то, что вы хотите - правило - это один первичный ключ для таблицы, и он не подлежит обсуждению.
Из того, что вы сказали о своей структуре базы данных, возможно, вам будет возможно написать миграцию script, которая может предоставить контактным объектам согласованный первичный ключ и обновить все остальное, чтобы ссылаться на них с помощью этого единственного первичного ключа, а не две системы, полученные из устаревших данных, так как вы, конечно же, можете присоединяться к любым полям, которые вам нравятся. Я не думаю, что вы получите бесшовно функциональную EF-модель без изменения вашей базы данных.
Оригинальный ответ, который не будет работать
Итак, vwContact
содержит ключ KeyField
, на который ссылаются многие SomeObject
и еще один ключ LegacyKeyField
, на который ссылаются многие LegacyObject
s.
Думаю, именно так вы должны подходить к этому:
Дайте свойства навигации 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
, они не могут быть и окончательным ключом для объекта, так что это либо составной ключ, который имеет оба из них, либо выбирает один, либо другое поле вы не упомянули, что является реальным первичным ключом. Отсюда невозможно сказать, какой именно вариант есть.
Ответ 3
Вы должны иметь возможность сделать это с помощью двух разных объектов для представления вида "Контакты".
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
}
Ответ 4
Я пробовал все, что вы можете себе представить, и обнаружил, что большинство решений не будет работать в этой версии EF... возможно, в будущих версиях он поддерживает ссылку на другой объект с помощью уникального поля, но это не так Теперь. Я также нашел два решения, которые работают, но они скорее являются обходным решением, чем решениями.
Я пробовал все следующие вещи, которые не работали:
- Отображение двух объектов в одну и ту же таблицу: это не разрешено в EF4.
- Наследование с базы, которая не имеет ключевых определений: все корневые классы должны иметь ключи, так что унаследованные классы имеют общий общий ключ... так как наследование работает в EF4.
- Унаследовать от базового класса, который определяет все поля, включая ключи, а затем использовать modelBuilder, чтобы указать, какие базовые свойства являются ключами производных типов: это не работает, потому что methos HasKey, Свойство и другие, которые принимают члены как параметры, должны ссылаться на элементы самого класса... ссылки на свойства базового класса недопустимы. Это невозможно сделать:
modelBuilder.HasKey<MyClass>(x => x.BaseKeyField)
Две вещи, которые я делал, работали:
-
Без изменений в базе данных: Карта в таблицу, которая является источником рассматриваемого представления... то есть, если vwContact
является представлением таблицы Contacts
, тогда вы может сопоставить класс с Contacts
и использовать его, установив ключ в KeyField
и другое сопоставление классов в представление vwContacts
, при этом ключ LegacyKeyField
. В классе Контакты, LegacyKeyField должен существовать, и вам придется управлять этим вручную при использовании класса "Контакты". Кроме того, при использовании класса vwContacts вам придется вручную управлять KeyField, если это не поле автоинкремента в БД, в этом случае вы должны удалить свойство из класса vwContacts.
-
Изменение DB: Создайте другое представление, как и vwContacts
, скажем vwContactsLegacy
, и сопоставьте его с классом, в котором ключ является LegacyKeyField
, а map vwContacts
в исходное представление, используя KeyField
в качестве ключа. Все ограничения в первом случае также применяются: vwContacts должен иметь LegacyKeyField, управляемый вручную. И vwContactsLegacy, должен иметь KetField, если он не является автоинкрементным idenitity, в противном случае он не должен быть определен.
Существуют некоторые ограничения:
Как я уже сказал, эти решения являются обходными... не реальными решениями, есть некоторые серьезные последствия, которые могут даже сделать их нежелательными:
-
EF не знает, что вы сопоставляете два класса с одним и тем же. Поэтому, когда вы обновляете одно, другое может быть изменено или нет, это зависит от того, кешируются ли объекты или нет. Кроме того, вы могли бы одновременно иметь два объекта, которые представляют одно и то же в хранилище резервных копий, поэтому скажите, что вы загружаете vwContact, а также vwContactLegacy, меняете оба, а затем пытаетесь сохранить оба... вам нужно будет заботиться об этом сами.
-
Вам нужно будет управлять одним из ключей вручную. Если вы используете класс vwContacts, KeyFieldLegacy существует, и вы должны его заполнить. Если вы хотите создать vwContacts и ассоциировать с LegacyObject
, вам нужно создать ссылку вручную, потому что LegacyObject
принимает vwContactsLegacy, а не vwContacts... вам нужно будет создать ссылку, установив ContactId
.
Надеюсь, что это скорее помощь, чем разочарование, EF - мощная игрушка, но она далека от совершенства... хотя я думаю, что в следующих версиях она станет намного лучше.
Ответ 5
Я думаю, что это может быть возможно с использованием методов расширения, хотя и не напрямую через 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();
Ответ 6
Иногда имеет смысл отображать его с другого конца отношений, в вашем случае:
modelBuilder.Entity<LegacyObject>().HasRequired(x => x.Contact).WithMany().HasForeignKey(u => u.LegacyKeyField);
однако это потребует, чтобы u.LegacyKeyField помечен как первичный ключ.
И тогда я дам свои два цента:
если Legacy db использует LegacyKeyField, тогда, возможно, устаревший db будет только прочитан. В этом случае мы можем создать два отдельных контекста Legacy и Non-legacy и сопоставить их соответственно. Это потенциально может стать немного беспорядочным, поскольку вам нужно будет помнить, какой из объектов происходит из какого контекста. Но опять же, ничто не мешает вам добавить один и тот же первый объект кода EF в 2 разных контекста
Другим решением является использование представлений с ContactId для всех остальных устаревших таблиц и их отображение в один контекст. Это будет налоговая производительность ради более чистых объектов контекста, но это может быть встречено на стороне SQL: индексированные представления, материализованные представления, сохраненные процедуры и т.д. Таким образом, LEGACY_OBJECT становится объектом VW_LEGACY с сообщением CONTACT.ContactId, затем:
modelBuilder.Entity<LegacyObject>().ToTable("VW_LEGACY_OBJECT");
modelBuilder.Entity<LegacyObject>().HasRequired(x => x.Contact).WithMany().HasForeignKey(u => u.ContactId);
Я лично поделился бы созданием "представлений картографа" с CustomerId в старых таблицах, так как он стал более чистым с точки зрения С# слоя, и вы можете сделать эти представления похожими на реальные таблицы. Также сложно предложить решение, не зная, в чем именно заключается сценарий, с которым вы сталкиваетесь: запрос, загрузка, сохранение и т.д.