Как определить дополнительные отношения внешнего ключа в FluentAPI/Data Annotations с помощью платформы Entity Framework?
У меня есть (примерное) приложение со следующим кодом:
public class Posts
{
[Key]
[Required]
public int ID { get; set; }
[Required]
public string TypeOfPost { get; set; }
public int PollID { get; set; }
public virtual Poll Poll { get; set; }
public int PostID { get; set; }
public virtual Post Post { get; set; }
}
В принципе, я не знаю, есть ли лучший способ сделать это, но у меня есть список сообщений, и люди могут выбрать, является ли это Poll
или Post
, As Entity Framework не работает с Enums, я просто сохраняю его как строку в TypeOfPost
, а затем в приложении, я программно запрашиваю либо опрос, либо сообщение на основе значения TypeOfPost
.
Я не думаю, что в любом случае можно установить "Только одно обязательное" или подобное, поэтому я обрабатываю все проверки и прочее в приложении. (Если кто-то знает лучший способ, скажите, пожалуйста!).
В любом случае проблема заключается в том, что я могу получить эту работу отлично, зайдя к SQL Management Studio и вручную отредактировав схему, чтобы разрешить нули, но я просто не могу понять, как это сделать в FluentAPI, и вам нужно некоторая помощь.
Я пробовал оба из следующих:
modelBuilder.Entity<Post>()
.HasOptional(x => x.Poll).WithOptionalDependent();
modelBuilder.Entity<Post>()
.HasOptional(x => x.Poll).WithOptionalPrincipal();
Первый, похоже, создает дополнительный столбец в базе данных, который допускает нулевые значения, а второй, похоже, ничего не делает.
Я считаю, что первый из них мне нужен, но мне нужно использовать его в комбинации с [ForeignKey] в классе Post
. Если я прав здесь, должен ли [ForeignKey] перейти к виртуальному свойству или идентификатору свойства?
Кроме того, какова фактическая разница между WithOptionalDependent
и WithOptionalPrincipal
? - Я читал на MSDN, но, я действительно не понимаю разницы.
Ответы
Ответ 1
Я бы, вероятно, попытался создать две взаимно-однозначные отношения как optional: required, потому что Poll
должен иметь ссылку на Posts
, а Post
также должен иметь ссылка на Posts
:
modelBuilder.Entity<Posts>()
.HasOptional(x => x.Post)
.WithRequired();
modelBuilder.Entity<Posts>()
.HasOptional(x => x.Poll)
.WithRequired();
Это делает Posts
автоматически директором в отношениях и Post
или Poll
зависимым. У принципала есть первичный ключ в отношении, зависимый внешний ключ, который также является первичным ключом в таблице Post
/Poll
, потому что это взаимно однозначное отношение. Только в отношениях "один-ко-многим" у вас будет отдельный столбец для внешнего ключа. Для отношения "один к одному" вы также должны удалить столбцы внешнего ключа PostId
и PollId
, потому что Posts
ссылается на свой первичный ключ на Post
и Poll
.
Альтернативный подход, который, по-видимому, подходит для вашей модели, - это наследование. Тогда модель будет выглядеть так:
public abstract class BasePost // your former Posts class
{
public int ID { get; set; }
public string UserName { get; set; }
}
public class Post : BasePost
{
public string Text { get; set; }
// other properties of the Post class
}
public class Poll : BasePost
{
// properties of the Poll class
}
Вам больше не нужен TypeOfPost
, потому что вы можете отфильтровать два конкретных типа с помощью оператора OfType
LINQ, например:
var x = context.BasePosts.OfType<Post>()
.Where(p => p.UserName == "Jim")
.ToList();
Это позволит выбрать все сообщения определенного пользователя, но не опросы.
Вы должны решить, какое именно сопоставление наследования вы хотите использовать - TPH, TPT или TPC.
Edit
Чтобы получить отношения "один ко многим", вы можете указать следующее сопоставление в Fluent API:
modelBuilder.Entity<Posts>()
.HasOptional(x => x.Post)
.WithMany()
.HasForeignKey(x => x.PostID);
modelBuilder.Entity<Posts>()
.HasOptional(x => x.Poll)
.WithMany()
.HasForeignKey(x => x.PollID);
Свойства внешнего ключа должны быть нулевыми (int?
) для этого, как вы уже нашли. Поскольку имена ваших свойств внешнего ключа следуют за соглашением об именах, которое использует EF для сопоставления, вы можете полностью исключить отображение Fluent. Это потребовалось бы только в том случае, если у вас были нетрадиционные имена (например, PostFK
или что-то еще). Затем вы можете использовать аннотации данных ([ForeignKey(...)]
) вместо Fluent API.
Ответ 2
Причина, по которой он не разрешал null, потому что следующее:
public int PollID { get; set; }
public virtual Poll Poll { get; set; }
public int PostID { get; set; }
public virtual Post Post { get; set; }
должно быть
public int? PollID { get; set; }
public virtual Poll Poll { get; set; }
public int? PostID { get; set; }
public virtual Post Post { get; set; }
Ответ 3
The ForeignKey просто должен быть Nullable, чтобы сделать его необязательным - виртуальный отдельный и требуется только для Lazy Loading.
Требуемая связь в декларативном коде EF First:
public User User { get; set; }
[ForeignKey("User")]
public int UserId { get; set; }
Необязательная связь в декларативном коде EF First:
public User User { get; set; }
[ForeignKey("User")]
public int? UserId { get; set; }
Вы увидите это при запуске update-database -verbose -f
:
ALTER TABLE [dbo].[MyTable] ALTER COLUMN [UserId] [int] NULL
Ответ 4
Что-то еще, что может помочь. Настройка атрибутов внешнего ключа (аннотации) с атрибутом [Обязательный] также обеспечит необходимые свойства навигации для EF EVEN WHEN, когда свойство FK имеет значение NULL. У меня есть специальный случай с устаревшими данными, где требуется свойство FK, но может или не может ссылаться на запись в отношении. Имеет смысл, но я не думал, что EF был "умным".
[Required] <-- even if the FK is nullable, OrgUnit will be Required
[StringLength(10)]
[ForeignKey("OrgUnit"), Column(Order = 1)]
public string OrgCode
{
get;
set;
}
... other FK, Column order 0
public virtual OrgUnit OrgUnit
{
get;
set;
}