EF Core неверное заявление о присоединении - PullRequest
0 голосов
/ 27 октября 2019

У меня есть следующие классы моделей в EF Core 2.2

public class User
{
    [Key]
    public long Id { get; set; }
    [ForeignKey("Post")]
    public long? PostId { get; set; }
    public virtual Post Post { get; set; }
    public virtual ICollection<Post> Posts { get; set; }
}

public class Post
{
    [Key]
    public long Id { get; set; }
    [ForeignKey("User")]
    public long UserId { get; set; }
    public virtual User User { get; set; }
}

Я проверил отношения с SSMS, и они в порядке.

Но когда я использую

 dbContext.Posts.Include(p => p.User);

EF Core генерирует следующий оператор соединения

FROM Posts [p] 
LEFT JOIN Users [p.Users] ON [p].[Id] = [p.Users].[PostId]

Я включаю пользователя из сообщения и ожидаю, что он будет таким, как показано ниже

FROM Posts [p]   
LEFT JOIN Users [p.Users] ON [p].[UserId] = [p.Users].[Id]

Что не так с моделями?

Предположим, что я хочу сохранить последний PostId в пользовательской модели.

Есть ли атрибут, указывающий ядру, какое свойство использовать при объединении моделей?

Ответы [ 3 ]

1 голос
/ 29 октября 2019

Из обсуждения другого ответа кажется, что вы хотите, чтобы пользователь содержал сообщения, а затем просил пользователя отслеживать ссылку на последнее сообщение. 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, и при правильном выполнении позволит избежать ловушек с отложенной загрузкой.

1 голос
/ 27 октября 2019

Неверные отношения между пользователем и почтой. Это должна быть ваша модель:

public class User
{
    [Key]
    public long Id { get; set; }
    public virtual ICollection<Post> Posts { get; set; }
}

public class Post
{
    [Key]
    public long Id { get; set; }
    [ForeignKey("User")]
    public long UserId { get; set; }
    public virtual User User { get; set; }
}

Это отношение один ко многим: у одного пользователя может быть много сообщений, а в сообщении может быть только один пользователь.

0 голосов
/ 29 октября 2019

У вас было достаточно ответов на этот вопрос о том, как создать модель, я просто выделю, что не так в вашем классе

У вас есть более высокий пост в пользовательской модели как FK, и поэтому он всегда будет рассматривать ее какОтношения между пользователем и постом, вы должны удалить это и спроектировать что-то похожее на то, что говорит Стив

...