如何以正确的方式在 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
的所有用户数据(即name
、email
、friendlist
等)并同时拉取name
score
.
解决方案 2:将 name
复制到 scores
表
如果这还不够理想,您可以随时在 score
表中复制 friend
的 name
。如下所示:
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>
但是,在这种情况下,您将读取更多数据(email
、friendlist
等)
我希望这会有所帮助。
【讨论】:
以上是关于如何以正确的方式在 Firebase 查询中加入结果的主要内容,如果未能解决你的问题,请参考以下文章