如何绕过 Firebase 缓存刷新数据(在 Android 应用程序中)?
How to bypass the Firebase cache to refresh data (in Android app)?
在大多数时间必须离线工作的 Android 应用程序上,我需要在它在线时执行一些同步操作,即:
User myUser = MyclientFacade.getUser();
If (myUser.getScore > 10) {
DoSomething()
}
其中User是Firebase填充的POJO;
Firebase缓存激活时出现问题
Firebase.getDefaultConfig().setPersistenceEnabled(true);
并且用户已经在缓存中,数据由第三方(甚至另一台设备)在 Firebase DB 上更新。事实上,当我查询 Firebase 以获取用户时,我首先从缓存中获取数据,然后使用来自 Firebase 服务器的最新数据获取第二个更改事件,但为时已晚!
让我们看看同步方法 MyclientFacade.getUser() :
Public User getUser() {
Firebase ref = myFireBaseroot.child("User").child(uid);
ref.keepSynced(true);
/* try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
final CountDownLatch signal = new CountDownLatch(1);
ref.addListenerForSingleValueEvent(new ValueEventListener() {
//ref.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
this.user = dataSnapshot.getValue(User.class);
signal.countDown();
}
@Override
public void onCancelled(FirebaseError firebaseError) {
signal.countDown();
}
});
signal.await(5, TimeUnit.SECONDS);
ref.keepSynced(false);
return this.user;
}
如果我将 addValueEventListener
或 addListenerForSingleValueEvent
与 ref.keepSynced
混合使用,我会获得相同的行为:
假设我的用户在缓存中的得分值为 5,而来自 Firebase DB 的得分值为 11。
当我调用getUser
时,我将获得5分(Firebase首先询问缓存)所以我不会调用doSomething()
方法。
如果我取消注释示例中的 Thread.sleep()
代码,Firebase 缓存将有足够的时间进行更新,并且我的 getUser
将 return 得到正确的分数值 (11)。
那么如何直接从服务器端直接获取最新值并绕过缓存呢?
这个问题也给我的申请带来了很大的压力。
我尝试了所有方法,从将 .addListenerForSingleValueEvent()
更改为 .addValueEventListener()
,尝试创造性地使用 .keepSynced()
,再到使用延迟(您在上面描述的 Thread.sleep()
方法)和没有什么能真正始终如一地工作(即使是 Thread.sleep()
方法,这在生产应用程序中是不可接受的,也没有给我一致的结果)。
所以我做的是这样的:在创建一个 Query 对象并调用 .keepSynced()
之后,我继续写一个 mock/token 对象我正在查询的节点,然后在该操作的完成侦听器中,我在删除模拟对象后执行我想执行的数据检索。
类似于:
MockObject mock = new MockObject();
mock.setIdentifier("delete!");
final Query query = firebase.child("node1").child("node2");
query.keepSynced(true);
firebase.child("node1").child("node2").child("refreshMock")
.setValue(mock, new CompletionListener() {
@Override
public void onComplete(FirebaseError error, Firebase afb) {
query.addListenerForSingleValueEvent(new ValueEventListener() {
public void onDataChange(DataSnapshot data) {
// get identifier and recognise that this data
// shouldn't be used
// it's also probably a good idea to remove the
// mock object
// using the removeValue() method in its
// speficic node so
// that the database won't be saddled with a new
// one in every
// read operation
}
public void onCancelled(FirebaseError error) {
}
});
}
});
}
到目前为止,这对我来说一直有效! (好吧,一天左右的时间,所以请加一点盐)。似乎在读取之前执行写入操作以某种方式绕过缓存,这是有道理的。所以数据回来是新鲜的。
唯一的缺点是读操作之前的额外写操作,这可能会导致小延迟(显然使用小对象)但如果这是始终新鲜数据的代价,我会接受!
希望对您有所帮助!
我发现的解决方法是使用 Firebase 的 runTransaction()
方法。这似乎总是从服务器检索数据。
String firebaseUrl = "/some/user/datapath/";
final Firebase firebaseClient = new Firebase(firebaseUrl);
// Use runTransaction to bypass cached DataSnapshot
firebaseClient.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData mutableData) {
// Return passed in data
return Transaction.success(mutableData);
}
@Override
public void onComplete(FirebaseError firebaseError, boolean success, DataSnapshot dataSnapshot) {
if (firebaseError != null || !success || dataSnapshot == null) {
System.out.println("Failed to get DataSnapshot");
} else {
System.out.println("Successfully get DataSnapshot");
//handle data here
}
}
});
只需在 应用程序 class 的 onCreate
方法中添加此代码。 (更改数据库引用)
示例:
public class MyApplication extendes Application{
@Override
public void onCreate() {
super.onCreate();
DatabaseReference scoresRef = FirebaseDatabase.getInstance().getReference("scores");
scoresRef.keepSynced(true);
}
}
对我来说效果很好。
参考:
https://firebase.google.com/docs/database/android/offline-capabilities
我的解决方案是在加载时调用 Database.database().isPersistenceEnabled = true,然后在我需要刷新的任何节点上调用 .keepSynced(true)。
这里的问题是,如果您在 .keepSynced(true) 之后立即查询您的节点,那么您可能会获得缓存而不是新数据。有点蹩脚但功能性的解决方法:延迟您对节点的查询一秒钟左右,以便给 Firebase 一些时间来获取新数据。如果用户离线,您将获得缓存。
哦,如果这是一个你不想永远在后台保持最新的节点,记得在完成后调用 .keepSynced(false)。
我尝试了接受的解决方案和交易。事务解决方案更干净、更好,但是只有当数据库在线时才会调用成功的 OnComplete,所以你不能从缓存中加载。但是您可以中止事务,然后在离线时(使用缓存数据)也会调用 onComplete。
我之前创建的函数只有在数据库获得足够的连接以进行同步时才有效。我通过添加超时来解决问题。我将致力于此并测试它是否有效。也许以后有空闲的时候,我会创建android lib并发布,但是到时候就是kotlin中的代码了:
/**
* @param databaseReference reference to parent database node
* @param callback callback with mutable list which returns list of objects and boolean if data is from cache
* @param timeOutInMillis if not set it will wait all the time to get data online. If set - when timeout occurs it will send data from cache if exists
*/
fun readChildrenOnlineElseLocal(databaseReference: DatabaseReference, callback: ((mutableList: MutableList<@kotlin.UnsafeVariance T>, isDataFromCache: Boolean) -> Unit), timeOutInMillis: Long? = null) {
var countDownTimer: CountDownTimer? = null
val transactionHandlerAbort = object : Transaction.Handler { //for cache load
override fun onComplete(p0: DatabaseError?, p1: Boolean, data: DataSnapshot?) {
val listOfObjects = ArrayList<T>()
data?.let {
data.children.forEach {
val child = it.getValue(aClass)
child?.let {
listOfObjects.add(child)
}
}
}
callback.invoke(listOfObjects, true)
removeListener()
}
override fun doTransaction(p0: MutableData?): Transaction.Result {
return Transaction.abort()
}
}
val transactionHandlerSuccess = object : Transaction.Handler { //for online load
override fun onComplete(p0: DatabaseError?, p1: Boolean, data: DataSnapshot?) {
countDownTimer?.cancel()
val listOfObjects = ArrayList<T>()
data?.let {
data.children.forEach {
val child = it.getValue(aClass)
child?.let {
listOfObjects.add(child)
}
}
}
callback.invoke(listOfObjects, false)
removeListener()
}
override fun doTransaction(p0: MutableData?): Transaction.Result {
return Transaction.success(p0)
}
}
如果你想让离线速度更快(不要在明显没有连接数据库的情况下愚蠢地等待超时)然后在使用上面的函数之前检查数据库是否已连接:
DatabaseReference connectedRef = FirebaseDatabase.getInstance().getReference(".info/connected");
connectedRef.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot snapshot) {
boolean connected = snapshot.getValue(Boolean.class);
if (connected) {
System.out.println("connected");
} else {
System.out.println("not connected");
}
}
@Override
public void onCancelled(DatabaseError error) {
System.err.println("Listener was cancelled");
}
});
使用这条线
FirebaseDatabase.getInstance().purgeOutstandingWrites();
我知道已经晚了,但对于面临同样问题的人来说,我终于找到了完美的解决方案。您可以使用以下方法检查数据是否来自缓存内存:
if (documentSnapShot.metadata.isFromCache){
// data is coming from the cached memory
} else {
// data is coming from the server
}
这是 Kotlin
中的完整代码片段:
gameRoomRef.addSnapshotListener { documentSnapshot, fireStoreException ->
fireStoreException?.let {
return@addSnapshotListener
}
documentSnapshot?.let {
if (it.metadata.isFromCache)
// data is from cached memory
else
// data is from server
}
在 Flutter 上,以下是强制从 realtimeDb 获取最新快照的方法:
Future<DataSnapshot> _latestRtdbSnapshot(DatabaseReference dbRef) async {
// ! **NASTY HACK**: Write/delete a child of the query to force an update.
await dbRef.keepSynced(true);
await dbRef.child('refreshMock').remove();
final res = await dbRef.once();
dbRef.keepSynced(false);
return res;
}
在大多数时间必须离线工作的 Android 应用程序上,我需要在它在线时执行一些同步操作,即:
User myUser = MyclientFacade.getUser();
If (myUser.getScore > 10) {
DoSomething()
}
其中User是Firebase填充的POJO;
Firebase缓存激活时出现问题
Firebase.getDefaultConfig().setPersistenceEnabled(true);
并且用户已经在缓存中,数据由第三方(甚至另一台设备)在 Firebase DB 上更新。事实上,当我查询 Firebase 以获取用户时,我首先从缓存中获取数据,然后使用来自 Firebase 服务器的最新数据获取第二个更改事件,但为时已晚!
让我们看看同步方法 MyclientFacade.getUser() :
Public User getUser() {
Firebase ref = myFireBaseroot.child("User").child(uid);
ref.keepSynced(true);
/* try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}*/
final CountDownLatch signal = new CountDownLatch(1);
ref.addListenerForSingleValueEvent(new ValueEventListener() {
//ref.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
this.user = dataSnapshot.getValue(User.class);
signal.countDown();
}
@Override
public void onCancelled(FirebaseError firebaseError) {
signal.countDown();
}
});
signal.await(5, TimeUnit.SECONDS);
ref.keepSynced(false);
return this.user;
}
如果我将 addValueEventListener
或 addListenerForSingleValueEvent
与 ref.keepSynced
混合使用,我会获得相同的行为:
假设我的用户在缓存中的得分值为 5,而来自 Firebase DB 的得分值为 11。
当我调用getUser
时,我将获得5分(Firebase首先询问缓存)所以我不会调用doSomething()
方法。
如果我取消注释示例中的 Thread.sleep()
代码,Firebase 缓存将有足够的时间进行更新,并且我的 getUser
将 return 得到正确的分数值 (11)。
那么如何直接从服务器端直接获取最新值并绕过缓存呢?
这个问题也给我的申请带来了很大的压力。
我尝试了所有方法,从将 .addListenerForSingleValueEvent()
更改为 .addValueEventListener()
,尝试创造性地使用 .keepSynced()
,再到使用延迟(您在上面描述的 Thread.sleep()
方法)和没有什么能真正始终如一地工作(即使是 Thread.sleep()
方法,这在生产应用程序中是不可接受的,也没有给我一致的结果)。
所以我做的是这样的:在创建一个 Query 对象并调用 .keepSynced()
之后,我继续写一个 mock/token 对象我正在查询的节点,然后在该操作的完成侦听器中,我在删除模拟对象后执行我想执行的数据检索。
类似于:
MockObject mock = new MockObject();
mock.setIdentifier("delete!");
final Query query = firebase.child("node1").child("node2");
query.keepSynced(true);
firebase.child("node1").child("node2").child("refreshMock")
.setValue(mock, new CompletionListener() {
@Override
public void onComplete(FirebaseError error, Firebase afb) {
query.addListenerForSingleValueEvent(new ValueEventListener() {
public void onDataChange(DataSnapshot data) {
// get identifier and recognise that this data
// shouldn't be used
// it's also probably a good idea to remove the
// mock object
// using the removeValue() method in its
// speficic node so
// that the database won't be saddled with a new
// one in every
// read operation
}
public void onCancelled(FirebaseError error) {
}
});
}
});
}
到目前为止,这对我来说一直有效! (好吧,一天左右的时间,所以请加一点盐)。似乎在读取之前执行写入操作以某种方式绕过缓存,这是有道理的。所以数据回来是新鲜的。
唯一的缺点是读操作之前的额外写操作,这可能会导致小延迟(显然使用小对象)但如果这是始终新鲜数据的代价,我会接受!
希望对您有所帮助!
我发现的解决方法是使用 Firebase 的 runTransaction()
方法。这似乎总是从服务器检索数据。
String firebaseUrl = "/some/user/datapath/";
final Firebase firebaseClient = new Firebase(firebaseUrl);
// Use runTransaction to bypass cached DataSnapshot
firebaseClient.runTransaction(new Transaction.Handler() {
@Override
public Transaction.Result doTransaction(MutableData mutableData) {
// Return passed in data
return Transaction.success(mutableData);
}
@Override
public void onComplete(FirebaseError firebaseError, boolean success, DataSnapshot dataSnapshot) {
if (firebaseError != null || !success || dataSnapshot == null) {
System.out.println("Failed to get DataSnapshot");
} else {
System.out.println("Successfully get DataSnapshot");
//handle data here
}
}
});
只需在 应用程序 class 的 onCreate
方法中添加此代码。 (更改数据库引用)
示例:
public class MyApplication extendes Application{
@Override
public void onCreate() {
super.onCreate();
DatabaseReference scoresRef = FirebaseDatabase.getInstance().getReference("scores");
scoresRef.keepSynced(true);
}
}
对我来说效果很好。
参考: https://firebase.google.com/docs/database/android/offline-capabilities
我的解决方案是在加载时调用 Database.database().isPersistenceEnabled = true,然后在我需要刷新的任何节点上调用 .keepSynced(true)。
这里的问题是,如果您在 .keepSynced(true) 之后立即查询您的节点,那么您可能会获得缓存而不是新数据。有点蹩脚但功能性的解决方法:延迟您对节点的查询一秒钟左右,以便给 Firebase 一些时间来获取新数据。如果用户离线,您将获得缓存。
哦,如果这是一个你不想永远在后台保持最新的节点,记得在完成后调用 .keepSynced(false)。
我尝试了接受的解决方案和交易。事务解决方案更干净、更好,但是只有当数据库在线时才会调用成功的 OnComplete,所以你不能从缓存中加载。但是您可以中止事务,然后在离线时(使用缓存数据)也会调用 onComplete。
我之前创建的函数只有在数据库获得足够的连接以进行同步时才有效。我通过添加超时来解决问题。我将致力于此并测试它是否有效。也许以后有空闲的时候,我会创建android lib并发布,但是到时候就是kotlin中的代码了:
/**
* @param databaseReference reference to parent database node
* @param callback callback with mutable list which returns list of objects and boolean if data is from cache
* @param timeOutInMillis if not set it will wait all the time to get data online. If set - when timeout occurs it will send data from cache if exists
*/
fun readChildrenOnlineElseLocal(databaseReference: DatabaseReference, callback: ((mutableList: MutableList<@kotlin.UnsafeVariance T>, isDataFromCache: Boolean) -> Unit), timeOutInMillis: Long? = null) {
var countDownTimer: CountDownTimer? = null
val transactionHandlerAbort = object : Transaction.Handler { //for cache load
override fun onComplete(p0: DatabaseError?, p1: Boolean, data: DataSnapshot?) {
val listOfObjects = ArrayList<T>()
data?.let {
data.children.forEach {
val child = it.getValue(aClass)
child?.let {
listOfObjects.add(child)
}
}
}
callback.invoke(listOfObjects, true)
removeListener()
}
override fun doTransaction(p0: MutableData?): Transaction.Result {
return Transaction.abort()
}
}
val transactionHandlerSuccess = object : Transaction.Handler { //for online load
override fun onComplete(p0: DatabaseError?, p1: Boolean, data: DataSnapshot?) {
countDownTimer?.cancel()
val listOfObjects = ArrayList<T>()
data?.let {
data.children.forEach {
val child = it.getValue(aClass)
child?.let {
listOfObjects.add(child)
}
}
}
callback.invoke(listOfObjects, false)
removeListener()
}
override fun doTransaction(p0: MutableData?): Transaction.Result {
return Transaction.success(p0)
}
}
如果你想让离线速度更快(不要在明显没有连接数据库的情况下愚蠢地等待超时)然后在使用上面的函数之前检查数据库是否已连接:
DatabaseReference connectedRef = FirebaseDatabase.getInstance().getReference(".info/connected");
connectedRef.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot snapshot) {
boolean connected = snapshot.getValue(Boolean.class);
if (connected) {
System.out.println("connected");
} else {
System.out.println("not connected");
}
}
@Override
public void onCancelled(DatabaseError error) {
System.err.println("Listener was cancelled");
}
});
使用这条线
FirebaseDatabase.getInstance().purgeOutstandingWrites();
我知道已经晚了,但对于面临同样问题的人来说,我终于找到了完美的解决方案。您可以使用以下方法检查数据是否来自缓存内存:
if (documentSnapShot.metadata.isFromCache){
// data is coming from the cached memory
} else {
// data is coming from the server
}
这是 Kotlin
中的完整代码片段:
gameRoomRef.addSnapshotListener { documentSnapshot, fireStoreException ->
fireStoreException?.let {
return@addSnapshotListener
}
documentSnapshot?.let {
if (it.metadata.isFromCache)
// data is from cached memory
else
// data is from server
}
在 Flutter 上,以下是强制从 realtimeDb 获取最新快照的方法:
Future<DataSnapshot> _latestRtdbSnapshot(DatabaseReference dbRef) async {
// ! **NASTY HACK**: Write/delete a child of the query to force an update.
await dbRef.keepSynced(true);
await dbRef.child('refreshMock').remove();
final res = await dbRef.once();
dbRef.keepSynced(false);
return res;
}