Designing many to many model with map - google-cloud-firestore

I am new to firestore and am wondering if anyone could tell me whether this solution is viable for a many-to-many relationship. I have a collection of Rosters and collection of Students which are related Many-to-Many. As the information I most frequently need about a student is just their name, would it be viable to have a map of students like {<StudentID> : "Student Name"} stored in rosters, and so if I want to retrieve more detailed information about students in a roster, I retrieve the map's keys and iterate through them to retrieve each student's document individually?
I am basing my solution off of this answer.
I'd greatly appreciate any advice! Thank you

Update to this, it is working fine. Here is my code for the cloud function to update athlete names if anyone in the future needs:
export const onUserUpdate =
functions.firestore.document("users/{user}/athletes/{athlete}").onUpdate(
async (change) => {
const after = change.after.data();
const before = change.before.data();
const bid = change.before.id;
console.log("BID: ");
console.log(bid);
const userId: any = change.before.ref.parent.parent?.id;
console.log(`users/${userId}/rosters`);
if (after.athleteName != before.athleteName) {
console.log("Change name detected");
const snapshot =
await db.collection(
`users/${userId}/rosters`).where(
`athletes.${bid}`, ">=", "").get();
const updatePromises : Array<Promise<any>> = [];
snapshot.forEach((doc) => {
console.log(doc.id);
updatePromises.push(db.collection(`users/${userId}/rosters`)
.doc(doc.id).update(`athletes.${bid}`, after.athleteName)
);
});
await Promise.all(updatePromises);
}
});

Related

How to make a WishedProduct Widget using FirebaseFirestore

I'm making a product display app.
I'm going to create a "Wish List" widget that brings up the product that the user picked as 'wish item'.
I structured it as shown in picture 1.
And to create a widget,
I get all the documents of the collection('wish') of doc('User A').
And with their String values(product name) of the doc,
Get the product data from the collection ('Product') using Query.
The product collection is shown in the picture below.
Is there a more efficient way?
I thought it might be easier to change the data structure.
However, even if I create a new Collection('Wish'), at the same level as Collection('Product'), and put the product's name and user's e-mail in it,
I need to focus on the 'Product' collection with the 'Product name'.
Because I have to use the price, brand, name of product, in Collection('Product').
Is there any other efficient way I'm missing? Thank you!
Instead of storing the product name in string you can use reference type to store direct reference of the document inside other document.
Example code
DocumentReference ref = db.collection('products').doc('product-document-id');
Map<String,dynamic> data = {
'name' : 'Product A',
'product_ref' : ref, // Product document Reference
};
db.collection("users")
.doc("user-id")
.collection("wish")
.doc("your_product_name")
.set(data);
This will add the wish product with dynamic id.
Now you can directly read the document using the product_ref. You can use something like this
final docRef = db.collection("users")
.doc("user-id")
.collection("wish").doc("your_product_name");
final docSnapshot = await docRef.get();
if (docSnapshot.exists) {
final data = doc.data() as Map<String, dynamic>;
var productRef = data?['product_ref'];
productRef.get().then((DocumentSnapshot documentSnapshot) {
if (documentSnapshot.exists) {
// you will get your product here
}
});
}

Searching multiple fields at ounce in Firestore

I am trying to search multiple fields in my Firebase database for app users to lookup contacts with FirstName, LastName, and JobTitle. I know that queries with "OR" is one of the limitations, but I was wondering if someone came up with a way to merge multiple queries to substitute the functionality of "OR". Below is the code I am using to search users based on their first name.
Thanks Folks!
final usersProfile = Firestore.instance.collection('Users');
handleSearch(String query) {
Future<QuerySnapshot> users =
usersProfile.where('First Name', isEqualTo: query).getDocuments();
setState(() {
searchResultsFuture = users;
});
}

Add a document reference to two different collections

I'm creating a simple job board site.
You have a JobSeeker, a JobListing and a JobApplication
Both the JobSeeker and the JobListing should have a collection of JobApplications.
When a JobSeeker applies for a job, I want to create a JobApplication document, and add it to both the JobSeeker's collection, and the JobListing's collection.
But that should be a reference to a single document. (ie. if you update it in one place, it should update in the other).
How do I achieve this?
I see according to this answer:
Cloud Firestore multiples document with the same reference
I can add a Reference as a datatype in Firestore - but I'm not exactly sure which method to use to add it.
ie. the collection.add method accepts DocumentData, but I can't see how to set that as a reference?
Can you tell me what syntax to use to:
Create the JobApplication document
Add the document reference to a collection.
Retrieve the document reference from either collection.
Here's the way I ended up solving this:
To set the data:
const docData = {
listingId: "someExistingId",
jobSeekerId: "anotherExistingId",
otherData: "whatever other data goes here",
}
const docRef = await db.collection("job-application-collection")
.add(docData);
await db.collection(`job-seeker-collection/${docData.jobSeekerId}/applications`)
.add({ref:docRef});
await db.collection(`job-listing-collection/${docData.listingId}/applications`)
.add({ref:docRef});
That as, what we do is we create one 'real' document, that goes into the job-application-collection and in the JobSeeker and JobListing collections we add a 'pointer document' that just contains a single field ref, containing the document reference.
To retrieve it (in this example, retrieve all of the applications for a given JobSeeker):
const jobSeekerId = "someJobSeekerId";
const colRef = await db.collection(`job-seeker-collection/$jobSeekerId}/applications`);
const colSnapshot = await colRef.get();
/**
* The docs on the collection are actually just documents containing a reference to the actual JobApplication document.
*/
const docsProms = colSnapshot.docs.map((async (colDocData) => {
const snapshot = await colDocData.data().ref.get();
return {
...snapshot.data(),
id: snapshot.id,
}
}));
const data = await Promise.all(docsProms);
return data;
Pretty straight forward, we get the collection on the JobSeeker document, and then on each of those documents, there is a ref field, which we can use the .get() method to return a document snapshot.

Query Firestore documents with role based security via Flutter

I´ve a role based data model on Firestore according to googles suggestion here: https://firebase.google.com/docs/firestore/solutions/role-based-access
Security rules are set up correctly and work fine. But now I´ve the problem on how to query for the roles.
This is my data model (one sample document):
id: "1234-5678-91234",
roles:
userId_1:"owner",
userId_2:"editor
title: "This is a sample document"
And this is my Firestore Query in Flutter which gets all documents for a specific user by its ID if the user has assigned the role "owner" for the document:
return firestore
.collection(path)
.where("roles.${user.firebaseUserId}", isEqualTo: "owner")
.snapshots().map((snapshot) {
return snapshot.documents.map((catalog) {
return SomeDocumentObject(...);
}).toList();
});
My problem now is, that I need some kind of "OR" clause - which does not exist as far as I know. The query above only retrieves documents for users with role "owner" but I need a query that also retrieves the document if the userId is associated with the role "editor".
I´ve tried "arrayContains:" which also doesn´t seem to work (cause it´s a map).
I´ve read about solutions with two independent queries which doesn´t sound like a good solution due to a lot of overhead.
Maybe someone of you have a hint for me? :)
Thanks & best,
Michael
Firestore doesn't currently have any logical OR operations. You'll have to perform two queries, one for each condition, and merge the results of both queries in the client app.
This is the final solution using RxDart, Observables and .combineLatest() - maybe it helps someone out there:
#override
Stream<List<Catalog>> catalogs(User user) {
// Retrieve all catalogs where user is owner
Observable<QuerySnapshot> ownerCatalogs = Observable(firestore
.collection(path)
.where("roles.${user.firebaseUserId}", isEqualTo: "owner")
.snapshots());
// Retrieve all catalogs where user is editor
Observable<QuerySnapshot> editorCatalogs = Observable(firestore
.collection(path)
.where("roles.${user.firebaseUserId}", isEqualTo: "editor")
.snapshots());
// Convert merged stream to list of catalogs
return Observable.combineLatest([ownerCatalogs, editorCatalogs],
(List<QuerySnapshot> snapshotList) {
List<Catalog> catalogs = [];
snapshotList.forEach((snapshot) {
snapshot.documents.forEach((DocumentSnapshot catalog) {
catalogs.add(Catalog(
id: catalog.documentID,
title: catalog.data['title'],
roles: catalog.data['roles'],
));
});
});
return catalogs;
}).asBroadcastStream();
}

Firestore - batch.add is not a function

The documentation for Firestore batch writes lists only set(), update() and delete() as permitted operations.
Is there no way to add an add() operation to the batch? I need a document to be created with an auto-generated id.
You can do this in two steps:
// Create a ref with auto-generated ID
var newCityRef = db.collection('cities').doc();
// ...
// Add it in the batch
batch.set(newCityRef, { name: 'New York City' });
// Commit at the end
await batch.commit();
The .doc() method does not write anything to the network or disk, it just makes a reference with an auto-generated ID you can use later.
In my case, using AngularFire2, I had to use the batch.set() method, passing as first parameter the document reference with an ID previously created, and the reference attribute:
import { AngularFirestore } from '#angular/fire/firestore';
...
private afs: AngularFirestore
...
batch.set(
this.afs.collection('estados').doc(this.afs.createId()).ref,
er.getData()
);
I'll offer an answer for Firebase 9 in which the syntax differs from Firebase 8.
For Firebase 9, the equivalent of add() is addDoc() as explained at https://firebase.google.com/docs/firestore/manage-data/add-data#web-version-9_6 . It is for when you're not using batch nor transaction. As per the original problem posted, there is no equivalent of addDoc() on batch nor transaction on Firebase 9 either.
I found a way to achieve the equivalent of addDoc() for a batch on Firebase 9 by following the answer https://stackoverflow.com/a/69859144/2848676 as follows:
const batch = writeBatch(db);
const docADocRef = doc(collection(db, "DocA"));
batch.set(docADocRef, {
fieldA: "This is field of an instance of DocA"
});
const docBDocRef = doc(collection(db, "DocB"));
batch.set(docBDocRef, {
docAID: docADocRef.id
});
batch.commit();
In this example, instances of DocA and DocB are created and DocB receives a pointers to the DocA instance.
According to the docs
Behind the scenes, .add(...) and .doc().set(...) are completely equivalent, so you can use whichever is more convenient.
Perhaps this applies to batches as well?
For PHP you can try :
$batch = $db->batch();
$newCityRef = $db->collection('cities')->newDocument();
$batch->set($newCityRef , [ 'name'=>'New York City' ]);
To create a document with auto-generated ID with firestore batch, you cannot use the addDoc(). You have to use batch.set() with a reference to the document to be created as below
const db = getFirestore();
// Create a transaction to update both the product stock value and add the new stock data
const batch = writeBatch(db);
const prodRef = doc(db, `products/${productId}`);
const stockRef = doc(collection(db, `stocks`);
// newDocId = stockRef.id;
batch.set(stockRef, stock, {merge: true}); //create new document with autoId
batch.update(prodRef, {available : increment(quantity), stock: increment(quantity)});
batch.commit()
Create the reference to the collection in which you are going to add the batch data
We loop over the req.body using forEach and set the each data to be added in to the collection using the set method
We commit the data and save the data to the collection using the commit method and on success ,send a success response.
cloud firestore
Lets assume that you have list of cities and you want to write them in batch.
final CityList = FirebaseFirestore.instance.collection('cities')
WriteBatch batch = FirebaseFirestore.instance.batch();
for(CityList city in cities) {
final newShoppingItem = ShoppingList.doc();
batch.set(newShoppingItem, {
'name': city.name,
'createdAt': DateTime
.now()
.millisecondsSinceEpoch
});
}
batch.commit();
Sam Stern's answer is the correct way to do it, although if you are using AngularFire, .doc() cannot be used withouth a parameter to generate a new docId (see https://github.com/angular/angularfire/issues/1974).
The AngularFire way of doing this would be:
// Create a ref with auto-generated ID
const id = this.db.createId();
const newCityRef= this.db.collection("cities").doc(id);
// ...
// Add it in the batch
batch.set(newCityRef, { name: 'New York City' });
This worked for me and it is mentioned in the docs for PHP
$batch = $db->batch();
# Set the data for NYC
$nycRef = $db->collection('samples/php/cities')->document('NYC');
$batch->set($nycRef, [
'name' => 'New York City'
]);
# Update the population for SF
$sfRef = $db->collection('samples/php/cities')->document('SF');
$batch->update($sfRef, [
['path' => 'population', 'value' => 1000000]
]);
# Delete LA
$laRef = $db->collection('samples/php/cities')->document('LA');
$batch->delete($laRef);
# Commit the batch
$batch->commit();