Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

'Joining' Firebase Queries in Angularfire2

Update:

The issues I was encountering with the empty value fields had to do with non-existent keys in my database, so most of the discourse here won't apply to your question. If you're looking for a way to 'join' queries in AngularFire2, the accepted answer below does a fine job of this. I'm currently using combineLatest instead of forkJoin. In order to do this you have to import 'rxjs/add/observable/combineLatest';.

I have the following denormalized Firebase structure:

members
  -memberid1
    -threads
       -threadid1: true,
       -threadid3: true
    -username: "Adam"
    ...

threads
  -threadid1
      -author: memberid3
      -quality: 67
      -participants
         -memberid2: true,
         -memberid3: true
     ...

I want to render username in my threads view, which is sorted by quality.

My service:

getUsername(memberKey: string) {
    return this.af.database.object('/members/' + memberKey + '/username')
}

getFeaturedThreads(): FirebaseListObservable<any[]> {
    return this.af.database.list('/threads', {
        query: {
            orderByChild: 'quality',
            endAt: 10
        }
    });
}

My component:

ngOnInit() {
    this.threads = this.featuredThreadsService.getFeaturedThreads()
    this.threads.subscribe( 
        allThreads => 
        allThreads.forEach(thisThread => {
            thisThread.username = this.featuredThreadsService.getUsername(thisThread.author)
            console.log(thisThread.username)
        })
    )
} 

For some reason this logs what looks like unfulfilled observables to the console.

enter image description here

I'd like to get these values into a property of threads so I can render it in my view like this:

<div *ngFor="let thread of threads | async" class="thread-tile">
    ...
    {{threads.username}}
    ...
</div>

Updated: console.log for allThreads and thisThread

enter image description here

enter image description here

Updated: subscribed to getUsername()

this.featuredThreadsService.getUsername(thisThread.author)
        .subscribe( username => console.log(username))

The result of this is objects with no values:

enter image description here

like image 620
J. Adam Connor Avatar asked Jan 20 '17 17:01

J. Adam Connor


People also ask

What is AngularFirestore?

AngularFirestore allows you to work with Cloud Firestore, the new flagship database for mobile app development. It improves on the successes of Realtime Database with a new, more intuitive data model. Cloud Firestore also features richer, faster queries and scales better than Realtime Database. Documents. Collections.

Can you query from Firebase?

Querying DataWith Firebase database queries, you can selectively retrieve data based on various factors. To construct a query in your database, you start by specifying how you want your data to be ordered using one of the ordering functions: orderByChild() , orderByKey() , or orderByValue() .


2 Answers

You can compose an observable based on getFeaturedThreads that queries members and replaces the values in each thread's participants property with user names:

import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/forkJoin';
import 'rxjs/add/operator/do';
import 'rxjs/add/operator/first';
import 'rxjs/add/operator/switchMap';

let featuredThreadsWithUserNames = this.getFeaturedThreads()

  // Each time the getFeaturedThreads emits, switch to unsubscribe/ignore
  // any pending member queries:

  .switchMap(threads => {

    // Map the threads to the array of observables that are to be
    // joined. When the observables emit a value, update the thread.

    let memberObservables = [];
    threads.forEach(thread => {

      // Add the author:

      memberObservables.push(this.af.database
        .object(`members/${thread.author}`)
        .first()
        .do(value => { thread.author = value.username; })
      );

      // Add the participants:

      Object.keys(thread.participants).forEach(key => {
        memberObservables.push(this.af.database
          .object(`members/${key}`)
          .first()
          .do(value => { thread.participants[key] = value.username; })
        );
      });
    });

    // Join the member observables and use the result selector to
    // return the threads - which will have been updated.

    return Observable.forkJoin(...memberObservables, () => threads);
  });

This will give you an observable that emits each time getFeaturedThreads emits. However, if the user names change, it won't re-emit. If that's important, replace forkJoin with combineLatest and remove the first operator from the composed member observables.

like image 82
cartant Avatar answered Oct 04 '22 01:10

cartant


To solve joins on users, I wrote a service that caches the users already fetched and maps them into the referencing data with minimal code. It uses a nested map structure to do the join:

constructor(public db: AngularFireDatabase, public users:UserProvider) {
    this.threads = db.list('threads').valueChanges().map(messages => {
      return threads.map((t:Message) => {
        t.user = users.load(t.userid);
        return m;
      });
    });
}

And the UserProvider service looks like so:

@Injectable()
export class UserProvider {
  db: AngularFireDatabase;
  users: Map<String, Observable<User>>;

  constructor(db: AngularFireDatabase) {
    this.db = db;
    this.users = new Map();
  }

  load(userid:string) : Observable<User> {
    if( !this.users.has(userid) ) {
      this.users.set(userid, this.db.object(`members/${userid}`).valueChanges());
    }
    return this.users.get(userid);
  }
}

There's a complete working example of the joins and all the boilerplate here

like image 31
Kato Avatar answered Oct 03 '22 23:10

Kato