【问题标题】:Filter, merge, sort and page data from multiple sources过滤、合并、排序和分页来自多个来源的数据
【发布时间】:2017-03-24 05:20:09
【问题描述】:

目前,我正在通过检索IQueryable<T1> 的方法从数据库中检索数据,过滤、排序然后对其进行分页(基本上所有这些都在数据库上),然后将结果返回到 UI 以显示分页表。

我需要整合来自另一个数据库的结果,而分页似乎是主要问题。

  • 模型相似但不完全相同(相同的字段,不同的名称,需要在返回之前映射到通用域模型);
  • 无法在数据库级别加入;
  • 目前两个数据库之间大约有 1000 条记录(在 过去 18 个月),并且可能以大致相同的速度增长(缓慢) 节奏;
  • 结果总是需要按 1-2 个字段排序(按日期)。

我目前在这两种解决方案之间纠结:

  1. 从两个来源检索所有数据,合并、排序然后缓存它们;然后在接收请求时简单地在所述缓存上进行过滤和分页 - 但我需要在修改集合时使缓存无效(我可以);
  2. 过滤每个源的数据(同样在数据库级别),然后在返回之前检索、合并、排序和分页。

我正在寻找一种在性能方面不错的算法。理想的解决方案可能是它们之间的组合(在数据库级别缓存 + 过滤),但我目前还没有考虑到这一点。

【问题讨论】:

  • 对面的数据库包含重复项?
  • 否定,无重复
  • 我喜欢你的问题 ;) 我做了类似的事情,但没有分页,我会尝试看看是否可以将分页添加到我在那里的内容中
  • 你认为你需要跳过页面吗?还是适合有previews/next url机制?
  • 目前我Skip|Take,但我不喜欢那个

标签: c# caching merge pagination iqueryable


【解决方案1】:

我在这里创建了一些东西,如果需要,我会回来解释。 我不确定我的算法是否适用于所有边缘情况,它涵盖了我想到的所有情况,但你永远不知道。为了您的愉快,我将代码留在这里,如果您需要,我会回答并解释那里做了什么,发表评论。

并使用值之间存在较大差距的项目列表执行多项测试。

using System;
using System.Collections.Generic;
using System.Linq;

namespace ConsoleApplication1
{
    class Program
    {
        //each time when this objects are accessed, consider as a database call
        private static IQueryable<model1> dbsetModel_1; 
        private static IQueryable<model2> dbsetModel_2;

        private static void InitDBSets()
        {
            var rnd = new Random();
            List<model1> dbsetModel1 = new List<model1>();
            List<model2> dbsetModel2 = new List<model2>();
            for (int i = 1; i < 300; i++)
            {
                if (i % 2 == 0)
                {
                    dbsetModel1.Add(new model1() { Id = i, OrderNumber = rnd.Next(1, 10), Name = "Test " + i.ToString() });
                }
                else
                {
                    dbsetModel2.Add(new model2() { Id2 = i, OrderNumber2 = rnd.Next(1, 10), Name2 = "Test " + i.ToString() });
                }
            }
            dbsetModel_1 = dbsetModel1.AsQueryable();
            dbsetModel_2 = dbsetModel2.AsQueryable();
        }

        public static void Main()
        {
            //generate sort of db data
            InitDBSets();
            //test
            var result2 = GetPage(new PagingFilter() { Page = 5, Limit = 10 });
            var result3 = GetPage(new PagingFilter() { Page = 6, Limit = 10 });
            var result5 = GetPage(new PagingFilter() { Page = 7, Limit = 10 });
            var result6 = GetPage(new PagingFilter() { Page = 8, Limit = 10 });
            var result7 = GetPage(new PagingFilter() { Page = 4, Limit = 20 });
            var result8 = GetPage(new PagingFilter() { Page = 200, Limit = 10 });

        }


        private static PagedList<Item> GetPage(PagingFilter filter)
        {
            int pos = 0;
            //load only start pages intervals margins from both database
            //this part need to be transformed in a stored procedure on db one, skip, take to return interval start value for each frame 
            var framesBordersModel1 = new List<Item>();
            dbsetModel_1.OrderBy(x => x.Id).ThenBy(z => z.OrderNumber).ToList().ForEach(i => {
                pos++;
                if (pos - 1 == 0)
                {
                    framesBordersModel1.Add(new Item() { criteria1 = i.Id, criteria2 = i.OrderNumber, model = i });
                }
                else if ((pos - 1) % filter.Limit == 0)
                {
                    framesBordersModel1.Add(new Item() { criteria1 = i.Id, criteria2 = i.OrderNumber, model = i });
                }

            });
            pos = 0;
            //this part need to be transformed in a stored procedure on db two, skip, take to return interval start value for each frame
            var framesBordersModel2 = new List<Item>();
            dbsetModel_2.OrderBy(x => x.Id2).ThenBy(z => z.OrderNumber2).ToList().ForEach(i => {
                pos++;
                if (pos - 1 == 0)
                {
                    framesBordersModel2.Add(new Item() { criteria1 = i.Id2, criteria2 = i.OrderNumber2, model = i });
                }
                else if ((pos -1) % filter.Limit == 0)
                {
                    framesBordersModel2.Add(new Item() { criteria1 = i.Id2, criteria2 = i.OrderNumber2, model = i });
                }

            });

            //decide where is the position of your cursor based on start margins
            //int mainCursor = 0;
            int cursor1 = 0;
            int cursor2 = 0;
            //filter pages start from 1, filter.Page cannot be 0, if indeed you have page 0 change a lil' bit he logic 
            if (framesBordersModel1.Count + framesBordersModel2.Count < filter.Page) throw new Exception("Out of range");
            while ( cursor1 + cursor2 < filter.Page -1)
            {
                if (framesBordersModel1[cursor1].criteria1 < framesBordersModel2[cursor2].criteria1)
                {
                    cursor1++;
                }
                else if (framesBordersModel1[cursor1].criteria1 > framesBordersModel2[cursor2].criteria1)
                {
                    cursor2++;
                }
                //you should't get here case main key sound't be duplicate, annyhow
                else
                {
                    if (framesBordersModel1[cursor1].criteria2 < framesBordersModel2[cursor2].criteria2)
                    {
                        cursor1++;
                    }
                    else
                    {
                        cursor2++;
                    }
                }
                //mainCursor++;
            }
            //magic starts
            //inpar skipable
            int skipEndResult = 0;
            List<Item> dbFramesMerged = new List<Item>();
            if ((cursor1 + cursor2) %2 == 0)
            {
                dbFramesMerged.AddRange(
                    dbsetModel_1.OrderBy(x => x.Id)
                        .ThenBy(z => z.OrderNumber)
                        .Skip(cursor1*filter.Limit)
                        .Take(filter.Limit)
                        .Select(x => new Item() {criteria1 = x.Id, criteria2 = x.OrderNumber, model = x})
                        .ToList()); //consider as db call EF or Stored Procedure
                dbFramesMerged.AddRange(
                    dbsetModel_2.OrderBy(x => x.Id2)
                        .ThenBy(z => z.OrderNumber2)
                        .Skip(cursor2*filter.Limit)
                        .Take(filter.Limit)
                        .Select(x => new Item() {criteria1 = x.Id2, criteria2 = x.OrderNumber2, model = x})
                        .ToList());
                ; //consider as db call EF or Stored Procedure
            }
            else
            {
                skipEndResult = filter.Limit;
                if (cursor1 > cursor2)
                {
                    cursor1--;
                }
                else
                {
                    cursor2--;
                }
                dbFramesMerged.AddRange(
                   dbsetModel_1.OrderBy(x => x.Id)
                       .ThenBy(z => z.OrderNumber)
                       .Skip(cursor1 * filter.Limit)
                       .Take(filter.Limit)
                       .Select(x => new Item() { criteria1 = x.Id, criteria2 = x.OrderNumber, model = x })
                       .ToList()); //consider as db call EF or Stored Procedure
                dbFramesMerged.AddRange(
                    dbsetModel_2.OrderBy(x => x.Id2)
                        .ThenBy(z => z.OrderNumber2)
                        .Skip(cursor2 * filter.Limit)
                        .Take(filter.Limit)
                        .Select(x => new Item() { criteria1 = x.Id2, criteria2 = x.OrderNumber2, model = x })
                        .ToList());
            }

            IQueryable<Item> qItems = dbFramesMerged.AsQueryable();
            PagedList<Item> result = new PagedList<Item>();
            result.AddRange(qItems.OrderBy(x => x.criteria1).ThenBy(z => z.criteria2).Skip(skipEndResult).Take(filter.Limit).ToList());

            //here again you need db cals to get total count
            result.Total = dbsetModel_1.Count() + dbsetModel_2.Count();
            result.Limit = filter.Limit;
            result.Page = filter.Page;
            return result;
        }
    }

    public class PagingFilter
    {
        public int Limit { get; set; }
        public int Page { get; set; }
    }



    public class PagedList<T> : List<T>
    {

        public int Total { get; set; }
        public int? Page { get; set; }
        public int? Limit { get; set; }
    }

    public class Item : Criteria
    {
        public object model { get; set; }
    }

    public class Criteria
    {
        public int criteria1 { get; set; }
        public int criteria2 { get; set; }
        //more criterias if you need to order
    }

    public class model1
    {
        public int Id { get; set; }
        public int OrderNumber { get; set; }
        public string Name { get; set; }
    }

    public class model2
    {
        public int Id2 { get; set; }
        public int OrderNumber2 { get; set; }
        public string Name2 { get; set; }
    }
}

【讨论】:

    【解决方案2】:

    我认为您可以使用以下算法。假设你的页面大小是 10,那么对于页面 0:

    1. 从数据库 A 中获取 10 个结果,在数据库级别进行过滤和排序。
    2. 从数据库 B 中获取 10 个结果,在数据库级别过滤和排序(与上述查询并行)
    3. 结合这两个结果,以正确的排序顺序获得 10 条记录。所以你有 20 条记录排序,但只取其中的前 10 条并显示在 UI 中

    那么对于第 1 页:

    1. 请注意在上一步中您曾在 UI 中显示的数据库 A 和 B 中的项目数量。例如,您使用了数据库 A 中的 2 个项目和数据库 B 中的 8 个项目。
    2. 从数据库 A 中获取 10 个结果,经过过滤和排序,但从位置 2 开始(跳过 2),因为这两个结果已经在 UI 中显示。
    3. 从数据库 B 中获取 10 个结果,经过过滤和排序,但从位置 8 开始(跳过 8)。
    4. 以与上述相同的方式合并从 20 中获取 10 条记录。假设现在您使用了 A 中的 5 项和 B 中的 5 项。现在,您总共显示了 A 中的 7 项和 B 中的 13 项。使用那些下一步的数字。

    这将不允许(轻松)跳过页面,但据我了解,这不是必需的。

    性能实际上应该与查询单个数据库时相同,因为对 A 和 B 的查询可以并行完成。

    【讨论】:

    • 我了解您提出的解决方案,并且之前考虑过,但我需要以 RESTful 方式执行此操作,并且不必记住单个项目索引 - 另外,我不愿意扩展到了
    • 但是你需要记住你现在在哪个页面,为什么不记住2个数字呢?您将所有内容存储在客户端而不是服务器上,因此不确定它与 RESTful ness 有什么关系
    • 您可能的意思是您不想在您的 REST api 中添加两个不清楚的参数。嗯,这可能是合理的。至于规模 - 将所有数据拉入内存并在那里工作会更糟。
    • 我发现此算法存在问题,如果数据库以任何方式更改,您将返回第二次/n 分页的无效数据。继续您的示例,假设在枚举第 0 页和第 1 页之间,有人在第一个数据库的位置 0 中插入了一个新条目,按照此算法跳过前 2 个项目将排除新条目。任何更新/删除操作也是如此。
    • @mematei 不确定这是个大问题。如果数据在两者之间发生变化,任何分页算法的行为都不完美。在常规单一数据库场景中的第 0 页和第 1 页之间,有人插入 100 个比第 0 页项目“更小”的项目(就订购标准而言,“更小”,比如日期更小)。在您刷新之前,您在浏览页面时永远不会看到它们,同样的故事。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2018-07-05
    • 2017-03-06
    • 1970-01-01
    • 2014-05-05
    • 2014-08-08
    • 2021-12-12
    • 1970-01-01
    相关资源
    最近更新 更多