I'm building a fullstack todo app using firebase to store user information. My idea is that each user can create multiple lists and write multiple 'todos' in every list (eg. Have a list for today and another for tomorrow). My data is structured in this manner:
users (collection)
-> usersID (every user is a document)
-> todos Lists (collection) -> todoId (document) -> (object with the data)
I can't seem to be able to query the multiple lists a user might have (by default, I'm currently just retriving data from the 'todos' collection). Either I'm failing to see some firestore functionality or I'm not structuring my database correctly. Any advice?
You are structuring your data fine. The problem is that the Firebase Client SDK doesn't offer a function to retrieve all subcollections a document has. To solve your problem you got multiple options.
1. The cleanest but most exhaustive way
You upgrade to the Blaze Plan and use Firebase Functions with the Admin SDK. The Admin SDK offers a function called listCollections() which exactly does what you want. The disadvantage is that Firebase Functions have something called coldstart which means that when your function was idle for some minutes it takes up to 20 seconds (from my experience) to run again after you called it, which can be really frustrating. Otherwise you can run a free node.js server on something like Heroku to use the Admin SDK there to avoid coldstart (better option in my opinion). The Admin SDK is explicitly not meant to be used on the client side. Check this thread
2. List all your todo lists (easiest way)
Everytime the user creates a new list, you add that to an array in your users document like this:
// Representation of your user document
user
name: "Florian"
lists: [
"todos",
"anotherList"
]
So in that way you can just fetch your user, get the lists property from it and call all lists with a loop over the lists array. As alternative just display the lists content and let the user open it explicitly to save reads.
3. Restructuring your database (I would recommend it)
If you don't have a lot of data yet, I would recommend to restructure your database like:
users - the collection with all your users (without the todo list subcollections
lists - containing a document for every created list
- each document contains a field with `owner`
- each document contains a field with the list name
- each document owns a subcollection with todos
If you use this way you could just query the database like
const getLists = async () => {
const ref = collection(db, 'lists');
const q = query(ref, where('owner', '==', theUserYouWantToQuery);
// returns array with all list (including listname) of that user.
const lists = (await getDocs(q)).docs.map(doc => ({ ...doc.data(), doc.id }));
}
when you have all list objects from that user you can either loop over that array to get all todos in the lists or let the user only fetch one list by opening it
Related
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.
I am trying to access the child value of a unique key value (that had been "pushed") within Firebase. Currently, my database looks like this: I want to access the value of "emailOfUser"
I am very new to Firebase so I am not familiar with the functions. Currently, this is my method of obtaining other values for a different section of the database:
Thank you so much for any feedback!
I've tried different methods to accessing this data within the Firebase, but I cannot get it to work/the methods I were using were outdated. I also tried to "update" the Firebase instead of "pushing" the values to prevent a unique key from generating, but it simply overwrote my current data rather than appending something new.
If you want to load all the users who voted and print their emails, you can do that with:
get(child(dbref, 'usersWhoVoted')).then((snapshot) => {
snapshot.forEach((childSnapshot) => {
console.log(childSnapshot.key, childSnapshot.val().emailOfUser);
});
})
Note that your current structure allows a user to vote multiple times. If you want to only allow them to vote once, use some identifier of the user as the key in your database structure:
userVotes: {
"uniqueIdOfUser1": "valueTheyVotedOn",
"uniqueIdOfUser1": "valueTheyVotedOn",
...
}
Now each user can by definition only vote once, If they vote again (assuming your security rules allow that), their new vote will simply replace the existing vote.
Recently I came across a problem, I am making a function where you can get rid of chats in my app so I have this function
function deleteconvo() {
const me = firebase?.auth?.currentUser?.uid;
const members = [me, recipient.uid];
firebase.db.collection("conversations").remove({ members });
}
Firebase is telling me that:
firebase.db.collection("conversations").remove({ members }); is not a function
I also tried .delete and I cant find any answers online. Anyone have a solution to this?
note: I understand that I have to grab the docs value too, because I am in the conversation collection of the the database each conversation has its own unique ID, which I dont know how to grab and store that in a variable.
A good starting point is the official Firestore documentation regarding how to delete data.
In that page you can find examples in different languages on how to delete documents, fields and collections.
For example to delete an entire collection or subcollection in Cloud Firestore you need to retrieve all the documents within the collection or subcollection and delete them.
And to delete a document you can use the delete() method:
const res = await db.collection('cities').doc('DC').delete();
`
What I need:
I want to save articles or notes in Firestore with their respective fields:
Title
Content (texts or paragraphs)
Creation date
Owners (to share that article with other
people and who can edit them like: https://firebase.google.com/docs/firestore/solutions/role-based-access)
But when I show the list of articles I don't need the "content" field (to save bandwidth). I've read that (maybe I'm wrong), it is not possible to make a query to get only specific fields from a document with Firestore.
If it were normal SQL to obtain specific columns from articles (without its content) It would be something like:
SELECT title, creation_date, ...
FROM table_name;
So I've opted to separate the content for two root-level collections (for flexibility and scalability)
My current structure:
Articles collection:
- `articles` [collection]
- `ARTICLE_ID` [document]
- `creatorId` [field]
- `title` [field]
- `date` [field]
- `owners` [obj field]
- {user1_id}: true
- {user2_id}: true
...
Contents collection:
- `contents` [collection]
- `{ARTICLE_ID}` [document]
- `content` [field]
To get articles list in realtime:
firebase.firestore().collection('articles')
.where(`owners.${user.uid}`, '==', true)
.onSnapshot(querySnapshot => {
const articles = []
querySnapshot.forEach((doc) => {
articles.push({
id: doc.id,
...doc.data()
})
})
// do something with articles array
})
To show in another view and get the entire article with its content:
const db = firebase.firestore()
const articleRef = db.collection('articles').doc(articleId)
const contentRef = db.collection('contents').doc(articleId) // same Id as article
articleRef.get().then(articleDoc => {
if (articleDoc.exists) {
contentRef.get().then(contentDoc => {
if (contentDoc.exists) {
const article = {
...articleDoc.data(),
...contentDoc.data()
}
// full article obj
}
})
}
})
My questions
Do you think it's better to do two queries (getArticle and getContent) at the same time and wait with Promise.all() instead of nesting the querys like I do?
Is there a better way to get the article and its content with one query or more efficiently? Some tips or ideas?
Thank you very much in advance!
According to the Firestore Query.select documentation you should be able to select the fields you want.
let collectionRef = firestore.collection('col');
let documentRef = collectionRef.doc('doc');
return documentRef.set({x:10, y:5}).then(() => {
return collectionRef.where('x', '>', 5).select('y').get();
}).then((res) => {
console.log(`y is ${res.docs[0].get('y')}.`);
});
Neither approach is pertinently better than the other. But there are a few key differences.
When you nest the reads, the second read only starts after the first read has completed. When you use Promise.all() both reads start at the same time, so can (partially) run in parallel.
On the other hand: when you use Promise.all() your completion handler (the code you run in then()) won't execute until both documents have loaded. If you nest the calls, you can update the UI after just the first document has loaded.
In the end, the differences are likely to be small. But since they may be significant to your use-case, measure the results and see what works best for you.
In order to output a single field from a Firestore document (version 9) - for example the 'title' in the articles collection you can use the following code snippet:
const q = query(collection(db, 'articles'))
let results = [];
await getDocs(q);
results = getLocation.docs.map((doc) => doc.data()['title']);
results.sort()
The results array will contain only the title field, sorted alphabetically
(Note you have to reference the Firestore db and import 'getDocs', 'query' and 'collection' modules from Firestore)
Firebase Hosting would be your best bet for static content such as articles. If you look at AMP-HTML for example, they strongly make the case for ultra-fast page loads and highlight benefits of edge caching. Firebase hosting is advertised to also support global edge caching.
Firestore and Firebase Realtime Database are database engines. These are not the proper tool for serving up articles.
I'm making a blog app using firebase.
I want to know the best practice of data structure.
As far as I know, there are 2 case.
(I'm using react native)
case 1:
posts
-postID
-title,content,author(userID),createdDate,favoriteCount
favorites
-userID
-favoriteList
-postID(onlyID)
-postID(onlyID)
In this case, for example, when we need to get favorite posts.
firebase.firestore().collection(`favorites/${userID}/favoriteList`)
.get()
.then((snapshot) => {
snapshot.forEach((favorite) => {
firebase.firestore().collection(`favorites/`).doc(`${favorite.id}`)
.get()
.then((post) => {
myPostList.push(post.data())
});
});
in this case, we can't order the favorite posts by createdDate. So, need to sort client side. Even if so, we don't use limit() function.
case 2:
posts
-postID
-title,content,author(userID),createdDate,favoriteCount
favorites
-userID
-favoriteList
-postID
-title,content,author(userID),createdDate,favoriteCount
-postID
-title,content,author(userID),createdDate,favoriteCount
firebase.firestore().collection(`favorites/${userID}/favoriteList`).orderBy('createdDate','desc').limit(30)
.get()
.then((snapshot) => {
snapshot.forEach((post) => {
myPostList.push(post.data())
});
});
in this case, When the favorite post is modified by the author,
we have to update all of the favorite posts. (e.g. If 100 users save the post as a favorite, we have to update to 100 data.)
(And I'm not sure we can increment favoritecount by a transaction, exactly same.)
I think if we use firebase.batch(), we can manage it. But I think it seems Inefficient.
It seems that both ways are not perfect. Do you know the best practice of this case?
What about using arrays or Collection Groups?
solution 1: arrays
posts
-postID
-title,content,author(userID),createdDate,favoriteCount
-[favoriters(userID)]
Now you can query for a user's favorites by querying posts that "array-contains" the user's ID. You can also modify individual posts without iterating through a bunch data copies.
There's a limit to this approach though. Maximum size for a document is 1 MiB; assuming that a user ID is 4 bytes, a document can contain no more than 250K favoriters. Clients would also have to do some O(N) processing to add / remove favoriters.
solution 2: Collection Groups
posts
-postID
-title,content,author(userID),createdDate,favoriteCount
-favoriters {collection}
-userID
A collection group consists of all collections with the same ID. By default, queries retrieve results from a single collection in your database. Use a collection group query to retrieve documents from a collection group instead of from a single collection.
So we can fetch a user's favorite posts via
db.collectionGroup("favoriters").whereEqualTo("userID", <userID>).get();
To favorite a post, we just do
const postsRef = db.collection("posts");
postsRef.document(<postID>).collection("favoriters").add({ "userID", <userID> });
Maybe not a direct answer to your question, but the official documentation has an example for that:
Working with arrays, lists, and sets
Summary: Store and query data in array-like structures in documents.
Use case: If your app requires complex data objects like arrays,
lists, or sets, follow the model outlined in this solution. For
example, in a blogging app, you might want to create a set of related
posts.
https://firebase.google.com/docs/firestore/solutions/arrays