I have the following structure.
Each user has his own data.
---+ root_child
|
+---+ Gy7FXRbRjDfAKWu7a95NgiGIZUk1 (Firebase User Id)
|
|
+---+ KlNlb71qtQUXIGA4cNa (random key, generated by Firebase)
| |
| +--- timestamp = 1234567890
| |
| +--- (other data field ...)
|
|
+---+ KlNlcmfMTDjxQ0BwW1K
| |
| +--- timestamp = 9876543211
| |
| +--- (other data field ...)
|
|
+---+ (...)
Adding records occurs in this way:
databaseReference = FirebaseDatabase.getInstance().getReference("root_child");
databaseReference.child(firebaseUser.getUid())
.push()
.setValue(val);
push() ... Locations generated on a single client will be sorted in
the order that they are created...
Now, how can I leave only 100 newest entries (for the specified user id) and delete all the rest?
Pseudocode :
databaseReference = FirebaseDatabase.getInstance()
.getReference("root_child")
.child(firebaseUser.getUid())
.deleteLastNnRecords();
I would probably use the sort and filter functions.
First, I would grab the children count with a single value listener: https://stackoverflow.com/a/43607203/7949696 , Or in the case of a cloud function I would count the children from the snap on the write trigger.
Then, if childCount > 100, I would OrderByChild() on your timestamp and then filter by LimitToLast(childCount - 100)
If I were you, I would use cloud functions to achieve this and set a function on write to root_child/${uid}/
Edit: Source code link from comments:
https://github.com/firebase/functions-samples/tree/master/limit-children
Cheers!
Edit 2: Looks like push() is chronologically sorted, so no need to OrderByChild
Algorithm:
AddListenerForSingleValueEvent is called only 1 time to get all the data and to delete all the old ones
Then we call only addChildEventListener to get only new data
databaseReference.orderByKey()
.addListenerForSingleValueEvent(valueEventListener =
new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
long count = dataSnapshot.getChildrenCount();
long deleteFirst = count - Const.FB_MAX_CHILD;
long i = 0;
Log.i(TAG, "DataSnapshot children count = " + String.valueOf(count));
for (DataSnapshot data : dataSnapshot.getChildren()) {
i++;
if (i <= deleteFirst) {
// delete
Log.i(TAG, "delete => " + data.getKey());
data.getRef().removeValue();
} else {
TrackData trackData = data.getValue(TrackData.class);
Log.i(TAG, data.getKey() + " = " + trackData.toString());
}
}
}
Related
I am using Firebase Realtime databse in an android app ,
the app contains articles and I want to add the Number of views to the articles through all the app users .
The database contains a child (ViewsCount) its value (is a number)
I need that value to be updated (incremented by 1) every time an article is viewed by a new user (Concept similar to YouTube video views)
The user have to be authenticated anonymously to update the value, but should not be able to do it more than once.
I got the logic integrated into the app to do just that and works great.
But what I am afraid of is if someone tries to connect to the database outside of my app (using their own code) and keeps incrementing the value.
My current security rules are:
"ViewsCount"
{
".write": "newData.exists() && auth!==null",
".validate" "newData.val()===data.val()+1"
}
"users": {
"$user_id": {
".write": "auth.uid===$user_id",
".read": "auth.uid===$user_id ",
}
}
is there a way to restrict the update of a that value to only once per user via security rules ?.
Error :
Update :
screenshot from Simulator
Database structure
There sure is, but you'll have to do some extra work. The first thing is to track in the database who has already viewed each article. For example keep a list of their UIDs, like this:
viewedBy: {
uidOfKarim: true,
uidOfPuf: true
}
Now when somebody views an article, they write their UID to viewedBy, and at the same time increment the view count for that article. In code that can be as simple as:
DatabaseReference countRef = FirebaseDatabase.getInstance().getReference("ViewsCount");
countRef.addListenerForSingleValueEvent(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
String uid = FirebaseAuthentication.getCurrentUser().getUid();
Long newCount = dataSnapshot.getValue(Long.class) + 1;
Map<String, Object> updates = new HashMap<String, Object>();
updates.put("viewedBy/"+uid, true);
updates.put("ViewsCount", newCount);
}
#Override
public void onCancelled(DatabaseError databaseError) {
throw databaseError.toException();
}
}
So this code send the UID of the user and the new view count to the database. Now we update the security to only accept the write operation if the UID hasn't been stored yet and if the count is incremented by 1:
"viewedBy": {
"$user_id": {
".validate": "
auth.uid===$user_id && !data.exists() && newData.exists() &&
newData.parent().parent().child('ViewsCount').val() === data.parent().parent().child('ViewsCount').val()+1
"
}
},
The syntax is a bit long, since it checks both the ViewsCount and the ViewedBy data. The write only succeeds if it increments by 1 and is from a user who hasn't been counted yet. If they have been counted before, or don't increment by 1, the write is rejected.
There is one edge case here: if multiple users view at almost the same time, one of their writes may be rejected, because they didn't actually increment the value:
user1 user2 database
| | |
read_count -------------------> |
| | |
| read_count --------> |
| | |
| <------------------- 2 |
| | |
| | <------------ 2 |
| | |
set_count 3 -------------------> |
| | |
| set_count --------> |
| | |
| <------------------- ok |
| | |
| | <------------ ACCESS |
| | DENIED |
| | |
In this case you should retry the update operation, reading the new value and determining the correct count based on that. This is actually how Firebase transactions work behind the scenes. Alternatively, you can first read from viewedBy to see if the current user has already been counted, before trying to count them. But either way: with these rules, each user can only count once.
Update: this is how I tested these rules in the simulator:
And this is the JSON I tested with:
"52469568": {
"ViewsCount": 10,
}
This is the security rules that worked in case any one else have the same issue:
{"rules": {
"views": {
".write": "auth != null",
".validate": "newData.hasChildren(['ViewsCount', 'viewedBy'])",
"ViewsCount": {
".read": "auth != null",
".validate":"data.parent().child('viewedBy').child(auth.uid).exists()== false"
},
"viewedBy": {
"$user_id": {
".validate": " auth.uid===$user_id && !data.exists() && newData.exists() &&
newData.parent().parent().child('ViewsCount').val() ===
data.parent().parent().child('ViewsCount').val()+1
&& newData.val()===true"}},
},
I return to this question.
I have the following structure. Each user has his own data.
---+ FB_ROOT_REFERENCE
|
+---+ Gy7FXRbRjDfAKWu7a95NgiGIZUk1 (Firebase User Id)
|
|
+---+ KlNlb71qtQUXIGA4cNa (random key, generated by Firebase)
| |
| +--- (data field ...)
|
|
+---+ KlNlcmfMTDjxQ0BwW1K
| |
| +--- (data field ...)
|
+---+ (...)
I've made some changes to the source code, but I do not know where to move next.
// Max number of lines
const MAX_RECORD_COUNT = 5;
const FB_ROOT_REFERENCE = '/locations';
// Removes siblings of the node that element that triggered the function if there are more than MAX_RECORD_COUNT.
// In this example we'll keep the max number of chat message history to MAX_RECORD_COUNT.
exports.truncate = functions.https.onRequest((req, res) => {
const cron_key = req.query.key;
// Exit if the keys don't match
if (!secureCompare(cron_key, functions.config().cron.key)) {
console.log('The cron_key provided in the request does not match the cron_key set in the environment. Check that', cron_key,
'matches the cron.key attribute in `firebase env:get`');
res.status(403).send('Security cron_key does not match. Make sure your cron_key URL query parameter matches the ' +
'cron.key environment variable.');
return;
}
// Next
// need iterate all the children in (FB_ROOT_REFERENCE + user id)
// and keep MAX_RECORD_COUNT data children
// most recent and delete all the rest (older ones)
// ...
// Here your code is required....
});
I would be very grateful if anyone could help me to write the source code. Thank you.
The code in this example will probably be useful "Limit number of child nodes"
I have a database like
--------------------------------------------------
| _id| poi_id | poi_name |poi_address |
--------------------------------------------------
| 1 | 101 | sight_1 |sight_1_adr |
--------------------------------------------------
| 2 | 101 | sight_2 |sight_2_adr |
--------------------------------------------------
| 3 | 100 | sight_3 |sight_3_adr |
--------------------------------------------------
| 4 | 101 | sight_4 |sight_4_adr |
--------------------------------------------------
| 5 | 100 | sight_5 |sight_5_adr |
--------------------------------------------------
First of all , i want to create a listview and group my data by "poi_id".
I use the following query:
"SELECT* FROM poi_table GROUP BY poi_id"
I use a cursor with that query and i create the list view as:
entry.open();
Cursor cursor = entry.getData();
adapter = new CustomListAdapter(this.getActivity(), cursor);
setListAdapter(adapter);
My problem is that, in every list row, i would like to print also the poi_name of every poi_id.So iwould like to have something like:
-------------------------------
-101-
sight 1
sight 2
sight 4
-------------------------------
-100-
sight 3
sight 5
-------------------------------
etc
This is where i stack!
In my CustomCursorAdapter, i get the poi_id as:
String ID = cursor.getString(cursor
.getColumnIndex(db.POI_ID));
and then i use a second cursor to get the data like:
Cursor c2 = entry.getDatabyID(ID);
for (int i = 0; i < entry.fetchPlacesCountByID(ID); i++) {
c2.moveToPosition(i);
String name= c2
.getString(c2
.getColumnIndex(db.POINT_NAME));}
and i create dynamically a new textView and i set the string name there as text.
How can i fix it?That way doesnt crash but i get as many poi_names as i scroll down..
In my app, I'm trying to have a list of custom servers, and I want to be able to add them and edit their respective settings individually.
The standard Android PreferenceFragment looks and works great for my purposes. Is there a way I can use it to edit selected items? Or, alternatively, can I re-create its look and behavior easily?
In a nutshell :
---------------- ----------------
| +| | |
| Item A | | Preference |
| Item B | --> Click on "A", "B" or "+" --> | Screen |
| ... | | |
| | | |
---------------- ----------------
Note that I also know how to use a screen hierarchy, and that doesn't fit the bill since I can't add or remove items dynamically, while the app is running.
From what I understand, you want to add preferences dynamically in your fragment.. Am I right??
Just look at this sample I found:
onCreate(){
this.setPreferenceScreen(createPreferenceHierarchy());
}
public PreferenceScreen createPreferenceHierarchy(){
PreferenceScreen root = getPreferenceManager().createPreferenceScreen(this);
// category 1 created programmatically
PreferenceCategory cat1 = new PreferenceCategory(this);
cat1.setTitle("title");
root.addPreference(cat1);
ListPreference list1 = new ListPreference(this);
list1.setTitle(getResources().getString(R.string.some_string_title));
list1.setSummary(getResources().getString(R.string.some_string_text));
list1.setDialogTitle(getResources().getString(R.string.some_string_pick_title));
list1.setKey("your_key");
CharSequence[] entries = calendars.getCalenders(); //or anything else that returns the right data
list1.setEntries(entries);
int length = entries.length;
CharSequence[] values = new CharSequence[length];
for (int i=0; i<length; i++){
CharSequence val = ""+i+1+"";
values[i] = val;
}
list1.setEntryValues(values);
cat1.addPreference(list1);
return root;
}//end method
I have 3 tables, USER, ENTRY (for entered products, not necessary to create a PRODUCT table), and USER_COLLECTION, which is a table inbetween USER and ENTRY, because an entry can have multiple users.
Basically:
User = USERID | USER_NAME
Entry = ENTRYID | ENTRY_NAME | ENTRYPRICE | ENTRY_DATE
Collection = COLLECTIONID | ENTRYID | USERID
I have a table with users that persist throughout the project. They can create entries (which is usually some kind of product with a price) and they can link multiple users to a certain entry (which can be selected from a list, hence the users persist throughout the project).
So for instance, my tables look like this:
User
--------------------------
user_id | user_name
--------------------------
1 | 'FOO'
2 | 'BAR'
3 | 'FOOBAR'
ENTRY
-----------------------------------------------------------------------
entryid | entry_name | entry_price | entry_date
-----------------------------------------------------------------------
0 | 'Banana' | 2.50 | 12/12/2012
COLLECTION
---------------------------------------
collectionid | entryid | userid
----------------------------------------
0 | 1 | 1
1 | 1 | 2
2 | 1 | 3
I have a Banana, with a price of 2.50 and 3 users linked to it, Foo, Bar and Foobar.
Now, I want to use this in my app and get the data; except I don't know where to start. I tried selecting the entry data, using that id to loop through the collection data, but that would mean I have two cursors open and it wouldn't work. Tried creating a join but I couldn't really make a good one, mainly because:
JOIN
---------------------------------------
collectionid | entryname | username
----------------------------------------
0 | Banana | FOO
1 | Banana | BAR
2 | Banana | FOOBAR
I can't iterate through this, because I would create multiple of the same entry objects in my Android code...
Hope I'm being clear on this.
if (cursor2.moveToFirst()) {
do {
Item i = new Item(<GET STUFF FROM CURSOR>);
i.addUser(new Person(<GET STUFF FROM CURSOR>)));
Log.d("TAG", i.getUsersPaying().size() + "");
} while (cursor2.moveToNext());
}
If I use this, I create mulitple instances of Item i. They'll all be Banana, whilst I should only have 1 item Banana, with multiple users added to it.
First, you might want to consider returning the IDs from your tables in your join query. Things would be a little easier if you returned the entryid column.
Just make a Map<Integer, Item> to store items that you have seen already in your loop. As you examine each cursor, check the map to see if you already have an instance. If you don't, just make a new one and insert it.
Let's assume your query results are:
JOIN
----------------------------------------------------
collectionid | entryname | entryname | username
----------------------------------------------------
0 | 1 | Banana | FOO
1 | 1 | Banana | BAR
2 | 1 | Banana | FOOBAR
2 | 2 | Apple | FOOBAR
You can modify your code as follows:
Map<Integer, Item> items = new HashMap<Integer, Item>();
if (cursor2.moveToFirst()) {
do {
int itemId = cursor2.getInt(1);
Item i;
if (items.containsKey(itemId))
i = items.get(itemId);
else
{
i = new Item(<GET STUFF FROM CURSOR>);
items.put(itemId, i);
}
i.addUser(new Person(<GET STUFF FROM CURSOR>)));
Log.d("TAG", i.getUsersPaying().size() + "");
} while (cursor2.moveToNext());
}
You need to maintain a dictionnary of your entities which are already loaded in memory. For instance in a background fragment which would be retained.
Basically you would do:
Item i = cacheFragment.createOrGetEntry( cursor.getLong( ENTRY_ID_COLUMN_INDEX ) );
Person p = cacheFragment.createOrGetPerson( cursor.getLong( PERSON_ID_COLUMN_INDEX ) );
Of course, your query must also return the IDs of all the rows you need (entryId and personId). But a join query is the way to do it efficiently, so keep what you did about that and just add the two missing ID columns.
a createOrGetPerson method would look like:
public Person createOrGetPerson(long id) {
Entry<Long, Person> p = personDictionnary.get( id ); // can be a HashMap or even better, a SparseArray
if (p==null) {
p = new Person(id);
personDictionnary.put(p); // Remember it for next time
}
return p;
}
You should also have a look at data persistence frameworks or ORM frameworks which are made to deal with this kind of problem (e.g. Hibernate, even though I don't know if that is working with Android).