【问题标题】:How to join results on a firebase query the right way如何以正确的方式在 Firebase 查询中加入结果
【发布时间】:2019-01-20 11:17:26
【问题描述】:

我正在使用 Firebase 实时数据库来构建我的应用程序 Unity。为了建立一个“朋友”排行榜,数据库将跟踪用户的朋友和他们的分数。

数据库结构如下:

scores{
user id : score
}
Users:{
    Id: {
        ageRange
        email
        name
        friendlist : {
             multiple friends user ids
        }
    }
}

问题是为了获得分数和每个朋友的名字,应用程序必须进行大量的 api 调用。至少如果我正确理解火力基地。如果用户有 10 个朋友,则需要 21 次调用才能填满排行榜。

我想出了以下用 c# 编写的代码:

List<UserScore> leaderBoard = new List<UserScore>();
db.Child("users").Child(uid).Child("friendList").GetValueAsync().ContinueWith(task => {
    if (task.IsCompleted)
    {
        //foreach friend
        foreach(DataSnapshot h in task.Result.Children)
        {
            string curName ="";
            int curScore = 0;
            //get his name in the user table
            db.Child("users").Child(h.Key).GetValueAsync().ContinueWith(t => {
                if (t.IsCompleted)
                {
                    DataSnapshot s = t.Result;
                    curName = s.Child("name").Value.ToString();
                    //get his score from the scores table
                    db.Child("scores").Child(h.Key).GetValueAsync().ContinueWith(q => {
                        if (q.IsCompleted)
                        {
                            DataSnapshot b = q.Result;
                            curScore = int.Parse(b.Value.ToString());
                            //make new userscore and add to leaderboard
                            leaderBoard.Add(new UserScore(curName, curScore));
                            Debug.Log(curName);
                            Debug.Log(curScore.ToString());
                        }
                    });
                }
            });
        }
    }
});

还有其他方法可以做到这一点吗?我已经阅读了多个堆栈溢出问题并观看了 firebase 教程,但我没有找到任何更简单或更有效的方法来完成工作。

【问题讨论】:

  • 您是否尝试过获取朋友列表而不是逐个获取?,只需通过 id 过滤器获取用户。这将创建 1 个 api 调用而不是 N 个调用。
  • @DiegoCardozo 在第一次通话中我得到了朋友列表(他们的 ID)。之后,我仍然需要获取他们的用户名并一一评分,这就是我创建 foreach 循环的原因。有没有办法喜欢在一个电话中获取这些朋友的所有用户名,而不是循环并尝试为每个朋友这样做?
  • 我的意思是你已经有了 ID 列表,是你的 friendsList 属性。您可以创建一个查询来获取与这些 ID 匹配的朋友列表。

标签: c# firebase unity3d firebase-realtime-database


【解决方案1】:

这主要是数据库结构问题。

首先,您需要leaderboard 表。

leaderboard: {
    <user_id>: <score>
}

其次,你需要users表。

users: {
    <user_id>: {
        name: <user_name>,
        ...,
        score: <user_score>,
        friendlist: {
            multiple friends user ids
        }
    }
}

而且你必须同时更新leaderboard的分数和users的分数。

如果你想避开Callback hell

你也可以这样试试。 (此代码为JAVA)

// Create a new ThreadPoolExecutor with 2 threads for each processor on the
// device and a 60 second keep-alive time.
int numCores = Runtime.getRuntime().availableProcessors();
ExecutorService executor = new ThreadPoolExecutor(
        numCores * 2,
        numCores * 2,
        60L,
        TimeUnit.SECONDS,
        new LinkedBlockingQueue<Runnable>()
);

Tasks.call(executor, (Callable<Void>) () -> {
    Task<Token> getStableTokenTask = NotificationUtil.getStableToken(token);
    Token stableToken;
    stableToken = Tasks.await(getStableTokenTask);

    if (stableToken != null) {
        Task<Void> updateStableTokenTask = NotificationUtil.updateStableToken(stableToken.getId(), versionCode, versionName);
        Tasks.await(updateStableTokenTask);
    }

    if (stableToken == null) {
        Token newToken = new Token(token, versionCode, versionName);
        Task<Void> insertStableTokenTask = NotificationUtil.insertStableToken(newToken);
        Tasks.await(insertStableTokenTask);
    }

    return null;
}).continueWith((Continuation<Void, Void>) task -> {
    if (!task.isSuccessful()) {
        // You will catch every exceptions from here.
        Log.w(TAG, task.getException());
        return null;
    }
    return null;
});

【讨论】:

    【解决方案2】:

    虽然以下不会减少调用次数,但它会创建用于检索数据的任务并同时运行它们,返回所需用户分数的列表。

    var friendList = await db.Child("users").Child(uid).Child("friendList").GetValueAsync();
    
    List<Task<UserScore>> tasks = new List<Task<UserScore>>();
    //foreach friend
    foreach(DataSnapshot friend in friendList.Children) {
        var task = Task.Run( async () => {
            var friendKey = friend.Key;
            //get his name in the user table
            var getName = db.Child("users").Child(friendKey).Child("name").GetValueAsync();
            //get his score from the scores table
            var getScore = db.Child("scores").Child(friendKey).GetValueAsync();
    
            await Task.WhenAll(getName, getScore);
    
            var name = getName.Result.Value.ToString();
            var score = int.Parse(getScore.Result.Value.ToString());
    
            //make new userscore to add to leader board
            var userScore = new UserScore(name, score);
            Debug.Log($"{name} : {score}");
            return userScore;
        });
        tasks.Add(task);
    }
    
    var scores = await Task.WhenAll(tasks);
    
    List<UserScore> leaderBoard = new List<UserScore>(scores);
    

    【讨论】:

      【解决方案3】:

      没有办法在不复制数据/重组数据库的情况下减少 API 调用次数。然而,减少 API 调用并不一定意味着整体读取策略更快/更好。我的建议是优化您的阅读策略,以减少整体数据读取并尽可能同时进行读取。

      方案一:优化阅读策略

      这是我推荐的解决方案,因为它不包含额外不必要的数据,也不包含管理数据的一致性。

      您的代码应如下所示:
      免责声明:我不是 C# 程序员,所以可能会有一些错误

      List<UserScore> leaderBoard = new List<UserScore>();
      db.Child("users").Child(uid).Child("friendList").GetValueAsync().ContinueWith(task => {
          if (task.IsCompleted)
          {
              //foreach friend
              foreach(DataSnapshot h in task.Result.Children)
              {
                  // kick off the task to retrieve friend's name
                  Task nameTask = db.Child("users").Child(h.Key).Child("name").GetValueAsync();
                  // kick off the task to retrieve friend's score
                  Task scoreTask = db.Child("scores").Child(h.Key).GetValueAsync();
                  // join tasks into one final task
                  Task finalTask = Task.Factory.ContinueWhenAll((new[] {nameTask, scoreTask}), tasks => {
                    if (nameTask.IsCompleted && scoreTask.IsCompleted) {
                      // both tasks are complete; add new record to leaderboard
                      string name = nameTask.Result.Value.ToString();
                      int score = int.Parse(scoreTask.Result.Value.ToString());
                      leaderBoard.Add(new UserScore(name, score));
                      Debug.Log(name);
                      Debug.Log(score.ToString());
                    }
                  })
              }
          }
      });
      

      上面的代码改进了整体读取策略,不拉取friend的所有用户数据(即nameemailfriendlist等)并同时拉取name score.

      解决方案 2:name 复制到 scores

      如果这还不够理想,您可以随时在 score 表中复制 friendname。如下所示:

      scores: {
        <user_id>: {
          name: <user_name>,
          score: <user_score>
        }
      }
      

      这将允许您每个friend 只拨打一个电话,而不是两个。但是,您仍将读取相同数量的数据,并且必须管理数据的一致性(使用 Firebase 函数传播用户名更改或写入两个位置)。

      解决方案 3:scores 表合并到 users 表中

      如果您不想管理一致性问题,您可以将scores 表合并到users 表中。
      您的结构将类似于:

      users: {
        <user_id>: {
          name: <user_name>,
          ...,
          score: <user_score>
        }
      }
      

      但是,在这种情况下,您将读取更多数据(emailfriendlist 等)

      我希望这会有所帮助。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2017-02-17
        • 1970-01-01
        • 1970-01-01
        • 2015-08-02
        • 2021-12-07
        • 2014-02-04
        • 2021-08-05
        • 1970-01-01
        相关资源
        最近更新 更多