flutter query multiple collections in firestore - firebase

I am playing with flutter but I ran into an issue with firestore that I can't figure out.
let's say I would like to retrieve the purchaser history of a customer and I have a firestore as described below so I have a collection of "user" within that contains a document of user_id's and then within that, I have a "products" collection with a document that contains the product_id's my aim is to gather the product id's for the selected user and then retrieve the products price from the products collection?
users
user_id_1
products
purchace_id
product_id
quantity
products
product_id
product_desc
price
the issue I am having is while I can get the id for the products from the user table but I can't then see a simple way of how I can use this data like with SQL where we would make a simple join on the product_id to get the price where the id's match?
any help would be much appreciated.
Thanks for replying Frank van Puffelen 2 seprate querys seams reasonable but it brings up other issues with the second query as now with the code below i can get the documents_id from the user and then do a query on the products and everything looks ok so i think i am going in the correct direction as i can print the id of the document in the loop to ensure i am accessing the correct ones but when i change this to get a snapshot of the document i cant access the data within this is probably me doing something silly or misunderstanding something but this feels very awkward compared to sql when trying to get and work with the data. For example if i return the data it wants it to be in the form of a future> however once i return the data in that format then i can no longer access the id's in the same way as i was doing.
Future<List<DocumentSnapshot>> getSeedID() async{
var data = await Firestore.instance.collection('users').document(widget.userId).collection('products').getDocuments();
var productList = data.documents;
print("Data length: ${productList.length}");
for(int i = 0; i < productList.length; i++){
var productId = Firestore.instance.collection('products').document(productList[i]['productId']).documentID;
if(productId != null) {
print("Data: " + productId);
}
}
return productList;
}

Short answer i didn't pay attention to how you use a future
Get the productId from the user table
Future<List<DocumentSnapshot>> getProduceID() async{
var data = await Firestore.instance.collection('users').document(widget.userId).collection('Products').getDocuments();
var productId = data.documents;
return productId;
}
Then call this using .then() rather than try to apply the returned data to the variable as that is not how a future it works
var products;
getProduceID().then((data){
for(int i = 0; i < s.length; i++) {
products = Firestore.instance.collection('products')
.document(data[i]['productID'])
.snapshots();
if (products != null) {
products.forEach((product) {
print(product.data.values);
});
}
}
});

Related

how to filter orderby nested list firebase to flutter App with rest database realtime

I use firebase database filter orderBy seller_id in nested list, why is the result null? thank you 🙏
:rules Firebase
var url = Uri.parse(
'https://MeDatabase.firebasedatabase.app/orders.json?auth=$authToken&orderBy="list_produk/seller_id"&equalTo="$idUser"');
try {
final response = await http.get(url);
if (response.statusCode >= 400) {
throw EasyLoading.showError('Cek Koneksi Internetmu!');
}
final extractData =
convert.jsonDecode(response.body) as Map<String, dynamic>;
print('inii orderan');
print(extractData);
if (extractData == null) {
return _orders = [];
}
} catch (e) {
throw e;
}
You're trying to index and order each order on its list_produk/seller_id property.
But if I look at your JSON data, there is no list_produk/seller_id property under the order. There's only list_produk/1/seller_id, and presumably list_produk/0/seller_id. Those are not the same as what you use in your rules/query, so the query returns no results.
Firebase Realtime Database queries function on a single, flat list of nodes. The value you order/filter on must exist at a fixed path under each direct child node.
So in your example, you can search across all orders on for example id_user, or you can search across the seller_id of the products in a specific order, but you can't search across all sellers of products in all orders.
In other words: your current data structure makes it easy to find the products and sellers per order, it does however not make it each to find the orders per product across all users. To allow that you'll want to add an additional data structure, typically known as a reverse index, that maps from each seller_id to the orders that contain products from that seller.
For more on this, I recommend checking out:
Firebase query if child of child contains a value
Firebase Query Double Nested
Many to Many relationship in Firebase

Flutter/Dart/Firebase - Updated nested map values

I am trying to record which on my users has purchased which ticket in my app. I am using firebase to store my data about my users and giveaways. When a purchase is complete, I am trying to update the relevant giveaway and assign each ticket to a user using their id.
Firstly, I am not sure if my data schema is the most appropriate for what I'm trying to achieve so open to suggestions for editing as I'm still quite new to the flutter world.
Second, here is how my data is currently structured:
Here is how I have structured my code. Here is my SingleBasketItem model:
class SingleBasketItem {
final String id;
final String uid;
final OurGiveAways giveaway;
final int ticketNumber;
SingleBasketItem(this.id, this.uid, this.giveaway, this.ticketNumber);
}
Here is my Shopping Basket model, I have added an Elevated Button to my shopping basket page which, when tapped, will execute the placeOrder() function:
class ShoppingBasket extends ChangeNotifier {
Map<String, SingleBasketItem> _items = {};
Map<String, SingleBasketItem> get items {
return {..._items};
}
void addItem(String id, String uid, OurGiveAways giveaway, int ticketNumber) {
_items.putIfAbsent(
id,
() => SingleBasketItem(id, uid, giveaway, ticketNumber),
);
notifyListeners();
}
void placeOrder(BuildContext context, OurUser user) {
for (var i = 0; i < _items.length; i++) {
FirebaseFirestore.instance
.collection('giveaways')
.doc(_items.values.toList()[i].giveaway.giveawayId)
.update(
{
'individual_ticket_sales'[_items.values.toList()[i].ticketNumber]:
user.uid,
},
);
}
}
}
Below is an image of the results:
By analysing the results it looks like my code is creating a new field with a title of the 1st index character of individual_ticket_sales ("n" because ive bought ticket 1), how can I set the nested "1" (or whatever ticket I choose) to my user id rather than creating a new field? Thanks.
I would recommend to refactor your database structure because the first problem you will hit with that one is that firestore for now does not support updating a specific index for an array field value. You can get more info about that here.
You could get the whole value individual_ticket_sales update it and save it again as whole but it would be just a matter of time when you would hit the problem that multiple users want to update the same value on almost the same time and one of the changes get's lost. Even the usage of transaction would not be 100% safe because of the size of the object and potential multiple changes.
Is it possible for you to store each ticketId as a firestore document in a firestore collection like this:
FirebaseFirestore.instance
.collection('giveaways')
.doc(_items.values.toList()[i].giveaway.giveawayId)
.collection('individual_ticket_sales')
.doc(i)
.update(user.uid);

Can I have duplicate document IDs?

My application uses dates as document Id.
Each day stores the items the user bought and their total price.
The user might buy items from his local store twice or more in a day.
How can I allow that in my flutter code?
All the questions I found, ask about how not to duplicate document ID. I guess I'm the only one who want to do this.
The code just in case:
/// Save to collection [user], document [day], the list of items in [cartMap], and the total amount [total]
static Future<void> saveToFirestore(String day, Map<GroceryItem, int> cartMap, double total) async {
bool paidStatus = false;
List<String> x = day.split('-');
assert(matches(x[2], '[0-9]{2}')); // make sure the day is two digits
assert(matches(x[1], '\\b\\w{1,9}\\b')); // make sure the month is a word between 1 and 9 length
assert(matches(x[0], '[0-9]{4}')); // make sure it's a number year of four digits
final groceries = Groceries.cartMap(cartMap).groceries;
final user = await CurrentUser.getCurrentUser();
CollectionReference ref = Firestore.instance.collection(user.email);
DocumentReference doc = ref.document(day);
final dateSnapshot = await doc.get();
Firestore.instance.runTransaction((transaction) async {
updatePaidUnpaidTotal(day, user.email, total);
if(dateSnapshot.exists) {
print('Updating old document');
List<dynamic> existingItems = dateSnapshot.data['items'];
var totalItems = existingItems + groceries;
return await transaction.update(doc, {'total': FieldValue.increment(total), 'items': totalItems, 'paidStatus': false},);
}
else {
print('New document');
return await transaction.set(doc, {'total': total, 'items': groceries, 'paidStatus': paidStatus});
}
});
}
I strongly recommend not using dates (or any actual data at all) in a document ID, for the reason that you're running into.
There is rarely a requirement in an app to use a specific format for a document ID. It might be convenient in some cases, but ultimately, it constrains the future expansion of your collection.
One flexible way of adding any document at all, is to simply use add() to accept a random ID for the new document. Then you can put the date in a field in the document, and use that in your queries as a filter on that field. This is going to give you much more freedom to change things up later if you want, so you are not bound the dates for IDs. There are no real performance penalties for this.
The only reason to use a non-random string as a document ID is if you need to absolutely enforce uniqueness on that value. Since your application no longer wants to enforce this, it's better to simply remove that constraint and use random IDs.
It is not possible to have several Firestore documents in the same (sub)collection with the same document ID.
A classical approach in your case is to create one document per day and create a subcollection which contains a document for each item the user bought during this day.
Something like:
orders (collection)
- 20200725 (doc)
- orderItems (subcollection)
- KJY651KNB9 (doc with auto generate id): {'items': totalItems, 'paidStatus': false ... }
- 20200726 (doc)
- orderItems (subcollection)
- AZEY543367 (doc with auto generate id): {'items': totalItems, 'paidStatus': false ... }
- AZEY5JKKLJ (doc with auto generate id): {'items': totalItems, 'paidStatus': false ... }
- AZEY598T36 (doc with auto generate id): {'items': totalItems, 'paidStatus': false ... }

What's the best way to paginate and filters large set of data in Firebase?

I have a large Firestore collection with 10,000 documents.
I want to show these documents in a table by paging and filtering the results at 25 at a time.
My idea, to limit the "reads" (and therefore the costs), was to request only 25 documents at a time (using the 'limit' method), and to load the next 25 documents at the page change.
But there's a problem. In order to show the number of pages I have to know the total number of documents and I would be forced to query all the documents to find that number.
I could opt for an infinite scroll, but even in this case I would never know the total number of results that my filter has found.
Another option would be to request all documents at the beginning and then paging and filtering using the client.
so, what is the best way to show data in this type of situation by optimizing performance and costs?
Thanks!
You will find in the Firestore documentation a page dedicated to Paginating data with query cursors.
I paste here the example which "combines query cursors with the limit() method".
var first = db.collection("cities")
.orderBy("population")
.limit(25);
return first.get().then(function (documentSnapshots) {
// Get the last visible document
var lastVisible = documentSnapshots.docs[documentSnapshots.docs.length-1];
console.log("last", lastVisible);
// Construct a new query starting at this document,
// get the next 25 cities.
var next = db.collection("cities")
.orderBy("population")
.startAfter(lastVisible)
.limit(25);
});
If you opt for an infinite scroll, you can easily know if you have reached the end of the collection by looking at the value of documentSnapshots.size. If it is under 25 (the value used in the example), you know that you have reached the end of the collection.
If you want to show the total number of documents in the collection, the best is to use a distributed counter which holds the number of documents, as explained in this answer: https://stackoverflow.com/a/61250956/3371862
Firestore does not provide a way to know how many results would be returned by a query without actually executing the query and reading each document. If you need a total count, you will have to somehow track that yourself in another document. There are plenty of suggestions on Stack Overflow about counting documents in collections.
Cloud Firestore collection count
How to get a count of number of documents in a collection with Cloud Firestore
However, the paging API itself will not help you. You need to track it on your own, which is just not very easy, especially for flexible queries that could have any number of filters.
My guess is you would be using Mat-Paginator and the next button is disabled because you cannot specify the exact length? In that case or not, a simple workaround for this is to get (pageSize +1) documents each time from the Firestore sorted by a field (such as createdAt), so that after a new page is loaded, you will always have one document in the next page which will enable the "next" button on the paginator.
What worked best for me:
Create a simple query
Create a simple pagination query
Combine both (after validating each one works separately)
Simple Pagination Query
const queryHandler = query(
db.collection('YOUR-COLLECTION-NAME'),
orderBy('ORDER-FIELD-GOES-HERE'),
startAt(0),
limit(50)
)
const result = await getDocs(queryHandler)
which will return the first 50 results (ordered by your criteria)
Simple Query
const queryHandler = query(
db.collection('YOUR-COLLECTION-NAME'),
where('FIELD-NAME', 'OPERATOR', 'VALUE')
)
const result = await getDocs(queryHandler)
Note that the result object has both query field (with relevant query) and docs field (to populate actual data)
So... combining both will result with:
const queryHandler = query(
db.collection('YOUR-COLLECTION-NAME'),
where('FIELD-NAME', 'OPERATOR', 'VALUE'),
orderBy('FIELD-NAME'),
startAt(0),
limit(50)
)
const result = await getDocs(queryHandler)
Please note that the field in the where clause and in orderBy must be the same one! Also, it is worth mentioning that you may be required to create an index (for some use cases) or that this operation will fail while using equality operators and so on.
My tip: inspect the error itself where you will find a detailed description describing why the operation failed and what should be done in order to fix it (see an example output using js client in image below)
Firebase V9 functional approach. Don't forget to enable persistence so you won't get huge bills. Don't forget to use where() function if some documents have restrictions in rules. Firestore will throw error if even one document is not allowed to read by user. In case bellow documents has to have isPublic = true.
firebase.ts
function paginatedCollection(collectionPath: string, initDocumentsLimit: number, initQueryConstraint: QueryConstraint[]) {
const data = vueRef<any[]>([]) // Vue 3 Ref<T> object You can change it to even simple array.
let snapshot: QuerySnapshot<DocumentData>
let firstDoc: QueryDocumentSnapshot<DocumentData>
let unSubSnap: Unsubscribe
let docsLimit: number = initDocumentsLimit
let queryConst: QueryConstraint[] = initQueryConstraint
const onPagination = (option?: "endBefore" | "startAfter" | "startAt") => {
if (option && !snapshot) throw new Error("Your first onPagination invoked function has to have no arguments.")
let que = queryConst
option === "endBefore" ? que = [...que, limitToLast(docsLimit), endBefore(snapshot.docs[0])] : que = [...que, limit(docsLimit)]
if (option === "startAfter") que = [...que, startAfter(snapshot.docs[snapshot.docs.length - 1])]
if (option === "startAt") que = [...que, startAt(snapshot.docs[0])]
const q = query(collection(db, collectionPath), ...que)
const unSubscribtion = onSnapshot(q, snap => {
if (!snap.empty && !option) { firstDoc = snap.docs[0] }
if (option === "endBefore") {
const firstDocInSnap = JSON.stringify(snap.docs[0])
const firstSaved = JSON.stringify(firstDoc)
if (firstDocInSnap === firstSaved || snap.empty || snap.docs.length < docsLimit) {
return onPagination()
}
}
if (option === "startAfter" && snap.empty) {
onPagination("startAt")
}
if (!snap.empty) {
snapshot = snap
data.value = []
snap.forEach(docSnap => {
const doc = docSnap.data()
doc.id = docSnap.id
data.value = [...data.value, doc]
})
}
})
if (unSubSnap) unSubSnap()
unSubSnap = unSubscribtion
}
function setLimit(documentsLimit: number) {
docsLimit = documentsLimit
}
function setQueryConstraint(queryConstraint: QueryConstraint[]) {
queryConst = queryConstraint
}
function unSub() {
if (unSubSnap) unSubSnap()
}
return { data, onPagination, unSub, setLimit, setQueryConstraint }
}
export { paginatedCollection }
How to use example in Vue 3 in TypeScript
const { data, onPagination, unSub } = paginatedCollection("posts", 8, [where("isPublic", "==", true), where("category", "==", "Blog"), orderBy("createdAt", "desc")])
onMounted(() => onPagination()) // Lifecycle function
onUnmounted(() => unSub()) // Lifecycle function
function next() {
onPagination('startAfter')
window.scrollTo({ top: 0, behavior: 'smooth' })
}
function prev() {
onPagination('endBefore')
window.scrollTo({ top: 0, behavior: 'smooth' })
}
You might have problem with knowing which document is last one for example to disable button.

Firestore - Fetch Subcollection Swift

Converting from Firebase to Firestore
I have a database that looks like the following
Above is a Firebase node for entries in a photo album. The top level node is the ID of the Album (reference from another node) and scoped under each albumID there is a node (auto gen id) for each album entry (or picture). The expanded album has two album entries, the first of which is expanded.
In Firebase - I could fetch the set of album entries (for a given album) by doing the following
I could offset to the subnode and read from there.
let dbKey = "byAlbum_entries"
let ref = Database.database().reference(fromURL: firebaseDB_URL)
let albumEntryReference = ref.child( dbKey).child( forAlbum.objectid)
albumEntryReference.observeSingleEvent(of : .value, with : { (snapshot) in ...
In Firestore how do I retrieve that same set of album entries?
is the correct syntax similar to below? Can I get documents from a document and Firestore will know I am asking to look for a subcollection because I said getDocuments()?
let dbKey = "byAlbum_entries"
let ref = Firestore.firestore()
let albumEntryReference = ref.collection( dbKey).document( forAlbum.objectid)
albumEntryReference.getDocuments() { (querySnapshot, err) in
if let err = err {
print("Error getting documents: \(err)")
// no album entries found
} else {
for document in querySnapshot!.documents {
print("\(document.documentID) => \(document.data())")
// loop through album entries
}
}
}
Part 2 --- Data Model Good Practices with FIRESTORE
It is better to restructure the data so that there is no subcollection and there is just a single collection where each entry includes both the albumID and the AlbumEntryID and I can just use a where clause to fetch only the album entries where albumID matches the one I am looking for? This would require an additional index whereas currently, in firebase, I can go directly to where I want to read and just read.
Even if restructuring does turn out to be better, I'd still appreciate help on the syntax to fetch all album entries for a given album as the database currently exists.

Resources