Firebase - atomic write of multiple values to multiple locations - ios

I need to register an object to Firebase. The object has multiple fields, and I need to write them all to multiple paths.
My code :
public func RegisterProductOnDatabase(database dataBase: DatabaseReference)
{
// Run in one transaction
RegisterProductOnDatabase(database: dataBase)
RegisterProductForAllUsers(database: dataBase)
}
private func RegisterProductForAllUsers(database dataBase: DatabaseReference)
{
dataBase.child("Products").child(self.UniqueID()).child("Name").setValue(self.Name())
dataBase.child("Products").child(self.UniqueID()).child("UniqueID").setValue(self.UniqueID())
dataBase.child("Products").child(self.UniqueID()).child("Price").setValue(self.Price())
dataBase.child("Products").child(self.UniqueID()).child("Description").setValue(self.Description())
dataBase.child("Products").child(self.UniqueID()).child("ToBuy?").setValue(self.m_ToBuy)
dataBase.child("Products").child(self.UniqueID()).child("ToSell?").setValue(self.m_ToSell)
dataBase.child("Products").child(self.UniqueID()).child("Owner").setValue(self.m_Owner)
dataBase.child("Products").child(self.UniqueID()).child("Amount").setValue(self.m_Amount)
dataBase.child("Products").child(self.UniqueID()).child("MainImage").setValue(self.m_PicturesURLs.first)
}
private func RegisterProductForAddingUser(database dataBase: DatabaseReference)
{
dataBase.child("Users").child(m_Owner).child("Products").child(self.UniqueID()).child("Name").setValue(self.Name())
dataBase.child("Users").child(m_Owner).child("Products").child(self.UniqueID()).child("UniqueID").setValue(self.UniqueID())
dataBase.child("Users").child(m_Owner).child("Products").child(self.UniqueID()).child("Price").setValue(self.Price())
dataBase.child("Users").child(m_Owner).child("Products").child(self.UniqueID()).child("Description").setValue(self.Description())
dataBase.child("Users").child(m_Owner).child("Products").child(self.UniqueID()).child("ToBuy?").setValue(self.m_ToBuy)
dataBase.child("Users").child(m_Owner).child("Products").child(self.UniqueID()).child("ToSell?").setValue(self.m_ToSell)
dataBase.child("Users").child(m_Owner).child("Products").child(self.UniqueID()).child("Amount").setValue(self.m_Amount)
dataBase.child("Users").child(m_Owner).child("Products").child(self.UniqueID()).child("MainImage").setValue(self.m_PicturesURLs.first)
}
I need the function "RegisterProductOnDatabase" to run as one transaction - meaning all written values to be written as one transaction.
1) How can I write all this data as ONE transaction using Swift ?
2) Is there a better way to write all these values without code multiplication?

Firstly, you should use dictionaries instead of manually modifying each node.
This is from the Firebase documentation:
let key = ref.child("posts").childByAutoId().key
let post = ["uid": userID,
"author": username,
"title": title,
"body": body]
let childUpdates = ["/posts/\(key)": post,
"/user-posts/\(userID)/\(key)/": post]
ref.updateChildValues(childUpdates)
Read more about writing data here.
Secondly, in order to modify in multiple locations, use this answer as I have recently had the same question.

Related

iOS : Firebase Send/Save Data in chunks

I have trying to save data in firebase like this
class FirebaseManager {
static let shared = FirebaseManager()
private let tableRef = Database.database().reference(withPath: "XYZDemo")
func add(item: HealthData) {
self.tableRef.childByAutoId().setValue(item.dictionary)
}
}
This code save data one bye one.
How can i add more than one data at a time i.e save 5 values at once?
To simultaneously write to specific children of a node without overwriting other child nodes, use the updateChildValues method.
Basicly you create an array with the updates/writes you want to perform and write that array to Firebase. Here is an example showing 1 post being written to two different locations:
//Create the new key
let key = self.tableRef.childByAutoId().key
//Post data that is going to be written
let post = ["uid": userID,
"author": username,
"title": title,
"body": body]
//Create the array with (two) updates
let childUpdates = ["/posts/\(key)": post,
"/user-posts/\(userID)/\(key)/": post]
//Write the array to the database
ref.updateChildValues(childUpdates)
A couple things to keep in mind when you are dong this:
It's all or nothing, either all writes fail or all writes succeed. If even 1 of the writes in the array fails then all writes will fail.
Make sure you use the correct reference for each write because it will override all the data at that location.

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.

Swift, Firebase, Flashlight, how to store arrays?

I am currently trying to use Firebase, Flashlight and Swift to create an search function to retrieve a random object from my realtime database.
I am trying to perform the following query to Firebase at /search/request
var searchSettings : [Any] = []
if Settings.searchPackage != 99 {
searchSettings.append(["match" : Settings.searchPackage])
}
if Settings.searchCountry != .world {
if let region = Locale.current.regionCode {
searchSettings.append(["match" : region])
}
}
if Settings.searchGender != .All {
searchSettings.append(["match" : Settings.searchGender.rawValue])
}
let postData = [
"index" : "firebase",
"type" : "test",
"body" : [
"query" : searchSettings
]
] as [String : Any]
ref.setValue(postData, withCompletionBlock: { (error, reference) in
if error == nil {
FIRDatabase.database().reference().child("search/response").child(ref.key).observeSingleEvent(of: .childAdded, with: { (snapshot) in
if snapshot.exists() {
print("found random snapshot based on settings \(snapshot)")
}
})
}
})
The problem is, as Firebase described in the documentation it does currently support Arrays, therefor the content of "query" will be:
Flashlight will throw an error because it expects the "query" to contain "match" fields and not the indexes of an array of them.
How would I fix this? I want to be able to search based on multiple fields.
Technically, Flashlight doesn't care at all what you put inside the body tag; it simply passes it on to ElasticSearch. So if there is an error generated about the format, it's ElasticSearch that's doing the complaining.
What you're probably running into here is either a) ElasticSearch doesn't like that syntax, or Firebase's array-like behaviors are converting the array to an object.
Note that Flashlight will allow you to pass a JSON string in place of body. So if this is a result of the array-like behaviors, you can JSON.stringify() the query before passing it into ES, and it will come out the other end as intended.
If the problem is the ES syntax (as it appears to me) then you can simply run the queries directly against ES until they work, and then modify your client to submit correct syntax accordingly.
Take a look at this gist:
BodyQuery in Swift
I wrote this in my Android application project and I'm using this to build queries for ES. Here is equivalent in Java for Android ElasticSearch needs a json query, and you can create it easily using Maps (Android)/Dictionaries (iOS). Enjoy :)

add json-based data into a sqlite database using swiftyJSON and sqlite.swift

Please bear with me this is my first swift project, I'm not yet up to speed with the syntax/language.
I retrieve data from a web service using AlamoFire and parse it using SwiftyJson. Then I want to insert it into a local SQLite database using SQLite.swift.
So far AlomoFire, SwiftyJson and SQLite.swift are installed and working in my project, things went very smooth to my surprise ;)
Now my question. The data I got is in JSON, in an Array so I want to do this:
let companiesTable = db["companies"] // get the table from our SQLite Database
db.transaction(.Deferred) { txn in
for (index,company) in data {
if companiesTable.insert(name <- company["name"]!), shortname <- company["short_name"]).statement.failed {
return .Rollback
}
}
return .Commit
}
My problem is in the insert. I have to force unwrap using a !, which is ok for name (required column in the database), but not ok for shortname or a dozen other columns (not mentioned in the example above for simplicity) which may be empty/null. Of course only columns with a value should be inserted.
Second question. I found the stuff about transactions here on stackoverflow, is the transaction automatically Committed or Roll-backed when doing the 'return'
You ask:
My problem is in the insert. I have to force unwrap using a !, which is ok for name (required column in the database), but not ok for shortname or a dozen other columns (not mentioned in the example above for simplicity) which may be empty/null.
If fields like shortname can be empty/null, then define it accordingly:
let company = db["companies"]
let name = Expression<String>("name")
let shortname = Expression<String?>("short_name")
When you do that, then when you insert the data, if the short_name is present, it uses it, and if it's not, it will be NULL in the database:
let data = [
(1, ["name": "United States of America", "short_name": "US"]),
(2, ["name": "United Kingdom", "short_name": "UK"]),
(3, ["name": "France"])]
let companiesTable = db["companies"] // get the table from our SQLite Database
db.transaction(.Deferred) { txn in
for (index,company) in data {
if companiesTable.insert(name <- company["name"]!, shortname <- company["short_name"]).statement.failed {
return .Rollback
}
}
return .Commit
}
You then ask:
Second question. I found the stuff about transactions here on stackoverflow, is the transaction automatically Committed or Roll-backed when doing the 'return'
If you're using transaction method, whether it commits or rolls back is a function of what TransactionResult value you return. In this example, it will commit if you return .Commit, and rollback if you return .Rollback.

Resources