I'm trying to check to see if a value saved in the database matches the user selection, if it does then update it otherwise don't do anything.
I've tried the following, but get an exception:
Caused by: com.google.firebase.firestore.FirebaseFirestoreException: Every document read in a transaction must also be written.
Here's my code:
//update toolbar titles if they match
final DocumentReference adminDocRef = mDbase.collection("admins").document(adminID);
mDbase.runTransaction(new Transaction.Function<Void>() {
#Override
public Void apply(#NonNull Transaction transaction) throws FirebaseFirestoreException {
DocumentSnapshot adminSnapshot = transaction.get(adminDocRef);
String toolbarTitle = adminSnapshot.getString("displayedUser");
if (userName.equals(toolbarTitle)) {
transaction.update(adminDocRef, "displayedUser", userName);
}
// Success
return null;
}
})
.addOnSuccessListener(new OnSuccessListener<Void>() {
#Override
public void onSuccess(Void aVoid) {
Log.d(TAG, "Transaction success!");
}
})
.addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
Log.w(TAG, "Transaction failure.", e);
}
});
Are transactions capable of being conditional? If not how would you solve this problem? Seems silly to have to do a separate get then nest an update inside of it.
Any help would be greatly appreciated.
Thanks!
There is a similar answer from Alex ,from documentation
A transaction can fail for the following reasons:
The transaction contains read operations after write operations. Read operations must always come before any write operations. //not your case
The transaction read a document that was modified outside of the transaction. In this case, the transaction automatically runs again. The transaction is retried a finite number of times.
In your case I would try
if (userName.equals(toolbarTitle)) {
//transaction operations
}
See if that works!
So in order to update my toolbarTitle conditionally, I would have to cascade my Firestore operations this way:
DocumentReference adminDocRef = mDbase.collection("admins").document(adminID);
adminDocRef.get().addOnCompleteListener(activity, new OnCompleteListener<DocumentSnapshot>() {
#Override
public void onComplete(#NonNull Task<DocumentSnapshot> task) {
if (task.isSuccessful()) {
DocumentSnapshot document = task.getResult();
if (document.exists()) {
//assign document value to toolbarTitle
toolbarTitle = document.getString("displayedUser");
if (userName.equals(toolbarTitle)) {
DocumentReference adminDocRef = mDbase.collection("admins").document(adminID);
adminDocRef
.update("displayedUser", toolbarTitle)
.addOnSuccessListener(new OnSuccessListener<Void>() {
#Override
public void onSuccess(Void aVoid) {
Log.d(TAG, "Displayed child has been updated");
}
})
.addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
Log.w(TAG, "Error: updating displayed child", e);
}
});
Log.d(TAG, "DocumentSnapshot data: " + document.getData());
}
} else {
Log.d(TAG, "No such document");
}
} else {
Log.d(TAG, "get failed with ", task.getException());
}
}
});
It's a bit disappointing that I would have to do it this way, as it seems having it wrapped inside of a transaction would make more sense. I'm hoping that they allow conditional behavior inside the Transaction.
Actually, google has a class called Tasks in which you could use to make firestore calls synchronous.
So following your example, you could fix it into something like:
try {
DocumentSnapshot adminDocument = Tasks.await(adminDocRef.get());
if (adminDocument.exists()) {
String toolbarTitle = adminDocument.getString("displayedUser");
if (userName.equals(toolbarTitle)) {
Tasks.await(adminDocRef.update("displayedUser", userName));
//Success
}
}
} catch (ExecutionException e) {
//Error
e.printStackTrace();
} catch (InterruptedException e) {
//Error
e.printStackTrace();
}
This way you don't need listeners and at the same time it is a conditional update. I know this is late but hopefully it'll help someone.
[UPDATE]:
Please note that Tasks.await(...) call should not be called on the main thread, a solution would be using AsyncTask.
Related
I wrote the following code that deletes two documents from the cloud Firebase database:
fireDB.document(groupPath).collection("users").document(phoneNumber).delete().addOnSuccessListener(new OnSuccessListener<Void>() {
#Override
public void onSuccess(Void aVoid) {
fireDB.collection("users").document(phoneNumber).delete().addOnSuccessListener(new OnSuccessListener<Void>() {
#Override
public void onSuccess(Void aVoid) {
Log.d(this.getClass().getName(), "DocumentSnapshot successfully deleted");
Toast.makeText(getApplicationContext(),R.string.successfully_deleted_user,Toast.LENGTH_LONG).show();
finish();
}
}).addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
Log.w(this.getClass().getName(), "Error deleting document", e);
Toast.makeText(getApplicationContext(),R.string.failed_to_delete_user,Toast.LENGTH_LONG).show();
}
});
Log.d(this.getClass().getName(), "DocumentSnapshot successfully deleted");
}
}).addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
Log.w(this.getClass().getName(), "Error deleting document", e);
Toast.makeText(getApplicationContext(),R.string.failed_to_delete_user,Toast.LENGTH_SHORT).show();
}
});
The problem with that code is that it deletes first document and then deletes second document, meaning if the first try will delete it successfully and the second one will fail to delete it, there is going to be a problem. Is it possible to delete two documents in Firebase cloud database so the result could be of of the following two options:
Both of the documents are deleted.
Both of the document are not deleted.
Is it possible to do?
As robsiemb commented, you'll want to use a batch write or transaction for this.
As far as I can see, the equivalent from your code would be something like this:
// Get a new write batch
WriteBatch batch = db.batch();
DocumentReference docRef1 = fireDB.document(groupPath).collection("users").document(phoneNumber);
DocumentReference docRef2 = fireDB.collection("users").document(phoneNumber)
DocumentReference laRef = db.collection("cities").document("LA");
batch.delete(docRef1);
batch.delete(docRef2);
// Commit the batch
batch.commit().addOnCompleteListener(new OnCompleteListener<Void>() {
#Override
public void onComplete(#NonNull Task<Void> task) {
// ...
}
});
i tried to do everything correctly and can't seem to find what's wrong here, i even created the project again from scratch but still it doesn't work, but i get the "failed" toast when trying to create an account, i added the internet permission too. i also don't get any error in logcat to show it here, how can this be solved ?
public class CreateAccountActivity extends AppCompatActivity {
private Button btnCreateAcc;
private FirebaseAuth firebaseAuth;
private FirebaseAuth.AuthStateListener authStateListener;
private FirebaseUser currentUser;
// firestore
private FirebaseFirestore database = FirebaseFirestore.getInstance();
private CollectionReference collectionReference = database.collection("Users");
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_create_account);
firebaseAuth = FirebaseAuth.getInstance();
authStateListener = new FirebaseAuth.AuthStateListener() {
#Override
public void onAuthStateChanged(#NonNull FirebaseAuth firebaseAuth) {
currentUser = firebaseAuth.getCurrentUser();
if (currentUser != null) {
} else {
}
}
};
btnCreateAcc = findViewById(R.id.create_acct_button);
btnCreateAcc.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
if (!TextUtils.isEmpty(etEmail.getText().toString()) &&
!TextUtils.isEmpty(etPassword.getText().toString()) &&
!TextUtils.isEmpty(etUserName.getText().toString())) {
String email = etEmail.getText().toString();
String password = etPassword.getText().toString();
String username = etUserName.getText().toString();
createUserEmailAccount(email, password, username);
} else {
Toast.makeText(CreateAccountActivity.this, "Please fill in all fields"
, Toast.LENGTH_SHORT).show();
}
}
});
}
private void createUserEmailAccount(String email, String password, final String username) {
if (!TextUtils.isEmpty(email) && !TextUtils.isEmpty(password) &&
!TextUtils.isEmpty(username)) {
progressBar.setVisibility(View.VISIBLE);
firebaseAuth.createUserWithEmailAndPassword(email, password)
.addOnCompleteListener(new OnCompleteListener<AuthResult>() {
#Override
public void onComplete(#NonNull Task<AuthResult> task) {
if (task.isSuccessful()) {
currentUser = firebaseAuth.getCurrentUser();
assert currentUser != null;
final String currentUserId = currentUser.getUid();
Map<String, String> userObj = new HashMap<>();
userObj.put("userId", currentUserId);
userObj.put("username", username);
collectionReference.add(userObj)
.addOnSuccessListener(new OnSuccessListener<DocumentReference>() {
#Override
public void onSuccess(DocumentReference documentReference) {
documentReference.get()
.addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() {
#Override
public void onComplete(#NonNull Task<DocumentSnapshot> task) {
if (Objects.requireNonNull(task.getResult()).exists()) {
progressBar.setVisibility(View.INVISIBLE);
String name = task.getResult()
.getString("username");
Intent intent = new Intent(CreateAccountActivity.this,
PostJournalActivity.class);
intent.putExtra("username", name);
intent.putExtra("userId", currentUserId);
startActivity(intent);
} else {
progressBar.setVisibility(View.INVISIBLE);
}
}
});
}
})
.addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
Toast.makeText(CreateAccountActivity.this, "failed"
, Toast.LENGTH_SHORT).show();
}
});
} else {
Toast.makeText(CreateAccountActivity.this, "failed task"
, Toast.LENGTH_SHORT).show();
}
}
})
.addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
Toast.makeText(CreateAccountActivity.this, "failed on complete"
, Toast.LENGTH_SHORT).show();
}
});
} else {
Toast.makeText(this, "failed else", Toast.LENGTH_SHORT).show();
}
}
#Override
protected void onStart() {
super.onStart();
currentUser = firebaseAuth.getCurrentUser();
firebaseAuth.addAuthStateListener(authStateListener);
}
}
i fixed, the problem was i had allow read, write: if false; in firestore
and it needed to be allow read, write: if true;
This could be solved by following good debugging practices.
Meaningful error messages
When you encounter an error and wish to send a toast/alert to the user, send something meaningful to inform them what went wrong. As an example, instead of "failed" or "failed task", use "failed to upload user data" or "failed to create new user".
Each function will normally return a handful of exception classes that can be used to provide a better toast/alert message. Consulting the documentation for createUserWithEmailAndPassword(email, password) you can see what exceptions are thrown and use instanceof to determine the cause of the problem. For example, if e instanceof FirebaseAuthInvalidCredentialsException was true, you could toast "failed to create new user: invalid email".
While this seems tedious, it will save head-scratching later when a user encounters a problem and sends you a bug report/email about it. These steps will help you find any issues without needing access to logs for trivial problems such as incorrectly filled forms.
Log exceptions
The reason you have no information on what went wrong is because you haven't made use of the exception provided in each onFailure handler (public void onFailure(#NonNull Exception e) { ... }). These handlers provide you with the exception that caused the problem which you can save to the log using Log.e("yourActivityName:yourFunctionName", "short message", e). You can also use e.getMessage() to get information about the thrown error.
In an onComplete(Task<?> task) handler, if task.isSuccessful() returns false, you can find out why it is false by calling Exception e = task.getException() and then log it.
Fail-fast programming
If you ever find that you have an if-else pair where the if section contains lots more code than the else section, it is likely to be a sign that you should flip the condition.
Whilst keeping your code cleaner by using less indentation, it also avoids having to scroll through a long if that probably contains more if and else statements.
For example,
if (!requiredVariable1.isEmpty() && !requiredVariable2.isEmpty()) {
// ...
// many (nested) lines of code
// ...
} else {
Log.e(TAG, "a required variable was empty");
}
if (requiredVariable1.isEmpty() || requiredVariable2.isEmpty()) {
Log.e(TAG, "a required variable was empty");
return;
}
// ...
// many lines of code
// ...
Example
As an example of applying these changes, I have made edits to the code you provided applying fail-fast techniques, simplifying error handling, logging exceptions, using OnSuccessListener and OnFailureListener instead of OnCompleteListener where appropriate,
private void createUserEmailAccount(String email, String password, final String username) {
if (TextUtils.isEmpty(email) || TextUtils.isEmpty(password) || TextUtils.isEmpty(username)) {
Toast.makeText(this, "Please fill in all fields", Toast.LENGTH_SHORT).show();
return;
}
progressBar.setVisibility(View.VISIBLE);
firebaseAuth.createUserWithEmailAndPassword(email, password)
.addOnSuccessListener(new OnSuccessListener<AuthResult>() {
#Override
public void onSuccess(#NonNull Task<AuthResult> task) {
currentUser = firebaseAuth.getCurrentUser();
assert currentUser != null;
final String currentUserId = currentUser.getUid();
Map<String, String> userObj = new HashMap<>();
userObj.put("userId", currentUserId);
userObj.put("username", username); // unknown source for variable: username
collectionReference.add(userObj)
.addOnSuccessListener(new OnSuccessListener<DocumentReference>() {
#Override
public void onSuccess(DocumentReference documentReference) {
documentReference.get() // why redownload from database? you could just use values of "userObj"
.addOnCompleteListener(new OnCompleteListener<DocumentSnapshot>() {
#Override
public void onComplete(#NonNull Task<DocumentSnapshot> task) {
progressBar.setVisibility(View.INVISIBLE);
if (!task.isSuccessful()
|| !Objects.requireNonNull(task.getResult()).exists()) {
// show a error message?
return;
}
String name = task.getResult()
.getString("username");
Intent intent = new Intent(CreateAccountActivity.this,
PostJournalActivity.class);
intent.putExtra("username", name);
intent.putExtra("userId", currentUserId);
startActivity(intent);
}
});
}
})
.addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
Toast.makeText(CreateAccountActivity.this, "failed to add user data"
, Toast.LENGTH_SHORT).show();
Log.e("CreateAccountActivity", "failed to add user data", e); // log error to logcat
}
});
}
})
.addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
Toast.makeText(CreateAccountActivity.this, "failed to create user"
, Toast.LENGTH_SHORT).show();
Log.e("CreateAccountActivity", "failed to create user", e); // log error to logcat
}
});
}
I am new to android development and firestore. What I am trying to do is compare whether a string exists in the database or not. However, the query always returns true even if I am trying to compare with string that is not present in the database. Here is my code:
public void loadData(View v){
db.collection("Machines")
.whereEqualTo("machineId","AYSLE004")
.get()
.addOnSuccessListener(new OnSuccessListener<QuerySnapshot>() {
#Override
public void onSuccess(QuerySnapshot queryDocumentSnapshots) {
//MachineId exists
Toast.makeText(MainActivity.this, "Machine Id exists", Toast.LENGTH_LONG).show();
}
})
.addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
//Machine Id does not exist
Toast.makeText(MainActivity.this, "Machine Id does not exist", Toast.LENGTH_LONG).show();
Log.d(TAG,e.toString());
}
});
}
The string "AYSLE004" does not exist in the database. However the toast message shows "Machine Id exists". Please assist me where I am going wrong. Thanks in advance.
Try adding the following:
.addOnSuccessListener(new OnSuccessListener<QuerySnapshot>() {
#Override
public void onSuccess(QuerySnapshot queryDocumentSnapshots) {
//MachineId exists
if(queryDocumentSnapshots != null && !queryDocumentSnapshots.isEmpty()) {
Toast.makeText(MainActivity.this, "Machine Id exists", Toast.LENGTH_LONG).show();
} else {
//it doesn't exist
}
}
}
The reason for this (I believe) is because the onSuccess listener is called because the request to Firebase was completed and achieved successfully, it just didn't happen to find your request.
I want to give the user in my app the possibility to delete his account so when he clicks on the delete button a document gets deleted which contains all his informations. The name of the document is his displayName so I get this as a string but when I run the code you are seeing below I get a NullpointerException in this line:
String currentUsername = user.getDisplayName();
even though the displayName is not null.
Edit:
I found the solution on my own, see the answer below.
Here is my method:
btn_delete_account.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
user.delete()
.addOnCompleteListener(new OnCompleteListener<Void>() {
#Override
public void onComplete(#NonNull Task<Void> task) {
if (task.isSuccessful()) {
deleteDocument();
}
}
});
}
});
...
public void deleteDocument (){
FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
String currentUsername = user.getDisplayName();
db.collection("User").document(currentUsername)
.delete()
.addOnSuccessListener(new OnSuccessListener<Void>() {
#Override
public void onSuccess(Void aVoid) {
Log.d(TAG, "DocumentSnapshot successfully deleted!");
Toast.makeText(PersonalSettings.this, "Your account was successfully deleted.", Toast.LENGTH_SHORT).show();
Intent i = new Intent(PersonalSettings.this, SignInActivity.class);
startActivity(i);
finish();
}
})
.addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
Log.w(TAG, "Error deleting document", e);
}
});
}
First thing you have to check that current user is not null
FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
if(user==null)
{
return;
}
if current user is not null then get its name and further check that it's name is not null.
String currentUsername = user.getDisplayName();
if(TextUtils.isEmpty(currentUsername))
{
return;
}
if name is not null then go for delete document as follows :
public void deleteDocument (){
FirebaseUser user = FirebaseAuth.getInstance().getCurrentUser();
if(user==null)
{
return;
}
String currentUsername = user.getDisplayName();
if(TextUtils.isEmpty(currentUsername))
{
return;
}
db.collection("User").document(currentUsername)
.delete()
.addOnSuccessListener(new OnSuccessListener<Void>() {
#Override
public void onSuccess(Void aVoid) {
Log.d(TAG, "DocumentSnapshot successfully deleted!");
Toast.makeText(PersonalSettings.this, "Dein Account wurde erfolgreich gelöscht.", Toast.LENGTH_SHORT).show();
Intent i = new Intent(PersonalSettings.this, SignInActivity.class);
startActivity(i);
finish();
}
})
.addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
Log.w(TAG, "Error deleting document", e);
}
});
}
I think you're misunderstanding the error. It's saying that user is null, not the display name. This means there is currently no user signed into the app. You will have to write some code to check for this case.
I also strongly suggest not using a display name as the ID for a document in Cloud Firestore. Since you're using Firebase Authentication, the user already has a unique ID assigned to their account. This is the preferred way to store per-user data.
I found the error:
I called my delete method after I used the user.delete() method which deletes the signed in user, so logically the displayName was also deleted.
I have created a database in firestore and loaded the test data successfully. But later I noticed that some data is missing in database. I found that when I load the same records with a different value for some fields the old record is replaced with the new record. I feel like this is the issue in the database design. I need to collect and save all the records even if it is a duplicate records at entire records level.
Could you please let me know how to do this ?
private void exportToFireStore() {
// Access a Cloud Firestore instance from your Activity
final FirebaseFirestore db = FirebaseFirestore.getInstance();
/* ----------------_-------Collection delete is not supported-----------------------
----------Hence get all the document (coins) for individual Docs delete----------
*/
//------------------------------- Getting document (coins) Ends ---------------------------------------------
final List<String> coinsFromFirestore = new ArrayList<>();
db.collection("cryptos").document(userEmailID).collection("coin")
.whereEqualTo("createdBy", userEmailID)
.get()
.addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() {
#Override
public void onComplete(#NonNull Task<QuerySnapshot> task) {
if (task.isSuccessful()) {
for (QueryDocumentSnapshot document : task.getResult()) {
System.out.println("Testing 1 Batch Read done" + document.getData());
coinsFromFirestore.add(document.getData().get("coinname").toString());
}
//------------------------------- Getting document (coins) Ends ---------------------------------------------
if(coinsFromFirestore.size()>0){
for (int i=0;i<coinsFromFirestore.size();i++) {
if ( i<(coinsFromFirestore.size()-1) ) {
db.collection("cryptos").document(userEmailID).collection("coin").document(coinsFromFirestore.get(i))
.delete()
.addOnSuccessListener(new OnSuccessListener<Void>() {
#Override
public void onSuccess(Void aVoid) {
System.out.println("Testing 1 Successfully Deleted the document " );
}
})
.addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
System.out.println("Testing 1 Error Deleting the document ");
}
});
}else{
db.collection("cryptos").document(userEmailID).collection("coin").document(coinsFromFirestore.get(i))
.delete()
.addOnSuccessListener(new OnSuccessListener<Void>() {
#Override
public void onSuccess(Void aVoid) {
addTranToFireBaseeNow(db);
}
})
.addOnFailureListener(new OnFailureListener() {
#Override
public void onFailure(#NonNull Exception e) {
}
});
//!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! For last coin Ends !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
}
}
}else{
addTranToFireBaseeNow(db);
}
} else {
Log.d(TAG, "Error getting documents: ", task.getException());
}
}
});
//------------------------------- Getting document (coins) Ends ---------------------------------------------
}
private void addTranToFireBaseeNow(FirebaseFirestore db) {
WriteBatch batch = db.batch();
DocumentReference newCoinRef;
//CollectionReference cryptos = db.collection("cryptos");
List<Tran> tranList = getAllTranForFireStore();
String firebaseUID = FirebaseAuth.getInstance().getCurrentUser().getUid();
for (Tran t : tranList) {
Map<String, Object> tranData = new HashMap<>();
tranData.put("firebaseid", firebaseUID);
tranData.put("createdBy", userEmailID);
tranData.put("coinid", t.getCoinID());
tranData.put("coinname", t.getCoinName());
tranData.put("coinsymbol", t.getCoinSymbol());
tranData.put("date", String.valueOf(t.getDate()));
tranData.put("qty", String.valueOf(t.getQty()));
tranData.put("price", String.valueOf(t.getPrice()));
tranData.put("priceunit", String.valueOf(t.getPriceUnit()));
newCoinRef= db.collection("cryptos").document(userEmailID).collection("coin").document(t.getCoinName());
batch.set(newCoinRef, tranData);
}
batch.commit().addOnCompleteListener(new OnCompleteListener<Void>() {
#Override
public void onComplete(#NonNull Task<Void> task) {
// ...
}
});
}
No index is setup for my DB
Since you are using set without any options, it will overwrite the existing data. But your requirement is to merge your data, so you have to use the merge option as follows:
batch.set(newCoinRef, tranData, SetOptions.merge());
You can read more about options here.
Furthermore there is a good post which lists the differences between set, update and create.
You can use update and change one variable
reference.document("documentname").update("field", variable)
or
reference.document("documentname").set({
field: variable
}, { merge: true });
Or.. if you have to update an entire object, you can use:
reference.document("documentname").set(newObject, { merge: true });
Check this article:
https://saveyourtime.medium.com/firebase-cloud-firestore-add-set-update-delete-get-data-6da566513b1b