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

时间:2018-08-13 16:56:09

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

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

数据库具有以下结构:

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

问题是为了获取应用程序必须进行大量api调用的分数和每个朋友的名字。如果我正确了解Firebase,请了解。如果用户有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教程的多个堆栈溢出问题,但是我没有找到任何更简单或更有效的方法来完成工作。

3 个答案:

答案 0 :(得分:2)

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

解决方案1:优化阅读策略

这是我推荐的解决方案,因为它不包括多余的数据,也不包括管理数据的一致性。

您的代码应如下所示:
免责声明:我不是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

如果这还不够理想,则可以始终在其friend表中复制name的{​​{1}}。如下所示:

score

这将使您每个scores: { <user_id>: { name: <user_name>, score: <user_score> } } 仅打一个电话,而不是两个。但是,您仍将读取相同数量的数据,并且必须管理数据的一致性(要么使用Firebase Function传播用户名更改,要么写到两个地方)。 / p>

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

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

users

但是,在这种情况下,您将读取更多数据(users: { <user_id>: { name: <user_name>, ..., score: <user_score> } } email等)

我希望这会有所帮助。

答案 1 :(得分: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);

答案 2 :(得分:1)

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

首先,您需要leaderboard表。

leaderboard: {
    <user_id>: <score>
}

第二,您需要users表。

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

并且您必须同时更新leaderboardusers的分数。

如果您想避免使用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;
});