Из обсуждения другого ответа кажется, что вы хотите, чтобы пользователь содержал сообщения, а затем просил пользователя отслеживать ссылку на последнее сообщение. EF может отобразить это, однако вам, вероятно, нужно будет немного прояснить отношения.
Например:
[Table("Users")]
public class User
{
[Key]
public int UserId { get; set; }
public string Name { get; set; }
public virtual ICollection<Post> Posts { get; set; } = new List<Post>();
public virtual Post LatestPost { get; set; }
}
[Table("Posts")]
public class Post
{
[Key]
public int PostId { get; set; }
public string PostText { get; set; }
public DateTime PostedAt { get; set; }
public virtual User User { get; set; }
}
затем Конфигурация, чтобы гарантировать, что EF связывает отношения между пользователем иотправляет правильно:
// EF6
public class UserConfiguration : EntityTypeConfiguration<User>
{
public UserConfiguration()
{
HasMany(x => x.Posts)
.WithRequired(x => x.User)
.Map(x=>x.MapKey("UserId"));
HasOptional(x => x.LatestPost)
.WithMany()
.Map(x=>x.MapKey("LatestPostId"));
}
}
// EFCore
public class UserConfiguration : IEntityTypeConfiguration<User>
{
public void Configure(EntityTypeBuilder<User> builder)
{
builder.HasMany(x => x.Posts)
.WithOne(x => x.User)
.HasForeignKey("UserId");
HasOne(x => x.LatestPost)
.WithMany()
.IsRequired(false)
.HasForeignKey("LatestPostId");
}
}
Вы можете сделать это в событии OnModelCreating, также используя ссылку на modelBuilder. Обратите внимание, что я не объявляю свойства FK в моих объектах. Это тоже вариант, но я обычно рекомендую не объявлять FK, чтобы избежать проблем с ссылками и обновлениями FK. Я назвал LatestPost FK как LatestPostId, чтобы немного точнее показать, для чего он нужен. Он может быть сопоставлен с «PostId», если вы выберете.
Теперь давайте скажем, что я хочу добавить новое сообщение и хочу связать его с пользователем, и назначить его как LatestPost для этого пользователя:
using (var context = new SomethingDbContext())
{
var user = context.Users.Include(x => x.Posts).Include(x => x.LatestPost)
.Single(x => x.UserId == 1);
var newPost = new Post { PostText = "Test", User = user };
user.Posts.Add(newPost);
user.LatestPost = newPost;
context.SaveChanges();
}
Вы можете обновить "последнюю" ссылку на публикацию, загрузив пользователя и установив ссылку на LastPost для требуемой записи.
Однако в этой структуре есть риск, который следует учитывать,Проблема заключается в том, что нет способа надежно обеспечить (на уровне данных), что ссылка LatestPost в пользователе фактически ссылается на сообщение, связанное с этим пользователем. Например, если у меня есть последнее сообщение, указывающее на конкретное сообщение, то я удаляю ссылку на это сообщение из коллекции сообщений пользователя, что может привести к ошибкам ограничения FK, или просто отсоединяет сообщение от пользователя, но последнее сообщение пользователя по-прежнему остается. указывает на эту запись. Я также могу назначить пост другого пользователя для ссылки на последний пост этого пользователя. Т.е.
using (var context = new SomethingDbContext())
{
var user1 = context.Users.Include(x => x.Posts).Include(x => x.LatestPost)
.Single(x => x.UserId == 1);
var user1 = context.Users.Include(x => x.Posts).Include(x => x.LatestPost)
.Single(x => x.UserId == 2);
var newPost = new Post { PostText = "Test", User = user1 };
user1.Posts.Add(newPost);
user1.LatestPost = newPost;
user2.LatestPost = newPost;
context.SaveChanges();
}
И это было бы прекрасно. «LatestPostId» пользователя 2 будет установлен на этот новый пост, хотя UserId этого поста относится только к User1.
Лучшим решением при работе с чем-то вроде последнего поста является , а не денормализациясхема для размещения его. Вместо этого используйте несопоставленные свойства в объекте для последней публикации или, лучше, используйте проекцию для извлечения этих данных, когда это необходимо. В обоих случаях вы удалили бы LatestPostId из таблицы User
Свойство Unmapped:
[Table("Users")]
public class User
{
[Key]
public int UserId { get; set; }
public string Name { get; set; }
public virtual ICollection<Post> Posts { get; set; } = new List<Post>();
[NotMapped]
public Post LatestPost
{
get { return Posts.OrderByDescending(x => x.PostedAt).FirstOrDefault(); }
}
}
Предостережение подхода с использованием неотображенного свойства заключается в том, что вам нужно помнить, чтобы загружать сообщения вПользователь, если вы хотите получить доступ к этому свойству, иначе вы отключите ленивый груз. Вы также не можете использовать это свойство в выражениях Linq, которые отправляются в EF (EF6), хотя они могут работать с EFCore, но рискуют проблемами с производительностью, если выражение переводится в оперативную память раньше. EF не сможет перевести LatestPost в SQL, поскольку в схеме не будет ключа.
Проекция:
[Table("Users")]
public class User
{
[Key]
public int UserId { get; set; }
public string Name { get; set; }
public virtual ICollection<Post> Posts { get; set; } = new List<Post>();
}
Тогда, если вы хотите получить пользователя и его последнее сообщение:
var userAndPost = context.Users.Where(x => x.UserId == userId)
.Select(x => new { User = x, LatestPost = x.Posts.OrderByDescending(PostedAt).FirstOrDefault()} ).Single();
Проекция с помощью Select
может извлекать интересующие объекты или, что еще лучше, просто возвращать поля из этих объектов в модель плоского представления или DTO для отправки в пользовательский интерфейс или тому подобное. Это приводит к более эффективным запросам к базе данных. Использование Select
для получения подробностей, вам не нужно беспокоиться об активной загрузке через Include
, и при правильном выполнении позволит избежать ловушек с отложенной загрузкой.