How to queryOrderedbyChild and queryEqualtoValue with nested data in Firebase? - ios

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

Related

Retrieve list of a particular value for a key from Firebase

I have below firebase structure:
2014 {
0 {
CityState: "New York, NY",
PinCode: "12345"
},
1 {
CityState: "Los Angeles, CA",
PinCode: "67890"
}
}
I have to get list of all the CityState field from the firebase database.
But I always get full data and then have to filter the data like below:
ref?.child("2014").observeSingleEvent(of: .value, with: { snapshot in
if let item = snapshot.value as? [Dictionary<String, Any>] {
for items in item {
print(items["CityState"])
}
} else {
print (snapshot.value)
}
})
It takes too long time because of huge data to be retrieved. Is there a firebase query where I can get list of values of "CityState" key?
I am using swift 3.2 and Xcode 9.2
Firebase always returns full nodes from the database. There is no API to get a subset of each node.
When using the Firebase Database (and most other NoSQL databases) it is recommended to model the data for the use-cases of your app. So if you need to show a list of city states in your app, you should probably store a list of city states in your database:
CityStates: {
0: "New York, NY",
1: "Los Angeles, CA"
}
If you want to show the city states for only a specific year, it might be worth also storing them by year (as your original JSON does):
CityStates: {
2014 {
0: "New York, NY",
1: "Los Angeles, CA"
}
}
With these structures, reading the data from your app becomes trivial and you're reading no more data than what you need to display in your app.
Note: The above structure stores the city states in an array-like structure. If the order is not important, consider storing them in a set-like structure:
CityStates: {
2014 {
"Los Angeles, CA": true,
"New York, NY": true
}
}
The advantage of this structure is that there can't be any duplicates, since keys are by definition guaranteed to be unique within a parent node.

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.

containedIn query for Firebase

Coming from Parse, I have heavily relied on the containedIn query to collect the right data. In Parse, I might have had an array of objectIds and queried for all objects with those ids. I am looking to achieve the same on Firebase.
I understand that it is important to flatten data, but I don't see how this helps with the problem. Let's say I have a chat room with a list of users inside it. I collect that data and now have an array of usernames. I would like to now navigate to the users in the database and retrieve all that match one element inside this username array. How can I accomplish something like this?
For example, a set of users in a official Firebase example:
{
"users": {
"alovelace": { ... },
"ghopper": { ... },
"eclarke": { ... }
}
}
I would like to perform a query to download the following users:
["alovelace", "eclarke"]
While a general answer would be helpful, an answer in Swift would be best. Thank you.
An example is that they are the two members of a chat room. Or that
the current user is following them.
So a theoretical users node
users
alovelace
followed_by
bill: true
frank: true
in_chat_room: room_42
location: France
ghopper
followed_by
jay: true
in_chat_room: room_27
location: USA
eclarke
followed_by
frank: true
in_chat_room: room_42
location: Canada
and some chat rooms
chat_rooms
room_27
ghopper: true
room_42
lovelace: true
eclarke: true
To get the detailed users nodes of users in chat room 42 (lovelace, eclarke)
let usersRef = self.myRootRef.childByAppendingPath("users")
usersRef.queryOrderedByChild("in_chat_room").queryEqualToValue("room_42")
.observeEventType(.Value, withBlock: { snapshot in
for child in snapshot.children {
let location = child.value["location"] as! String
print(location) //prints France and Canada
}
})
To get the users Frank is following (lovelace, eclarke):
let usersRef = self.myRootRef.childByAppendingPath("users")
usersRef.queryOrderedByChild("followed_by/frank").queryEqualToValue(true)
.observeEventType(.Value, withBlock: { snapshot in
for child in snapshot.children {
let userName = child.key as String
print(userName)
}
})
Note that using user names as node key's is generally a bad idea - they should be stored by their uid.
Also note we didn't really do anything with the chat_rooms node but to maintain the relationship, having nodes refer to each other can be useful for observing changes etc.
Edit:
In response to a comment, here's the structure for each user to show who they are following instead of who is following the user
users
alovelace
following_user
bill: true
frank: true
in_chat_room: room_42
location: France
with this structure, alovelace is following bill and frank.
To get all of the users following frank:
let usersRef = self.myRootRef.childByAppendingPath("users")
usersRef.queryOrderedByChild("following_user/frank").queryEqualToValue(true)
.observeEventType(.Value, withBlock: { snapshot in
for child in snapshot.children {
let userName = child.key as String
print(userName)
}
})

Best practice to retrieve list of friends information. How to detect multiple queries are finished?

I have users structure lke this:
{
"users": {
"uniqueID1": {
"name": "Anon",
"friends": {
"uniqueID2": true,
"uniqueID3": true
}
}
"uniqueID2": { },
"uniqueID3": { },
}
}
I want to show a user's friends' names. I have to access $user/friends/ to get list of unique IDs, and iterate the list to get friend's information. But iterating the unique ID is making multiple queries, and I have to always check if all of my queries are finished. According to the doc, it seems multiple queries will not impact the performance too much, but if I want to update my view only when all of the queries are finished, I have to check how many queries are finished.
Is there no way of 'execute a completion block when all queries are finished'?
Pseudocode
var totalNumOfFriends = 0
var tempArray = NewArray()
ref(/users/uniqueID1/friends).observeEventType{ snapshot
var uIDList = snapshot.children's keys
totalNumOfFriends = uIDList .count
for uID in uIDList {
var nameRef = ref(/users/uID/name) i.e. /users/uniqueID3/name
nameRef.observeSingleEventOfType { snapshot
var username = snapshot.value
tempArray.append(username)
if tempArray.count == totalNumOfFriends {
// If counts are the same, tempArray has all of my friends' names
// Now update view using tempArray
}
}
}
}
Pseudocode explanation:
Get list of unique IDs from /users/uniqueID1/friends
'Save' number of unique IDs. (Explained in step 4)
For each unique IDs from the list, get user's name by using ref like this /users/uniquedID2/name
For each name retrieved, add it to temporary array. Once the count of the temporary array equals to the count from step 2, update my view as I have retrieved all the names.
Firebase has no built-in way to signal when a number of queries has finished. But you can easily implement this in your own code. Your approach with a counter that checks how many items have already been loaded is the most common approach for that.

Resources