We have a business rule that one account can only have 3 projects at any given time.
In order to keep it efficient, we track the number of projects in an "userData" field instead of doing a COUNT query
Consider the following example objects already in DynamoDB:
userData : { createdProjects : 2 }
project1 : { id : 1 }
project2 : { id : 2 }
In order to enforce this rule, we've done the following when creating a project (pseudo code)
in transaction:
putItem(key = "project3", object = { id : 3 })
updateItem(
key = "userData",
expression = "createdProjects = createdProjects + 1"
condition = "createdProjects < 3"
)
Now, if the user tries to create a project at the same time with two computers let's say, will DynamoDB guarantee that he won't be able to create more than 3?
I know there are similar questions like this one, but I wanted to know if this also works in a transaction, because my condition is in another object.
Also, is my pseudo code the best approach? open to other ways
You can use a transaction for this.
Just include the PutItem request and UpdateItem request with the condition in a transaction and either both will complete or none of them.
Transactions are the way to provide this all or nothing behavior.
With the transaction write API, you can group multiple Put, Update, Delete, and ConditionCheck actions. You can then submit the actions as a single TransactWriteItems operation that either succeeds or fails as a unit. The same is true for multiple Get actions, which you can group and submit as a single TransactGetItems operation.
— docs
I'm working on a Flutter Restaurant application where each restaurant has a cloud firestore document and it in a field called queueNumber this value starts at 1 and with every order it increases by 1.
I'm trying to make sure each order has a unique queue number. I have a cloud function that triggers whenever a new document created in the orders collection. Here is the following code.
.onCreate(async (snapshot, context) => {
const orderData = snapshot.data();
const id = orderData.id;
if (orderData && orderData.restaurantId != null) {
return restDoc.update({
queueNumber: admin.firestore.FieldValue.increment(1)
})
}
});
So the user places an order with the existing queueNumber in the restaurant document. Than the cloud function increments the queueNumber so the next request has a queueNumber that is 1 higher than the previous.
Here is the problem: Sometimes when two orders are placed one after another they get the same queueNumber. The end result in restaurant document is correct but the individual orders get the wrong number (ex: Order 1 has 51 Order 2 has 51 Restaurant document has 53)
Is there a way to fix this method or a better approach to handle the queue numbers
Thanks.
You're running into a race condition between each of the clients that's adding a document. Firestore doesn't offer a built-in way to ensure that a field is unique, nor does it offer a way to automatically and safely set a value of a field based on the contents of other documents. This wouldn't scale in the way that Firestore requires.
You should first find a way to implment your app without increasing numbers like this. Check if maybe a timestamp is a better way to track the time order in which documents are added. That will scale much better.
If you absolutely need increasing numbers like this, you will have to involve a whole new document just to track the latest number assigned, and use that document in a transaction when adding new documents. The transaction will have to:
Read the counter document
Increment the count value in memory
Create the new document with this value
Also update the counter document with this value
All of this must be done within the transaction, or will not be safe.
Think of a poll app, which has a map in a Firestore document.
This map has the vote key as string and a number as value (counter).
For instance “what is the best food”:
{
...
...
poll : [
pizza : 10,
barbecue: 3,
pasta: 1,
...
]
...
}
The items can vary depending on the poll. Only one vote per user. Only logged in users can vote (Firestore rule).
The minimum amount is zero, people can switch from one to another (decrease moment).
Also, the increase should be only +1.
Transactions protect the app to avoid less than 0 for any item and +1 is controlled there.
Is there a way to protect also with the Firestore rules?
To be honest, I’m not sure if it’s necessary.
I can get the affected items with map diff functions. But how do I check for that specific changed item value?
The map diff result brings a set, however I cannot use the keys retrieved by the set in the request.resource.data, otherwise the problem would be solved.
I could check whether it’s 0 and also check with current data if the difference is 1.
The key will be in the map for sure, since it comes from the diff function from the map itself.
Update Feb 2021: this doesn't work for the latest firebase since the keys are a set which can't be indexed
You can get the affected keys using
let affectedKeys = request.resource.data.poll.diff(resource.data.poll).affectedKeys();
However according to the firebase docs on MapDiff it's not possible to get the changes in values, only in keys.
If you know how many poll items there are, you can do something like
let differenceOfOne = request.resource.data.poll[affectedKeys[0]] == resource.data.poll[affectedKeys[0]] + 1 || request.resource.data.poll[affectedKeys[0]] == resource.data.poll[affectedKeys[0]] - 1
and repeat that line for every index up to the maximum number of items (loops are not allowed in firebase rules).
Using firebase real time database i want to move points from user to another but to keep conflicts away ( may user get coins from multi other users at the same time ) i have to use transactions.
My data structure :
{
uid-1:
{
points: 30
},
uid-2:
{
points:60
}
}
So i need two transactions one substracts uid-1 and second increases uid-2
But I'm afraid of that if one transaction success and other one fails .. any sol to revert the operation or update both same time?
There is no secure way to implement conditionality between multiple transactions.
If both operations depend on each other they should be run as a single transaction. That means you have an optimistic lock on the entire "users", but in your current data structure and solution that is required.
An alternative is to not update the balance, but just keep a list of transactions. In that case you can ensure both the addition for the first user and subtraction for the second user are written atomically by using a multi-location update. In JavaScript this would look something like:
ref = firebase.database().ref("users");
var updates = {};
let transactionID = ref.push().key;
updates["uid1/transactions/"+transactionID] = 20;
updates["uid2/transactions/"+transactionID] = -20;
ref.update(updates);
The above write operation will either succeed completely, or fail completely. This ensures your database is always correct.
I'm struggling to make a (not so) complex query in firebase cloud firestore.
I simply need to get all docs where the id field == a given id.
Then order the results by their date and limit to 10 results.
So this is my current situation
db.firestore().collection('comments')
.where("postId",'==',idOfThePost)
.orderBy('date','asc').limit(10).get().then( snapshot => {
//Nothing happens and the request wasn't even been executed
})
I can get the result only if i don't use the orderBy query but i have to process this sorting for the needs of my application.
Someone has an idea to help me to fix this ?
thanks
You can do this by creating an Index in the firestore.
The first field of the index should be the equality field and the second field of the index should be the order by field.
Given your problem, you would need the following index:
first field: postId, 'asc'
second field: date, 'asc'
Please check the doc. It says
However, if you have a filter with a range comparison (<, <=, >, >=), your first ordering must be on the same field
you can try this code
db.firestore().collection('comments')
.where("postId",'==',idOfThePost)
.orderBy('postId')
.orderBy('date','asc').limit(10).get().then( snapshot => {
.....
})
My Workaround
If you're googling this you've probably realized it can't be done traditionally. Depending on your problem though there may be some viable workarounds, I just finished creating this one.
Scenario
We have an app that has posts that appear in a feed (kind of like Reddit), each post has an algorithmic score 'score' and we needed a way to get the 'top posts' from 12-24 hours ago. Trying to query sorted by 'score' where timestamp uses > and < to build the 12-24 hour ago range fails since Firebase doesn't allow multiple conditional querying or single conditional querying with an descending sort on another field.
Solution
What we ended up doing is using a second field that was an array since you can compound queries for array-contains and descending. At the time a post was made we knew the current hour, suppose it was hour 10000 since the server epoch (i.e. floor(serverTime/60.0/60.0)). We would create an array called arrayOfHoursWhenPostIsTwelveToTwentyFourHoursOld and in that array we would populate the following values:
int hourOffset = 12;
while (hourOffset <= 24) {
[arrayOfHoursWhenPostIsTwelveToTwentyFourHoursOld addObject:#(currentHour+hourOffset)];
hourOffset++;
}
Then, when making the post we would store that array under the field hoursWhenPostIsTwelveToTwentyFourHoursOld
THEN, if it had been, say, 13 hours since the post was made (the post was made at hour 10000) then the current hour would be 10013, so we could use the array-contains query to see if our array contained the value 10013 while also sorting by algorithm score at the same time
Like so:
FIRFirestore *firestore = [Server sharedFirestore];
FIRCollectionReference *collection = [firestore collectionWithPath:#"Posts"];
FIRQuery *query = [collection queryOrderedByField:#"postsAlgorithmScore" descending:YES];
query = [query queryWhereField:#"hoursWhenPostIsTwelveToTwentyFourHoursOld" arrayContains:#(currentHour)];
query = [query queryLimitedTo:numberToLoad];
Almost Done
The above code will not run properly at first since it is using a compound index query, so we had to create a compound index query in firebase, the easiest way to do this is just run the query then look at the error in the logs and firebase SDK will generate a link for you that you can navigate to and it will auto-generate the compound index for your database for you, otherwise you can navigate to firebase>database>index>compound>new and build it yourself using hoursWhenTwelveToTwentyFourHoursOld: Arrays, score: Descending
Enjoy!
same here, it is weird why can't. below is another sample. can't get the result. Hoping firebase can reply about this and update the document.
dbFireStore.collection('room').where('user_id.'+global.obj_user.user_id,'==',true).orderBy('last_update').get().then((qs)=>{
console.log(qs);
});
using other work-around solution is javascript array and array.sort()
I ran into the same issue yesterday on Android. The Callback was just not called. Today I suddenly got an error message. FAILED_PRECONDITION: The query requires an index. It even contains a URL in the error message to generate that index with one click.
It seems that if you want to use orderBy on your data, you need to create an index for that field. The index also needs to be in the correct order (DESC, ASC).
As per firestore document,
If you attempt a compound query with a range clause that doesn't map to an existing index, you receive an error. The error message includes a direct link to create the missing index in the Firebase console.
So just click that link you get in Logcat, it will be redirected to create index page, just create index. It will take some time. after enabling composite index, you will get the result as your requested query.
Stumbled across this looking for help when i found that using the orderBy function didnt work and the documentation still says it does not support it. A bit weird and unclear to be honest, because it does support it so long as you index your Firestore database. For example, this query now works fine for me having set up indexing:
const q = query(docRef, where("category", "==", 'Main'), orderBy('title', 'asc')
Indexing in Firestore
Console Log that even gives you the url to automatically create the index if you try and run with the above command.
Maybe I am missing something, or a later version of Firebase (I am using v9) simply does support it.