My data structure is as follows:
users:
user1:
-carModel: evo x
-username: importguy
-region: north east
user2:
-carModel: evo x
-username: evoguy
-region: north east
user3:
-carModel: mustang gt
-username: muscleguy
-region: south east
I want the user to be able to search for a car, say evo, and display results of users who own those particular cars. I need to paginate these results for my app. The problem is I can't figure out how to properly query this. Here is what i have so far.
func fetchUsersBy(car: String) {
if self.carCurrentKey == nil {
let ref = USER_REF.queryOrdered(byChild: "carModel").queryStarting(atValue: car).queryLimited(toFirst: 3)
ref.observeSingleEvent(of: .value, with: { (snapshot) in
guard let snap = snapshot.children.allObjects as? [FIRDataSnapshot] else { return }
guard let last = snapshot.children.allObjects.last as? FIRDataSnapshot else { return }
snap.forEach({ (snapshot) in
guard let userDict = snapshot.value as? Dictionary<String, AnyObject> else { return }
guard let carModel = userDict["carModel"] as? String else { return }
if carModel.contains(car) {
print(snapshot)
}
})
self.carCurrentKey = last.key
self.carCurrentValue = last.childSnapshot(forPath: "carModel").value as? String
})
} else {
// where to start next query?
let ref = USER_REF.queryOrder(byChild: "carModel").queryStarting(atValue: self.carCurrentValue)
}
}
I have to order the query by carModel, in order to group all of the users with that particular car type together in a snapshot. Since all the car models are the same value, I cannot figure out where to start or end the next query for the pagination. Using the reference i have in the else block starts the query at the same place as the block above. Any help or advice would be much appreciated.
I considered doing a fan out, and making a separate structure for car types. This would be difficult though.
For both startAt() and endAt(), you can pass a second value, childKey as shown here.
So your query will look something like this:
let ref = USER_REF.queryOrdered(byChild: "carModel").queryStarting(atValue: self.carCurrentValue, childKey: self.carCurrentKey).queryLimited(toFirst: 3+1)
Note that I used toFirst: 3+1. That's because, annoyingly, startAt() is inclusive and there's no way to skip the first record. So, since we started from the last record retrieved on the previous page, you will want to query for one extra record and discard the first result.
Here's a more complete example in JavaScript. Not familiar enough to translate this to Swift, but it should give you the algorithm in completion.
class Cursor {
constructor(baseRef, pageSize) {
this.baseRef = baseRef;
this.lastKey = null;
this.lastValue = null;
this.pageSize = pageSize;
}
next() {
let ref = this.baseRef;
if( this.lastValue !== null ) {
// a previous page has been loaded so get the next one using the previous value/key
// we have to start from the current cursor so add one to page size
ref = ref.startAt(this.lastValue, this.lastKey).limitToFirst(this.pageSize+1);
}
else {
// this is the first page
ref = ref.limitToFirst(this.pageSize);
}
return ref.once('value').then(snap => {
const keys = [];
const data = []; // store data in array so it's ordered
snap.forEach(ss => {
data.push(ss.val());
keys.push(ss.key);
});
if( this.lastValue !== null ) {
// skip the first value, which is actually the cursor
keys.shift();
data.shift();
}
// store the last loaded record
if( data.length ) {
const last = data.length - 1;
this.lastKey = keys[last];
this.lastValue = data[last].author;
}
return data;
});
}
}
And here's a working fiddle.
Keep in mind that this is a realtime data stream. So pagination is tricky. It's generally easier to just do infinite scroll than to try and maintain a realistic cursor on a moving data set (records can reorder when data changes, get deleted, added in the middle, etc).
Related
I am facing a serious callback hell on firebase realtime database update.
Situation:
I have comments node which store all the comment's detail information, such as belong to whose userId (uid) , message, and post id (pid). Please see image below.
I have another post-comment nodes, which store comments key under each post id key. Please see image below.
Finally the third nodes is user-comment, which store all comments key under unique user account id key. Please see image below.
Problem:
Everything work fine on "Write comment" function, because it just create a comment key and update comment data to these nodes.
But, when user call "Delete post" function, which will delete all the comments data belong to this post id. Therefore, I have this code logical to loop all the comments data. The whole point is that first I have to get the post-comment snapshot in order to limitation the query amount on comments node (because comments node store all the app user's comment detail data. Without knowing the quantity of comment belong to the target post, it will need to for loop all over the comments node, it is too overload.)
For looping the post-comment will get the commentKey, then I can set Null on comments node and post-comment node.
But the issues happen on I need to use comments node to find out the userId, in order to set NSNull on user-comment. When I calling the event below:
commentsRef.child((snap as AnyObject).key).observeSingleEvent(of:
.value, with: { (commentSnapshot) in
})
The commentsRef callback scope become another thread. Therefore, if I call rootRef.updateChildValues out side of this scope and in the end of for loop (post-comment) which will only update comments node and post-comment node. The user-comment updates data will still assign key:value on the other thread.
updates["user-comment/(userId)/comments/(commentKey)"] = NSNull()
I have to put the rootRef.updateChildValue in the
commentsRef.child((snap as AnyObject).key).observeSingleEvent(of:
.value, with: { (commentSnapshot) in
...
rootRef.updateChildValues(updates)
})
This logic will cause updateChildValues being called too many time if the comments over 10,000 or more than 1 million, because it is in the for looping. I use count down method try to call update only once on the for loop end. But the count number always be 0 in the commentRef scope... I don't know why...
Please help me out with a better solution to dealing with this nested observeSingleEvent update issues without changing the current nodes structure. My goal is to only call rootRef.updateChildValue one time.
Thanks for your help.
Demo code:
func deleteAllCommentsRelateTo(postId: String, callback: ((CommentServiceError?) -> Void)?) {
var error: CommentServiceError?
guard session.isValid else {
error = .authenticationNotFound(message: "Authentication not found.")
callback?(error)
return
}
let uid = session.user.id
let rootRef = Database.database().reference()
let path1 = "posts/\(postId)/comments_count"
let path2 = "posts/\(postId)/uid"
let commentCountRef = rootRef.child(path1)
let authorRef = rootRef.child(path2)
authorRef.observeSingleEvent(of: .value, with: { authorSnapshot in
guard let authorId = authorSnapshot.value as? String else {
error = .failedToDelete(message: "Author not found")
callback?(error)
return
}
if uid != authorId {
error = .failedToDelete(message: "User has no permission to delete this post comments")
callback?(error)
return
}
commentCountRef.runTransactionBlock({ (data) -> TransactionResult in
if let _ = data.value as? Int {
data.value = 0
}
return TransactionResult.success(withValue: data)
}) { (err, committed, snapshot) in
guard err == nil, committed else {
error = .failedToDelete(message: "Unable to delete a comment")
callback?(error)
return
}
var updates: [AnyHashable: Any] = [:]
/**
* [CHECKED] Set NSNull() on comments, post-comment, and user-comment nodes.
*/
let commentsRef = rootRef.child("comments")
let postCommentRef = rootRef.child("post-comment")
let query = postCommentRef.child(postId).child("comments").queryOrderedByKey()
query.observeSingleEvent(of: .value, with: { (data) in
guard data.hasChildren() else {
error = .failedToDelete(message: "No comments data")
callback?(error)
return
}
var count = data.childrenCount
print("post-comment count!!!!!!!: ", data.childrenCount)
for snap in data.children {
guard let commentKeySnap = snap as? DataSnapshot else {
continue
}
count -= 1
let commentKey = commentKeySnap.key
if count == 0 {
print("this is totally not right!!!!!")
}
updates["comments/\(commentKey)"] = NSNull()
updates["post-comment/\(postId)/comments/\(commentKey)"] = NSNull()
commentsRef.child((snap as AnyObject).key).observeSingleEvent(of: .value, with: { (commentSnapshot) in
guard let userId = commentSnapshot.childSnapshot(forPath: "uid").value as? String else {
return
}
updates["user-comment/\(userId)/comments/\(commentKey)"] = NSNull()
print("In this observeSingleEvent will always be 0 count::::: ", count)
if count == 0 {
rootRef.updateChildValues(updates, withCompletionBlock: { err, ref in
guard err == nil else {
error = .failedToDelete(message: "Failed to delete comment")
callback?(error)
return
}
})
print("deleteAllComments: ", updates)
callback?(nil)
}
})
print("count down: ", count)
}
})
})
}
})
}
Solution:
I accidentally found out the correct place to put count -= 1. Originally I put it in the for loop scope, but the count did not decrease in the commentRef in scope. Therefore, I put count -= 1 in the commentRef scope which success count to zero and only call rootRef.update one time.
I'm trying to retrieve all available drivers from my Firebase Database, then maybe put them in an array, So I can Calculate which driver is closer to a user. Or is there a better way of doing it than Array
Here is my Code Below for Retrieving the drivers, Its working and Below is the image for my database table
ref.child("drivers").queryOrdered(byChild: "status").queryEqual(toValue: "available").observe(.childAdded) { (snapshot) in
print(snapshot.value!)
}
Here is a Code that would sort out the closest driver to a user but I need to sort out drivers into an array first
var closestLocation: CLLocation?
var smallestDistance: CLLocationDistance?
for location in locations {
let distance = currentLocation.distanceFromLocation(location)
if smallestDistance == nil || distance < smallestDistance {
closestLocation = location
smallestDistance = distance
}
}
print("smallestDistance = \(smallestDistance)")
To populate an array from Firebase, we need to take each driver node, obtain the child nodes we are interested in, group them up and add them to an array.
To do that we are going to take the presented Snapshot, map it to a Dictionary and then read the child nodes by their keys. Then leverage a Structure to hold the distance and drivers name in an array.
Keep in mind that using .childAdded will iterate over all of the existing nodes once, and then call the code in the closure any time a new child is added - you may want to consider reading the node once with .value and iterate over the nodes in that snapshot instead (i.e. .observeSingleEvent(of: .value))
Here's some very verbose code to read your drivers node and add them, and their distance from our current location into an array.
Please be aware that Firebase is asynchronous so processing the array distances should be done within the closure and any code following this code will run before this code completes.
struct DriverAndDistance {
var driver = ""
var distance = 0.0
}
var driverArray = [DriverAndDistance]()
func readDriversAndAddObserver() {
let driversRef = self.ref.child("drivers")
driversRef.observe(.childAdded, with: { snapshot in
if snapshot.exists() == false { return }
let dict = snapshot.value as! [String: Any]
let firstName = dict["Firstname"] as! String
let long = dict["long"] as! Double
let lat = dict["lat"] as! Double
let distanceFromHere = self.getDistance(long, lat)
let dd = DriverAndDistance(driver: firstName, distance: distanceFromHere)
self.driverArray.append(dd)
//do calculations here and present data to user
})
}
I have been trying to fetch data from Firebase using Realtime database. I want to check the contacts in iPhone and then if any contact number matches with that of any number in "numbers" table in db, then I have to get the user_key from it and then using that key, I have to obtain the corresponding details from users table.
for number in numbers {
Database.database().reference().child("numbers/\(number)").observeSingleEvent(of: .value, with: { (snapshot) in
if snapshot.exists() {
let userKey = snapshot.value as! String
// We found this user, no determine there name, (TODO has_image?)
Database.database().reference().child("users/\(userKey)/public/name").observeSingleEvent(of: .value, with: { (namesnapshot) in
if namesnapshot.exists() {
let name = namesnapshot.value as! String
print("FOUND \(name)")
complete(.success((userID: userKey, name: name)))
}
})
} else {
if numbers.index(of: number) == numbers.count - 1 { // Last Number checked and not found yet, so fail
complete(.failure(UserApiError.UserNotFound))
}
}
})
}
numbers is the array of contact numbers for a particular contact. For a contact having single number, this works fine. But for contacts having multiple numbers, the
Database.database().reference().child("users/\(userKey)/public/name").observeSingleEvent(of: .value, with: { (namesnapshot) in
will call after some time due to which the next index in the for loop gets called. So even if I have the data in first number in a contact, it will return failure because the next number will be iterated before the success of the observeSingleEvent.
I have been sitting for hours now, no ideas left with me. Please help!
I think a better approach is:
1 - Get all numbers from DB.
2 - Get all contact numbers that exists on DB.
3 - Finally, get the name of that contacts.(Exactly the way you are doing).
OBS: To do that you must change your DB. Your numbers must be saved as key-value pair. For exemple "555-0000" : true.
Database.database().reference().child("numbers").observeSingleEvent(of: .value, with: { (snapshot) in
guard let numbersFromDB = snapshot.value as? [String: Any] else{
print("Fail get numbers from db")
}
let numbersMatchedOnDB = numbersFromDB.keys.filter{ numbers.contains($0) }//get numbers from contact that exist on DB.
if numbersMatchedOnDB.isEmpty{
complete(.failure(UserApiError.UserNotFound))
}
else{
//For each contact number that exist on DB. it gets its name.
numbersMatchedOnDB.forEach{ numberMatchedOnDB in
Database.database().reference().child("numbers/\(numberMatchedOnDB)").observeSingleEvent(of: .value, with: { (snapshot) in
if snapshot.exists() {
let userKey = snapshot.value as! String
// .... nothing changed here ....
}
})
}
}
})
When I try to search the couchbase documents of size around 10K, the searching is taking very long time. Below are the code snippet. Can anyone optimize it or suggest me any alternative approach. Thank you.
1) Search function
func search(keyword:String) -> [[String:AnyObject]] {
var results:[[String:AnyObject]]=[]
let searchView = database.viewNamed(AppConstants().SEARCH)
if searchView.mapBlock == nil {
startIndexing()
}
let query = searchView.createQuery()
var docIds = Set<String>()
let result = try query.run()
while let row = result.nextRow() {
let key = "\(row.key)"
let keyArr = keyword.characters.split(" ")
for (index, element) in keyArr.enumerate() {
let keyItem = String(element)
if key.lowercaseString.containsString(keyItem.lowercaseString) {
let value = row.value as! [String:AnyObject]
let id = value["_id"] as? String
if id != nil && !docIds.contains(id!) {
results.append(value)
docIds.insert(id!)
}
}
}
}
}
2) Indexing
func startIndexing() {
let searchView = database.viewNamed(AppConstants().SEARCH)
if searchView.mapBlock == nil {
searchView.setMapBlock({ (doc, emit) in
let docType = doc[AppConstants().DOC_TYPE] as! String
if AppConstants().DOC_TYPE_CONTACT.isEqual(docType) {
self.parseJsonToKeyValues(doc)
for value in self.fields.values {
emit(value, doc)
}
self.fields.removeAll()
}
}, version: "1")
}
}
self.parseJsonToKeyValues(doc) will return me the key value store of my documents to index.
You're emitting the entire document along with every field for your view. This could easily cause your queries to be slow. It also seems unlikely you want to do this, unless you really need to be able to query against every field in your document.
It's considered best practice to set your map function right after opening the database. Waiting until right before you query may or may not slow you down.
See https://developer.couchbase.com/documentation/mobile/current/guides/couchbase-lite/native-api/view/index.html for more, especially the section labeled "Development Considerations".
I've got a class that has Title, users and a timestamp.
My database is set up like :
users
ablksjdlf39493
rooms
room1: true
rooms
room1
timestampOfLastPost: 39403942
users
039403fsjlkj: true
;alkjsdksdkj: true
I'm running a query on my user's rooms and grabbing the snapshot.key to get in this example "room1". Then inside of the first query I run another query and I use that key as a child reference to my rooms node to go to that room (rooms-->room1), and set is as my class object's title. From the 2nd query I'm pulling out the timestamp and use that for my object's timestamp.
I'm a bit confused how to get the childcount of out users.
I was running yet another nested query inside of my second query. But then I was getting all sorts of weird duplicate posts being added to my tableview when I was loading them. And that just doesn't seem very efficient anyways.
Is there a way in my second query to get both the timestamp and the children.count of my users node? The only way I've seen to do that is to run an observer on the specific node and do a snapshot.childrenCount.
edit:
currentUserFirebaseReference.child("rooms").observeEventType(.Value) { (snapshot: FIRDataSnapshot) in
self.interestsArray = []
if let snapshots = snapshot.children.allObjects as? [FIRDataSnapshot] {
for snap in snapshots {
let eachInterest = Interest()
let interestKey = snap.key
let title = snap.key.uppercaseString
eachInterest.title = title
//grabbing the user's current rooms and getting the snap.key to use for my next query.
DataService.ds.REF_INTERESTS.child(interestKey).observeEventType(.Value, withBlock: { (snapshot: FIRDataSnapshot) in
if let lastuser = snapshot.value!["lastJoin"] as? Int {
eachInterest.latestUser = lastuser
} else {
print("couln't pull that value")
}
//getting the timestamp
DataService.ds.REF_INTERESTS.child(interestKey).child("users").observeEventType(.Value, withBlock: { (snapshot: FIRDataSnapshot) in
// I can get the users children.count by doing yet another query, but my
// interestsArray gets all messed up and I get duplicates / weird
// loading in my tableview. So I'm looking to not do this whole
// separate query and just grab it from up above where I get the
// timestamp?
let snapshotChildren = String(Int(snapshot.childrenCount))
eachInterest.users = snapshotChildren
self.interestsArray.append(eachInterest)
//self.interestsArray.sortInPlace({ $0.latestUser < $1.latestUser })
dispatch_async(dispatch_get_main_queue(), {
self.tableView.reloadData()
})
})
})
}
}
}
}