Consecutive transactions - firebase

I was trying to simulate a situation where two users (on seperate devices) both run a Transaction at the same time. To imitate this, I made a List<String> of strings which would be added to the database without a delay between them.
However, only the first item in the List was added to the database, the second never arrived. What am I doing wrong? I am trying to have both items added to the database.
The call to the Transaction happens in the code below, along with the creation of the list:
List<String> items = new List<String>();
items.add("A test String 1");
items.add("A test String 2");
for (String q in questions)
{
database.updateDoc( q );
}
The code I use for updating the data in my database:
void updateDoc( String item ) async
{
var data = new Map<String, dynamic>();
data['item'] = item;
Firestore.instance.runTransaction((Transaction transaction) async {
/// Generate a unique ID
String uniqueID = await _generateUniqueQuestionCode();
/// Update existing list
DocumentReference docRef = Firestore.instance
.collection("questions")
.document("questionList");
List<String> questions;
await transaction.get(docRef)
.then (
(document) {
/// Convert List<dynamic> to List<String>
List<dynamic> existing = document.data['questions'];
questions = existing.cast<String>().toList();
}
);
if ( ! questions.contains(uniqueID) )
{
questions.add( uniqueID );
var newData = new Map<String, dynamic>();
newData['questions'] = questions;
transaction.set(docRef, newData );
}
/// Save question
docRef = Firestore.instance
.collection("questions")
.document( uniqueID );
transaction.set(docRef, data);
});
}
In reality, I have a few fields in the document I'm saving but they would only complicate the code.
I keep track of a list of documents because I need to be able to retreive a random document from the database.
When executing the first code snippet, only the first item in the list will be added to the database and to the list that keeps track of the documents.
No error is thrown in the debug screen.
What am I missing here?

As explained by Doug Stevenson in the comments under my question:
That's not a typical use case for a single app instance. If you're
trying to find out if transactions work, be assured that they do.
They're meant to defend against cases where multiple apps or processes
are making changes to a document, not a single app instance.
And also:
The way the Firestore SDK works is that it keeps a single connection
open and pipelines each request through that connection. When there
are multiple clients, you have multiple connection, and each request
can hit the service at a different time. I'd suspect that what you're
trying to simulate isn't really close to the real thing.

Related

How to batch update arrays?

So I am creating an attendance app in which there is a list named attendees which contains the list of all students attending the lecture.
List<String> attendees = [uid0, uid1, uid2];
and the student data model is as follows
Student(
List<String> attendance = [at0, at1]; //this is the list of lectures attended
String name;
)
and the document name of every student is their user-id
How do I update the list of every student via the batch() function?
I assume you already have a list of attendees that contains user-id as you mentioned the userId would be the documentId in Firestore.
Therefore, you can do a loop and update your doc directly and then use commit to apply your changes.
updateBatch(List<String> attendees) async {
try {
final db = FirebaseFirestore.instance;
final WriteBatch batch = db.batch();
// based on this doc https://firebase.flutter.dev/docs/firestore/usage/#batch-write
// Each transaction or batch of writes can write to a maximum of 500 documents.
for (final userid in attendees) {
// Use the batch to update a document that exist otherwise it will throw error,
// in case you want to create that doc, maybe you should use `set` instead
// ref: https://pub.dev/documentation/cloud_firestore/latest/cloud_firestore/WriteBatch/update.html
batch.update(
//Give the ref of document.
db.document('Path/to/firestore/document'), // e.g: /Students/userId
//And the data to update.
{"key": value},
);
}
await batch.commit();
print('success');
} catch (e) {
print('error $e');
}
}
A few notes:
According to this doc, Each transaction or batch of writes can write to a maximum of 500 documents.
You can check out different WriteBatch methods doc here

Flutter and Firestore: How to update multiple specific documents from collection group

Okay, so I have multiple specific document ids that are all the same, but each one is used for different collections and purposes. Using a collection group as these documents are nested in subcollections, I was able to locate these documents and print them. How would I go about updating a specific field in all of these documents at the same time with some other data? In my case these documents despite being in different collections have a field called, plastics and I want to update them with int data.
Here is the code I used to retrieve these documents and filter to only print the one's I specificly need:
final String uid = FirebaseAuth.instance.currentUser.uid;
QuerySnapshot querySnapshot = await FirebaseFirestore
.instance
.collectionGroup("Members")
.get();
for (int i = 0; i < querySnapshot.docs.length; i++) {
var a = querySnapshot.docs[i];
if (a.id == uid) {
print(a.id);
}
}
Also here is some code I've used before for updating a field in a single document, but not all of them like I need to in this case.
Future<bool> addPlastic(String amount) async {
try {
String uid = auth.currentUser.uid;
var value = double.parse(amount);
DocumentReference documentReference =
FirebaseFirestore.instance.collection('some collection').doc(some document);
FirebaseFirestore.instance.runTransaction((transaction) async {
DocumentSnapshot snapshot = await transaction.get(documentReference);
if (!snapshot.exists) {
documentReference.set({'plastics': value});
return true;
}
double newAmount = snapshot.data()['plastics'] + value;
transaction.update(documentReference, {'plastics': newAmount});
return true;
});
} catch (e) {
return false;
}
}
You have two options, either loop through all the collections and sub collections (tedious), or store a list document references in one of the documents and change the data by looping through all of these document references ( better option). If you need some code or guidelines on how to do that, let me know.
EDIT
To use the document reference part, first, when creating the document, you have to do something like this
document(id).set({
"value1":"oko",
"plastic":"240",
//And the rest of your values
"otherSnapshots":[/*Your list of documentSnapshots with the same ID. Update it in this whenever you have new one by following the next snippet*/]
})
When you create a new document, navigate to this and add in the document Reference by
snapshot.reference.update({
'otherSnapshots':FieldValue.arrayUnion([/*Document Reference*/])
});
And next time, when you want to update all of them, use this field, loop through it and then you will get all the document references. I cannot create a code that you can directly copy paste into your code without seeing how u store data

Flutter: How to move a firebase node - API REST vs SDK

I have a node in Firebase that has multiple childs and childs of childs. I want to move all from one place to another, then I made a function with Firebase REST API, but I would like to optimize it and migrate it to SDK.
The function I'm using with API REST is:
Future<bool> moverLoteActual() async {
//TO READ FROM SOURCE PATH
final urlLoteActual = '$_PATH_ORIGEN.../loteActual.json?auth=${_prefs.token}';
final resp = await http.get(urlLoteActual);
//TODO: Probably I neew an error management here
//TO COPY IN DESTINATION
final urlLotesCerrados = '$PATH_DESTINO.../lotesCerrados.json?auth=${_prefs.token}';
final resp2 = await http.post(urlLotesCerrados, body: resp.body);
final decodedData2 = json.decode(resp2.body);
print(decodedData2);
//TO DELETE THE SOURCE NODE
final resp3 = await http.delete(urlLoteActual);
print(json.decode(resp3.body));
return true;
}
I tried with SDK but I get a lot of parsing errors and finally I think a sequential approach as I implemented with API REST is not the best way.
How can I get this change of location using SDK?
I found a solution.
First, you need to understand that snapshot.value is a Map<dynamic, dynamic>, then you need to assign the answer (resp) to this type of map.
Later, you need to copy the node in a new location, then you need to convert the previous Map to a new one of other type (Map<String, dynamic> to write)
The last step (delete the original node) is easy... the standard function.
Future<bool> moverLoteActual() async {
//TO READ FROM SOURCE PATH
Map<dynamic, dynamic> _map = new Map<dynamic, dynamic>();
Query resp = db.child('$_PATH_ORIGEN.../loteActual');
final snapshot = await resp.once();
_map = snapshot.value;
//TO COPY IN DESTINATION
db.child('$PATH_DESTINO.../lotesCerrados')
.push().update(Map<String, dynamic>.from(_map));
//TO DELETE THE SOURCE NODE
db.child('$_PATH_ORIGEN.../loteActual').remove();
return true;
}
PD. db is the Firebase Database Reference

Is there a simple way to cache cloud firestore documents locally in flutter (not offline persistence)?

I am not referring to firestore offline persistence, but a way to permanently cache documents that will survive closing the database connection and app. I want to cache entire documents.
For example, in a simple chat app. Say there are 100 messages in the conversation, and the user has already read them all. A new message gets sent, so the user opens the app to read the new message. To re-download all 100 messages from firestore will have you charged for 100 document reads. But since the user has already read and retrieved those, I want them cached locally and not read again from the database (since a chat message will never change once created). I understand pagination could help, but I'd rather not read the same static document more than once.
Is SQFlite the best option for this cross-platform, or is there something even better?
For a chat application I'd typically keep track of what the last message is that the user has already seen. If you show the messages ordered by their timestamp, that means you'll need to keep the timestamp of the latest message they've seen, and its document ID (just in case there are multiple documents with the same timestamp). With those two pieces of information, you can request only new documents from Firestore with collection.startAfter(...).
I recommend to save the last time the user made login in the device locally, an then use it to only get the messages them didn't receive.
Here is an oversimplified example:
import 'package:cloud_firestore/cloud_firestore.dart';
/// This class represents your method to acess local data,
/// substitute this class for your method to get messages saved in the device
/// I highly recommend sembast (https://pub.dev/packages/sembast)
class LocalStorage {
static Map<String, Message> get savedMessages => {};
static DateTime get lastLogin => DateTime.now();
static void saveAllMessages(Map<String, Message> messages) {}
static void saveLastLogin(DateTime lastLogin) {}
}
class Message {
String text;
String senderId;
DateTime timestamp;
Message.fromMap(Map<String, dynamic> map) {
text = map['text'] ?? 'Error: message has no text';
senderId = map['senderId'];
timestamp = map['timestamp'];
}
Map<String, dynamic> toMap() {
return {
'text': text,
'senderId': senderId,
'timestamp': timestamp,
};
}
}
class User {
DateTime lastLogin;
String uid;
Map<String, Message> messages;
void updateMessages() {
this.messages = LocalStorage.savedMessages;
this.lastLogin = LocalStorage.lastLogin;
/// Listening to changes in the firestore collection
final firestore = Firestore.instance;
final ref = firestore.collection('users/$uid');
final query = ref.where('timestamp', isGreaterThan: this.lastLogin);
query.snapshots().listen((querySnapshot) {
/// Updating messages in the user data
querySnapshot.documents.forEach((doc) {
messages[doc.documentID] = Message.fromMap(doc.data);
});
/// Updating user last login
this.lastLogin = DateTime.now();
/// Saving changes
LocalStorage.saveAllMessages(this.messages);
LocalStorage.saveLastLogin(this.lastLogin);
});
}
}

Get users by name property using Firebase

I'm trying to create an application where I can get/set data in specific users accounts and I was tempted by Firebase.
The problem I'm having is that I don't know how to target specific users data when my structure looks like this:
online-b-cards
- users
- InnROTBVv6FznK81k3m
- email: "hello#hello"
- main: "Hello world this is a text"
- name: "Alex"
- phone: 12912912
I've looked around and I can't really find anything on how to access individual data let alone when they're given some random hash as their ID.
How would I go about grabbing individual user information based of their name? If there is a better way of doing this please tell me!
Previously, Firebase required you to generate your own indexes or download all data at a location to find and retrieve elements that matched some child attribute (for example, all users with name === "Alex").
In October 2014, Firebase rolled out new querying functionality via the orderByChild() method, that enables you to do this type of query quickly and efficiently. See the updated answer below.
When writing data to Firebase, you have a few different options which will reflect different use cases. At a high level, Firebase is a tree-structured NoSQL data store, and provides a few simple primitives for managing lists of data:
Write to Firebase with a unique, known key:
ref.child('users').child('123').set({ "first_name": "rob", "age": 28 })
Append to lists with an auto-generated key that will automatically sort by time written:
ref.child('users').push({ "first_name": "rob", "age": 28 })
Listen for changes in data by its unique, known path:
ref.child('users').child('123').on('value', function(snapshot) { ... })
Filter or order data in a list by key or attribute value:
// Get the last 10 users, ordered by key
ref.child('users').orderByKey().limitToLast(10).on('child_added', ...)
// Get all users whose age is >= 25
ref.child('users').orderByChild('age').startAt(25).on('child_added', ...)
With the addition of orderByChild(), you no longer need to create your own index for queries on child attributes! For example, to retrieve all users with the name "Alex":
ref.child('users').orderByChild('name').equalTo('Alex').on('child_added', ...)
Engineer at Firebase here. When writing data into Firebase, you have a few different options which will reflect different application use cases. Since Firebase is a NoSQL data store, you will need to either store your data objects with unique keys so that you can directly access that item or load all data at a particular location and loop through each item to find the node you're looking for. See Writing Data and Managing Lists for more information.
When you write data in Firebase, you can either set data using a unique, defined path (i.e. a/b/c), or push data into a list, which will generate a unique id (i.e. a/b/<unique-id>) and allow you to sort and query the items in that list by time. The unique id that you're seeing above is generated by calling push to append an item to the list at online-b-cards/users.
Rather than using push here, I would recommend using set, and storing the data for each user using a unique key, such as the user's email address. Then you can access the user's data directly by navigating to online-b-cards/users/<email> via the Firebase JS SDK. For example:
function escapeEmailAddress(email) {
if (!email) return false
// Replace '.' (not allowed in a Firebase key) with ',' (not allowed in an email address)
email = email.toLowerCase();
email = email.replace(/\./g, ',');
return email;
}
var usersRef = new Firebase('https://online-b-cards.firebaseio.com/users');
var myUser = usersRef.child(escapeEmailAddress('hello#hello.com'))
myUser.set({ email: 'hello#hello.com', name: 'Alex', phone: 12912912 });
Note that since Firebase does not permit certain characters in references (see Creating References), we remove the . and replace it with a , in the code above.
You can grab the details by the following code.
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference myRef = database.getReference("users");
myRef.orderByChild("name").equalTo("Alex").addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
for (DataSnapshot childDataSnapshot : dataSnapshot.getChildren()) {
Log.d(TAG, "PARENT: "+ childDataSnapshot.getKey());
Log.d(TAG,""+ childDataSnapshot.child("name").getValue());
}
I think the best approach is to define the ids of the users based o the auth object provided by the Firebase. When I create my users, I do:
FirebaseRef.child('users').child(id).set(userData);
This id comes from:
var ref = new Firebase(FIREBASE);
var auth = $firebaseAuth(ref);
auth.$authWithOAuthPopup("facebook", {scope: permissions}).then(function(authData) {
var userData = {}; //something that also comes from authData
Auth.register(authData.uid, userData);
}, function(error) {
alert(error);
});
The Firebase auth services will always ensure a unique id among all their providers to be set at uid. This way always you will have the auth.uid and can easily access the desired user to update it, like:
FirebaseRef.child('users').child(id).child('name').set('Jon Snow');
This was a paraphrasing of a post that helped me when trying to access the auto-generated unique id. Access Firebase unique ids within ng-repeat using angularFire implicit sync
Thanks, bennlich (source):
Firebase behaves like a normal javascript object. Perhaps the example below can get you on the right track.
<div ng-repeat="(name, user) in users">
{{user.main}}
</div>
Edit: Not 100% sure of your desired outcome, but here's a bit more that might spark an 'aha' moment. Click on the key that you are trying to access right in your Firebase dashboard. From there you can use something like:
var ref = new Firebase("https://online-b-cards.firebaseio.com/users/<userId>/name);
ref.once('value', function(snapshot) {
$scope.variable= snapshot.val();
});
This is how to access the auto generated unique keys in Firebase:
data structure:
- OnlineBcards
- UniqueKey
database.ref().on("value", function(snapshot) {
// storing the snapshot.val() in a variable for convenience
var sv = snapshot.val();
console.log("sv " + sv); //returns [obj obj]
// Getting an array of each key in the snapshot object
var svArr = Object.keys(sv);
console.log("svArr " + svArr); // [key1, key2, ..., keyn]
// Console.log name of first key
console.log(svArr[0].name);
}, function(errorObject) {
console.log("Errors handled: " + errorObject.code);
});
The simplest way is to stop using the .push(){}
function which will generate that random key. But instead use the .update(){} function where you may specify the name of the child instead of having the random key.
Retrieving data:
In your database, you are using a random id that is generated using the push(), therefore if you want to retrieve the data then do the following:
Using Firebase in Android App:
DatabaseReference ref=FirebaseDatabase.getInstance().getReference().child("users");
ref.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
for (DataSnapshot datas : dataSnapshot.getChildren()) {
String name=datas.child("name").getValue().toString();
}
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});
Using Firebase in Javascript:
firebase.database().ref().child("users").on('value', function (snapshot) {
snapshot.forEach(function(childSnapshot) {
var name=childSnapshot.val().name;
});
});
Here you have the snapshot(location of the data) at users then you loop inside all the random ids and retrieve the names.
Retrieving data for a Specific User:
Now if you want to retrieve information for a specific user only, then you need to add a query:
Using Firebase in Android App:
DatabaseReference ref=FirebaseDatabase.getInstance().getReference().child("users");
Query queries=ref.orderByChild("name").equalTo("Alex");
queries.addListenerForSingleValueEvent(new ValueEventListener() {...}
Using Firebase with Javascript
firebase.database().ref().child("users").orderByChild("name").equalTo("Alex").on('value', function (snapshot) {
snapshot.forEach(function(childSnapshot) {
var name=childSnapshot.val().name;
});
});
Using orderByChild("name").equalTo("Alex") is like saying where name="Alex" so it will retrieve the data related to Alex.
Best Way:
The best thing is to use Firebase Authentication, thus generating a unique id for each user and using it instead of a random id push(), this way you do not have to loop through all the users since you have the id and can easily access it.
First, the user needs to be signed in then you can retrieve the unique id and attach a listener to retrieve the other data of that user:
Using Firebase with Android:
DatabaseReference ref = FirebaseDatabase.getInstance().getReference("users");
String uid = FirebaseAuthentication.getInstance().getCurrentUser().getUid();
ref.child(uid).addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
String name=dataSnapshot.child("name").getValue().toString();
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});
Using Firebase with Javascript:
var user = firebase.auth().currentUser;
var uid=user.uid;
firebase.database().ref().child("users").child(uid).on('value', function (snapshot) {
var name=snapshot.val().name;
});
The simplest and better way to add unique Id to ur database according to authenticated user is this:
private FirebaseAuth auth;
String UId=auth.getCurrentUser().getUid();
FirebaseDatabase database = FirebaseDatabase.getInstance();
DatabaseReference myRef = database.getReference("Users");
User user = new User(name,email,phone,address,dob,bloodgroup);
myRef.child(UId).setValue(user);
The UId will be a unique Id for a specific authenticated email/user

Resources