adriancarriger / angularfire2-offline

🔌 A simple wrapper for AngularFire2 to read and write to Firebase while offline, even after a complete refresh.
https://angularfire2-offline.firebaseapp.com/
MIT License
207 stars 48 forks source link

Querying lists doesn't work #9

Open giorgiopiatti opened 7 years ago

giorgiopiatti commented 7 years ago

Using the same pattern code of this example the data aren't queried as expected.

this.moData = this.db.list('/personal/routines/'+ this.authService.userId, {query: {orderByChild: 'day',equalTo: 1}}); This code gets all the data fine but the query doesn't work.

JSON of the db

{
    "routines" : {
      "TaMPvE9d0nSM6iLqBWoz3wTdJ0E3" : {
        "-KeQBZCteClbOZKwieZb" : {
          "day" : 0,
          "description" : "",
          "timestamp" : 1488662446111,
          "title" : "Test",
        }
      }
    }
  }

Ionic info Ionic Framework: 2.0.0 Ionic Native: 2.4.1 Ionic App Scripts: 1.1.3 Angular Core: 2.2.1 Angular Compiler CLI: 2.2.1 Node: 7.4.0 OS Platform: Windows 10 Navigator Platform: Win32 User Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36 `

adriancarriger commented 7 years ago

Thanks for the issue report! I put together a demo to show what I found:

Example

It looks like the query is filtering the results if you're only reading from Firebase (online and offline), but there are a few issues if writing to Firebase while offline:

Issues

I'll see what I can do to fix this. Let me know if you experience anything else. Thanks!

lamquangphuc commented 7 years ago

Yes, I have the same problem.

  this.afo.database.list(`/chats-messages/${this.chatId}` , {
        query: {
            orderByChild: 'createdAt',
            limitToLast: 1
        }
    }).subscribe((data : any) => {
        if (data.length > 0) {
            this.lastKey = data.$key;
        } else {
            this.lastKey = '';
        }
    });
    this.messages = this.afo.database.list(`/chats-messages/${this.chatId}`, {
      query: {
          orderByChild: 'createdAt',
          limitToLast: 10
      }
    });

My logic want to select lastest data, then select 10 lastest record. But data always 1 record is lastest.

However, I used angularfire2, that's OK. No problem.

davemecha commented 7 years ago

I have the same problem with AFO 2.0.3.

I have a dashboard, where I fetch all available tasks for all projects:

this.afo.database.list('/tasks').subscribe(...

On a project detail page, I have the tasks listed by project:

this.afo.database.list('/tasks', {
    query: {
        orderByChild: 'project',
        equalTo: key
    }
}).subscribe(...

I get the whole task list of all projects for each project, when I first visit the dashboard. When I first visit a project page I always get the result of this query for all later queries on tasks.


I digged a bit into the code of AFO. I AngularFireOfflineDatabase.prototype.setupList the query is applied and the result is cached. Maybe it's useful to ignore the query for building up the cache and somehow mirror the query capabilities in the "list" method.

adriancarriger commented 7 years ago

This should be fixed as of AngularFire2 Offline v4.1.1 🎉🎉

Please post if you're still experiencing issues. Thanks!

rodrifmed commented 7 years ago

Hi @adriancarriger I update angularfire2 offline to v4.1.1, but the problem still working.

I'm using equalTo in my query.

adriancarriger commented 7 years ago

Hi @rodrifmed, I just tried the equalTo query and I can't reproduce your issue. Can you post your relevant code and data structure? Thanks! 👍

rodrifmed commented 7 years ago

Hi @adriancarriger, The problema occurs when you execute the query with a equalTo, and them execute the same query with another value. The query always return the first value

Like this example:

Data:

"test":{
         "X": {
        "child_test":"A"
        }
    "Y":{
        "child_test":"A"
    }
    "Z":{
        "child_test":"B"
    }
}

1) For example this query is returning a list like that [X,Y]

this.afo.database.list('/test', {
    query: {
        orderByChild: 'child_test',
        equalTo: "A"
    }

2) When you make the second query like:

this.afo.database.list('/test', {
    query: {
        orderByChild: 'child_test',
        equalTo: "B"
    }

the hight return is [z], but is returning [X,Y]

adriancarriger commented 7 years ago

I just made an update that supports multiple queries to the same Firebase reference in AngularFire2 Offline version 4.1.2

Here is a live demo that has an equalTo query and an unfiltered example on the same page.

Here is the code for the example. Let me know if this is working for you. Thanks!

rodrifmed commented 7 years ago

@adriancarriger Still doenst work. Now the second query doenst return any value.

adriancarriger commented 7 years ago

@rodrifmed thanks for hanging in there! 👍 I hope this will help solve your issue:

Demos

Steps to solve

Next steps

If you can view and run the demo without an issue, then maybe there are some specific implementation details in your code that are affecting queries. If this is the case, please share as much of your code as possible or feel free to create a minimal example of the issue. Thanks!

terrycollinson commented 7 years ago

I had the same problem and can confirm with the update applied it works great. Thanks so much

rodrifmed commented 7 years ago

Hi @adriancarriger

This example is more real:

I have to make two querys, but one inside another, is because I have to wait the two querys finish.

this.query(this.urlPath, "READ")
    .subscribe((readResultList) => {

        console.log(readResultList)

        this.query(this.urlPath, "NOT_READ")
            .subscribe((unReadList) => {

                console.log(unReadList)

            })
    })

The query is:

function query(urlPath:string, status:string) : Observable<any[]>{

      return this.dbOff.list(urlPath,
            {
                query: {
                    orderByChild: 'categoryId',
                    equalTo: status,
                    limitToFirst: 15
                }
            })
            .first()

}

adriancarriger commented 7 years ago

@rodrifmed thanks for finding this! It should now be fixed in version 4.1.4. Here is a demo (with code).

Can you confirm that this solves your issue?

rodrifmed commented 7 years ago

@adriancarriger You forgot to put first() on your example. I try without first and it's works partially, the first request brings the two lists, but the second when paging doesnt bring nothing.

And the console is showing twice the not read list.

adriancarriger commented 7 years ago

Updated demo

@rodrifmed I've updated the demo (and code) by adding the .first() method.

Console

Also, my console is only showing one log per query. You may need to refresh the page a few times to see any new updates I posted. When you view the demo do you get different results? Here's a screenshot of what I'm seeing:

screen shot 2017-05-24 at 7 19 59 pm

Paging

It sounds like your setup is more complex than the demo I posted. Can you share the part of your code that involves paging?

rodrifmed commented 7 years ago

Hi @adriancarriger If you clear your storage data you will see the second query showing twice.

I didnt say nothing about the pagination because I thought that if the second query had returned value, the problem would be solved.

I'm using ionic infinity scroll to trigger the method to call the query with new parameters:

But you could make a timeout to reproduce:

queryNext(urlPath: string, category: string, lastStoreKey: string) {
        console.log(urlPath)
        return this.dbOff.list(urlPath,
            {
                query: {
                    orderByChild: "categoryId",
                    startAt: { value: category, key: lastStoreKey },
                    limitToFirst: 15 + 1
                }
            })
            .first()
    }

setTimeout(() => {
     // call queryNext
    }, 500);
rodrifmed commented 7 years ago

I upgrade to new version, and the first problem with the second query returning empty list is happening again

adriancarriger commented 7 years ago

Hi @rodrifmed I can confirm that the nested query is returning empty if I first clear the device's storage, and I will be working on fixing the issue. Thanks!

adriancarriger commented 7 years ago

@rodrifmed thanks again for pointing out this interesting issue! First I'll explain what I found, and then propose a solution. Anyone interested in the issue is welcome to chime in.

Also, if you just want to know how to use Ionic infinite scroll, just go to the end of this post.

What is happening

Demos

If you clear app storage before each demo you'll notice that:

The only difference is that 9.4 uses .first() and 9.5 doesn't.

Step by step

  1. The first request for items matching READ is sent to Firebase
  2. subscribe returns the expected results
  3. The second request for items matching NOT_READ is sent to Firebase
  4. Afo checks the in-memory cache (not device storage)
    • because both queries belong to the same reference it finds the last dataset from Firebase which only contains results matching READ
    • Afo runs the query locally
    • subscribe returns the results available (empty array)
  5. Afo receives Firebase data for the second query (items matching NOT_READ)
    • Demo 9.4 ignores the new data (because of the .first() method)
    • Demo 9.5 subscribe returns the expected results

Is this a bug?

Some apps may prioritize speed over query breadth and vise versa.

Possible solution 1

Afo cannot know ahead of time what queries will be made to the same reference. I suggest an optional param called largestQuery telling Afo the largest query that will be made to Firebase. If given this param, then the request will be made to Firebase once and the remaining queries would run locally.

Proposed usage

The proposed solution would look something like this:

this.afoDatabase.list('firebase/ref', {
  query: {
    limitToFirst: 15
  },
  largestQuery: {
    limitToFirst: 500
  }
});

Possible solution 2

Another solution would be to add a boolean param called waitForFirebase. When true, Afo would wait for Firebase to return data if the breadth of the combined queries to a single reference increases.

Proposed usage

this.afoDatabase.list('firebase/ref', {
  query: {
    limitToFirst: 15
  },
  waitForFirebase : true
});

Both solutions can be implemented without conflict allowing developers to use what is best for their use case.

Ionic infinite scroll paging

Setup for infinite scroll/paging can already be done using Observables with a single query like this:

// Properties
items: AfoListObservable<any[]>;
limit = 20;
limitObservable = new ReplaySubject(20);

constructor(private afoDatabase: AngularFireOfflineDatabase) {
  // Set initial limit
  this.limitObservable.next(this.limit);

  // Subscribe to list
  this.items = afoDatabase.list('/issues/9/9-6', {
    query: {
      orderByChild: 'categoryId',
      limitToFirst: this.limitObservable
    }
  });
}

doInfinite(infiniteScroll) {
  // Add 10 per page load
  this.limit += 10;

  // Update subject
  this.limitObservable.next(this.limit);

  // Call complete
  setTimeout(() => infiniteScroll.complete(), 1500);
}
rodrifmed commented 7 years ago

Very good @adriancarriger! Could you explaine more the possible solution 2 ?

adriancarriger commented 7 years ago

@rodrifmed here's how solution 2 would work:

Solution 2 details

Data sharing

When multiple queries are made to a single reference they share the same "pool" of local data. During an offline write, all queries pointing to the same reference are updated without a network connection because they are all looking at the same pool of data.

Shared query scope

If there are multiple queries to a single reference and they all only need some of the total data belonging to that reference, then Afo tries to only ask Firebase for as much data as necessary to run all existing queries. For example if one query has limitToFirst: 5 and another has limitToFirst: 10, then Afo's query will be limitToFirst: 10.

Increasing shared scope

If a new query comes along (e.g. limitToFirst: 15), then Afo will need to expand the query it actually sends to Firebase. Currently, queries run immediately without waiting for Firebase to return the expanding query data. This is good, because it can potentially be hours before someone gets an internet connection.

Solution 2

Solution 2 would allow the developer to have more control over this behavior by indicating that all updates to a specific query should wait for Firebase to return updated data if the query scope has increased.

Next steps

I wanted to allow people to comment on their use cases and get feedback before implementing because this is a feature that strays from the primary list of AngularFire2 features. That said, this does seem important for managing data offline. Feel free to comment. Thanks!

ghost commented 7 years ago

Sorry for my stupid question: How can i count all the "milk" items? Thank you for your help.

rodrifmed commented 7 years ago

Hi @adriancarriger, any evolution about this topic?

ix-xerri commented 7 years ago

I've tried to query using orderByChild: "people/" + queryEmail, equalTo: "true"but it doesn't work. No results are returned while the correct results are returned using AngularFire2

adriancarriger commented 7 years ago

@rodrifmed do you have a preferred option (1 or 2 from above) for your use case?

@ix-xerri can you show code of a full component that reproduces the issue?

ix-xerri commented 7 years ago

I'm simply trying to get a list of subscribed chats

let queryEmail = user.email.replace(/\./g,',');

this.events = this.db.list("/events/", {
    query: {
        orderByChild: "people/" + queryEmail,
        equalTo: "true"
    }
});

this.events.subscribe((chats) => {
    if (chats) {
        this.storage.set("chats", chats);
    }

    if (chats.length === 0) {
        return this.showError = true;
    }
});

It does return chats if using AngularFire but none if I use you package.

rodrifmed commented 7 years ago

@adriancarriger I think solution 2 better. What do you think?

adriancarriger commented 7 years ago

I think they both can be useful for different cases. Hopefully I can find some time to add support for solution 2 soon! 👍

eolant commented 7 years ago

@adriancarriger I'm sorry but I'm pretty new to Ionic and angular but I'm trying to implement this solution to the app I'm developing: https://angularfirebase.com/lessons/infinite-scroll-with-firebase-data-and-angular-animation/ It works fine but with afoDatabase it doesn't work properly and after first query second returns only one element. I tried your solution of infinite scroll above but for some reason it makes this weird jumps to the bottom of the page from the scroll position it was before when it loads new elements. As well as it loads whole lot of elements, not just new ones and I don't know how to determine when there is no more elements to load. I believe the issue I'm encountering is somehow connected to the query problem.