Как включить только выбранные свойства в связанные объекты

Я могу включить только связанные лица.

using (var context = new BloggingContext()) 
{ 
    // Load all blogs, all related posts
    var blogs1 = context.Blogs 
                       .Include(b => b.Posts) 
                       .ToList(); 
}

Однако мне не нужна вся сущность BlogPost. Я заинтересован только в определенных свойствах, например:

using (var context = new BloggingContext()) 
{ 
    // Load all blogs, all and titles of related posts
    var blogs2 = context.Blogs 
                       .Include(b => b.Posts.Select(p => p.Title) //throws runtime exeption
                       .ToList(); 

    foreach(var blogPost in blogs2.SelectMany(b => b.Posts))
    {
        Console.Writeline(blogPost.Blog.Id); //I need the object graph
        Console.WriteLine(blogPost.Title); //writes title
        Console.WriteLine(blogPost.Content); //writes null
    }
}

4 ответа

Вы либо используете Include который загружает всю сущность, или вы проецируете то, что вам нужно .Select:

var blogs2 = context.Blogs 
    .Select(x => new 
    {
        BlogName = x.BlogName, //whatever
        PostTitles = x.Post.Select(y => y.Title).ToArray()
    }) 
   .ToList(); 

Или вы можете сделать что-то вроде этого:

var blogs2 = context.Blogs 
    .Select(x => new 
    {
        Blog = x,
        PostTitles = x.Post.Select(y => y.Title).ToArray()
    }) 
   .ToList(); 

Select всегда лучше, когда вам не нужен весь ребенок, так как это предотвращает запрос ненужных данных.

На самом деле вы хотите: разделить сущность на общую, репрезентативную часть и специальную часть, которую вы не всегда хотите извлечь из базы данных. Это не редкое требование. Подумайте о продуктах и ​​изображениях, файлах и их контенте, или о сотрудниках с открытыми и закрытыми данными.

Ядро Entity Framework поддерживает два способа достижения этого: собственный тип и разбиение таблицы.

Тип собственности

Собственный тип - это тип, заключенный в другой тип. Доступ к нему возможен только через его владельца. Вот как это выглядит:

public class Post
{
    public int ID { get; set; }
    public Blog Blog { get; set; }
    public string Title { get; set; }
    public PostContent Content { get; set; }
}

public class PostContent
{
    public string Content { get; set; }
}

И отображение собственного типа:

modelBuilder.Entity<Post>().OwnsOne(e => e.Content);

куда Blog является

public class Blog
{
    public Blog()
    {
        Posts = new HashSet<Post>();
    }
    public int ID { get; set; }
    public string Name { get; set; }

    public ICollection<Post> Posts { get; set; }
}

Тем не менее, в соответствии с документами:

При запросе владельца принадлежащие типы будут включены по умолчанию.

Что означает, что заявление, как...

var posts = context.Posts.ToList();

... всегда будет получать сообщения и их содержание. Следовательно, тип собственности, вероятно, не является правильным подходом для вас. Я все еще упоминал об этом, потому что я узнал, что когда Posts являются Included...

var blogs = context.Blogs.Include(b => b.Posts).ToList();

... принадлежащие типы, PostContents, не включены (ОТКАЗ ОТ ОТВЕТСТВЕННОСТИ: я не уверен, если это ошибка или особенность...). В этом случае, когда принадлежащие типы должны быть включены ThenInclude необходимо:

var blogs = context.Blogs.Include(b => b.Posts)
        .ThenInclude(p => p.Content).ToList();

Так что если PostS всегда будет запрашиваться через Blogs, тип собственности может быть уместным.

Я не думаю, что это применимо здесь, но это имеет место, когда дети, имеющие собственные типы, имеют идентифицирующие отношения со своими родителями (классический пример: Order-OrderLine).

Разделение таблицы

При разбиении таблицы таблица базы данных разделяется на две или более сущностей. Или со стороны объектов: две или более сущностей отображаются в одну таблицу. Модель практически идентична. Единственная разница в том, что PostContent теперь имеет обязательное свойство первичного ключа (IDконечно же, имеющее то же значение, что и Post.ID):

public class Post
{
    public int ID { get; set; }
    public Blog Blog { get; set; }
    public string Title { get; set; }
    public PostContent Content { get; set; }
}

public class PostContent
{
    public int ID { get; set; }
    public string Content { get; set; }
}

И расщепление таблицы:

modelBuilder.Entity<Post>()
    .HasOne(e => e.Content).WithOne()
    // or .WithOne(c => c.Post) if there is a back reference
    .HasForeignKey<PostContent>(e => e.ID);
modelBuilder.Entity<Post>().ToTable("Posts");
modelBuilder.Entity<PostContent>().ToTable("Posts");

Сейчас Posts всегда будут запрашиваться без их содержимого по умолчанию. PostContent всегда должен быть Include()-ед явно.

Также, PostContent теперь можно запросить без его владельца Post:

var postContents = context.Set<PostContent>().ToList();

Я думаю, что это именно то, что вы ищете.

Конечно, вы можете обойтись без этих сопоставлений, если вы всегда будете использовать проекции, когда хотите получать посты без содержимого.

Вы можете попробовать это:

using (var context = new BloggingContext())
{
    var blogProps = context.Blogs
        .SelectMany(b => 
            b.Posts.Select(p => 
                new { Blog = b, PostTitle = p.Title }
            )
         )
        .ToList();
}

РЕДАКТИРОВАТЬ
Если вы хотите придерживаться своей модели данных, вы можете попробовать что-то вроде этого:

using (var context = new BloggingContext())
{
    var blogProps = context.Blogs
        .Select(b => 
            new Blog 
            { 
                Name = b.Name, 
                Posts = new List<Post>(b.Posts.Select(p => 
                    new Post 
                    { 
                        Title = p.Title 
                    })
            }
        )
        .ToList();
}

Я думаю, что есть гораздо более простой способ сделать это. Проекция хороша и все такое, но что, если вам нужны все столбцы из родительской сущности и большинство из них из дочерней? Когда эти типы имеют много свойств, использование проекции означает, что вам нужно написать много строк кода, чтобы выбрать все, что вы хотите, кроме нескольких, которые вам не нужны. Что ж, поскольку использование проекции означает, что ваши объекты не будут отслеживаться, его гораздо проще использовать. .AsNoTracking()а затем просто выкиньте то, что вам не нужно.

      var foos = await _context.DbSet<Foo>()
    .AsQueryable()
    .Where(x => x.Id == id)
    .Include(x => x.Bars)
    .AsNoTracking()
    .ToListAsync();

foreach (var foo in foos)
{
    foreach (Bar bar in foo.Bars)
    {
        bar.Baz = null;
    }
}
Другие вопросы по тегам