Firestore: Multiple whereArrayContains - android

I have a collection called "service". And the services have two attributes called, serviceableAt (where the service is available) and accessibleBy (who can access it).
service-1
serviceableAt
0 - Location-1
1 - Location-2
accessibleBy
0 - Seller
1 - Customer
I am trying to fetch all the services who are serviceable at Location-1 and accessible to Seller. So my query was:
fun services(at: String, by: UserRole) = firestore().collection(Refs.SERVICE_REF)
.whereArrayContains(Fields.SERVICEABLE_AT, at)
.whereArrayContains(Fields.ACCESSIBLE_BY, by)
Looks like multiple whereArrayContains are not supported. So what could be the alternate solution for temporarry basis until Firebase team comes up with a solution?

The common alternative would be to store the information as map fields:
servicableAtMap: {
"Location-1": true,
"Location-2": true
},
accessibleByMap: {
"Seller": true,
"Buyer": true
}
With the above you can now use equality conditions to find the matches, and you can have multiple equality filters in a query.
The downside of the above approach is that it will create/require a separate index for each sub-field, so that takes up more storage and contributes towards the maximum number of indexes you can have.

Related

Iterating over a List with multiple Place AddressComponents - Kotlin

I am receiving a Place Detail (from Google Maps SDK Android) from a specific place.
This Place has different Place Details as a Result.
The output is the following:
Place found: AddressComponents{asList=[AddressComponent{name=58, shortName=58, types=[street_number]}, AddressComponent{name=Schwertstraße, shortName=Schwertstraße, types=[route]}, AddressComponent{name=Sindelfingen, shortName=Sindelfingen, types=[locality, political]}, AddressComponent{name=Böblingen, shortName=BB, types=[administrative_area_level_3, political]}, AddressComponent{name=Stuttgart, shortName=Süd, types=[administrative_area_level_2, political]}, **AddressComponent{name=Baden-Württemberg, shortName=BW, types=[administrative_area_level_1, political]}**, AddressComponent{name=Deutschland, shortName=DE, types=[country, political]}, AddressComponent{name=71065, shortName=71065, types=[postal_code]}]}
I am looking for the specific information of the typ=administrative_area_level_1 and level_2.
But i am not able to find a way of iteration.
With Kotlin i am able to access values hard, but this is not the solution.
The Objects AddressComponent change their posisitions in new requests to Google Place API. Even some places do only provide less information.
System.out.println(place.addressComponents?.asList()?.get(5)?.name)
System.out.println(place.addressComponents?.asList()?.get(5)?.shortName)
Results in:
Baden-Württemberg
BW
So how can i access the PlaceResult without hard-coding the positions, ignoring the order and find the specific information i am looking for?
The functionality that you're looking for is collections filtering. Assuming your list is named place, the following will return the two matching rows:
val desiredTypes = setOf("administrative_area_level_1",
"administrative_area_level_2")
println(place.filter { it.types.any(desiredTypes::contains) } )
// Prints: [name=Stuttgart, shortName=Süd, types=[administrative_area_level_2, political], name=Baden--Württemberg, shortName=BW, types=[administrative_area_level_1, political]]
The filter function returns any matching elements. it is the (default) name of the element under consideration. The any function returns whether the types sub-element for each element contains any of the items in the desiredTypes set.

Firestore query where map contains string

Data structure:
houses (collection)
name (string)
users (map)
90c234jc23 (map)
percentage: 100% (string/number)
Rules:
allow read: request.auth.uid in resource.data.users;
The problem is when I try to query houses which user owns:
FirebaseFirestore.getInstance().collection(House.COLLECTION)
// .whereArrayContains(House.USERS_FIELD, currentUser.getUid()) // does not work
.whereEqualTo("users." + currentUser.getUid(), currentUser.getUid()) // does not work either
.get()
No result are returned.
You cannot perform this type of query in firestore as there is no 'map-contains-key' operator. However, there are very simple workarounds for implementing this by making slight adjustments to your datastructure.
Specific Solution
Requirement: For this solution to work, each map value has to be uniquely identifyable in a firestore query, meaning it cannot be a map or an array.
If your case meets the listed requirements, you can go with #Dennis Alund's solution which suggests the following data structure:
{
name: "The Residence",
users: {
uid1: 80,
uid2: 20
}
}
General Solution
If your map values are maps or arrays, you need to add a property to each value which will be constant across all created values of this type. Here is an example:
{
name: "The Residence",
users: {
uid1: {
exists: true,
percentage: 80,
...
},
uid2: {
exists: true,
percentage: 20,
...
},
}
}
Now you can simply use the query:
_firestore.collection('houses').whereEqualTo('users.<uid>.exists', true)
Edit:
As #Johnny Oshika correctly pointed out, you can also use orderBy() to filter by field-name.
You can use orderBy to find documents where map contains a certain key. Using this example document:
{
"users": {
"bob": {},
"sam": {},
}
}
.orderBy('users.bob') will only find documents that contain users.bob.
This query is not working because your users field is a map and not an array.
.whereArrayContains(House.USERS_FIELD, currentUser.getUid())
This query
.whereEqualTo("users." + currentUser.getUid(), currentUser.getUid())
is not working because your map value for users.<uid> is a string that says percentage: xx% and that statement is testing if percentage: xx% === <uid>, which is false.
And that strategy will be problematic since you can not do queries to find items that "are not null" or "strings not empty", etc.
I'm assuming that the percentage is the user's ownership in the house (?). If so, you might have better luck in trying to structure your house document data like this if you want to maintain the same structure of document as in your question
{
name: "The Residence",
users: {
uid1: 80,
uid2: 20
}
}
That will allow you to do a query such as
.whereGreaterThan("users." + currentUser.getUid(), 0)
to find users that has some shares of ownership in that house.
But a fair bit of warning, as soon as you need composite indexes you will start having problems to maintain that structure. You might instead want to consider storing an array of users that owns that house for ease of querying.

Firebase realtime database GroupBy approach

I have a Firebase realtime database that acts as the backend to my Android app. The app is to display fixtures for a soccer league. The results are shown in a listview in a fragment.
I have a record structure like so;
awayTeam
date
gameID
hometeam
pitch
time
What I'd like to do is group games by a specific date in chronological order. So if I had 10 games (records), the first 5 played on the 01/04/2018 and the next 5 played on the 01/05/2018, I want to be able to present all of the 01/04/2018 games in there own grouped box and so forth.
In SQL Group By would be easily achieved, but I'm unsure if this is achievable either through a query or if I have to write a recursive function that will build the tree view up and order them by data at the array level.
Any help is appreciated.
There is no group by operation in Firebase. In fact, you'll find that many operations you're used to from relational databases are missing from Firebase.
What you get in return is a more flexible data model. For example: if you want to group your games by a specific data, you could store them under that specific date as a key:
games
2018-02-28
game1: { ... }
game2: { ... }
game3: { ... }
2018-03-01
game4: { ... }
Now you can do a simple lookup for all games for a given date.
If you want to group the games by different criteria, you'll probably keep the original list and then create lookup lists (also often called indexes) for each criterium. E.g.
gamesByDate
2018-02-28
game1: true
game2: true
game3: true
2018-03-01
game4: true
gamesByTeam
Team1
game1: true
game2: true
Team2
game1: true
game3: true
game4: true
Now you can look up the game IDs either by date or by team, and then look up the properties for each game by that ID.

How to reference one Node value in Other Node in firebase Android [duplicate]

I've read the Firebase docs on Stucturing Data. Data storage is cheap, but the user's time is not. We should optimize for get operations, and write in multiple places.
So then I might store a list node and a list-index node, with some duplicated data between the two, at very least the list name.
I'm using ES6 and promises in my javascript app to handle the async flow, mainly of fetching a ref key from firebase after the first data push.
let addIndexPromise = new Promise( (resolve, reject) => {
let newRef = ref.child('list-index').push(newItem);
resolve( newRef.key()); // ignore reject() for brevity
});
addIndexPromise.then( key => {
ref.child('list').child(key).set(newItem);
});
How do I make sure the data stays in sync in all places, knowing my app runs only on the client?
For sanity check, I set a setTimeout in my promise and shut my browser before it resolved, and indeed my database was no longer consistent, with an extra index saved without a corresponding list.
Any advice?
Great question. I know of three approaches to this, which I'll list below.
I'll take a slightly different example for this, mostly because it allows me to use more concrete terms in the explanation.
Say we have a chat application, where we store two entities: messages and users. In the screen where we show the messages, we also show the name of the user. So to minimize the number of reads, we store the name of the user with each chat message too.
users
so:209103
name: "Frank van Puffelen"
location: "San Francisco, CA"
questionCount: 12
so:3648524
name: "legolandbridge"
location: "London, Prague, Barcelona"
questionCount: 4
messages
-Jabhsay3487
message: "How to write denormalized data in Firebase"
user: so:3648524
username: "legolandbridge"
-Jabhsay3591
message: "Great question."
user: so:209103
username: "Frank van Puffelen"
-Jabhsay3595
message: "I know of three approaches, which I'll list below."
user: so:209103
username: "Frank van Puffelen"
So we store the primary copy of the user's profile in the users node. In the message we store the uid (so:209103 and so:3648524) so that we can look up the user. But we also store the user's name in the messages, so that we don't have to look this up for each user when we want to display a list of messages.
So now what happens when I go to the Profile page on the chat service and change my name from "Frank van Puffelen" to just "puf".
Transactional update
Performing a transactional update is the one that probably pops to mind of most developers initially. We always want the username in messages to match the name in the corresponding profile.
Using multipath writes (added on 20150925)
Since Firebase 2.3 (for JavaScript) and 2.4 (for Android and iOS), you can achieve atomic updates quite easily by using a single multi-path update:
function renameUser(ref, uid, name) {
var updates = {}; // all paths to be updated and their new values
updates['users/'+uid+'/name'] = name;
var query = ref.child('messages').orderByChild('user').equalTo(uid);
query.once('value', function(snapshot) {
snapshot.forEach(function(messageSnapshot) {
updates['messages/'+messageSnapshot.key()+'/username'] = name;
})
ref.update(updates);
});
}
This will send a single update command to Firebase that updates the user's name in their profile and in each message.
Previous atomic approach
So when the user change's the name in their profile:
var ref = new Firebase('https://mychat.firebaseio.com/');
var uid = "so:209103";
var nameInProfileRef = ref.child('users').child(uid).child('name');
nameInProfileRef.transaction(function(currentName) {
return "puf";
}, function(error, committed, snapshot) {
if (error) {
console.log('Transaction failed abnormally!', error);
} else if (!committed) {
console.log('Transaction aborted by our code.');
} else {
console.log('Name updated in profile, now update it in the messages');
var query = ref.child('messages').orderByChild('user').equalTo(uid);
query.on('child_added', function(messageSnapshot) {
messageSnapshot.ref().update({ username: "puf" });
});
}
console.log("Wilma's data: ", snapshot.val());
}, false /* don't apply the change locally */);
Pretty involved and the astute reader will notice that I cheat in the handling of the messages. First cheat is that I never call off for the listener, but I also don't use a transaction.
If we want to securely do this type of operation from the client, we'd need:
security rules that ensure the names in both places match. But the rules need to allow enough flexibility for them to temporarily be different while we're changing the name. So this turns into a pretty painful two-phase commit scheme.
change all username fields for messages by so:209103 to null (some magic value)
change the name of user so:209103 to 'puf'
change the username in every message by so:209103 that is null to puf.
that query requires an and of two conditions, which Firebase queries don't support. So we'll end up with an extra property uid_plus_name (with value so:209103_puf) that we can query on.
client-side code that handles all these transitions transactionally.
This type of approach makes my head hurt. And usually that means that I'm doing something wrong. But even if it's the right approach, with a head that hurts I'm way more likely to make coding mistakes. So I prefer to look for a simpler solution.
Eventual consistency
Update (20150925): Firebase released a feature to allow atomic writes to multiple paths. This works similar to approach below, but with a single command. See the updated section above to read how this works.
The second approach depends on splitting the user action ("I want to change my name to 'puf'") from the implications of that action ("We need to update the name in profile so:209103 and in every message that has user = so:209103).
I'd handle the rename in a script that we run on a server. The main method would be something like this:
function renameUser(ref, uid, name) {
ref.child('users').child(uid).update({ name: name });
var query = ref.child('messages').orderByChild('user').equalTo(uid);
query.once('value', function(snapshot) {
snapshot.forEach(function(messageSnapshot) {
messageSnapshot.update({ username: name });
})
});
}
Once again I take a few shortcuts here, such as using once('value' (which is in general a bad idea for optimal performance with Firebase). But overall the approach is simpler, at the cost of not having all data completely updated at the same time. But eventually the messages will all be updated to match the new value.
Not caring
The third approach is the simplest of all: in many cases you don't really have to update the duplicated data at all. In the example we've used here, you could say that each message recorded the name as I used it at that time. I didn't change my name until just now, so it makes sense that older messages show the name I used at that time. This applies in many cases where the secondary data is transactional in nature. It doesn't apply everywhere of course, but where it applies "not caring" is the simplest approach of all.
Summary
While the above are just broad descriptions of how you could solve this problem and they are definitely not complete, I find that each time I need to fan out duplicate data it comes back to one of these basic approaches.
To add to Franks great reply, I implemented the eventual consistency approach with a set of Firebase Cloud Functions. The functions get triggered whenever a primary value (eg. users name) gets changed, and then propagate the changes to the denormalized fields.
It is not as fast as a transaction, but for many cases it does not need to be.

Append to Firebase array without key

I have an array without keys. How can I append an element to the keyless array?
favorites:
- 0
|
-- name: "bs41"
- 1
|
-- name: "uie"
So my result would be:
favorites:
- 0
|
-- name: "bs41"
- 1
|
-- name: "uie"
- 2
|
-- name: "pts4"
Your best approach here would be a transaction.
But as usual: there are a lot of reasons why Firebase recommends against using arrays.
For example: can the same value be specified twice in favorites? Chances are that each user can only specify each favorite only once. So one of your next questions in that case is going to be: how can I prevent duplicate values in this Firebase array? And the answer to that one is again going to be: with a transaction.
Transactions hurt the performance and scalability of your application; and it also means that your app won't work when the user of offline. Whenever you need a transaction, it pays off to wonder if there is a data model that accomplishes the same use-case that doesn't require a transaction.
For storing favorites that could be as simple as putting the values into the keys of the collection:
favorites:
"bs41": true
"uie": true
"pts4": true
Now you have an automatic guarantee that each favorite value can be present only once. Adding a new item is also incredibly simple: ref.child("favorites").child("newfav").setValue(true).
The data model I just shared is how you model a set in Firebase. In my experience when you have an array/list and are checking if it contains a specific value before adding a new item, you often should be using a set instead.

Categories

Resources