Firestore transaction sequential creation of collections and updating of documents - javascript

Enviroment: nodejs, firebase-admin, firestore.
Database scructure (space):
Database scructure (user):
Creating new space (example):
// init data
const userId = "someUserId";
// Create new space
const spaceRef = await db.collection("spaces").add({ name: "SomeName" });
// Get spaceId
spaceId = spaceRef.id;
// Get user Doc for upate their spaces
const userRef = await db.collection("users").doc(userId);
// Add "spaceId" to user spaces list
userRef.collection("spaces").doc(spaceId).set({ some: "data" });
// Create collection "members" in new space with "userId"
spaceRef.collection("members").doc(userId).set({role: "OWNER"})
Question: I want to execute this code inside single runTransaction, but as I see transactions support only one-time read and multiple update, this does not suit me, since I get the spaceId I need during the execution of the code.
Why I want to use transaction: In my data structure, the relationship between the created space and the presence of the ID of this space on the user is required. If we assume that an error occurred during the execution of this code, for example, the space was created, but this space was not added inside the user profile, then this will be a fatal problem in my database structure.
Similar to other databases, transactions solve this problem, but I can't figure out how to do it with firestore.
Maybe you know a better way to protect yourself from consistent data in this case?

Actually, you don't need a Transaction for that, since you are not reading documents.
With db.collection("users").doc(userId); you are actually not reading a document, just calling "locally" the doc() method to create a DocumentReference. This method is not asynchronous, therefore you don't need to use await. To read the doc, you would use the asynchronous get() method.
So, using a batched write, which atomically commits all pending write operations to the database, will do the trick:
const userId = 'someUserId';
const userRef = db.collection('users').doc(userId);
const spaceRef = firestore.collection('spaces').doc();
const spaceId = spaceRef.id;
const writeBatch = firestore.batch();
writeBatch.set(spaceRef, { name: "SomeName" });
writeBatch.set(userRef.collection("spaces").doc(spaceId), { some: "data" });
writeBatch.set(spaceRef.collection("members").doc(userId), {role: "OWNER"});
await writeBatch.commit();
You should include this code in a try/catch block and if the batch commit fails you will be able to handle this situation in the catch block, knowing that none of the write were committed.

Related

Reducing the amount of reads in a where query

I have recently implemented firebase into my project and I have created a user collection, this collection has a document for each user and each document has about 8 fields, when my user launches the app, I am trying to pull the document that corresponds to his data, so im doing the following query:
async function getUserData() {
const _collection = collection(db, "users")
const _query = query(_collection, where("userid", "==", uniqueUserID))
const querySnapshot = await getDocs(_query)
querySnapshot.forEach((doc) => {
console.log(doc.data())
})
setLoadingStatus(false)
}
This query works and gives me the corresponding user data, but the problem is, if the user is too far down the collection, this will execute 8 reads per document until it gets to the corresponding user, I have tried to implement a cache system using a lastModified but I still need to read the document data for that field and it will end up using more or less the same amount of reads. My question is: How do I reduce the amount of read operations that get executed when im trying to compare values in the documents, I have also thought of adding an a like so a_uniqueUserID so it gets ordered alphabetically and takes the first spot of the document but it's hacky.
EDIT: Here is what my structure looks like:
I think you are misunderstanding the definition of a document and a field. When you read a document, you always get all fields out of it. The snapshot contains everything read, even if you don't use it. There is no additional cost per field, other than the storage required to hold it all. In your screenshot, you show 5 documents, and one of those documents have 8 fields.
You are probably misunderstanding the metrics in the console. When you read and write documents using the console, those are also billed as reads and writes - use of the console is not "free". What you are seeing is a combination of what your app is doing in combination with what you're doing in the console.

Pg-promise: does changing the search_path inside a db.task() also change it for queries running outside?

Let's say I have the following code:
db.task(t => {
return t.none('set search_path to myschema').then(() => {
return t.any('select * from mytable').then(results => {
return t.none('set search_path to originalschema').then(() => {
return results
})
})
})
})
Could a query outside of db.task(), that happened to run in between of the two search_path changes inside db.task(), actually access the data in 'myschema' instead of 'originalschema'?
Could a query outside of db.task(), that happened to run in between of the two search_path changes inside db.task(), actually access the data in 'myschema' instead of 'originalschema'?
No.
SET search_path is a session-based operation, i.e. it applies only to the current connection, which the task allocates exclusively for the entire duration of its execution.
Once the task has finished, it releases the connection back to the pool. At that point, any query that gets that same connection will be working with the alternative schema, unless it is another task that sets the schema again. This gets tricky, if you are setting schema in just one task, and generally not recommended.
Here's how it should be instead:
If you want to access a special-case schema inside just one task, best is to specify the schema name explicitly in the query.
If you want to set custom schema(s) dynamically, for the entire app, best is to use option schema, of the Initialization Options. This will propagate the schema automatically through all new connections.
If you want to set schema statically, there are queries for setting schema permanently.
Addition:
And if you have a very special case, whereby you have a task that needs to run reusable queries inside an alternative schema, then you would set the schema in the beginning of the task, and then restore it to the default schema at the end, so any other query that picks up that connection later won't try to use the wrong schema.
Extra:
Example below creates your own task method (I called it taskEx), consistent across the entire protocol, which accepts new option schema, to set the optional schema inside the task:
const initOptions = {
extend(obj) {
obj.taskEx = function () {
const args = pgp.utils.taskArgs(arguments); // parse arguments
const {schema} = args.options;
delete args.options.schema; // to avoid error thrown
if (schema) {
return obj.task.call(this, args.options, t => {
return t.none('SET search_path to $1:name', [schema])
.then(args.cb.bind(t, t));
});
}
return obj.task.apply(this, args);
}
}
});
const pgp = require('pg-promise')(initOptions);
So you can use anywhere in your code:
const schema = 'public';
// or as an array: ['public', 'my_schema'];
db.taskEx({schema}, t => {
// schema set inside task already;
});
Note that taskEx implementation assumes that the schema is fully dynamic. If it is static, then there is no point re-issuing SET search_path on every task execution, and you would want to do it only for fresh connections, based on the following check:
const isFreshConnection = t.ctx.useCount === 0;
However, in that case you would be better off using initialization option schema instead, as explained earlier.

Firestore JS - incrementing within transactions

I have some code that looks like the following:
export const createTable = async (data) => {
const doc = db.collection("tables").doc();
const ref = db
.collection("tables")
.where("userId", "==", data.userId)
.orderBy("number", "desc").limit(1);
db.runTransaction(async transaction => {
const query = await transaction.get(ref);
let number = 1;
if (!query.empty) {
const snapshot = query.docs[0];
const data = snapshot.data();
const id = snapshot.id;
number = data.number + 1;
}
data = {number, ...data};
transaction.set(doc, data);
});
Basically I have a tables collection and each table has an auto generated number like #1, #2, #3
When creating new tables, I want to fetch the latest table number and create the new table with that number incremented by 1.
I wanted to wrap it in a transaction so that if a table created while running the transaction, it will restart so that I don't end up with duplicate numbers.
However, this errors out on the .get(), and from googling I've read that Firestore can't monitor a whole collection within transactions, but instead it requires a specific doc passed to it. Which I obviously can't do because I need to monitor for new docs created in that collection, not changes in a particular doc.
If so, what's the correct way to implement this?
The Firestore transaction API for client apps requires that you get() each individual document that you want to participate in the transaction. So, if you have a query whose results you want to transact with, you will need to:
Perform the query (outside of the transaction)
Collect document references for each document in the result set
In the transaction, get() them all individually.
You will be limited to 500 documents per transaction.
If you want to dynamically look for new documents to modify, you will probably much better off implementing that on the backend using a Firestore trigger in Cloud Functions to automatically handle each new document as they are created, without requiring any code on the client.
Because you're updating just one document, you probably don't need to use transactions for incrementing values.
You can use Firestore Increment to achieve this.
Here is an example taken from here:
const db = firebase.firestore();
const increment = firebase.firestore.FieldValue.increment(1);
// Document reference
const storyRef = db.collection('stories').doc('hello-world');
// Update read count
storyRef.update({ reads: increment });
This is the easiest way to increment values in Firestore.

Could I get 'creationTime' when I create a new document in Firestore (instead of using 'documentRef.get()' after creation and thus avoid that reading?

Goal: get the creation time of a new Firestore document in the document creation process
First I create a new document with the corresponding data newDocumentData and a field creationTime with the creation time, as shown below:
const newDocumentRef = await collectionRef.add({
...newDocumentData,
creationTime: firebase.firestore.FieldValue.serverTimestamp()
})
Then, I need the creationTime, and I follow like this...
const document = await newDocumentRef.get()
if (document.exists) {
const documentData = document.data()
}
...to finally get my documentData.creationTime.
My question is: Is there any way to get creationTime with newDocumentRef in the first step and, therefore, avoiding the rest of the process?
Thank you!
No, it is not possible with the Client SDKs, "to get the value of creationTime in the first step". FieldValue.serverTimestamp() returns a sentinel value that tells the server to assign a server-generated timestamp in the written data.
In other words the exact value for creationTime will be calculated by the server, and if you want to get this value, you need to query the database for it.
Note that, on the other hand, with the Firestore REST API, when you create a document, you get back (i.e. through the API endpoint response) a Document object that contains, among others, a createTime field.

More direct method for updating fields in Firebase document without document ID?

The goal is to update a Firebase document.
The document ID is unknown at the time of updating. What is known is a property that acts as unique key on the document (email address in the case below).
Based on the official documentation and this answer, the code below works as a method for updating an array within the document.
This feels clunky, though.
Are there more direct methods for retrieving a reference to a single document and updating its fields?
// Set query.
let query = firebase.firestore().collection('users').where('emailAddress', '==', 'test#test.com');
// Run query.
try {
const querySnapshot = await query.get();
return querySnapshot.docs[0].ref.update({
designs: firebase.firestore.FieldValue.arrayUnion('foobar')
})
} catch(e) {
console.log('Error getting user: ', e);
}
No, what you're doing is the best you can do. If you don't know the ID, you have to query to find it. Firestore has no equivalent of a SQL "update where" command.

Categories