Связи Entity Framework между различными DbContext и различными схемами
Итак, у меня есть два основных объекта: член и гильдия. Один член может владеть Гильдией, и одна Гильдия может иметь несколько Членов.
У меня есть класс Members в отдельном DbContext и отдельной библиотеке классов. Я планирую повторно использовать эту библиотеку классов в нескольких проектах и помочь дифференцировать, я установил схему базы данных "acc". Я тестировал эту библиотеку широко и могу добавлять, удалять и обновлять члены в таблице acc.Members.
Класс Гильдии таков:
public class Guild
{
public Guild()
{
Members = new List<Member>();
}
public int ID { get; set; }
public int MemberID { get; set; }
public virtual Member LeaderMemberInfo { get; set; }
public string Name { get; set; }
public virtual List<Member> Members { get; set; }
}
с отображением:
internal class GuildMapping : EntityTypeConfiguration<Guild>
{
public GuildMapping()
{
this.ToTable("Guilds", "dbo");
this.HasKey(t => t.ID);
this.Property(t => t.MemberID);
this.HasRequired(t => t.LeaderMemberInfo).WithMany().HasForeignKey(t => t.MemberID);
this.Property(t => t.Name);
this.HasMany(t => t.Members).WithMany()
.Map(t =>
{
t.ToTable("GuildsMembers", "dbo");
t.MapLeftKey("GuildID");
t.MapRightKey("MemberID");
});
}
}
Но когда я пытаюсь создать новую Гильдию, она говорит, что нет dbo.Members.
Я получил ссылку на проект члена EF и добавил сопоставление с классом Members в DbContext, частью которого является Гильдия. modelBuilder.Configurations.Add(new MemberMapping());
(Не уверен, что это лучший способ.)
В результате этой ошибки:
{"The member with identity 'GuildProj.Data.EF.Guild_Members' does not exist in the metadata collection.\r\nParameter name: identity"}
Как я могу использовать внешний ключ между этими двумя таблицами, пересекающими DbContexts и с разными схемами базы данных?
UPDATE
Я сузил причину ошибки. Когда я создаю новую гильдию, я устанавливаю идентификатор члена лидера гильдии в MemberID. Это прекрасно работает. Но, когда я затем пытаюсь добавить этот элемент Member-лидера в список гильдий членов (членов), что вызывает ошибку.
ОБНОВЛЕНИЕ 2
Вот код того, как я создаю контекст, в котором находится класс Гильдии (по просьбе Хусейна Халила)
public class FSEntities : DbContext
{
public FSEntities()
{
this.Configuration.LazyLoadingEnabled = false;
Database.SetInitializer<FSEntities>(null);
}
public FSEntities(string connectionString)
: base(connectionString)
{
}
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Configurations.Add(new GuildMapping());
modelBuilder.Configurations.Add(new KeyValueMappings());
modelBuilder.Configurations.Add(new LocaleMappings());
modelBuilder.Configurations.Add(new MemberMapping());
}
public DbSet<Guild> Guilds { get; set; }
public DbSet<KeyValue> KeyValues { get; set; }
public DbSet<Locale> Locales { get; set; }
}
Вот как я сохраняю его в репо:
public async Task CreateGuildAsync(Guild guild)
{
using (var context = new FSEntities(_ConnectionString))
{
context.Entry(guild.Members).State = EntityState.Unchanged;
context.Entry(guild).State = EntityState.Added;
await context.SaveChangesAsync();
}
}
ЗАКЛЮЧИТЕЛЬНОЕ РЕШЕНИЕ
Итак, мне пришлось добавлять сопоставления к Member
, Role
и Permission
в DbContext, которые содержали Guild
. Мне пришлось добавить роль и разрешение, потому что у члена был List<Role> Roles
, и каждая роль имела List<Permission> Permissions
.
Это приблизило меня к решению. Я все еще получал ошибки, например:
{"The member with identity 'GuildProj.Data.EF.Member_Roles' does not exist in the metadata collection.\r\nParameter name: identity"}
Здесь, когда вы вытаскиваете член из Session
, вы получаете что-то вроде этого:
System.Data.Entity.DynamicProxies.Member_FF4FDE3888B129E1538B25850A445893D7C49F878D3CD40103BA1A4813EB514C
Сущность Framework, похоже, не очень хорошо справляется с этим. Зачем? Я не уверен, но я думаю, что это потому, что ContextM создает прокси-член Member и клонирует член в новый объект-член, ContextM больше не имеет ассоциации. Это, я думаю, позволяет ContextG свободно использовать новый объект Member. Я попытался установить ProxyCreationEnabled = false в своих DbContexts, но объект Member, который вытащил из сеанса, продолжал быть типа System.Data.Entity.DynamicProxies.Member.
Итак, я сделал это:
Member member = new Member((Member)Session[Constants.UserSession]);
Мне пришлось клонировать каждый Role
и каждый Permission
, а также внутри их соответствующих конструкторов.
Это помогло мне на 99%. Мне пришлось изменить свое репо и как я сохранил объект Guild
.
context.Entry(guild.LeaderMemberInfo).State = EntityState.Unchanged;
foreach(var member in guild.Members)
{
context.Entry(member).State = EntityState.Unchanged;
}
context.Entry(guild).State = EntityState.Added;
await context.SaveChangesAsync();
Ответы
Ответ 1
Это рабочий код:
В сборке "М":
public class Member
{
public int Id { get; set; }
public string Name { get; set; }
}
public class MemberMapping : EntityTypeConfiguration<Member>
{
public MemberMapping()
{
this.HasKey(m => m.Id);
this.Property(m => m.Name).IsRequired();
}
}
В сборке "G":
- ваш
Guild
класс
- ваше отображение
Guild
, хотя с WillCascadeOnDelete(false)
в отображении LeaderMemberInfo
.
-
modelBuilder.Configurations.Add(new GuildMapping());
и modelBuilder.Configurations.Add(new MemberMapping());
код:
var m = new Member { Name = "m1" };
var lm = new Member { Name = "leader" };
var g = new Guild { Name = "g1" };
g.LeaderMemberInfo = lm;
g.Members.Add(lm);
g.Members.Add(m);
c.Set<Guild>().Add(g);
c.SaveChanges();
Выполненный SQL:
INSERT [dbo].[Members]([Name])
VALUES (@0)
SELECT [Id]
FROM [dbo].[Members]
WHERE @@ROWCOUNT > 0 AND [Id] = scope_identity()
-- @0: 'leader' (Type = String, Size = -1)
INSERT [dbo].[Guilds]([MemberID], [Name])
VALUES (@0, @1)
SELECT [ID]
FROM [dbo].[Guilds]
WHERE @@ROWCOUNT > 0 AND [ID] = scope_identity()
-- @0: '1' (Type = Int32)
-- @1: 'g1' (Type = String, Size = -1)
INSERT [dbo].[GuildsMembers]([GuildID], [MemberID])
VALUES (@0, @1)
-- @0: '1' (Type = Int32)
-- @1: '1' (Type = Int32)
INSERT [dbo].[Members]([Name])
VALUES (@0)
SELECT [Id]
FROM [dbo].[Members]
WHERE @@ROWCOUNT > 0 AND [Id] = scope_identity()
-- @0: 'm1' (Type = String, Size = -1)
INSERT [dbo].[GuildsMembers]([GuildID], [MemberID])
VALUES (@0, @1)
-- @0: '1' (Type = Int32)
-- @1: '2' (Type = Int32)
Это также работает при связывании существующих объектов.
Оригинальный ответ для более общего случая:
Вы не можете комбинировать типы в разных контекстах в один граф объектов. Это означает, что вы не можете сделать что-то вроде
from a in context.As
join b in context.Bs on ...
... потому что всегда существует один контекст, который должен создать весь запрос SQL, поэтому он должен иметь всю необходимую информацию о сопоставлении.
Вы можете зарегистрировать один и тот же тип в двух разных контекстах, хотя и с разных сборок. Таким образом, вы можете сопоставить Member
в контексте сборки Guild
, позвоните ему contextG
, но только если
-
Member
не относится к другим типам, которые не отображаются в contextG
. Это может означать, что свойства навигации в Member
должны быть явно проигнорированы.
-
Member
не может ссылаться на типы в contextG
, поскольку эти типы не являются частью контекста Member
.
Если какое-либо из этих условий не может быть выполнено, вы можете лучше всего создать новый класс Member
в Guild
и зарегистрировать его отображение в контексте. Возможно, вы хотите использовать другое имя, чтобы предотвратить неоднозначность, но это единственная альтернатива.
Ответ 2
Я обнаружил, что, когда у меня возникают проблемы с Entity и строятся отношения, это часто происходит потому, что я иду против потока фреймворка или пытаюсь построить отношения или абстракции, которые не являются технически хорошо сформированными. Я бы предложил здесь сделать быстрый шаг назад и проанализировать несколько вещей, прежде чем глубже изучить эту конкретную проблему.
Во-первых, мне любопытно, почему вы используете другую схему здесь для того, что может быть одним приложением, обращающимся к графу объектов. Несколько схем могут быть полезны в некотором контексте, но я думаю, что Брент Озар делает очень важный момент в этой статье . Учитывая эту информацию, я склонен сначала предположить, что вы объедините множественную схему в одну и перейдете к использованию одного контекста БД для базы данных.
Следующее, что нужно решить - это граф объектов. По крайней мере, для меня самая большая борьба, с которой я столкнулась при моделировании данных, - это когда я не впервые выясню, какие вопросы у приложения есть в базе данных. Что я имею в виду, это выяснить, какие данные требуется приложению в разных контекстах, а затем посмотреть, как оптимизировать эти структуры данных для производительности в реляционном контексте. Посмотрим, как это можно сделать...
Основываясь на вашей модели выше, я вижу, что у нас есть несколько ключевых терминов/объектов в домене:
- Коллекция гильдий
- Коллекция участников
- Коллекция лидеров гильдии.
Кроме того, у нас есть некоторые бизнес-правила, которые необходимо реализовать:
- Гильдия может иметь 1 лидера (и, возможно, более 1?)
- Лидер гильдии должен быть членом
- Гильдия имеет список из 0 или более членов
- Член может принадлежать Гильдии (и, возможно, более 1?)
Поэтому, учитывая эту информацию, давайте рассмотрим, какие вопросы могут иметь ваши приложения в этой модели данных. я приложение может:
- найдите участника и просмотрите его свойства.
- найдите участника и посмотрите их свойства и что они лидеры гильдии
- найдите гильдию и посмотрите список всех ее членов.
- найдите гильдию и посмотрите список лидеров гильдии.
- найдите список всех лидеров гильдии
Хорошо, теперь мы можем перейти к латунным клавишам, как говорится...
Использование таблицы соединений между гильдиями и членами в этом случае является оптимальным. Он предоставит вам возможность иметь членов в нескольких гильдиях или без гильдий и обеспечить низкую фиксированную стратегию обновления - так хороший вызов!
Перемещение на Лидеры Гильдии есть несколько вариантов, которые могут иметь смысл. Несмотря на то, что, возможно, никогда не будет случая для сержантов-гильдий, я считаю, что имеет смысл рассмотреть новую организацию под названием "Лидер гильдии". То, что этот подход позволяет, в несколько раз. Вы можете кэшировать в приложении список идентификаторов лидера гильдии, поэтому вместо того, чтобы совершить db-поездку, чтобы разрешить действие гильдии, предпринятое лидером, вы можете поразить локальный кеш приложения, который имеет только список лидеров, а не весь объект-лидер; наоборот, вы можете получить список лидеров для гильдии, и независимо от направления запроса вы можете столкнуться с кластеризованными индексами на основных объектах или с простым в обслуживании промежуточным индексом для объекта объединения.
Как я уже отмечал в начале этого пути, чтобы долго "отвечать", когда я сталкиваюсь с такими проблемами, как ваш, это обычно потому, что я иду против зерна Entity. Я рекомендую вам заново подумать о своей модели данных и о том, как с более низким коэффициентом трения - потерять множественную схему и добавить промежуточный объект guild_leader. Ура!
Ответ 3
Если вы явно не говорите, что объект Member
должен быть сопоставлен с acc.Members
, EF ожидает, что он будет находиться в таблице dbo
schema Members
. Для этого вам нужно указать EntityTypeConfiguration
для этого типа или аннотировать его с помощью System.ComponentModel.DataAnnotations.Schema.TableAttribute
как [Table("acc.Members")]
Ответ 4
Я отвечаю на ваш обновленный вопрос:
попробуйте использовать эту строку перед обновлением контекста
context.Entry(Guild.Members).State = Entity.EntityState.Unchanged
это решит вашу ошибку