I'm still on Firebase and this time I have a question related on the deletion of objects.
I have a structure like the following:
users: {
UsErId1:{
name: "Jack",
email: "m#i.l"
},
UsErId2: { + },
UsErId3: { + }
},
user_contacts: {
UsErId1:{
UsErId2: true,
UsErId3: true
},
UsErId2: {
UsErId1: true
}
}
So if I want to delete an user I have to:
Delete the user object
Delete the user object under the user_contacts branch
Remove all the indexes from user_contacts that are pointing to that user
My performance problems comes from the point 3, because I need to iterate all the user_contacts entries to see if a user it's present in the childrens.
An example of code is the following:
private void deleteUser(String userId) {
firebaseDatabase.getReference("users").child(userId).removeValue();
firebaseDatabase.getReference("users_contacts").child(userId).removeValue();
firebaseDatabase.getReference("users_contacts").addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
for (DataSnapshot usersSnapshot : dataSnapshot.getChildren()) {
for( DataSnapshot contactSnapshot : usersSnapshot.getChildren() ){
String contactId = contactSnapshot.getValue(String.class);
if( contactId.equals(userId) ){
contactSnapshot.getRef().removeValue();
}
}
}
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});
}
I've thought of two possible solutions:
Solution 1: Don't delete the indexes from user_contacts and when I've to load the user contacts, I've to do a call to each user to see if the user is null (has been deleted), and in that case, don't show it. By the way, this results in a dirty database.
Solution 2: create a reverse index contacts_users where I store the users for which the user I'm trying to delete is referenced. As follow:
contacts_user: {
UsErId1: {
UsErId2: true
},
UsErId2: {
UsErId1: true
},
UsErId3: {
UsErId1: true
}
}
So, when I have to delete a user, I will look at its childs in contacts_users and know every users that has it in its contacts, and proceed to delete it (now that I can know the entire path). This seems to me to be a more NoSql-like approach.
What do you think? Is there another way to do it?
Your second solution is how I would suggest doing it, no need to search. You could store that information on a per user basis, but if it grows too large having it elsewhere is better.
Likewise deleting in the other direction also becomes easier.
Related
Reading the documentation, It seems childEventListener does not fire when the path does not exist.
This is a problem since I want to display a message to the user that there is no data.
I could add a valueEventListener like in this answer but I'm limiting the query to the latest value i.e query.limitToLast() and a valueEventListener doesn't limitTolast but gets all the data in the path.
Example, I have:
posts
{
$userid
{
$postid {
post_content:content
timestamp:1234567
}
$postid {
post_content:content
timestamp:1234567
}
$postid {
post_content:content
timestamp:1234567
}
$postid {
post_content:content
timestamp:1234567
}
}
}
I'm only interested in the latest post so I do firebaseRef.child(users).child(userid).limitToLast(1).addChildEventListener but the user might not have posts yet and childEventListener does not fire in that case.
If you want to handle both the children and the case where no children exists, you can add both a value and a child listener:
Query query = firebaseRef.child(users).child(userid).limitToLast(1);
query.addChildEventListener(new ChildEventListener() {
void onChildAdded(DataSnapshot snapshot, String previousChildKey) {
...
}
...
});
query.addValueEventListener(new ValueEventListener() {
void onDataChange(DataSnapshot snapshot) {
if (!snapshot.exists()) {
// TODO: handle the "no data available" scenario
}
});
});
The Firebase client is smart enough to only load data once, even if there are multiple listeners like in the above case.
If you want, you can also accomplish this with a single ValueEventListener like this:
query.addValueEventListener(new ValueEventListener() {
void onDataChange(DataSnapshot snapshot) {
if (!snapshot.exists()) {
// TODO: handle the "no data available" scenario
}
else {
for (DataSnapshot childSnapshot: snapshot.getChildren()) {
// TODO: handle the child snapshot
}
}
});
});
Since we now get all matching child nodes in snapshot, we loop over the snapshot.getChildren() to get the same data as in onChildAdded.
Since its impossible to attach a listener to a non existing path, you could try adding a property to your user that sets the number of posts he has. Add a listener to that property and if it changes, then you are certain that indeed the user has a path reference in posts, then you can add a listener to that node and get the query every time a child is added with .childAdded .
I'm creating an android studio voting application. It is using recyclerview to render candidates information from the database. Once the voter clicks on a vote button, the candidate is added a vote on the firebase realtime database.
I wanted to make sure that a voter can only vote once. Is there a firebase rule I can use or do I have to do it in code?
public void onBindViewHolder(#NonNull final MyViewHolder holder, final int position) {
holder.name.setText(candidates.get(position).getFirstname());
holder.party.setText(candidates.get(position).getParty());
holder.category.setText(candidates.get(position).getCategory());
Picasso.get().load(candidates.get(position).getImageurl()).into(holder.profilepic);
holder.vote.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
updateTotalVotes("increaseTotalVotes", candidates.get(position).getImageurl());
}
});
}
public static void updateTotalVotes(final String operation, String key) {
System.out.println("Inside updateTotalVotes");
DatabaseReference rootRef = FirebaseDatabase.getInstance().getReference();
DatabaseReference totalVotesRef = rootRef.child("candidates").child(key).child("totalVotes");
totalVotesRef.runTransaction(new Transaction.Handler() {
#Override
public Transaction.Result doTransaction(MutableData mutableData) {
System.out.println("Inside Transactions");
Integer votes = mutableData.getValue(Integer.class);
if (votes == null) {
System.out.println("Inside first if statement = null");
return Transaction.success(mutableData);
}
if (operation.equals("increaseTotalVotes")) {
System.out.println("Inside update Votes by adding 1");
mutableData.setValue(votes + 1);
} else if (operation.equals("decreaseTotalVotes")){
mutableData.setValue(votes - 1);
}
return Transaction.success(mutableData);
}
#Override
public void onComplete(DatabaseError databaseError, boolean b, DataSnapshot dataSnapshot) {
// Log.d(TAG, databaseError.getMessage()); //Don't ignore errors!
}
});
}
Firebase's security rules cannot enforce unique value in a specific property under a single node. But (as is often the case with NoSQL databases) you can use a specific data model to implement the use-case.
The usual solution for this is to use the UID of the voter as the key.
votes
uid1: "candidate A"
uid2: "candidate B"
uid3: "candidate A"
Since keys must be unique in a JSON object, this structure ensures by definition that each UID can only vote once.
This is separate from keeping the total votes for a candidate. For that you can either use security rules, or Cloud Functions.
Doing this is in security is appealing, since it means you won't need any server-side code. But the rules can become quite complex. For an example of this, see my answer to this question: Is the way the Firebase database quickstart handles counts secure?
The simpler, and these days more common, approach is to do this with a Cloud Function. From a recent project I worked on, I have this Cloud Function:
exports.countVote = functions.database.ref('/votes/{uid}').onCreate((snapshot, context) => {
let value = snapshot.val();
let countRef = snapshot.ref.parent.parent.parent.child(`totals/${value}`);
return countRef.transaction(function(current) {
return (current || 0) + 1;
})
});
So this tallies the votes for each unique value. It then ensures that users can't change their existing vote with:
{
"rules": {
"votes": {
"$uid": {
".write": "auth.uid === $uid && !data.exists()"
}
}
}
}
So a user can only vote if they user their own UID (the auth.uid variable is prepopulated and can't be spoofed), and if they haven't voted yet.
Short answer is No, you can't by the rules of db.
But you can do it with help of authentication, which user can make an account and his vote record in a sub tree of the candidate. So that the number of sub tree children is the number of votes for this candidate.
Notice: my solution could be broken if fake accounts were made, my advice using phone authentication too, to approve the account is not fake.
I'm using firebase with android to create a simple chat app. When the user chooses another user to chat with I want to check whether they've chatted together or not.
In onCreate() method I'm retrieving all the rooms that the current user used before, and I'm putting them in an arraylist called MyChatRooms<>.
Then I want to check each room to see the users of the room.
The problem is that the loop I'm using to iterate through rooms name is finishing before I'm able to retrieve any data from the database.
I know there's similar questions to mine, but none of the answers worked for me.
Here's the related code:
if (!MYChatRooms.isEmpty()) {
for (j = 0; j < MYChatRooms.size(); j++) {
roomref.child(MYChatRooms.get(j)).child("First User").addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot1) {
if (!dataSnapshot1.getValue().toString().equals(Username) && dataSnapshot1.getValue().toString().equals(NUsername)) {
Users += dataSnapshot1.getValue().toString() + ",,, ";
} else if (dataSnapshot1.getValue().toString().equals(Username)) {
roomref.child(MYChatRooms.get(j)).child("Second User").addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot2) {
if (dataSnapshot2.getValue().toString().equals(NUsername)) {
Users += dataSnapshot2.getValue().toString() + ",,, ";
}
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});
}
}
#Override
public void onCancelled(DatabaseError databaseError) {
}
});
}
}
I would suggest that you change the structure of your data. Imagine if a user has 100 chats that means your have to query 200 times to Firebase that of course does not look feasible.
What i would suggest is that your add a recentChat list in every user and whenever a user starts a new chat with someone you add the id of the second user to that list. That way you can track easily with whom the current user has interacted with.
It structure in firebase can look something like this:
User
recentChats
id of the other user
Try to change you database hierarchy or use firestore instead of real time database
Please check the following topic: https://firebase.google.com/docs/database/usage/optimize?
In my case, I had added and index column and limited the query in Firebase Rules.
I'm working on a small app to practice my JAVA/Firebase skills, and I have come into a roadblock. I do admit that I'm not very familiar with Firebase, and rules associated with the database portion. But I have tried looking at other SO posts and searching through documentation.
Problem:
Users create an account (through Firebase Authentication - E-mail/Password). Then they are able to create a "character" and provide this "character" with a name. So I need the "charName" to be unique. And obviously the authenticated ID is also unique already. So I need the app to tell users if the name is already taken or if it isn't, then to go ahead with adding it to the database.
Here are simplified snippits of my code:
btnCreate.setOnClickListener(new View.OnClickListener() {
#Override
public void onClick(View v) {
final String charName = "MyCharactersName";
final int charID = 123;
mFirebaseDatabase.child("characters").addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot snapshot) {
if (!(snapshot.child(charName).exists())) {
Character newCharacter = new Character(charID, charName);
mFirebaseDatabase.child("characters").child(getNewCharID()).setValue(newCharacter); // add to database
Snackbar.make(findViewById(R.id.view_root), "Success", BaseTransientBottomBar.LENGTH_INDEFINITE).show();
} else {
Snackbar.make(findViewById(R.id.view_root), "Error creating a character.", BaseTransientBottomBar.LENGTH_INDEFINITE).show();
}
} else {
Snackbar.make(findViewById(R.id.view_root), "That character name is already taken.", BaseTransientBottomBar.LENGTH_INDEFINITE).show();
}
}
#Override
public void onCancelled(DatabaseError databaseError) {
Snackbar.make(findViewById(R.id.view_root), "Error - Did not connect with Database", BaseTransientBottomBar.LENGTH_INDEFINITE).show();
}
});
}
});
Currently: The app creates a new character on the database, but you can add duplicate characters. (the charID is unique by the way, I hardcoded it in the snippit above... but it is timeStamp + 4 random digits).
So that obviously happens with the default database Rules. And in the examples that I did read, it looks like I might have to modify those?
My Database structure is as such:
App / characters / charID / charName
I tried to adapt some code from this SO post: How do you prevent duplicate user properties in Firebase?
and this is what I wrote, but it doesn't work and as I admitted before, I'm not familiar with rules, so I'm not sure what I did/did wrong. haha.
{
"rules" : {
"characters" : {
"$charID" : {
".validate": "root.child('charName_lookup/'+newData.val()).val() === auth.uid"
}
},
"charName_lookup" : {
"$charName" : {
".write" : "!data.exists()",
".validate": "newData.val() === auth.uid"
}
}
}
}
If you have any questions/clarifications please let me know. I will be stepping away from the computer periodically but I will check back promptly (I hope!)
Basically, the userName or the email address should be the name of your node. Regarding rules, you can use wildcards. If you create a userName George, to verify if exists you only need to put a reference on users node and use exists() method.
Please take a look at Frank van Puffelen's explanation from this video. Even if you'll need to remodel a bit your database, remember that this is the best practice when we are talking about duplicates.
Reading the documentation, It seems childEventListener does not fire when the path does not exist.
This is a problem since I want to display a message to the user that there is no data.
I could add a valueEventListener like in this answer but I'm limiting the query to the latest value i.e query.limitToLast() and a valueEventListener doesn't limitTolast but gets all the data in the path.
Example, I have:
posts
{
$userid
{
$postid {
post_content:content
timestamp:1234567
}
$postid {
post_content:content
timestamp:1234567
}
$postid {
post_content:content
timestamp:1234567
}
$postid {
post_content:content
timestamp:1234567
}
}
}
I'm only interested in the latest post so I do firebaseRef.child(users).child(userid).limitToLast(1).addChildEventListener but the user might not have posts yet and childEventListener does not fire in that case.
If you want to handle both the children and the case where no children exists, you can add both a value and a child listener:
Query query = firebaseRef.child(users).child(userid).limitToLast(1);
query.addChildEventListener(new ChildEventListener() {
void onChildAdded(DataSnapshot snapshot, String previousChildKey) {
...
}
...
});
query.addValueEventListener(new ValueEventListener() {
void onDataChange(DataSnapshot snapshot) {
if (!snapshot.exists()) {
// TODO: handle the "no data available" scenario
}
});
});
The Firebase client is smart enough to only load data once, even if there are multiple listeners like in the above case.
If you want, you can also accomplish this with a single ValueEventListener like this:
query.addValueEventListener(new ValueEventListener() {
void onDataChange(DataSnapshot snapshot) {
if (!snapshot.exists()) {
// TODO: handle the "no data available" scenario
}
else {
for (DataSnapshot childSnapshot: snapshot.getChildren()) {
// TODO: handle the child snapshot
}
}
});
});
Since we now get all matching child nodes in snapshot, we loop over the snapshot.getChildren() to get the same data as in onChildAdded.
Since its impossible to attach a listener to a non existing path, you could try adding a property to your user that sets the number of posts he has. Add a listener to that property and if it changes, then you are certain that indeed the user has a path reference in posts, then you can add a listener to that node and get the query every time a child is added with .childAdded .