简体   繁体   中英

Convert SQL query to linq lambda Entity Framework Core

I need select top 10 LINQ query with group by, sum(quantity) and where clause using Entity Framework Core.

Could somebody help me please to convert this SQL code to linq lambda?

Thank you very much.

 SELECT TOP 10 
     OrderItems.ProductName,
     OrderItems.ProductId,
     SUM(OrderItems.Units) AS Quantity 
 FROM
     Orders, OrderItems  
 WHERE
     OrderItems.OrderId = Orders.Id  
     AND Orders.OrderDate >= '2019-12-18 16:38:27' 
     AND Orders.OrderDate <= '2020-12-18 16:38:27' 
     AND Orders.OrderStatusId = 2 
 GROUP BY  
     ProductName, OrderItems.ProductId  
 ORDER BY 
     Quantity DESC

I tried this EF query:

var query = (from sta in _context.Set<OrderItem>().AsQueryable()
                         join rec in _context.Set<Order>().AsQueryable() on sta.OrderId equals rec.Id
                         where rec.OrderStatusId == Convert.ToInt32(orderStatusId) && rec.OrderDate >= Convert.ToDateTime(startDate) && rec.OrderDate <= Convert.ToDateTime(endDate)
                         group sta by new
                         {
                             sta.ProductName,
                             sta.ProductId
                         } into grp
                         select new OrderDto()
                         {
                             ProductName = grp.Key.ProductName,
                             ProductId = grp.Key.ProductId,
                             Quantity = grp.Max(t => t.Units),
                         }).OrderBy(x => x.Quantity).Take(Convert.ToInt32(top)).ToList();

I believe your statement SELECT... FROM Orders, OrderItems... WHERE OrderItems.OrderId = Orders.Id while looking like a CROSS JOIN , ends up being optimised into an INNER JOIN .

So, assuming you've got your model set up with navigation properties you might be better off using .Include() . Apart from that I think you are pretty much there:

var query = _context.Set<OrderItem>().Include(o => o.Order)
                    .Where(rec => rec.Order.OrderStatusId == Convert.ToInt32(orderStatusId))
                    .Where(rec => rec.Order.OrderDate >= Convert.ToDateTime(startDate) && rec.Order.OrderDate <= Convert.ToDateTime(endDate))
                    .GroupBy(g => new { g.ProductName, g.ProductId })
                    .Select(grp => new OrderDto
                    {
                        ProductName = grp.Key.ProductName,
                        ProductId = grp.Key.ProductId,
                        Quantity = grp.Sum(t => t.Units)
                    })
                    .OrderBy(x => x.Quantity)
                    .Take(Convert.ToInt32(top));

This produces the following output:

SELECT TOP(@__p_3) [o].[ProductName], [o].[ProductId], SUM([o].[Units]) AS [Quantity]
FROM [OrderItems] AS [o]
INNER JOIN [Orders] AS [o0] ON [o].[OrderId] = [o0].[Id]
WHERE ([o0].[OrderStatusId] = @__ToInt32_0) AND (([o0].[OrderDate] >= @__ToDateTime_1) AND ([o0].[OrderDate] <= @__ToDateTime_2))
GROUP BY [o].[ProductName], [o].[ProductId]
ORDER BY SUM([o].[Units])

Suppose you can't add the navigation property to your OrderItem model, then your code seems pretty much there:

var query2 = (from sta in _context.Set<OrderItem>() 
                from rec in _context.Set<Order>()
                where sta.OrderId == rec.Id && rec.OrderStatusId == Convert.ToInt32(orderStatusId) 
                        && rec.OrderDate >= Convert.ToDateTime(startDate) && rec.OrderDate <= Convert.ToDateTime(endDate)
                group sta by new
                {
                    sta.ProductName,
                    sta.ProductId
                } into grp
                select new OrderDto()
                {
                    ProductName = grp.Key.ProductName,
                    ProductId = grp.Key.ProductId,
                    Quantity = grp.Max(t => t.Units),
                }
                )
                .OrderBy(x => x.Quantity)
                .Take(Convert.ToInt32(top));

This produces the following SQL:

SELECT TOP(@__p_3) [o].[ProductName], [o].[ProductId], MAX([o].[Units]) AS [Quantity]
FROM [OrderItems] AS [o]
CROSS JOIN [Orders] AS [o0]
WHERE ((([o].[OrderId] = [o0].[Id]) AND ([o0].[OrderStatusId] = @__ToInt32_0)) AND ([o0].[OrderDate] >= @__ToDateTime_1)) AND ([o0].[OrderDate] <= @__ToDateTime_2)
GROUP BY [o].[ProductName], [o].[ProductId]
ORDER BY MAX([o].[Units])

Here's my full test bench for reference


using Microsoft.EntityFrameworkCore
using Microsoft.EntityFrameworkCore.Query.SqlExpressions
using Microsoft.EntityFrameworkCore.Query

#region EF Core 3.1 .ToSql() helper method courtesy of https://stackoverflow.com/a/51583047/12339804
public static class IQueryableExtensions
{
    public static string ToSql<TEntity>(this IQueryable<TEntity> query) where TEntity : class
    {
        var enumerator = query.Provider.Execute<IEnumerable<TEntity>>(query.Expression).GetEnumerator();
        var relationalCommandCache = enumerator.Private("_relationalCommandCache");
        var selectExpression = relationalCommandCache.Private<SelectExpression>("_selectExpression");
        var factory = relationalCommandCache.Private<IQuerySqlGeneratorFactory>("_querySqlGeneratorFactory");

        var sqlGenerator = factory.Create();
        var command = sqlGenerator.GetCommand(selectExpression);

        string sql = command.CommandText;
        return sql;
    }

    private static object Private(this object obj, string privateField) => obj?.GetType().GetField(privateField, BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(obj);
    private static T Private<T>(this object obj, string privateField) => (T)obj?.GetType().GetField(privateField, BindingFlags.Instance | BindingFlags.NonPublic)?.GetValue(obj);
}
#endregion 

public class OrderItem
{
    public int Id {get;set;}
    public int OrderId {get;set;}
    public int ProductName {get;set;}
    public int ProductId {get;set;}
    public int Units {get;set;}
    
    public Order Order {get;set;} // added navigation property for .Include() to pick up on
}

public class Order { 
    public int Id {get;set;}
    public int OrderStatusId {get;set;}
    public DateTime OrderDate {get;set;}    
}

public class OrderDto
{
    public int ProductName { get; set; }
    public int ProductId { get; set; }
    public int Quantity { get; set; }
}

class Dbc : DbContext
{
    public DbSet<Order> Orders {get;set;}
    public DbSet<OrderItem> OrderItems {get;set;}

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
            optionsBuilder.UseSqlServer(@"Server=.\SQLEXPRESS;Database=Test;Trusted_Connection=True");
        base.OnConfiguring(optionsBuilder);
    }
}

void Main()
{
    var _context = new Dbc();
    var orderStatusId = "2";
    var top = "10";
    var startDate = DateTime.Parse("2019-12-16 16:38:27");
    var endDate = DateTime.Parse("2019-12-18 16:38:27");

var query = _context.Set<OrderItem>().Include(o => o.Order)
                    .Where(rec => rec.Order.OrderStatusId == Convert.ToInt32(orderStatusId))
                    .Where(rec => rec.Order.OrderDate >= Convert.ToDateTime(startDate) && rec.Order.OrderDate <= Convert.ToDateTime(endDate))
                    .GroupBy(g => new { g.ProductName, g.ProductId })
                    .Select(grp => new OrderDto
                    {
                        ProductName = grp.Key.ProductName,
                        ProductId = grp.Key.ProductId,
                        Quantity = grp.Sum(t => t.Units)
                    })
                    .OrderBy(x => x.Quantity)
                    .Take(Convert.ToInt32(top));
    query.ToSql().Dump();

    //alternatively, trying to force a cross join syntax with extra WHERE condition. This way you don't need `public Order Order {get;set;}` navigation property on `OrderItem`
    var query2 = (from sta in _context.Set<OrderItem>() 
                from rec in _context.Set<Order>()
                where sta.OrderId == rec.Id && rec.OrderStatusId == Convert.ToInt32(orderStatusId) 
                        && rec.OrderDate >= Convert.ToDateTime(startDate) && rec.OrderDate <= Convert.ToDateTime(endDate)
                group sta by new
                {
                    sta.ProductName,
                    sta.ProductId
                } into grp
                select new OrderDto()
                {
                    ProductName = grp.Key.ProductName,
                    ProductId = grp.Key.ProductId,
                    Quantity = grp.Max(t => t.Units),
                }
                )
                .OrderBy(x => x.Quantity)
                .Take(Convert.ToInt32(top));
    query2.ToSql().Dump();
}

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM