如何绕过 Firebase 缓存来刷新数据(在 Android 应用中)?

Posted

技术标签:

【中文标题】如何绕过 Firebase 缓存来刷新数据(在 Android 应用中)?【英文标题】:How to bypass the Firebase cache to refresh data (in Android app)? 【发布时间】:2016-05-29 00:52:13 【问题描述】:

在大部分时间必须离线工作的 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;

如果我将addValueEventListeneraddListenerForSingleValueEventref.keepSynced 混合使用,我会获得相同的行为:

假设我的用户在缓存中的得分值为 5,而来自 Firebase DB 的得分值为 11。

当我调用getUser时,我会得到5分(Firebase先询问缓存)所以我不会调用doSomething()方法。

如果我取消注释示例中的 Thread.sleep() 代码,Firebase 缓存将有足够的时间进行更新,我的 getUser 将返回正确的分数值 (11)。

那么如何直接从服务器端直接询问最新值,绕过缓存呢?

【问题讨论】:

还有:groups.google.com/forum/#!topic/firebase-talk/jFnO-QXKSwA 见***.com/questions/34486417/…、***.com/questions/33260450/…和groups.google.com/forum/#!msg/firebase-talk/ptTtEyBDKls/… 不幸的是,您发布的主题没有给出方便的答案。即使我使用经典的“addValueEventListener”,我也会遇到与“onDataChange(DataSnapshot data) . . . ”事件相同的问题:无法知道数据是来自缓存还是来自网络,因此它是否是最新的;所以不可能信任同步处理中的数据值。 “在同步处理中不可能信任数据值” 是的。由于 Firebase 没有“同步状态”,因此最好的选择是不要尝试。尝试将用例改写为“当分数大于 10 时,执行 abc”。这可能并不理想,但它是目前最好的方法。 @FrankvanPuffelen 为什么 firebase 没有“同步状态”?这是关于 Firebase 本身的错误吗? firebase 何时检查增量并将服务器数据更改为客户端?难道没有用户强制的机制吗? 【参考方案1】:

这个问题也给我的申请带来了很大的压力。

我尝试了所有方法,从将 .addListenerForSingleValueEvent() 更改为 .addValueEventListener() 到尝试创造性地使用 .keepSynced() 到使用延迟(您在上面描述的 Thread.sleep() 方法),但没有任何东西真正有效(即使是 Thread.sleep()方法,这在生产应用程序中是不可接受的,并没有给我一致的结果)。

所以我所做的是:在创建一个 Query 对象并在其上调用.keepSynced() 之后,我继续在我正在查询的节点中编写一个模拟/令牌对象,然后在该操作的完成监听器,我在删除模拟对象后进行我想做的数据检索。

类似:

 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 的架构如此奇怪。没有更多的方法了吗? :// 不错的解决方法。在此期间,你们提出了其他解决方案吗? @HoangHuu 我想用新的数据库(firestore),你可以轻松查询新鲜的一次性数据。 refreshMock 不必是我们节点的子节点。它可以是任何查询,例如firebase.child("any_node").child("refreshMock") 也可以【参考方案2】:

我发现的一种解决方法是使用 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
            
        
    );

【讨论】:

有趣,我需要对其进行测试(在这种情况下还要测试离线行为....) 这可行,但如果您处于离线状态,则在您重新在线之前它不会完成。最终将其包装在超时中并回退到 addListenerForSingleValueEvent,并在它重新联机时丢弃该值。 你认为我也可以使用这种“runtransaction”方法来查询大列表(而不是使用 ChildEventListener)吗? 最后我验证了之前的响应(通过编写 MockObject 来同步),因为使用事务强制在您查询的任何节点上授予写访问权限,这并不适合所有模型的规则;而模拟对象可以写入专用的可写节点)【参考方案3】:

我的解决方案是在加载时调用 Database.database().isPersistenceEnabled = true,然后在我需要刷新的任何节点上调用 .keepSynced(true)。

这里的问题是,如果您在 .keepSynced(true) 之后立即查询您的节点,那么您可能会获得缓存而不是新数据。有点蹩脚但功能性的解决方法:延迟你对节点的查询一秒钟左右,以便给 Firebase 一些时间来获取新数据。如果用户离线,您将获得缓存。

哦,如果它是一个您不想在后台永远保持最新状态的节点,请记住在完成后调用 .keepSynced(false)。

【讨论】:

某些节点可以一直同步(当前用户的),但问题是您的解决方案中的“何时停止同步”。标记为正确的答案很好 - 您可以执行一项简单的操作(我在不存在的节点上使用 removeValue)将同步您,在 onComplete 中您可以关闭同步,因为它已经同步。使同步更安全 感谢您的反馈 :) 如果确实在无意义的节点上使用 removeValue 会强制同步(我没有尝试过),那么这似乎是一个很好的解决方案。写入然后删除一个值,就像在接受的答案中一样,对我来说似乎有点可怕。说实话,这一切都有点老套,我很惊讶地发现 Firebase 同步的行为方式就是这样。 在我的解决方案中删除不存在的节点有效。是的,它仍然是一个 hack,我需要和我的同事一起思考该怎么做,也许我们会关闭离线数据库,可悲的是它变成了真正的问题 我添加了一个不错的解决方案我猜的答案(没有测试太多,但可能很有趣) 酷!看起来很有趣。与您最初使用 removeValue 的建议相比,您认为您的新方法有什么优势吗?【参考方案4】:

只需在您的应用程序类上的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

【讨论】:

这仅适用于同步“分数”节点。在编写 mockObject 时,允许在任何节点上查询“新鲜”数据 是的,但是如果您想保持任何节点同步,只需删除 Firebase.getDefaultConfig().setPersistenceEnabled(true);就我而言,我只想保持一个节点同步。 这不是一个好的解决方案,因为你会监听节点上的每一个数据变化。例如,如果您有子节点 - 每个节点都用于其他用户,那么您将获得大量数据。一个用户数据的一次更改将触发每个其他侦听该节点的用户的数据同步。最好只为真正需要的数据打开和关闭 keepSynced。【参考方案5】:

我尝试了两种公认的解决方案并尝试了交易。事务解决方案更干净,更好,但是只有在db在线时才调用成功OnComplete,因此您无法从缓存中加载。但是您可以中止事务,然后在离线时(带有缓存数据)也会调用 onComplete。

我之前创建的函数只有在数据库连接 lomng 足以进行同步时才起作用。我通过添加超时来解决问题。我将对此进行研究并测试它是否有效。也许以后有空的时候,我会创建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");
  
);

【讨论】:

【参考方案6】:

使用这条线

FirebaseDatabase.getInstance().purgeOutstandingWrites();

【讨论】:

对于那些想知道的人:非常适合取消事务操作,在 onComplete 回调中返回“DatabaseError:用户取消了写入”。谢谢,你是现实生活中的“萨尔瓦多”! :D【参考方案7】:

在 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;

【讨论】:

【参考方案8】:

我知道它已经晚了,但对于面临同样问题的人来说,我终于找到了完美的解决方案。您可以使用此方法检查数据是否来自缓存内存:

if (documentSnapShot.metadata.isFromCache)
   // data is coming from the cached memory
 else 
   // data is coming from the server

这是Kotlin中的完整代码sn-p:

gameRoomRef.addSnapshotListener  documentSnapshot, fireStoreException ->

            fireStoreException?.let 

                return@addSnapshotListener
            

            documentSnapshot?.let 

                if (it.metadata.isFromCache) 
                   // data is from cached memory
                else 
                   // data is from server
               
            

【讨论】:

以上是关于如何绕过 Firebase 缓存来刷新数据(在 Android 应用中)?的主要内容,如果未能解决你的问题,请参考以下文章

删除firebase中的数据后如何刷新TableViewCell?

全局缓存服务器数据并刷新视图

如何使用 Firebase 刷新令牌保持用户身份验证?

应用离线时如何绕过 Firebase 身份验证?

如何在 Flutter 上刷新 firebase 令牌?

如何在 Flutter 中缓存 Firebase 数据?