Query Firebase nested data to check specific value - iOS Swift - ios

I have an array of User ID Strings and I want to query data in Firebase Realtime Database to see if each User ID has a specific gender.
var nearbyUserArray = [String]()
Below is an example of the data I have in Firebase. The second level deep is the User ID (0UFhIgGAwIY1btnBeNaNkJruYKJ3). What I am trying to do is loop through my User ID array and for each User ID check if that specific user is Male. If the user is Female, remove that User ID from the array.
"profiles" : {
"0UFhIgGAwIY1btnBeNaNkJruYKJ3" : {
"dateOfBirth" : "May 11, 1987",
"gender" : "Female",
"higherAgeRange" : 36,
"interestedIn" : "Male",
"lowerAgeRange" : 29,
"name" : "Sally"
},
"6vNjngZqoTbd4oGudip3NX78pu32" : {
"dateOfBirth" : "December 23, 1990",
"gender" : "Male",
"higherAgeRange" : 39,
"interestedIn" : "Female",
"lowerAgeRange" : 25,
"name" : "Bob"
}
How would you recommend I accomplish this? I want to minimize the amount of data I have to download and manipulate on the client side. Below is what I am attempting to do but I cannot get it to work. Any ideas why this always returns Null?
func getUsersWithinGenderCriteria() {
for user in nearbyUserArray {
DatabaseService.shared.profilesRef.child(user).queryOrdered(byChild: "gender").queryEqual(toValue: "Female").observeSingleEvent(of: .value) { [weak self] (snapshot) in
print("snap: \(snapshot)")
// The idea is if snapshot is Female, remove the user ID from the array. If the snapshot is Null, keep it in the array. However, this is not working as all snapshots come back as null.
}
}
}
Also, here are the security rules I set up.
"profiles": {
"$userId": {
".indexOn": ["gender"]
}
}

When you perform a query, Firebase compares a property of each child node under the location you query to the value you pass in.
I assume that profilesRef points to /profiles in your database. In that case profilesRef.child(user) in your code points to the profile for a specific user already. When you then run a query, you are querying each of the properties of that specific user.
What you like want to do is to get all users with gender equal to Female. In that case you should query the user list:
DatabaseService.shared.profilesRef.queryOrdered(byChild: "gender").queryEqual(toValue: "Female")...
Note that you also should define the index on the level where the query is run, so on /users:
"profiles": {
".indexOn": ["gender"]
}
Another alternative (the might scale better) is to not use a query altogether, but instead load the gender property of each individual user in your array. In (approximate) code:
for user in nearbyUserArray {
DatabaseService.shared.profilesRef.child(user).child( "gender").observeSingleEvent(of: .value) { [weak self] (snapshot) in
if snapshot.value == "Female" {
print("snap: \(snapshot)")
}
}
}

Related

How to queryOrderedbyChild and queryEqualtoValue with nested data in Firebase?

Histories
Places
ChIJ-REgsXyPwTARJh1vn16yZBc (placeID)
name: "Tokyo Tower"
userInfo
M1nlb7iEg9ZU6cr28DOwJOqrlKA2 (userID)
Lxqcl6Zys2cjmYSzcWU (autoID)
dateAdded: "2020-01-05"
interactionType: "placeTap"
let historiesPlacesRef = Database.database().reference().child("Histories").child("Places")
When I fetch the place with the name "Tokyo Tower" using the following, it works.
historiesPlacesRef
.queryOrdered(byChild: "name")
.queryEqual(toValue: "Tokyo Tower")
.observe(.value) { (dataSnapshot) in
print(dataSnapshot)
}
What I want to query is places that the current user visited.
I tried the following. But it does not work.
historiesPlacesRef
.queryOrdered(byChild: "userInfo")
.queryEqual(toValue: Auth.auth().currentUser!.uid)
.observe(.value) { (dataSnapshot) in
print(dataSnapshot)
}
Firebase queries can only check for values that are at a known location under each child node of the location that you query. So in your current structure you can find all places with a specific name, or al the times a specific user went to Tokyo Tower in 2020. But you can't find all users across all cities, nor search all visits for a city (thanks to the Auto ID).
In other words: your current data structure makes it easy to find all users who went to a city you queries for, but it doesn't allow you to easily query for all cities a user went to. To allow this use-case, you'll need to add an additional data structure.
UserPlaces: {
UID1: {
placeID1: {
dateAdded: "2020-01-05",
interactionType: "placeTap"
}
placeID2: {
dateAdded: "2020-01-04",
interactionType: "somethingElse"
}
}
UID2: {
placeID1: {
dateAdded: "2020-01-05",
interactionType: "somethingElseAgain"
}
}
}
With this additional structure (often called an inverted index), you can then look up (or query) data for a specific user. You may need to change this data structure for your specific use-cases, or even add multiple such structures to meet all use-cases.
Also see my answers here:
Firebase query if child of child contains a value
Firebase Query Double Nested

Compare values with an array of values with Firebase. Swift

I have a value which needs to be compared with an array of values. Basically a user needs to check if it has the same item as another user. But I am struggling to solve this as how do I get the item of an array of users? Normally when you observe a value you do something like this:
Database.database().reference(withPath: "users/\(userID)/itemList").observeSingleEvent...
However, when it comes to an array of users itemList how can this be achieved if there are multiple ID's? I'd like compare a user item or items with other users item and check if they match in order to sort an array of users that have a match.
If there is already an example for this please help direct me there.
Update
This is how my data structure looks:
{
"users": {
"EWJGFYmVTzOiHgnq42ebhrg2fj": {
"firstName": "Friederike",
"itemList": [
2: true,
3: true,
0: true
]
},
"C076OYmVTzOiHgnq4wPQtY2XpED2": {
"firstName": "Ian",
"itemList": [
0: true,
1: true,
3: true
]
},
"Juoiuf0N6qNmkm32jrtu6X6UK62": {
"itemList": [
0: true
],
"firstName": "Jack"
}
}
}
Update 2.0
With the answer below I am able to query through the items table but still unable to query the keys that match as there can be multiple arrays and therefore I cannot use it to filter or anything.
Ok so with your current data structure you'd have to query all nodes under users and then compare the arrays, which is very inefficient. There isn't a straight forward or easy way to do that without modifying your structure. So I suggest you modify your data structure so that each item has a list of all users that have it. Something like this:
{
"items": {
"0": {
"EWJGFYmVTzOiHgnq42ebhrg2fj": "Friederike",
"C076OYmVTzOiHgnq4wPQtY2XpED2": "Ian",
"Juoiuf0N6qNmkm32jrtu6X6UK62": "Jack"
},
"1": {
"C076OYmVTzOiHgnq4wPQtY2XpED2": "Ian"
},
"2": {
"EWJGFYmVTzOiHgnq42ebhrg2fj": "Friederike"
}
//....
}
}
Depending on what you want to display you might want to store more information than just the users UID and username. You can query all the users that have the same items as you using a query like this:
let ref = Database.database().reference()
// assuming you already have the current users items stored in an array
for item in items {
ref.child("items").child(String(item)).observeSingleEvent(of: .value, with: { snap in
for child in snap.children {
let child = child as? DataSnapshot
if let key = child?.key, let name = child?.value as? String {
// do something with this data
}
}
})
}
Firebase database is noSQL, so data is meant to be denormalized or duplicated so that queries can be optimized. Firebase actually recommends that you avoid nesting data. Take a look at this question for more information.
Hope that helps
Code related to question asked in comments
Assuming you are storing the UID's or names of users with the same items in a string array you can prevent duplicates using .contains()
var namesWithMatchingItems = [String]()
if !namesWithMatchingItems.contains(nameYouJustFetched) {
namesWithMatchingItems.append(nameYouJustFetched)
}

Firebase load related data iOS

What's the best way to load "related" data in swift?
Common setup, if I have a list of users all stored under uid node and contains a list of follows which stores uids, something like:
"users" : {
"abc123" : {
"email" : "test#test.com",
"follows" : {
"xyz789" : true
}
},
"xyz789" : { ... }
}
What's the most efficient way of loading in the data for all the users one user follows? Is it best to loop through each of the uid's with observeSingleEvent(of: .value)?
This is the solution I've come up with, but feels somewhat cumbersome:
func loadRelated(user: User, completion: #escaping (Bool, [UserObject]) -> ()) {
let ref = Database.database().reference(withPath: "users/" + user.uid + "/follows")
ref.observeSingleEvent(of: .value) { snapshot in
var uids = [String]()
for child in snapshot.children {
let userData = child as! DataSnapshot
uids.append(userData.key)
}
let userRef = Database.database().reference(withPath: "users")
var users = [UserObject]()
var count = 0
uids.forEach { uid in
userRef.child(uid).observeSingleEvent(of: .value) { snapshot in
let user: UserObject(from: snapshot)
users.append(user)
count += 1
if count == uids.count {
completion(true, users)
}
}
}
}
}
I don't really want to go down the denormalization path and store each users data under the top level user.
If you are decided on using Realtime Database, it is best practice to create another root node in your case called user-follows. You can create a follow at the path user-follows/$uid/$fid by setting the value to true, then on your app you would have to observeSingleEvent for each snapshot key ($fid) at user-follows/$uid.
To avoid having to observe each follow separately, instead of setting the value to true, you can just store the data you need about a user in user-follows/$uid. However, a user may change their username for example and so you would need to keep the data inside each user-follows up to date. You can utilise Firebase Cloud Functions to maintain the user-follows when a user changes their information.
Otherwise, I would suggest looking at Firebase Firestore, where some nesting is allowed.
If you know that your node at /users will always contain few users, you could try to get all the users at once with a observeSingleEvent(of:) at path /users. Then filter the users with the ones who are in ../follows.
This may pull more data but it might be faster (not sure) and will need less code to handle.
In fact your initial implementation is quite performant already. Just make sure to handle correctly failing of observeSingleEvent(of:) or the condition count == uids.count will never be fulfilled.
By the way storing each user under ../follows will just duplicate your data and will be hard to maintain updated. So yes avoid it.

Getting a specific child value from nodes only in which user's UID is present

I am working on a group chat application, and I'm running into trouble trying to make a specific query to Firebase DB.
I need to get all of the profile pictures of users in a room. But only the rooms that the current user is a part of.
private func observeRooms() {
let databaseRef = FIRDatabase.database().reference()
let groupRef = databaseRef.child("groups")
guard let uid = FIRAuth.auth()?.currentUser?.uid else {return}
let queryRef = groupRef.queryOrdered(byChild: "participants/\(uid)").queryEqual(toValue: nil)
queryRef.observe(.childAdded, with: { snapshot in
let roomDict = snapshot.value as! [String: AnyObject]
print("roomDict: \(roomDict)")
let id = snapshot.key
let avatar = roomDict["profilePicture"] as! String
})
}
Obviously setting queryEqual(toValue: nil) returns all the rooms that the current user is NOT a part of - the opposite of what I need. I know the inverse will be to check queryEqual(toValue: true), but the : true is not showing up in the database.
This is how I add a node to "groups" in Firebase:
// Start group node for new room
let groupRef: FIRDatabaseReference = FIRDatabase.database().reference().child("groups")
let roomID = newRoomRef.key
let groupRoomRef = groupRef.child(roomID)
let groupDict = ["roomName" : roomName, "participants": [uid : true]] as [String : Any]
groupRoomRef.setValue(groupDict)
And this is how the data structure ends up:
"groups" : {
"-KkGzZ7frbnXmImt0JpV" : {
"participants" : {
"tzfHgGKWLEPzPU9GvkO4XE1QKy53" : {
"gender" : "male",
"handle" : "Testing",
"name" : "Test User",
"profilePicture" : "https://graph.facebook.com/*removed*/picture?type=large&return_ssl_resources=1",
"status" : "F8B016"
}
},
"roomName" : "Test Room"
},
How can I properly the profilePicture values for all users in a room, only for the groups that the current user's UID is a participant of?
You're on the right track. When using queryEqual your query needs to match the value actually present in the database. Unfortunately that gets complicated when the values you're querying against are complex objects. In fact I'm not sure the function will work even if you did match the entire object value. The issue here is that you're trying to look up objects by the presence of a child key, instead of actually sorting by or matching their values. That doesn't quite fit the use case of any of the orderBy methods.
There are a few ways to work around this, ordered by effort.
First, you could take advantage of the sort order to get only those who have some object with the name of the UID. Since Firebase sorts nulls first, then primitives, and finally objects, you should be able to order by child and start at true (or any primitive) to get all object instances with that key (assuming, of course, you're not storing UID: true anywhere).
groupRef.queryOrdered(byChild: "participants/\(uid)").queryStartingAtValue(true)
Second, duplicate the UID as a key inside the object with the value true so that you can sort on it directly. Notice the extra step in the query.
"groups" : {
"-KkGzZ7frbnXmImt0JpV" : {
"participants" : {
"tzfHgGKWLEPzPU9GvkO4XE1QKy53" : {
"tzfHgGKWLEPzPU9GvkO4XE1QKy53" : true,
...
groupRef.queryOrdered(byChild: "participants/\(uid)/\(uid)").queryEqual(toValue: true)
Finally, and this is what I recommend, reconsider your data structure. You're nesting a lot of data inside this collection of collections that is going to make your queries and updates unwieldy. Firebase tends to work better when you denormalize your data, preferring flat/shallow structures with some duplication of simple data. In your case, instead of storing the user's profile data inside of their group membership, you might have a collection of groups and another collection of users, with each group containing only a list of UIDs to its members and vice versa. The example in the documentation is a very similar case to yours involving membership in chat conversations. At that point your UIDs (and group IDs, if you're querying on users instead) will have a value of true so you can again use a simple value query.
"groups" : {
"-KkGzZ7frbnXmImt0JpV" : {
"participants" : {
"tzfHgGKWLEPzPU9GvkO4XE1QKy53" : true
...
"users" : {
"tzfHgGKWLEPzPU9GvkO4XE1QKy53" : {
"groups" : {
"-KkGzZ7frbnXmImt0JpV" : true
...
groupRef.queryOrdered(byChild: "participants/\(uid)").queryEqual(toValue: true)

How to query nested elements in Firebase?

"John" : {
"David" : {
"-KIMA0aPsujdAOpkzP0w" : {
"message" : "hallu",
"sender" : "10154053432889835",
"time" : 1463898873196
}
}
},
"Harry" : {
"Christina" : {
"-KIMA0aPsujdAOpkzP0v" : {
"message" : "hallu",
"seen" : true,
"sender" : "self",
"time" : 1463898873195
}
},
"Pierce" : {
"-KILZ_GH7Ji9hQYNK-6p" : {
"message" : "Eli there.",
"seen" : true,
"sender" : "179914035712208",
"time" : 1463888795301
},
"-KIM8yPz2UDOZwHEg_nn" : {
"message" : "hahjajak",
"seen" : true,
"sender" : "self",
"time" : 1463898597847
},
}
I wanted to query the count of nodes where "seen" key has value "true" of top node which is John OR Harry There are multiple child node inside it and each child node has multiple child node which has automatic id set.
I just want to know the count of objects which has "seen" key set to true and also count of objects which has "seen" key set to false
I can extract all the values in node as follows:
ref.observeSingleEventOfType(.Value, withBlock: { snap in
for (key,value) in snap.value as! NSDictionary {
}
})
I can loop through the dictionary and count the number of objects manually. But that is not too computationally or data efficient as firebase is volume based.
What I want is to know if there is any query to count the number of objects whose "seen" key in root node is "true" or whose "seen" key in root node is "false".
UPDATE:
Data Structure
John and Harry are user unique ID. and the node David which is immediate child of John node is unique ID of person who the user has chatted with earlier.
And the KIMA0aPsujdAOpkzP0w node represent a unique message from user John to/from David.
So what i wanted was to count the number of messages from David to John not seen by John
If you want to get only the "seen" value, you should reconsider your database structure and denormalize where possible to make reading as fast as you can.
Keeping your structure to get "seen" value, each time you read you have to get the entire node and loop through it to get the value, downloading unnecessary data, wasting processing time and making overall operation slower.
You should create a new node and store just the "seen" value the way you are going to get it later. Lets say, the best way to structure your database is based on which read operations are going to do and what data do you need in those operations.
You shouldn't worry about duplicating data. Remember that Firebase has the "updateChildren" method that allows you to update data in different nodes in a single operation.
For more info you can read the docs about structuring data here: https://firebase.google.com/docs/database/android/structure-data
Cheers!
Not sure if this is the most efficient way to handle it, but for a rapid prototype I've been searching for the answer to this problem as well. I settled on using this strategy to isolate child nodes of the data structure:
ref.observeSingleEventOfType(FIRDataEventType.Value, withBlock: { snapshot in
for (key,value) in snapshot.value as! NSDictionary {
if (key as! String == "John/David/etc")
{
let newRef = FIRDatabase.database().reference().child("John").child(key as! String)
newRef.observeSingleEventOfType(FIRDataEventType.Value, withBlock: { snapshot in
for (childKey, childValue) in snapshot.value as! NSDictionary{
//do whatever you want with child values/etc
}
})
}
}
})
Keep in mind I am VERY new to Firebase on iOS and they just changed their documentation so I'm working through it as I can. I'm sure there are way better ways to accomplish accessing child data but this worked for my MVP so I went with it.
EDIT: This assumes that your original reference is aimed at the part of the data tree that you want to target. For example:
let ref = FIRDatabase.database().reference().child("John")
That way when you did ref.observeSingle..etc, it would look for the values underneath that child object, instead of the entire tree.

Resources