Understanding Firebase data binding with user identifiers - ios

Guys I am trying to learn Firebase framework and I do have question about storing data, so as I understood, the data is stored as entire JSON file in the Firebase database.
Let's say I have few data models (just to simplify my description):
Users and Articles
Using Firebase SDK we can simple create user using very simple query. The Firebase user has uid which I can use for my purposes. My general question right now is how to bind uid with my data.
Correct me if I am wrong but I plan to use uid retrieved from the Firebase to create few nodes in Realtime DataBase like these:
{
"users": {
"$uid": {
"name": "Alex",
"lastname": "Matrosov"
},
"$uid": {
"name": "John",
"lastname": "Malkovich"
}
}
}
So when I create/sign in user I plan to add related info to the users/user node.
Then I want to store user article
{
"articles": {
"$article_id": {
"name": "Some name",
"$uid": "32431kjkjfj232"
},
"$article_id": {
"name": "Some name",
"$uid": "5555fffflllll"
}
}
}
So as I understood right when I will request articles it will contain all articles? Let me know if I am on right way with understanding of this uid and data binding.

Well, I'd say you're almost right. Just a few notes:
Inside article's JSON structure, since the '$' dollar mark on '$uid' field, usually, refers to a variable on firebase's database, I'd recommend you to change it to something else like 'userUID' or anything you want. Leaving the structure to something like this:
{
"articles": {
"$article_id": {
"name": "Some name",
"userUID": "32431kjkjfj232"
},
"$article_id": {
"name": "Some name",
"userUID": "5555fffflllll"
}
}
}
Then, you just query them as follow:
Using Swift 4 and Firebase 4:
let articlesRef = Database.database().reference(withPath: "articles")
articlesRef.observe(.value) { snapshot in
// Snopshot will contain all articles on your db reference.
print(snapshot)
}
let userUID = "..."
let articlesByUser = articlesRef.queryOrdered(byChild: "userUID").queryEqual(toValue: userUID)
articlesByUser.observe(.value) { snapshot in
// In this case, snapshot will contain all articles that has the 'userUID' key equal to what you've passed (what in this case, is the value of userUID constant.)
print(snapshot)
}

Related

How to fetch nested data from Firebase realtime database by implementing observer in swift

Firebase data-structure
lastLocations ( batteryStatus, lat, long, timestamp, uid)
profiles (name, phoneNumber, picture, uid)
userFriends ( basis on the uid -> how many friends -> conversationUid, friendStatus, notify, phoneNumber, uid)
My Code:
I have already created tableview and xib for it.
I have created model for last location, profiles, userFriends.
I already fetched the friend list but on Observe .ChildAdded
My uid zzV6DQSXUyUkPHgENDbZ9EjXVBj2
Link: https://drive.google.com/file/d/19cnkY03MXjrTFgzzPCdvmOuosDRvoMx9/view?usp=sharing
Issues:
Not getting an idea how to fetch location & profile with friend list efficient way with observer so any change come it reflects. Firebase is asyncrohonous process.
observer implementation so data not completely load every-time
Results to achieve:
I need to show friends list ( name, profile picture, battery status, lat long(address), timeStamp ) on tableview on the basis of my uid.
Firebase JSON
{
"lastLocations": {
"FTgzbZ9uWBTkiZK9kqLZaAIhEDv1": {
"batteryStatus": 22,
"latitude": 40.9910537,
"longitude": 29.020425,
"timeStamp": 1556568633477,
"uid": "FTgzbZ9uWBTkiZK9kqLZaAIhEDv1"
},
"zzV6DQSXUyUkPHgENDbZ9EjXVBj2": {
"batteryStatus": 88,
"latitude": 41.0173995,
"longitude": 29.1406086,
"timeStamp": 1571778174360,
"uid": "zzV6DQSXUyUkPHgENDbZ9EjXVBj2"
}
},
"profiles": {
"FTgzbZ9uWBTkiZK9kqLZaAIhEDv1": {
"fcmToken": "fp09-Y9ZAkQ:APA91bFgGB1phr4B9gZScnz7ngpqTb5MchgWRFjHmLCVmWGMJVsyFx0rtrz7roxzpE_MmuSaMc4is-XIu7j718qjRVCSHY4PvbNjL1LZ-iytaeDP0oa8aJgE02wET3cXqKviIRMH",
"name": "Skander",
"phoneNumber": "+95644125503",
"uid": "FTgzbZ9uWBTkiZK9kqLZaAIhEDv1"
},
"zzV6DQSXUyUkPHgENDbZ9EjXVBj2": {
"fcmToken": "enMneewiGgg:APA91bHyA4HypWUYhxGTUTTch8ZJ_6UUWhEIXRokmR-Y-MalwnrtV_zMsJ9p-sU_ZT4pVIvkmtJaCo7LFJYJ9ggfhc1f2HLcN9AoIevEBUqyoMN-HDzkweiUxAbyc84XSQPx7RZ1Xv",
"name": "Murad",
"phoneNumber": "+915377588674",
"picture": "profile/zzV6DQSXUyUkPHgENDbZ9EjXVBj2/a995c7f3-720f-45bf-ac58-b2df934e3dff.jpeg",
"uid": "zzV6DQSXUyUkPHgENDbZ9EjXVBj2"
}
},
"userFriends": {
"FTgzbZ9uWBTkiZK9kqLZaAIhEDv1": {
"zzV6DQSXUyUkPHgENDbZ9EjXVBj2": {
"conversationUid": "-L_w2yi8gh49GppDP3r5",
"friendStatus": "STATUS_ACCEPTED",
"notify": true,
"phoneNumber": "+915377588674",
"uid": "zzV6DQSXUyUkPHgENDbZ9EjXVBj2"
}
},
"zzV6DQSXUyUkPHgENDbZ9EjXVBj2": {
"FTgzbZ9uWBTkiZK9kqLZaAIhEDv1": {
"conversationUid": "-L_w2yi8gh49GppDP3r5",
"friendStatus": "STATUS_ACCEPTED",
"notify": true,
"phoneNumber": "+915644125503",
"uid": "FTgzbZ9uWBTkiZK9kqLZaAIhEDv1"
}
}
}
}
Swift Function:
func getFrndDataList(){
AppData.removeAll()
ref.child("userFriends").child("zzV6DQSXUyUkPHgENDbZ9EjXVBj2").observe(.childAdded, with: { (snapshot) in
guard let data = try? JSONSerialization.data(withJSONObject: snapshot.value as Any) else { return }
let frndList = try? JSONDecoder().decode(Friend.self, from: data)
self.AppData.append(frndList!)
self.tableView.reloadData()
print([frndList])
})
}
Note: After writing this answer I realized it was way long but this is a big question and there are a lot of elements to address.
My first suggestion is to change the structure as it's overly complicated for what's being done with the data. Also, there is repetitive data that's not needed so that should be changed as well. For example, here's your profiles node
"profiles": {
"FTgzbZ9uWBTkiZK9kqLZaAIhEDv1": {
"fcmToken": "fp09-Y9ZAkQ:APA91bFgGB1phr4B9gZScnz7ngpqTb5MchgWRFjHmLCVmWGMJVsyFx0rtrz7roxzpE_MmuSaMc4is-XIu7j718qjRVCSHY4PvbNjL1LZ-iytaeDP0oa8aJgE02wET3cXqKviIRMH",
"name": "Skander",
"phoneNumber": "+95644125503",
"uid": "FTgzbZ9uWBTkiZK9kqLZaAIhEDv1" <- remove this, not needed.
},
As you can see, each child node has a key of the user id. But, you are also storing the user id as a child node as well. They key is the uid and will always be available so no need for duplication there and the child node should be removed.
Based on comments, this is a much better structure
/users
FTgzbZ9uWBTkiZK9kqLZaAIhEDv1
"batteryStatus": 22,
"latitude": 40.9910537,
"longitude": 29.020425,
"timeStamp": 1556568633477,
"fcmToken": "fp09-Y9ZAkQ:APA91bFgGB1phr4B9gZScnz7ngpqTb5MchgWRFjHmLCVmWGMJVsyFx0rtrz7roxzpE_MmuSaMc4is-XIu7j718qjRVCSHY4PvbNjL1LZ-iytaeDP0oa8aJgE02wET3cXqKviIRMH",
"name": "Skander",
"phoneNumber": "+95644125503",
"conversationUid": "-L_w2yi8gh49GppDP3r5",
"friendStatus": "STATUS_ACCEPTED",
"notify": true,
"phoneNumber": "+915377588674",
and then, to keep track of a users friends, it becomes this
/userFriends
zzV6DQSXUyUkPHgENDbZ9EjXVBj2 //this user
FTgzbZ9uWBTkiZK9kqLZaAIhEDv1: true //their friend
IRoo0lbhaihioSSuFETngEEFEeoi: true //another friend
To load this users friends, we read the data at /userFriends/this_users_id and then iterate over the child nodes loading the data for display in the tableView
Lets start with an object that will be used to hold each friends data, and then an array that will be used as a tableView Datasource
class FriendClass {
var uid = ""
var name = ""
//var profilePic
//var batteryStatus
init(withSnapshot: DataSnapshot) {
self.uid = withSnapshot.key
self.name = withSnapshot.childSnapshot(forPath: "name").value as? String ?? "No Name"
}
}
var myFriendsDataSource = [FriendClass]()
Then a functions to read the users node, iterate over the users friends uid's and read in each users data, populating the FriendClass object and storing each in an array. Note that self.ref points to my firebase.
func loadUsersFriends() {
let uid = "zzV6DQSXUyUkPHgENDbZ9EjXVBj2"
let myFriendsRef = self.ref.child("userFriends").child(uid)
myFriendsRef.observeSingleEvent(of: .value, with: { snapshot in
let uidArray = snapshot.children.allObjects as! [DataSnapshot]
for friendsUid in uidArray {
self.loadFriend(withUid: friendsUid.key)
}
})
}
func loadFriend(withUid: String) {
let thisUserRef = self.ref.child("users").child(withUid)
thisUserRef.observeSingleEvent(of: .value, with: { snapshot in
let aFriend = FriendClass(withSnapshot: snapshot)
self.myFriendsDataSource.append(aFriend)
})
}
Now that we have the code to read in the data, you also want to watch for changes. There are a number of options but here's two.
1) I'll call this brute force.
Simply attach a .childChanged observer to the /users node and if something changes, that changed node is passed to the observer. If the key to that node matches a key in myFriendsDataSource array, update that user in the array. If no match, then ignore it.
func watchForChangesInMyFriends() {
let usersRef = self.ref.child("users")
usersRef.observe(.childChanged, with: { snapshot in
let key = snapshot.key
if let friendIndex = self.myFriendsDataSource.firstIndex(where: { $0.uid == key} ) {
let friend = self.myFriendsDataSource[friendIndex]
print("found user \(friend.name), updating")
//friend(updateWithSnapshot: snapshot) //leave this for you to code
}
})
}
2) Selective observing
For this, we simply attach an .childChanged observer to each friend node - and that can be done within the code example from above
func loadFriend(withUid: String) {
let thisUserRef = self.ref.child("users").child(withUid)
thisUserRef.observeSingleEvent(of: .value, with: { snapshot in
let aFriend = FriendClass(withSnapshot: snapshot)
self.myFriendsDataSource.append(aFriend)
//add an observer to this friends node here.
})
}
One last thing: I didn't address this
"friendStatus": "STATUS_ACCEPTED",
I would think that only friends you accepted are in the friends list so the use is a tad unclear. However, if you want to use it you could do this
/userFriends
zzV6DQSXUyUkPHgENDbZ9EjXVBj2 //this user
FTgzbZ9uWBTkiZK9kqLZaAIhEDv1: "STATUS_ACCEPTED"
IRoo0lbhaihioSSuFETngEEFEeoi: "STATUS_DECLINED"
and then as you're itering over friends to load, ignore the ones that are declined.
If you MUST keep your current structure (which I do NOT recommend) the techniques in this answer will work for that structure as well, however, it will be a lot more code and you're going to be moving around a lot of unneeded extra data so the Firebase bill will be higher.

Firebase iOS -What is the difference between .queryOrdered().queryEqual(toValue: and child().observeSingleEvent

In Firebase when searching for a username (or anything search related for that matter) what is the difference between
.queryOrdered().queryEqual(toValue: and child().observeSingleEvent(
I looked at the answer and comments below from #Frank van Puffelen
In these comments he says to use:
reference.child("usernames").child(usernameYourSearchingFor).observeSingleEvent(of: .value, with: { (snapshot) in
if snapshot.exists() { ... } else { ... }
})
But in this answer he answered with:
ref.child("users").queryOrdered(byChild:"username").queryEqual(toValue: usernameYourSearchingFor).observeSingleEvent(of: .value, with: { snapshot in
if snapshot.exists() { ... } else { ... }
})
When searching the database using either of these methods, is there a difference between speed and efficiency, or is it simply a matter of using 2 different methods to achieve the same exact goal?
Basically, those operations are meant for two different data structures.
In the first one, you'd have a data structure similar to this:
{
"usernames": {
"usernameYourSearchingFor": {
// some data
},
"otherUsername": {
// different data
}
}
}
As you can see, the username you're searching for is a key under the "usernames" node. You can access it directly, by calling the child() function. (and then a single event observer is attached to it)
Now in the second structure:
{
"users": {
"user1": {
"username": "usernameYourSearchingFor",
"email": "user1#email.com"
},
"user2": {
"username": "otherUsername",
"email": "user2#email.com"
}
}
}
The username is actually an attribute belonging to the User object. There's no way you can access it directly, so you need to query the "users" node in order to find the username you're searching for, hence the need of .queryOrdered(byChild:"username").queryEqual(toValue: usernameYourSearchingFor).

Listing Firebase data and relationships

I have just started working with Firebase database and I am a bit confused how to structure the database. In the following example I have a users object and a groups object. Each user can be part of multiple groups and every group can have multiple users. The proposed database structure is the following, according to "Structure Your Database".
{
"users": {
"alovelace": {
"name": "Ada Lovelace",
"groups": {
"techpioneers": true,
"womentechmakers": true
}
}
},
"groups": {
"techpioneers": {
"name": "Historical Tech Pioneers",
"startDate": "24-04-1820",
"members": {
"alovelace": true,
"ghopper": true,
"eclarke": true
}
}
}
}
Let's say I want to display all groups in a list in my app, with the group name and start date. How would I make that database call? Since the user object only contains the id of the groups, would I then have to make a separate call to the database for every group just to find out the name and start date of the group? If there are many groups in the list, then that becomes a lot of calls. My group might contain a lot of other information as well so this doesn't seem good for performance. Can I get all the groups in the groups list of the user, in one call?
One though I had was to include the name and start date in the groups object under the user:
"users": {
"alovelace": {
"name": "Ada Lovelace",
"groups": {
"techpioneers":{
"name": "Historical Tech Pioneers",
"startDate": "24-04-1820"
},
"womentechmakers":{
"name": "Women in Technology",
"startDate": "13-10-1823"
}
}
}
}
}
but this solution seems to add a lot of duplicate data. Also if I want to update the name I would have to do that in multiple locations. And maybe I want to add a sponsor organization object, that also contains group, and then want to list them. Then there would be 3 places to update the information on. How would I solve this?
You would then have two possibilities, one would be to store the data you need (duplicating it) in the groups node of each user.
The other, which is the one that I would recommend the most, would be to add an observeSingleEvent(of: .value) inside your first observer (that could be an observe(.value), observe(.childAdded) or whatever).
Say you have an array of all your group members, and an object named AppUser that represents a user in your app :
var groupMembers = [AppUser]()
To detect whenever a new member is added to a group for example, you could use a .childAdded observer for example :
func groupWasAddedObserver(completion: #escaping () -> ()) {
// Add a .childAdded observer to the group's members node (groupId should be defined somewhere) :
groupsRef.child(groupId).child("members").observe(.childAdded, with: { [weak self] (snapshot) in
// When you get the snapshot, store its key, which represents the member id :
let memberId = snapshot.key
// fetch this member's profile information using its id :
self?.getUser(memberId, completion: { (groupMember) in
// Once you got the profile information of this member, add it to an array of all your members for example :
self?.groupMembers.append(groupMember)
// Call the completion handler so that you can update the UI or reload a table view somewhere maybe depending on your needs :
completion()
})
})
}
And the second method to fetch a user data knowing his or her id :
func getUser(_ userId: String, completion: #escaping (AppUser) -> ()) {
// Add the observerSingleEvent observer :
usersRef.child(userId).observeSingleEvent(of: .value, with: { (snapshot) in
// Get the data you need using the snapshot you get :
guard let email = snapshot.childSnapshot(forPath: "email").value as? String else { return }
guard let name = snapshot.childSnapshot(forPath: "name").value as? String else { return }
guard let picUrl = snapshot.childSnapshot(forPath: "picUrl").value as? String else { return }
// Call the completion handler to return your user/member :
completion(AppUser(id: snapshot.key, email: email, name: name, picUrl: picUrl))
})
}
As you can see you get the memberId of each user using the snapshot key, and you use this memberId to fetch this specific user data.

Deleting Realm Object entry if its deleted in API Response

First response in this I get two user i.e abc#gmail.com & xyz#gmail.com
[{
"email": "abc#gmail.com",
"type": "primary_email",
"linked_to": {
"_id": "DAS44564dasdDASd",
"image": null,
"company": null,
"designation": null,
"name": null
},
"active_platforms": [
"asd",
"qwe"
]
},
{
"email": "xyz#gmail.com",
"type": "primary_email",
"linked_to": {
"_id": "DAS44564dasdDASd",
"image": null,
"company": null,
"designation": null,
"name": null
},
"active_platforms": [
"asd",
"qwe"
]
}]
Now if abc#gmail.com is deleted if I do API call again then still I get abc#gmail.com in my object as it is not deleted from my realm. So how to handle such situation ?
// write request result to realm database
let entries = json["data"]
realm.beginWrite()
for (_, subJson) : (String, JSON) in entries {
let entry: AppUsers = Mapper<AppUsers>().map(JSONObject: subJson.dictionaryObject!)!
realm.add(entry, update: true)
}
do {
try realm.commitWrite()
} catch {
}
Update your logic as below. This is one of method to this.
Add one extra bool field to AppUsers model say 'active'. Update your code as below
// write request result to realm database
let entries = json["data"]
realm.beginWrite()
//Fetch all realm AppUsers objects
let allAppUsers = //TODO fetch all AppUsers objects here
for user in allAppUsers {
user.active = false
}
for (_, subJson) : (String, JSON) in entries {
let entry: AppUsers = Mapper<AppUsers>().map(JSONObject: subJson.dictionaryObject!)!
entry.active = true
realm.add(entry, update: true)
}
for user in allAppUsers {
if !user.active {
realm.delete(user)
}
}
do {
try realm.commitWrite()
} catch {
}
This sounds like an issue where the data in your Realm database has become stale as the contents no longer match what is on the server.
The Realm API realm.add(_, update: true) will update any objects that were passed to it, but simply not passing an object does not imply it should be deleted (More that you just didn't want to update it).
There's no way for Realm to automatically know if an object needs to be deleted. You'll need to be in charge of that logic yourself.
Since your mechanism for checking if an object is deleted is via its email address, you could capture the email addresses of each object you've updated, and then delete any other objects whose email address is not in there.
// write request result to realm database
realm.beginWrite()
let entries = json["data"]
var updatedEmails = [String]()
for (_, subJson) : (String, JSON) in entries {
let entry: AppUsers = Mapper<AppUsers>().map(JSONObject: subJson.dictionaryObject!)!
// Save the email we just processed
updatedEmails.append(entry.email)
realm.add(entry, update: true)
}
// Delete all objects not in the updated emails list
let realmEntries = realm.objects(AppUsers.self)
for entry in realmEntries {
if !updatedEmails.contains(entry.email) {
realm.delete(entry)
}
}
do {
try realm.commitWrite()
} catch {
}
If your REST API brings down all of your objects in their complete form each time, a much quicker solution would also be to simply empty the Realm file each time and just add the objects as new objects each time too.

will my structure for database in firebase affect the speed of loading?

I am using firebase as backend for my app. My app will be some sort of expense tracker. The way that I am thinking I should restructure my data so that I can filter the dates easily is as such.
uid/expenses/category/year/month/day/uniquekey/details
to dig into the snapshots would be something like this
func retrieveData (){
_ = dataRef.observeEventType(.ChildAdded, withBlock: { (snapshotOne) in
if let snapshotTwo = snapshotOne.children.allObjects as? [FIRDataSnapshot] {
if let snapshotThree = snapshotTwo.children.allObjects as? [FIRDataSnapshot] {
if let snapshotFour = snapshotThree.children.allObjects as? [FIRDataSnapshot] {
for item in snapshotFour {
//retrieve details
}
}
}
}
}
}
I have no idea what the consequences are to have to dig so deep into the database and whether my structure for database is okay? could anyone advise?
Any Change to Data structure will effect your database.
Firebase prefers Flattened Data
& Using Indices to Define Complex Relationships:
A lot of times in building apps, it's preferable to download a subset of a list. This is particularly common if the list contains thousands of records or more. When this relationship is static, and one-directional, we can use queries to grab a subset of data, or simply nest the entries under the logical grouping.
{
"messages": {
"john": {
"rec1": "Walk the dog",
"rec2": "Buy milk",
"rec3": "Win a gold medal in the Olympics"
}
}
}
However, we already know that flattening data is a best practice. So let's see why, by examining where this structure begins to break down. If we move into something more dynamic, like shared chat rooms, then suddenly our data (e.g. lists of rooms, lists of messages) now have two-way relationships.
Users can belong to a group and groups comprise a list of users. A first attempt at resolving this data structure would probably look this:
{
"users": {
"mchen": { "name": "Mary Chen" },
"brinchen": { "name": "Byambyn Rinchen" },
"hmadi": { "name": "Hamadi Madi" }
},
"groups": {
"alpha": {
"name": "Alpha Tango",
"members": {
"m1": "mchen",
"m2": "brinchen",
"m3": "hamadi"
}
},
"bravo": { ... },
"charlie": { ... }
}
}
You can read more here.
https://www.firebase.com/docs/web/guide/structuring-data.html
The whole structure of your database is related to the data you structure to store.
Also consider reading more about the Security and Rules.
https://www.firebase.com/docs/security/quickstart.html

Resources