使用 Firebase/Firestore 具有重新排序功能的任务列表

Posted

技术标签:

【中文标题】使用 Firebase/Firestore 具有重新排序功能的任务列表【英文标题】:Task list with re-ordering feature using Firebase/Firestore 【发布时间】:2019-08-31 04:18:27 【问题描述】:

我想列出可以更改顺序的任务,但我不确定如何将其存储在数据库中。

我不想使用数组,因为我以后还要做一些查询。

这是我的数据库的截图:

我正在尝试制作类似于 Trello 的东西,用户可以在其中添加任务并可以根据任务的优先级向上和向下移动任务。我还需要更改数据库中任务的位置以维护记录。我无法理解如何在任何数据库中做到这一点。我是一位经验丰富的开发人员,我曾使用过 mongodb 和 firebase,但这对我来说是独一无二的。

这是创建和获取所有任务的代码。当我尝试在集合中移动某些任务时。我在每个任务中都维护了一个索引。 假设当我将任务从索引 5 的位置移动到索引 2 时,我必须通过 +1 编辑所有即将到来的索引,是否有更好的方法?

代码示例

class taskManager 
    static let shared = taskManager()
    typealias TasksCompletion = (_ tasks:[Task],_ error:String?)->Void
    typealias SucessCompletion = (_ error:String?)->Void

    func addTask(task:Task,completion:@escaping SucessCompletion)
        Firestore.firestore().collection("tasks").addDocument(data: task.toDic)  (err) in
            if err != nil 
                print(err?.localizedDescription as Any)
            
            completion(nil)
        
    

    func getAllTask(completion:@escaping TasksCompletion)
        Firestore.firestore().collection("tasks")
            .addSnapshotListener  taskSnap, error in
                taskSnap?.documentChanges.forEach( (task) in
                    let object = task.document.data()
                    let json = try! JSONSerialization.data(withJSONObject: object, options: .prettyPrinted)
                    var taskData = try! JSONDecoder().decode(Task.self, from: json)
                    taskData.id = task.document.documentID

                    if (task.type == .added) 
                        Task.shared.append(taskData)
                    
                    if (task.type == .modified) 
                        let index = Task.shared.firstIndex(where:  $0.id ==  taskData.id)!
                        Task.shared[index] = taskData
                    
                )
                if error == nil
                    completion(Task.shared,nil)
                else
                    completion([],error?.localizedDescription)
                
            
    

【问题讨论】:

这听起来像XY problem。提供更详细的问题描述以及您尝试过但不起作用的代码 这个问题应该有更多的赞成票。太棒了。 @charlietfl 添加信息 我已经添加了更多细节,请任何人批准我没有 2k 代表这样做 正确的解决方案将与数据集的大小有关,用户是否想要不同的数据“视图”(如 iTunes 中的播放列表)以及数据集是否可供多个用户访问.这是 100 个任务还是数百万个任务,是每个用户吗? 【参考方案1】:

我认为您要问的问题更多的是关于数据库设计

如果您希望能够保持一组项目的顺序,同时能够重新排序它们,您将需要一个列来保持顺序。

如果它们是按顺序订购的,那么当您尝试订购它们时会遇到问题。

示例

例如,如果您想将Item1 移到Item4 后面:

之前

具有排序索引的项目。

 1. Item1, order: 1
 2. Item2, order: 2
 3. Item3, order: 3
 4. Item4, order: 4
 5. Item5, order: 5
 6. Item6, order: 6

之后

问题:我们必须更新被移动的项目和放置位置之间的每条记录。

为什么会出现问题:这是一个大 O(n) - 对于我们移动的每个空间,我们都必须更新那么多记录。随着您获得更多任务,这将成为一个更大的问题,因为它需要更长的时间并且无法很好地扩展。最好有一个 Big O(1),我们有恒定数量的变化或尽可能少的变化。

 1. Item2, order: 1 - Updated
 2. Item3, order: 2 - Updated
 3. Item4, order: 3 - Updated
 4. Item1, order: 4 - Updated
 5. Item5, order: 5
 6. Item6, order: 6

可能的解决方案 #1(可以吗?) - 间距

您可以尝试想出一种巧妙的方法,尝试将订单号隔开,这样您就可以在不更新多条记录的情况下填补漏洞。

但这可能会变得棘手,您可能会想,“为什么不按顺序存储 Item1:4.5”我在下面添加了一个 related question 来说明这个想法以及为什么应该避免它。

您也许可以验证订单客户端的安全性,避免访问数据库以确定移动的新订单ID。

这也有限制,因为您可能必须重新平衡间距,或者您的项目数量不足。您可能需要检查是否存在冲突,当发生冲突时,您需要对所有内容进行重新平衡,或者递归地对冲突周围的项目进行重新平衡,以确保其他平衡更新不会导致更多冲突并且解决其他冲突。

 1. Item2, order: 200
 2. Item3, order: 300
 3. Item4, order: 400
 4. Item1, order: 450 - Updated
 5. Item5, order: 500
 6. Item6, order: 600

可能的解决方案 #2(更好) - 链接列表

正如related link below 中提到的,您可以使用像链表这样的数据结构。这保留了要更新的恒定数量的更改,因此它是 Big O(1)。如果你还没有玩过数据结构,我会稍微介绍一下链表。

正如您在下面看到的,此更改只需要 3 次更新,我相信最大值为 5,如 Expected Updates 所示。您可能会想,“好吧,第一个原始问题/示例需要这么多!”问题是,与原始方法 [Big O(n)] 的数千或数百万次更新的可能性相比,这将始终是最多 5 次更新。

 1. Item2, previous: null, next: Item3 - Updated // previous is now null
 2. Item3, previous: Item2, next: Item4
 3. Item4, previous: Item3, next: Item1 - Updated // next is now Item1
 4. Item1, previous: Item4, next: Item5 - Updated // previous & next updated
 5. Item5, previous: Item1, next: Item4 - Updated // previous is now Item1
 6. Item6, previous: Item6, next: null

预期更新

    正在移动的项目(上一个,下一个) 旧的上一个项目的下一个 旧的下一个项目的上一个 新的上一个项目的下一个 新的下一个项目的上一个

链接列表

我想我用了double linked list。您可能只使用一个没有previous 属性而只有next 的单链表。

链表背后的想法是把它想象成一个链环,当你想移动一个项目时,你会将它与它前面和后面的链接解耦,然后将这些链接链接在一起。接下来,您将打开您想要放置它的位置,现在它的每一侧都有新链接,对于这些新链接,它们现在将链接到新链接而不是彼此链接。

可能的解决方案 #3 - 文档/Json/阵列存储

您说您希望远离数组,但您可以利用文档存储。您仍然可以拥有一个可搜索的项目表,然后每个项目集合将只有一个项目 ID/引用数组。

物品表

 - Item1, id: 1
 - Item2, id: 2
 - Item3, id: 3
 - Item4, id: 4
 - Item5, id: 5
 - Item6, id: 6

物品收藏

 [2, 3, 4, 1, 5, 6]

相关问题

Storing a reorderable list in a database

Big O 上的资源

A guide on Big O More on Big O Wiki Big O

其他注意事项

您的数据库设计将取决于您要完成的任务。项目可以属于多个版块或用户吗?

您能否将一些订单卸载到客户端并让它告诉服务器新订单是什么?您仍然应该避免在客户端使用低效的排序算法,但是如果您信任它们,并且如果多人同时处理相同的项目,您可以让它们做一些肮脏的工作,并且不会有任何数据完整性问题时间(这些是其他设计问题,可能与数据库相关,也可能不相关,具体取决于您处理它们的方式。)

【讨论】:

现在我假设只是待办事项列表,所以我可以重新安排项目 感谢您的回答,我认为链表是一种很好的方法,但在 firebase 中无法使用,第三种方法是维护一个 ids 数组列表,因此我们需要获取每个 id,然后再获取该 id 的项跨度> 下一个/上一个只是项目 ID。我很长一段时间没有使用firebase,但我想你可以做标准的关联。我认为在我链接的相关问题中,他们甚至提到使用连接表来做到这一点。您需要加入 id 位于#3 的项目集合列表中的项目(同样,我没有太多的 firebase 经验,但这就是数据库的想法)。我认为您关心的是每个 ID 的请求 #3,但您应该能够在一个请求中获得所有这些。 感谢您的出色回答。我想对您的解决方案发表评论:如果您只有一个列表,解决方案 2 可以很好地工作。如果一项任务可以出现在多个列表中,它会很快变得混乱。您必须为要使用的每个列表创建链接。如果您想拥有多个列表,解决方案 3 可能是最佳解决方案。它易于维护并且应该是高性能的。 解决方案 1 是最难维护的。但是,如果您想在多个列表中对所有任务进行唯一排名,则需要。 Atlassian 将此解决方案用于 Jira-Boards。无论它们出现在哪个列表中,所有问题都是按顺序排列的。有趣的是,他们使用词法排序来减少变基的痛苦。他们制作了一个有趣的视频。youtu.be/OjQv9xMoFbg【参考方案2】:

我在同一个问题上停留了很长时间。我发现的最佳解决方案是按按字典顺序排列

尝试管理小数位数(1、2、3、4...)会遇到很多问题,这些问题都在此问题的其他答案中提到。相反,我将排名存储为字符串('aaa'、'bbb'、'ccc'...),并使用字符串中字符的字符代码在进行调整时找到排名之间的位置.

例如,我有:


    item: "Star Wars",
    rank: "bbb"
,

    item: "Lord of the Rings",
    rank: "ccc"
,

    item: "Harry Potter",
    rank: "ddd"
,

    item: "Star Trek",
    rank: "eee"
,

    item: "Game of Thrones",
    rank: "fff"

现在我想将"Game of Thrones" 移动到第三个位置,低于"Lord of the Rings" ('ccc') 和高于"Harry Potter" ('ddd')。

所以我使用'ccc''ddd' 的字符代码在数学上找到两个字符串之间的平均值;在这种情况下,最终是'cpp',我会将文档更新为:


    item: "Game of Thrones",
    rank: "cpp"

现在我有:


    item: "Star Wars",
    rank: "bbb"
,

    item: "Lord of the Rings",
    rank: "ccc"
,

    item: "Game of Thrones",
    rank: "cpp"
,

    item: "Harry Potter",
    rank: "ddd"
,

    item: "Star Trek",
    rank: "eee"

如果两个等级之间的空间不足,我可以简单地在字符串末尾添加一个字母;所以,在'bbb''bbc'之间,我可以插入'bbbn'

这比小数排名更有优势。

注意事项

不要将'aaa''zzz' 分配给任何项目。这些需要保留,以便轻松地将项目移动到列表的顶部或底部。如果"Star Wars" 的等级为'aaa' 并且我想在它上面移动一些东西,就会出现问题。可解决的问题,但如果您从等级'bbb' 开始,这很容易避免。然后,如果您想将某些东西移到最高排名之上,您可以简单地找到'bbb''aaa' 之间的平均值。

如果您的列表经常被重新洗牌,最好定期刷新排名。如果将事物移动到列表中的同一位置数千次,您可能会得到像'bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbn' 这样的长字符串。当字符串达到一定长度时,您可能希望刷新列表。

实施

实现这个效果的算法和函数的解释可以在here找到。这个想法归功于那篇文章的作者。

我在项目中使用的代码

同样,这段代码的功劳归于我上面链接的文章的作者,但这是我在项目中运行的代码,用于查找两个字符串之间的平均值。这是用 Dart 为 Flutter 应用编写的

import 'dart:math';

const ALPHABET_SIZE = 26;

String getRankBetween(String firstRank, String secondRank) 
  assert(firstRank.compareTo(secondRank) < 0,
      "First position must be lower than second. Got firstRank $firstRank and second rank $secondRank");

  /// Make positions equal
  while (firstRank.length != secondRank.length) 
    if (firstRank.length > secondRank.length)
      secondRank += "a";
    else
      firstRank += "a";
  
  var firstPositionCodes = [];
  firstPositionCodes.addAll(firstRank.codeUnits);
  var secondPositionCodes = [];
  secondPositionCodes.addAll(secondRank.codeUnits);
  var difference = 0;
  for (int index = firstPositionCodes.length - 1; index >= 0; index--) 
    /// Codes of the elements of positions
    var firstCode = firstPositionCodes[index];
    var secondCode = secondPositionCodes[index];

    /// i.e. ' a < b '
    if (secondCode < firstCode) 
      /// ALPHABET_SIZE = 26 for now
      secondCode += ALPHABET_SIZE;
      secondPositionCodes[index - 1] -= 1;
    

    /// formula: x = a * size^0 + b * size^1 + c * size^2
    final powRes = pow(ALPHABET_SIZE, firstRank.length - index - 1);
    difference += (secondCode - firstCode) * powRes;
  
  var newElement = "";
  if (difference <= 1) 
    /// add middle char from alphabet
    newElement = firstRank +
        String.fromCharCode('a'.codeUnits.first + ALPHABET_SIZE ~/ 2);
   else 
    difference ~/= 2;
    var offset = 0;
    for (int index = 0; index < firstRank.length; index++) 
      /// formula: x = difference / (size^place - 1) % size;
      /// i.e. difference = 110, size = 10, we want place 2 (middle),
      /// then x = 100 / 10^(2 - 1) % 10 = 100 / 10 % 10 = 11 % 10 = 1
      final diffInSymbols =
          difference ~/ pow(ALPHABET_SIZE, index) % (ALPHABET_SIZE);
      var newElementCode = firstRank.codeUnitAt(secondRank.length - index - 1) +
          diffInSymbols +
          offset;
      offset = 0;

      /// if newElement is greater then 'z'
      if (newElementCode > 'z'.codeUnits.first) 
        offset++;
        newElementCode -= ALPHABET_SIZE;
      

      newElement += String.fromCharCode(newElementCode);
    
    newElement = newElement.split('').reversed.join();
  
  return newElement;

【讨论】:

你介意发布你的方法来获得两个字符串之间的平均值吗? 我编辑了答案以包含我在项目中运行的代码。希望这会有所帮助! 您的代码的复制粘贴在我的应用程序中有效,所以我可以说它有效,非常好!现在对我来说完成它,似乎有点先进(至少对我来说),会给它一些时间。谢谢分享:-)【参考方案3】:

您可以采用多种方法来实现此类功能,

方法#1:


你可以给你的任务远程位置而不是连续位置,像这样:

日期:2019 年 4 月 10 日 名称:“一些任务名称” 指数:10 ... 指数:20 ... 指数:30

这里总共有 3 个任务,位置分别为 10、20、30。现在假设您想将第三个任务移动到中间,只需将位置更改为 15,现在您有三个位置分别为 10、15、20 的任务,我我确信你可以在从数据库中获取所有任务时根据位置进行排序,并且我还假设你可以获得任务的位置,因为用户将在移动应用程序或 Web 应用程序上重新排列任务,这样你就可以轻松地获得位置周边任务,计算周边任务的中间位置, 现在假设您想将第一个任务(现在位置索引为 10)移动到中间,只需获取周围任务的位置 15 和 20 并计算中间 17.5 ( (20-15)/2= 17.5 ) 来了,现在你有位置 15, 17.5, 20

有人说 1 和 2 之间有无穷大,所以我认为你不会运行我们的数字,但你们中的一些人仍然认为你很快就会用完除法,你可以增加差异,你可以达到 100000。 ..00 而不是 10

方法 #2:


您可以将所有任务保存在同一个文档中,而不是以分层 json 形式单独保存文档,如下所示:

任务:'[ name:"some name",date: "some date" ,name:"some name",date: "some date",name:"some name",date: "某个日期”]'

通过这样做,您将在屏幕上一次获得所有任务,并将 json 解析为本地数组,当用户重新排列任务时,您只需在本地更改该数组元素的位置并保存任务的分层版本在数据库中,这种方法也有一些缺点,如果您使用分页可能很难这样做,但希望您不会在任务管理应用程序中使用分页,并且您可能希望在屏幕上显示所有任务就像 Trello 一样

【讨论】:

有人请更正我答案的格式,我在移动设备上,很难通过移动浏览器做到这一点:-(

以上是关于使用 Firebase/Firestore 具有重新排序功能的任务列表的主要内容,如果未能解决你的问题,请参考以下文章

该服务目前不可用 | Firebase cloud_firestore 问题

Firebase Firestore iOS 多种型号

如何从 Dart VM / Dart 集成测试访问 Firebase Firestore?

如何从 Firebase Firestore 访问特定用户的信息? [关闭]

在Android的Firebase Firestore中创建复杂的查询

无法在firebase.firestore.CollectionReference中使用Array firebase.firestore.Query为什么?