在按字母顺序排列的列表中优化EF核心查询

时间:2019-06-07 07:04:41

标签: c# wpf listbox entity-framework-core

我最近一直在处理一个问题,尽管我有一些解决方案,但我希望从各个角度寻找最佳解决方案。

假设我有一个带有EF Core的WPF应用程序。我的数据库中大约有3000个客户(在我的情况下为SQLite,但将来也应与速度较慢的客户一起使用)。当用户打开客户列表时,我仅按字母顺序加载其中的一些(数量= 50,页面= 0)。用户向下滚动到底部时,会再加载50个(数量= 50,页面= 1)。

CustomerRepository.GetQueryableAll().Skip(page * quantity).Take(quantity).ToList();

一切正常。问题来了:这里有一个创建新客户的按钮,它会打开一个模态窗口。假设用户创建了一个以字母W开头的客户。当他/她点击SAVE时,新客户被保存到数据库中,关闭窗口,并且必须重新加载列表。但是,将整个列表加载到W无疑是非常缓慢的。

到目前为止,我已经尝试在后台任务中查询数据库,并在静态字典中存储有多少个客户以数据库的每个字母开头:一旦命中SAVE,我就可以猜测多少在数据库中“跳至” Skipp(),获得新客户所在的50个组。它可以运行,速度很快,但是我担心它在使用非拉丁字母的国家/地区无法使用:

public async Task<Dictionary<char, int>> GetCustomersByInitialsCount()
{
    return await Task.Run(async delegate
    {
        var dictionary = new Dictionary<char, int>();
        for (char c = 'A'; c <= 'Z'; c++)
        {
            var count = await CustomerRepository.GetCustomerCountStartingWith(c.ToString());
            dictionary.Add(c, count);
        }
        return dictionary;
    });
}

[... and in the repository:]

public async Task<int> GetCustomerCountStartingWith(string startingLetter)
{
    using (var dbContext = new MyDbContext())
    {
        return await dbContext.Set<Customer>().CountAsync(p => p.LastName.ToUpper().StartsWith(startingLetter.ToUpper()));
    }
}

否则,除了此后台查询之外,我还可以尝试根据起始字符来“猜测”正确的页面,但是我仍然对非拉丁语言可能产生的意外结果感到困惑。

如果有人知道更好的工具或有其他有用的想法,我会很乐意考虑它们!

非常感谢您,并祝您编程愉快。

2 个答案:

答案 0 :(得分:1)

如果添加请求以获取表中所有第一个“字母”怎么办?

public async Task<List<string>> GetCustomerFirstLetter()
{
    using (var dbContext = new MyDbContext())
    {
        return await dbContext.Set<Customer>().Select(x => x.lastName.Substring(0, 1)).Distinct().ToList();
    }
}

然后

public async Task<Dictionary<char, int>> GetCustomersByInitialsCount()
{
    return await Task.Run(async delegate
    {
        var dictionary = new Dictionary<char, int>();
        var letters = GetCustomerFirstLetter();
        foreach(letter in letters)
        {
            var count = await CustomerRepository.GetCustomerCountStartingWith(letter);
            dictionary.Add(letter, count);
        }
        return dictionary;
    });
}

答案 1 :(得分:1)

替代解决方案从我的角度来看效率更高

您的问题归结为如何在按客户名称排序的整个数据集中获取新客户的行号。

首先,对于SQLite或MSSQL,在普通SQL中,您可以使用ROW_NUMBER函数来解决获取正确页码的问题。查询示例:

SELECT TOP 1 rnd.rownum, rnd.LastName
  from (SELECT  ROW_NUMBER() OVER( ORDER BY c.LastName) AS rownum, c.LastName
  FROM [Customer] c) rnd
WHERE rnd.LastName = '<your new customers name here>'

因此,在获得确切的行号值并已具有页面计数参数之后,您可以轻松计算所需的页面。

回到您的代码。可以使用Select方法的重载版本在EF中实现此功能,但遗憾的是,尚未在IQueryablesee this)的EF Core中实现此功能。 但是您仍然可以使用FromSql方法将确切的查询权限传递给db。

解决方案包括两个步骤:

  1. 要获取必需的数据,您需要以这种方式为模型构建器定义Query(例如,其他字段,您只需要RowNum

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Query<CustomerRownNum>();
    }
    
    public class CustomerRownNum
    {
        public long RowNum { get; set; }
        public Guid Id { get; set; }
        public string LastName { get; set; }
    }
    
  2. 然后您需要通过以下方式将上述SQL查询传递给上下文的Query方法:

        string customerLastName = "<your customer's last name>";
        var result = dbContext.Query<CustomerRownNum>().FromSql(
            @"select top 1 rnd.RowNum, rnd.Id, rnd.LastName
                from 
                (SELECT  ROW_NUMBER() OVER( ORDER BY c.LastName) AS RowNum
                    , c.Id, c.LastName
                  FROM [Customer] c) rnd
                WHERE rnd.LastName = {0}", customerLastName).FirstOrDefault();
    

最后,您将在result变量中获得所需的数据。

希望有帮助!