如何以正确的方式在 Firebase 查询中加入结果

Posted

技术标签:

【中文标题】如何以正确的方式在 Firebase 查询中加入结果【英文标题】:How to join results on a firebase query the right way 【发布时间】: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 匹配的朋友列表。 【参考方案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 等)

我希望这会有所帮助。

【讨论】:

以上是关于如何以正确的方式在 Firebase 查询中加入结果的主要内容,如果未能解决你的问题,请参考以下文章

如何以正确的方式从 firebase 数据库加载图像

如何以编程方式查询 Firebase 转换数据 [关闭]

如何正确构建 Firebase 数据库以方便读取和删除

如何获取Firebase Analytics的数据受众?

如何以编程方式将 LINQ 查询转换为正确描述 linq 表达式的可读英文文本?

如何在 Firebase 实时数据库中加载/检索多对多关系