This question already has answers here:
Many to Many relationship in Firebase
(2 answers)
Closed 5 years ago.
I have been trying (without success) to retrieve a list of users in a group from a Firebase database; and I was wondering if its possible based on my data structure (below), or is the problem my swift code (also below)
This is my Firebase structure:
Users
---- <UserID>
---------- Username
---------- Email
---------- etc...
Groups
---- <GroupID>
---------- GroupName
---------- CreationDate
---------- GroupAdmin
---------- etc...
UsersInGroups
------- UserID
---------- GroupID : true <---- User is in Group
With the above data structure is it possible for me to retrieve the list of all users in the a particular group?
Currently my swift code is as follows:
ref = Database.database()reference(withPath: "UsersInGroups")
handle = ref.queryOrdered(byChild: <userID>).queryEqual(toValue: true).observe(.value, ....
As you can imagine, it is not pulling the userID where the groupID = true!?
Lastly, I was wondering if this is possible: I would like to get a list of all the GroupEntries a User has done.
the Firebase structure is as follows:
GroupEntry
-------- <GroupID>
--------------- <entryID> : <userID>
the is a dynamic and unique string (ex 3:8) and the userID is the user that created the entry.
The swift code is below:
ref = Database.database()reference(withPath: "GroupEntry")
handle = ref.child(<groupID>).queryEqual(toValue: <userID>).observe(.value, ...
Can anyone offer any assistance!?
You can represent the relationships between the groups and users using the following locations:
group-users/$groupId/$uid
user-groups/$uid/$groupId
Storing both inversions of the relationship will give you more querying abilities. To retrieve the users in a group, you observe the children at group-users/$groupId – on the other hand, you can get the groups a user is in by observing the children at user-groups/$userId.
Group entries can be represented with two more locations:
group-entries/$groupId/$entryId
user-entries/$userId/$entryId
You could get all the entries a user has made by observing user-entries/$userId; you can query further by ordering by the child that contains the entry's $groupId.
The challenge with all of these locations is maintaining them – ensuring that data is kept consistent throughout. This can be in a somewhat manual way using one of the Firebase client SDKs, however you could consider using Cloud Functions to create triggers that update the relevant locations in the database.
Related
SendBird treats every channel as their GroupChannel. The 1:1 chat too is technically a GroupChannel with only two users (with isDistinct = true so it would return the personal chat when you attempt to create it again).
My question is, how do I search GroupChannels by their name those include group AND 1:1 chat? The group chat would have a common name that would be shown to all the users in the group. But for 1:1 chat, the GroupChannel won't have a name, and if it has, that won't be shown to the users as for 1:1 chat, we always show the other person's name (like almost all the chat systems work).
Typically the main UI list contains mixture of the group chat and 1:1 chats (all the GroupChannels).
--------------------------------
| Search Chat TextField |
|--------------------------------|
|1 John (1:1) |
|2 John's Birthday Plan (group) |
|3 Johnney Eve (1:1) |
|4 Johansson Fans (group) |
| ... |
--------------------------------
All the items are technically GroupChannel. Note that all the 1:1 chats don't have actual name as shown in the list. The name shown in the list is the other person's nickname.
Expectation:
Now, if the user searches something like "joh", then it should return all the group chats whose name contains "joh" OR all the 1:1 chats where the other person's name contains "joh". (Basically all the items shown in the above example.)
My Attempt:
My initial solution to achieve this is to keep the 1:1 channel name as <user1 nickname> & <user2 nickname>, so when the user searches for the other user by their name, the 1:1 channel would appear just like a group channel.
Example Code:
query = SBDGroupChannel.createMyGroupChannelListQuery()
query?.order = .latestLastMessage
query?.limit = 30
query?.channelNameContainsFilter = "joh"
query.loadNextPage(...)
The Problem:
The problem with this are:
If the user searches for their own name (or just the separator character & or just a whitespace), then too all the personal chat would be visible, which is irrelevant.
My system allows user to change their nickname, so every time a user changes their nickname, then all the 1:1 channel names have to be updated (which is painful).
Sunil,
Typically when you retrieve a list of group channels for a user, it retrieves all channels that the user is potentially a part of (Depending on the memberStateFilter).
If you were explicitly looking to search, rather than providing an ongoing list of channels the user is part of, you may be able to filter channels by userIds. You'd have to filter for a channel that consists of the searching user, and the desired user.
Lets look at an example, assuming your userId is John and you're looking for your chat with Jay:
let listQuery = SBDGroupChannel.createMyGroupChannelListQuery()
listQuery?.userIdsExactFilter = ["John", "Jay"]
listQuery?.loadNextPage(completionHandler: { (groupChannels, error) in
guard error == nil else {
// Handle error.
}
// Only channelA is returned in a result list through the "list" parameter of the callback method.
...
})
If you wanted to explicitly use nicknames:
let listQuery = SBDGroupChannel.createMyGroupChannelListQuery()
listQuery?.nicknameContainsFilter = ["John", "Jay"]
listQuery?.loadNextPage(completionHandler: { (groupChannels, error) in
guard error == nil else {
// Handle error.
}
// Only channelA is returned in a result list through the "list" parameter of the callback method.
...
})
You mention that you allow users to change their nicknames, and thus rooms have to be updated. It may be worth giving your group channels (even 1:1) generic names, and then dynamically generate the display name of each chat.
Since each channel returns the list of members, you could look at the array of members, filter out the user that is logged in, and then pull the nickname of the remaining user from the array. This would ensure that no matter what the user changes their nickname to, its always accurate, and you don't have to update every channel when the user updates their nickname.
************ Updated 02/10 ************
Thanks for providing an example of what you're looking to achieve. It looks like you're essentially trying to search both channelNameContainsFilter and nicknameContainsFilter using the OR operator. This is not something we (Sendbird), currently support within the iOS SDK. So the question is, what could you do to achieve this?
One option would be to utilize the Platform API to obtain this information. The List my group channels has the search_query and search_fields parameters which would allow you to utilize that OR operator to find both channel names and nicknames that match your value.
Alternatively, since the SDK does return all of the necessary data that would be required to filter for these results, you could create a front-end filter that would only display the items that match your filter results. So the SDK returns the complete channel list, you store that list, and then when the user searches, you filter through the list to find channels that match your needs and display only those.
As a side note, Stackoverflow may not be the best place for this type of discussion as there is a lot of back and forth. Please feel free to join us in our community for more support.
I'm implementing chat using Firestore. Here is the structure of Firestore:
|- Chats (collection)
|-- AutoID (document)
|--- user1ID (String)
|--- user2ID (String)
|--- thread (collection)
... and then thread has further fields.
In order to get chat between two users I'm fetching it like:
let db = Firestore.firestore().collection("Chats")
.whereField("user1ID", isEqualTo: Auth.auth().currentUser?.uid!)
.whereField("user2ID", isEqualTo: user2UID!)
It works fine if user 1 is the current user otherwise if I open chat from other account and current user is user 2 it doesn't fetch this document.
Upon searching I found that I can user arrayContains. So I made an array instead called users and in it I've added both these IDs. So now the structure is:
|- Chats (collection)
|-- AutoID (document)
|--- users (Array)
|---- 0: user1ID (String)
|---- 1: user2ID (String)
|--- thread (collection)
... and then thread has further fields.
But when I do:
let db2 = Firestore.firestore().collection("Chats")
.whereField("users", arrayContains: Auth.auth().currentUser?.uid!)
.whereField("users", arrayContains: user2UID!)
It's going to fetch the first document it found that has currentUser.uid (Haven't tested it, I'm saying this based on the documentation I've read).
So, how can I get this chat, if an array contains both id's?
Firstly, the document you outlined doesn't have any array type fields, so arrayContains isn't going to be helpful. arrayContains only matches items of a field that contains an array.
For your current document structure, a single query will not be able to get all the documents between both users, since Cloud Firestore doesn't offer any logical OR type queries. You are going to need two queries: one for getting all documents where user1 is the current user, and one for where user2 is the current user. Then you can merge the results of those two queries in the client code to build the entire list.
What I typically do is to name the document for the two users that are having the chat. That way you don't need to do a query to find the document, but can just directly access is based on the UIDs.
To ensure the order in which the UIDs are specified doesn't matter, I then add them in lexicographical order. For an example of this, see my answer here: Best way to manage Chat channels in Firebase
I am creating an iOS app where I have a standard set of user data in my firebase real time database. The user data consists of values such as name, age etc. Here is the structure:
user
- userId1
- name : Bob
- age : 25
- userId2
- name : Rob
- age : 24
- userId3
- name : Dylan
- age : 13
I also have another data named "connections" that has a structure as follows:
connections
- userId1
- userId2 : 1
- userId3 : 1
Let us say the current user ID is userId1. Now, when I press a button, I want to fetch all connections data for the userId1, which is userId2 and userId3. The question here is, how do I look into the "user" structure and get the relevant user information for userId2 and userId3?
In simple words, how do I filter data in a structure based on the data in another structure? Just can't seem to find the right solution online.
I am only able to fetch all the data. Have no clue how to filter the data.
First, you aren't filtering data, you are joining data. So having the correct mindset is an important starting point.
Assuming you're not fetching thousands of these at each request (which you shouldn't be in any case), the answer is that it probably doesn't matter. RTDB uses websockets and holds a pipeline open to the server, so requesting them individually or as a single query doesn't make much difference. It's really mostly about the byte count which doesn't change if you fetch each record or all the records in one go. So just fetch each user as needed.
At scale, don't be afraid to duplicate a little data for read efficiency. It's fine if you just need the names of the users to copy them directly into your data in places where you'll be performing large scale reads (in the hundreds of thousands or more).
There's a great series for Firestore that is mostly applicable to RTDB as well, which is a great primer to NoSQL data structures, particularly episodes #3 and #4.
Firebase can be a bit tricky when you first get started so here's some code to go along with the correct answer from Kato.
Conceptually, to begin with you want to know the uid's of this users connections. To do that, since we know this users uid, we can read the connection data directly from the connections node like this.
func readConnections() {
let uid = "userId1"
let thisUsersConnections = self.ref.child("connections").child(uid)
thisUsersConnections.observeSingleEvent(of: .value, with: { snapshot in
let connections = snapshot.children.allObjects as! [DataSnapshot]
for connection in connections {
let connectionUid = connection.key
self.printConnectionInfo(forUserId: connectionUid)
}
})
}
the snapshot that's populated will contain all of the child data of the userId1 node, which is
- userId2 : 1
- userId3 : 1
In this case, I want to read them in the order in which they appear in Firebase so we take that data contained in the snapshot and populate an array.
let connections = snapshot.children.allObjects as! [DataSnapshot]
Note the elements in the connections array are they in themselves DataSnapshots.
That array is iterated over to get each object (a DataSnapshot), and the .key of each snapshot is the uid of each of that users connections. The key is then passed to another function (for clarity) which reads in the user info for the passed in uid and prints the name and age to console.
func printConnectionInfo(forUserId: String) {
let usersRef = self.ref.child("users").child(forUserId)
usersRef.observeSingleEvent(of: .value, with: { snapshot in
let name = snapshot.childSnapshot(forPath: "name").value as! String
let age = snapshot.childSnapshot(forPath: "age").value as! Int
print(name, age)
})
}
I am struggling with writing data to Firebase in the desired way.
The situation: I want to store multiple "trips" for each user. Every "trip" contains locations with lat, long, altitude and a timestamp.
On certain conditions, I want to add a new location to the most recent "trip".
Data Structure is the following:
- root
-- users
--- userID
---- trips
------ trip1
------- location
------ trip2
------- location
What would be the best way to add locations to the most recent trip (trip2 in this case)?
I had hoped something similar to this is possible:
func setupFirebase() {
ref = FIRDatabase.database().reference()
userRef = ref.child("users")
userID = FIRAuth.auth()?.currentUser?.uid
tripsRef = self.userRef.child(userID!).child("trips")
currentTripRef = tripsRef.queryLimited(toLast: 1)
}
and then I could simply go and:
currentTripRef.child(dateString).setValue([
"timestamp": dateString,
"lat": lat,
"lng": lng,
"alt": alt
])
wherever I need to write something. This obviously is not the right way to do it, but I can't seem to figure out what the correct approach is.
I figured I could observe my tripsRef, get an array of keys and inside of that observing function write to the child ref with the last key. But that doesn't sound efficient at all.
Any solution on this?
Thanks in advance!
First, remember it's best practice to disassociate your node names from you data. You may be doing that but wanted to stress that point.
Here's a proposed structure that handles users and the their trips
root
users
uid_0
name: "Jamie"
uid_1
name: "Adam"
trips
-Yijaisioaooisd
user_id: "uid_0"
trip_number: 1
trip_name: "France"
locations
-Yuijiaissso
location_name: "Bordeaux"
stop_number: 1
-Yijs9ojnas9
location_name: "Burgundy"
stop_number: 2
-Yijispoais
location_name: "Châteauneuf-du-Pape"
stop_number: 3
If you wanted to add another stop/location to uid_0's trip to France, the ref is
let locationsRef = rootRef.child("trips").child("-Yijaisioaooisd").child("locations")
and then
let anotherLocationRef = locationsRef.childByAutoId()
let stopDict = ["location_name": "Médoc", "stop_number": 4]
locationsRef.setValue(stopDict)
That being said, there may be situations where you want to denormalize your data and break the locations out (for queries etc)
So that would be
locations
trip_id: "-Yijaisioaooisd"
locations:
-Yuijiaissso
location_name: "Bordeaux"
stop_number: 1
-Yijs9ojnas9
location_name: "Burgundy"
stop_number: 2
-Yijispoais
location_name: "Châteauneuf-du-Pape"
stop_number: 3
based on a comment, adding some additional info:
If you want to add another stop to uid_0's latest trip there are several options and the actual answer would depend on the structure of the data.
Using queryLimitedToLast, the last stop in the locations node could be queried to get the stop number (3 in the above example) to where you can increment it and then write out the next stop. This would work if you know the parent key -Yijaisioaooisd as the ref would be -Yijaisioaooisd/locations.
If you don't know the parent key you can easily obtain it with a query on the trips node for user_id equal to uid_0. That will return a snapshot. Then snapshot.key would provide the -Yijaisioaooisd parent key.
I am developing a feeds application with ruby on rails, and I want to show in the main page of the application some courses activities managing by the users.
Basically, the idea is a user can create a course, other users can follow the course and comment inside that event, later in the future, I Want that it will be more complex.
My solution is having a notifications table which manage all this. Above it shows the table which will have a feed type (1. create course, 2. pay the course, 3. comment into this group, 4. course deleted, 5. share course... and could be more). user_id field refiere to the user who should see the notification, message_id not nil only when the notification consist in add a comment.
<<Table>>
Feed
----------------
+ id : integer (Pk)
+ created_at : date_time
+ notification_type : string
+ text : text
+ course_id : integer (Fk)
+ user_id : integer (Fk)
+ comment_id : integer (Fk)
I am wondering if that would be the best implementation of this problem if I Want to make it more complex in the future. Please any feedback is welcome!
Thank you!
You're implementing the observer patter for this application. Based on your requirements, it looks like a fine implementation.
Here are some resources about this pattern.
http://www.sitepoint.com/design-patterns-in-ruby-observer-singleton/
Implementing Observer Pattern for my Rails App