最新消息:雨落星辰是一个专注网站SEO优化、网站SEO诊断、搜索引擎研究、网络营销推广、网站策划运营及站长类的自媒体原创博客

c# - How to flatten a nested entity using Linq (opposite of SelectMany)? - Stack Overflow

programmeradmin2浏览0评论

I am trying to convert SQL to Linq that flattens two related tables into one record per parent table row (opposite of SelectMany).

My first attempt is below, but the resulting SQL is different although the result is the same. The example uses Product and ProductImage where a single Product can have multiple ProductImages.

Is my Ling correct and the SQL produced is equivalent, or what Linq can produce the SQL below?

Tables:

  • Product (productid int, name varchar(50))

  • ProductImage (productid int, imageid int, imagepath varchar(450))

    • ProductImage.productid is a foreign key pointing to Product
    • ProductImage.imageid is numbered 1,2,3,4 for each ProductImage

SQL select to flatten the first three photos of a product

select 
    p.productid, p.name,
    i1.imagepath as i1imagepath,
    i2.imagepath as i2imagepath,
    i3.imagepath as i3imagepath 
from 
    product as p
join 
    productimage as i1 on p.productid = i1.productid
join 
    productimage as i2 on p.productid = i2.productid
join 
    productimage as i3 on p.productid = i3.productid
where 
    i1.imageid = 1 and
    i2.imageid = 2 and
    i3.imageid = 3

First attempt at Linq to convert this query:

Product.Select( p => new { p.productid, p.name,
    i1imagepath = p.ProductImage.AsQueryable()
                 .Where(i => i.imageid == 1)
                 .Select(i => i.imagepath).FirstOrDefault(),
    i2imagepath = p.ProductImage.AsQueryable()
                 .Where(i => i.imageid == 2)
                 .Select(i => i.imagepath).FirstOrDefault(),
    i3imagepath = p.ProductImage.AsQueryable()
                 .Where(i => i.imageid == 3)
                 .Select(i => i.imagepath).FirstOrDefault()
    }).Take(1).Dump();  // LinqPad Dump()

but it produces this SQL instead of above which looks less efficient (a side note, the actual execution numbers for the below SQL are nearly half of the original SQL above.)

select top 1 p.productid, p.name,
    (select top 1 p0.imagepath
     from productimage as p0
     where p.productid = p0.productid and
           p0.imageid = 1) as i1imagepath,
    (select top 1 p1.imagepath
     from productimage as p1
     where p.productid = p1.productid and
           p1.imageid = 2) as i2imagepath,
    (select top 1 p2.imagepath
     from productimage as p2
     where p.productid = p2.productid and
           p2.imageid = 3) as i3imagepath
from product

I am trying to convert SQL to Linq that flattens two related tables into one record per parent table row (opposite of SelectMany).

My first attempt is below, but the resulting SQL is different although the result is the same. The example uses Product and ProductImage where a single Product can have multiple ProductImages.

Is my Ling correct and the SQL produced is equivalent, or what Linq can produce the SQL below?

Tables:

  • Product (productid int, name varchar(50))

  • ProductImage (productid int, imageid int, imagepath varchar(450))

    • ProductImage.productid is a foreign key pointing to Product
    • ProductImage.imageid is numbered 1,2,3,4 for each ProductImage

SQL select to flatten the first three photos of a product

select 
    p.productid, p.name,
    i1.imagepath as i1imagepath,
    i2.imagepath as i2imagepath,
    i3.imagepath as i3imagepath 
from 
    product as p
join 
    productimage as i1 on p.productid = i1.productid
join 
    productimage as i2 on p.productid = i2.productid
join 
    productimage as i3 on p.productid = i3.productid
where 
    i1.imageid = 1 and
    i2.imageid = 2 and
    i3.imageid = 3

First attempt at Linq to convert this query:

Product.Select( p => new { p.productid, p.name,
    i1imagepath = p.ProductImage.AsQueryable()
                 .Where(i => i.imageid == 1)
                 .Select(i => i.imagepath).FirstOrDefault(),
    i2imagepath = p.ProductImage.AsQueryable()
                 .Where(i => i.imageid == 2)
                 .Select(i => i.imagepath).FirstOrDefault(),
    i3imagepath = p.ProductImage.AsQueryable()
                 .Where(i => i.imageid == 3)
                 .Select(i => i.imagepath).FirstOrDefault()
    }).Take(1).Dump();  // LinqPad Dump()

but it produces this SQL instead of above which looks less efficient (a side note, the actual execution numbers for the below SQL are nearly half of the original SQL above.)

select top 1 p.productid, p.name,
    (select top 1 p0.imagepath
     from productimage as p0
     where p.productid = p0.productid and
           p0.imageid = 1) as i1imagepath,
    (select top 1 p1.imagepath
     from productimage as p1
     where p.productid = p1.productid and
           p1.imageid = 2) as i2imagepath,
    (select top 1 p2.imagepath
     from productimage as p2
     where p.productid = p2.productid and
           p2.imageid = 3) as i3imagepath
from product
Share Improve this question edited Jan 24 at 15:11 Zachary Scott asked Jan 17 at 22:19 Zachary ScottZachary Scott 21.2k35 gold badges124 silver badges208 bronze badges 2
  • 3 "which looks less efficient" - have you checked execution plans and compared them? – Guru Stron Commented Jan 18 at 1:25
  • @GuruStron though I am not certain what the numbers mean, the nested SQL selects appear to be nearly half of the values that the joined tables produce when comparing the actual execution plans. Good note. – Zachary Scott Commented Jan 24 at 15:10
Add a comment  | 

2 Answers 2

Reset to default 2

Use navigation properties. Product should have a ProductImages collection set up.

public virtual ICollection<ProductImage> ProductImages { get; } = [];

This is how you leverage EF to handle related entities rather than explicit Joins like in SQL. EF manages all of the table joining automatically and you just build Linq expressions through the relations. Explicit joins are an exception to the norm where you have unofficial or unconventional relationships that cannot be expressed with FKs.

From there you just use projection:

var product = _context.Products
    .Where(p => p.ProductId == productId)
    .Select(p => new 
    {
        p.ProductId,
        p.Name,
        ImagePath1 = p.ProductImages.FirstOrDefault(pi => pi.ImageId == 1).ImagePath,
        ImagePath2 = p.ProductImages.FirstOrDefault(pi => pi.ImageId == 2).ImagePath,
        ImagePath3 = p.ProductImages.FirstOrDefault(pi => pi.ImageId == 3).ImagePath
   }.Single();

Normally with a Linq expression in memory you would need to handle the possibility of #null on the FirstOrDefault i.e. p.ProductImages.FirstOrDefault(pi => pi.ImageId == 1)?.ImagePath but when EF translates this down to SQL it will handle the possibility of null results (I.e. if there is no image #3 associated with the product) and it may complain if you explicitly add the short=hand null check. "?.". (Disclaimer: I haven't tried ?. with EF Core 9 yet to see if that behaviour has changed)

Alternatively a cleaner structure would be to project the images to a set, even if you just want the top 3:

var product = _context.Products
    .Where(p => p.ProductId == productId)
    .Select(p => new 
    {
        p.ProductId,
        p.Name,
        TopThreeImagePaths = p.ProductImages
           .OrderBy(pi => pi.ImageId)
           .Select(pi => pi.ImagePath)
           .Take(3)
           .ToList()
   }.Single();

This would product an object for the product containing a collection of 0-3 image path strings.

If the SQL query you've provided meets your requirements, it can be easily translated into LINQ query syntax as follows:

var query =
    from p in Product
    from i1 in p.ProductImage
    from i2 in p.ProductImage
    from i3 in p.ProductImage
    where 
        i1.imageid == 1 &&
        i2.imageid == 2 &&
        i3.imageid == 3
    select new 
    {
        p.productid, 
        p.name,
        i1imagepath = i1.imagepath,
        i2imagepath = i2.imagepath,
        i3imagepath = i3.imagepath 
    };

This LINQ query mirrors the structure of your SQL query, iterating through the related ProductImage entries and filtering based on the specified imageid values.

发布评论

评论列表(0)

  1. 暂无评论