Flutter Firebase global Auto Incremental value and retrieving in document field dart - firebase

I'm quite new to Firebase Flutter. I'm developing a mobile application to share books among others.
In firebase firestore,
I have 'users' collections which contain all the user data with unique id
I have 'books' collection which contain all the book data with unique id created automatically
Also I have 'global' collection with single document with one integer field called 'bookcount'.
Users can can have many books.
Now I want to create a another unique id field for book. idea is to have simple integer id.
One way of doing this is get list of books and find the length (count) and add 1 when creating a new record. I have ruled out this method as if many users using simultaneously, I think this can lead to duplicate ids.
So I have created a another collection global with single document and field name bookcount. Which hold number of books (rough count) on books collection. So idea is each time when adding a book to a collection increase bookcount and use this value as simple unique id for a book. This bookcount may not represent actual books as user can discard the book entry before saving it, which is okay as I only need a simple unique id.
class DatabaseService {
...
...
//final CollectionReference bookCollection = Firestore.instance.collection('users');
//final CollectionReference bookCollection = Firestore.instance.collection('books');
final CollectionReference globalData = Firestore.instance.collection('global');
...
...
Future<String> bookId() async
{
String uniquebookid = await globalData.document('SomeHardcodedID').updateData(
{
'bookcount': FieldValue.increment(1)
}).then((voidvalue) async
{
String cid = await globalData.getDocuments().then((bookvalue) => bookvalue.documents.single.data['bookcount'].toString());
return cid;
});
return uniquebookid;
}//future bookId
...
...
}//class
Now this works. well somewhat, Can we do this better? In here there are two parts, first increment the value bookcount, and then retrieve it.
Can we do this in one go?
If I try to call this method consecutively really fast when returning a value it might skip few numbers. I have call this from a button and try to press as fast I could. I think counter increase but it return
same number few times. and then skip some when press again. for example 1,2,3,4,8,8,8,8,9,10,... So at counter 4 I try to press the button multiple times. I wonder how this will behave when multiple users adding multiple books at the same time.
How Can I fix this?
Please Help, Thanks.

I think the problem was since await globalData.document('SomeHardcodedID').updateData is not producing a return value (void), as soon as this fired next call also execute which is okay, which okay for most scenarios.
However if bookId called few times within very short period (milliseconds) this produce number before FieldValue.increment(1) process.

Related

Firestore rule to only add/remove one item of array

To optimize usage, I have a Firestore collection with only one document, consisting in a single field, which is an array of strings.
This is what the data looks like in the collection. Just one document with one field, which is an array:
On the client side, the app is simply retrieving the entire status document, picking one at random, and then sending the entire array back minus the one it picked
var all = await metaRef.doc("status").get();
List tokens=all['all'];
var r=new Random();
int numar=r.nextInt(tokens.length);
var ales=tokens[numar];
tokens.removeAt(numar);
metaRef.doc("status").set({"all":tokens});
Then it tries to do some stuff with the string, which may fail or succeed. If it succeeds, then no more writing to the database, but if it fails it fetches that array again, adds the string back and pushes it:
var all = await metaRef.doc("status").get();
List tokens=all['all'];
List<String> toate=(tokens.map((element) => element as String).toList());
toate.add(ales.toString());
metaRef.doc("status").set({"all":toate});
You can use the methods associated with the Set object.
Here is an example to check that only 1 item was removed:
allow update: if checkremoveonlyoneitem()
function checkremoveonlyoneitem() {
let set = resource.data.array.toSet();
let setafter = request.resource.data.array.toSet();
return set.size() == setafter.size() + 1
&& set.intersection(setafter).size() == 1;
}
Then you can check that only one item was added. And you should also add additional checks in case the array does not exist on your doc.
If you are not sure about how the app performs the task i.e., successfully or not, then I guess it is nice idea to implement this logic in the client code. You can just make a simple conditional block which deletes the field from the document if the operation succeeds, either due to offline condition or any other issue. You can find the following sample from the following document regarding how to do it. Like this, with just one write you can delete the field which the user picks without updating the whole document.
city_ref = db.collection(u'cities').document(u'BJ')
city_ref.update({
u'capital': firestore.DELETE_FIELD
})snippets.py

Flutter Firebase firestore append data with unique ID

I'm working on the Flutter app where users can save multiple addresses. Previously I used a real-time database and it was easier for me to push data in any child with a unique Id but for some reason, I changed to Firestore and the same thing want to achieve with firestore. So, I generated UUID to create unique ID to append to user_address
This is how I want
and user_address looks like this
And this is how it's getting saved in firestore
So my question Is how I append data with unique id do I have to create a collection inside users field or the above is possible?
Below is my code I tried to set and update even user FieldValue.arrayUnion(userServiceAddress) but not getting the desired result
var uuid = Uuid();
var fireStoreUserRef =
await FirebaseFirestore.instance.collection('users').doc(id);
Map locationMap = {
'latitude': myPosition.latitude,
'longitude': myPosition.longitude,
};
var userServiceAddress = <String, dynamic>{
uuid.v4(): {
'complete_address': completedAddressController.text,
'floor_option': floorController.text,
'how_to_reach': howtoreachController.text,
'location_type': locationTag,
'saved_date': DateTime.now().toString(),
'user_geo_location': locationMap,
'placeId': addressId
}
};
await fireStoreUserRef.update({'user_address': userServiceAddress});
If I use set and update then whole data is replaced with new value it's not appending, so creating a collection is the only solution here and If I create a collection then is there any issue I'll face?
You won't have any issues per se by storing addresses in a separate collection with a one-to-many relationship, but depending on your usage, you may see much higher read/write requests with this approach. This can make exceeding your budget far more likely.
Fortunately, Firestore allows updating fields in nested objects via dot notation. Try this:
var userServiceAddress = {
'complete_address': completedAddressController.text,
'floor_option': floorController.text,
'how_to_reach': howtoreachController.text,
'location_type': locationTag,
'saved_date': DateTime.now().toString(),
'user_geo_location': locationMap,
'placeId': addressId
};
await fireStoreUserRef.update({'user_address.${uuid.v4()}': userServiceAddress});

How add data to a map in Firestore document Flutter

I am trying to add key value pairs to a map of maps in a firestore document. You can see the document set up below. The "Bills" map consists of key value pairs representing each bill. Each bill is represented by a map. For example: "Bill 1" consists of key value pairs which represent the name of the item and the cost of the item.
I want to add key value pairs to the bill. For example, I want to add an item to "Bill 1", so that it consists of both "Bacon: 14" and the other items I add.
Here is the code I have right now. However, this code simply replaces Bacon with the new item I add. It doesn't add the new value to the existing map, simply replaces the entire map (the contents of Bill 1) with the new item and cost.
Here is the current code:
static Future addItemToBill(GroceryItem groceryItem, double cost) async {
try{
final FirebaseUser currentUser = await FirebaseAuth.instance.currentUser();
int currentBillNumber = await getBillCount(currentUser.uid);
if(currentBillNumber == 0){
currentBillNumber = 1;
}
final DocumentReference documentReference = usersCollection.document(currentUser.uid);
await documentReference.updateData({
"Bills.Bill $currentBillNumber": {
"${groceryItem.itemName}" : cost
}
});
}catch(error){
}
}
Current Code
Thank you for the help!
When you call updateData, Firestore will always replace everything behind each key you provide. In your case, the key is "Bills/Bill $currentBillNumber", and it will contain only what you provide, replacing what's already there at the given key.
If you want to add something new, your will will have to be specific enough that it does not replace other values.
await documentReference.updateData({
"Bills.Bill $currentBillNumber.${groceryItem.itemName}": cost
});
I guess could be better you create a collection of bills inside of each user, then you will have a collection of documents (bills), inside of each document (bill) you create another collection called items and then ou will have a list of documents (items), will be easy to query later i think.
You should have like this:
-collection(users)
--document(user)
---collection(bills)
----document(bill)
-----collection(items)
------document(item)
Got it?
What about having all bills as a new, highest level, collection and then use the userUID as a key in each bill document?
Then, if you want all the bills for a single user:
db.collection('allBills').
.whereEqualTo("userUID", currentUser.uid)
.get()
Less nesting of collections and documents.

Fetch collection startAfter documentID

Is there a way to fetch document after documentID like
private fun fetchCollectoionnAfterDocumentID(limit :Long){
val db = FirebaseFirestore.getInstance()
var query:Query = db.collection("questionCollection")
.startAfter("cDxXGLHlP56xnAp4RmE5") //
.orderBy("questionID", Query.Direction.DESCENDING)
.limit(limit)
query.get().addOnSuccessListener {
var questions = it.toObjects(QuestionBO::class.java)
questions.size
}
}
I want to fetch sorted questions after a given Document ID. I know I can do it using DocumentSnapShot. In order to fetch the second time or after the app is resume I have to save this DocumentSnapshot in Preference.
Can It be possible to fetch after document ID?
startAfter - > cDxXGLHlP56xnAp4RmE5
Edit
I know I can do it using lastVisible DocumentSnapshot . But I have to save lastVisible DocumentSnapshot in sharedPreference.
When app launch first time 10 question are fetched from questionCollection. Next time 10 more question have to be fetched after those lastVisible. So for fetching next 10 I have to save DocumentSnapshot object in sharedPreference. Suggest me a better approach after seeing my database structure.
And one more thing questionID is same as Document reference ID.
There is no way you can pass only the document id to the startAfter() method and simply start from that particular id, you should pass a DocumentSnapshots object, as explained in the official documentation regarding Firestore pagination:
Use the last document in a batch as the start of a cursor for the next batch.
first.get().addOnSuccessListener(new OnSuccessListener<QuerySnapshot>() {
#Override
public void onSuccess(QuerySnapshot documentSnapshots) {
=// Get the last visible document
DocumentSnapshot lastVisible = documentSnapshots.getDocuments()
.get(documentSnapshots.size() -1);
// Construct a new query starting at this document,
Query next = db.collection("cities")
.orderBy("population")
.startAfter(lastVisible) //Pass the DocumentSnapshot object
.limit(25);
// Use the query for pagination
}
});
See, here the lastVisible is a DocumentSnapshot object which represents the last visible object. You cannot pass only a document id. For more information, you can check my answer from the following post:
How to paginate Firestore with Android?
It's in Java but I'm confident you can understand it and write it in Kotlin.
Edit:
Please consider defining an order of your results so that all your pages of data can exist in a predictable way. So you need to either specify a startAt()/startAfter() value to indicate where in the ordering to begin receiving ordered documents or use a DocumentSnapshot to indicate the next document to receive, as explained above.
Another solution might be to put the document id into the document itself (as a value of a property) and order on it, or you can use FieldPath.documentId() to order by the id without having to add one.
You can also check this and this out.
There is one way to let startAfter(documentID) works.
Making one more document "get", then using the result as startAfter input.
val db = FirebaseFirestore.getInstance()
// I use javascript await / async here
val afterDoc = await db.collection("questionCollection").doc("cDxXGLHlP56xnAp4RmE5").get();
var query:Query = db.collection("questionCollection")
.startAfter(afterDoc)
.orderBy("questionID", Query.Direction.DESCENDING)
.limit(limit)
A simple way to think of this: if you order on questionID you'll need to know at least the value of questionID of the document to start after. You'll often also want to know the key, to disambiguate between documents with the same values. But since it sounds like your questionID values are unique within this collection, that might not be needed here.
But just knowing the key isn't enough, as that would require Firestore to scan its entire index to find that document. Such an index scan would break the performance guarantees of Firestore, which is why it requires you to give you the information it needs to perform a direct lookup in the index.

Transaction only works once with simultaneous calls

We are using the code below in an application to execute a write to the database. However, multiple clients may be writing at the same time so we used a Transaction as this is supposed to run again if the data is modified by another Transaction during execution.
Something seemed to go wrong though: When seperate clients (two different mobile devices) are running the code below ± simultaneously, only one of them seems to update the value in the database.
What could be the cause of this and how can we prevent this in the future?
static Future< bool > voteOnUser(GroupData groupData, String voteeID) async {
await Firestore.instance.runTransaction((Transaction transaction) async {
DocumentReference groupRef = Firestore.instance
.collection("groups")
.document( groupData.getGroupCode() );
DocumentSnapshot ds = await transaction.get( groupRef );
/// Initialize lists
Map<dynamic, dynamic> dbData = ds.data['newVotes'];
Map<String, int> convertedData = new Map<String, int>();
/// Loop the database Map and add the values to the data Map
/// All data is assumed to exist and definitely be of the desired types
dbData.forEach( (key, value) {
convertedData[key.toString()] = value;
} );
if (convertedData.containsKey( voteeID ))
convertedData[voteeID] += 1;
else convertedData[voteeID] = 1;
Map<String, dynamic> incremented = new Map<String, dynamic>();
incremented['newVotes'] = convertedData;
await transaction.update(groupRef, incremented);
});
return true;
}
Edit: The purpose of this code is to vote on a user that is located in a map within a document. Several clients may vote at the same time and each vote should increment the value of one element in the map by one.
When two clients vote at the same time on for example the user with ID 'joe' who had no votes before, the expected result is that joe would have two votes. However, when testing simultaneous voting, we observed that only one vote was added to the database instead of the desired two votes.
Edit 2: At first, the document exists and contains an empty map newVotes. We were testing with two different mobile devices and casted a vote at ± the same point in time. Almost instantly, the document was updated and a user ID was added to the previously empty map with one vote while we expected two votes to be mapped to that key.
Edit 3: First, a document is created with the following fields: newVotes (map) to map a number of votes to a unique user id, totalVotes (map) to keep track of the total votes and members (List) which is a list of all user id's in that group. newVotes And totalVotes are empty at this point and members contains a few IDs (in this case, two unique Google IDs)
Once this document exists, clients can vote with the given code. In my case we both voted on the same google user ID (say: "ID1") and expected the map newVotes to contain one element: {"ID1": 2}. However, we both voted at the same time (no errors were thrown) and the newVotes field contained {"ID1":1}.
Transactions does not guarantee protection from modification of the same entity concurently - unless SERIALIZED (or "Read for update locking"). It guarantees that every operations in transaction will be applied at once upon commit or discarded if tx is rolled back
To guarantee that there are no conflicts either:
Synchronize access to entity - lock upon reading
Synchrionize method execution
Serialize transaction
Use version controll mechanism

Resources