Reputation: 57
I am new to react native and firebase but understand SQL relationships. I am trying to understand how to model many to many relationships with firebase in NoSQL.
In my scenario, users can own multiple groups and groups can have multiple user owners.
I want to display group information (name, city state) for a particular user. (In this case, doc id: WQQZ6pcMMgQoTxH9cr2XGaPOq9W2).
This is how data is structured in the firebase filestore:
{
"__collections__": {
"Group": {
"group3": {
"city": "Aurora",
"state": "CO",
"name": "Group Three",
"__collections__": {}
},
"group4": {
"state": "CO",
"name": "Group Four",
"city": "Denver",
"__collections__": {}
},
"group5": {
"city": "Aurora",
"state": "CO",
"name": "Group Five",
"__collections__": {}
}
},
"User": {
"Hm56Zhn9TJP9jVYrzJRqHAB8T8H3": {
"last_name": "Sneed",
"first_name": "Sam",
"__collections__": {}
},
"WQQZ6pcMMgQoTxH9cr2XGaPOq9W2": {
"last_name": "Smith",
"first_name": "Will",
"__collections__": {}
}
},
"UserGroups": {
"F4GubhZKqWcJnHahL0TQTOP62Jj1": {
"group3": true,
"group4": true,
"__collections__": {}
},
"WQQZ6pcMMgQoTxH9cr2XGaPOq9W2": {
"group5": true
"__collections__": {}
}
}
}
}
Here is my code:
export default class GroupSelect extends Component {
constructor(props) {
super(props);
this.userGroupRef = firebase.firestore().collection("UserGroups").doc('WQQZ6pcMMgQoTxH9cr2XGaPOq9W2');
this.state = {
userGroups: [
{
uid: 'group1',
name: 'Group One',
city: 'Chicago',
state: 'IL'
},
{
uid: 'group2',
name: 'Group Two',
city: 'Denver',
state: 'CO'
}
]
}
}
componentDidMount() {
this.userGroupRef.get().then((doc) => {
var obj = doc._data;
var group_ids = Object.keys(obj).map(function (key) {
return key;
});
var groups = [];
var group = {};
group_ids.forEach(function (group_id) {
console.log(group_id);
this.groupRef = firebase.firestore().collection('Group').doc(group_id);
this.groupRef.get().then((groupDoc) => {
group.name = groupDoc._data['name'];
group.city = groupDoc._data['city'];
group.state = groupDoc._data['state'];
groups.push(group);
});
});
console.log(groups); //not populated correctly
// this.setState({
// userGroups: groups
// });
});
}
render() {
return (
this.state.userGroups.map((userGroup, index) => (
<Text>{userGroup.name} - {userGroup.city}, {userGroup.state}</Text>
))
)
}
}
If I comment out everything in ComponentDidMount(), the render() shows the original contents of state.UserGroups correctly. But, when I try to populate the array in the ComponentDidMount() and reset the state userGroups var, there is a problem with the join and the timing of the population of the array.
How best to do this?
I am patterning this off of how firebase joins are described here: https://www.youtube.com/watch?v=Idu9EJPSxiY
Users, eventAttendees and Events many-to-many join in firebase realtime db
But, this uses the realtime database instead of filestore, which is what I want to use.
I want to do the same but with: Users, UserGroups, and Groups
Should the data be structured differently?
I implemented Frank van Puffelen's answer below. It works but that brings up other questions.
If I use the UserGroups collection as described...
constructor(props) {
super(props);
this.userGroupRef = firebase.firestore().collection("UserGroups").doc(firebase.auth().currentUser.uid);
this.state = {
userGroups: []
}
}
...it works, but see possible "issues" below.
componentDidMount() {
this.userGroupRef.get().then((doc) => {
var obj = doc._data;
//get array of group ids
var group_ids = Object.keys(obj).map(function (key) {
return key;
});
let promises = [];
//issue: making singleton database calls for each group id, maybe not good.
group_ids.forEach(function (group_id) {
promises.push(firebase.firestore().collection('Group').doc(group_id).get())
});
let groups = [];
let parentThis = this; //issue: get ref to parent this to set in Promise.all below, kinda weird, maybe not so clear
Promise.all(promises).then(function(docs) {
docs.forEach((groupDoc) => {
let group = {};
group.name = groupDoc._data['name'];
group.city = groupDoc._data['city'];
group.state = groupDoc._data['state'];
groups.push(group);
});
parentThis.setState({
userGroups: groups
});
});
});
}
If I restructure data and add a groups collection below User...
"User": {
"WQQZ6pcMMgQoTxH9cr2XGaPOq9W2": {
"last_name": "Carter",
"first_name": "Will",
"__collections__": {
"Groups": {
"group1": {
"city": "Chicago",
"state": "IL",
"name": "Group One",
"__collections__": {}
},
"group2": {
"city": "Denver",
"state": "CO",
"name": "Group Two",
"__collections__": {}
}
}
}
}
}
Then, code becomes more straightforward.
constructor(props) {
super(props);
//path: User/WQQZ6pcMMgQoTxH9cr2XGaPOq9W2/Groups
this.userGroupRef = firebase.firestore().collection("User").doc(firebase.auth().currentUser.uid).collection("Groups");
this.state = {
userGroups: []
}
}
componentDidMount() {
this.userGroupRef.get().then((doc) => {
let groups = [];
doc._docs.forEach(function (groupDoc) {
let group = {};
group.name = groupDoc._data['name'];
group.city = groupDoc._data['city'];
group.state = groupDoc._data['state'];
groups.push(group);
});
this.setState({
userGroups: groups
});
});
}
This method seems cleaner to me. But now I have duplicate data in a root Group collection and under User collection.
Is it better to do it in this method with duplicate Group data in the db?
I have a relational db background and learning NoSQL best practices. My instinct is not to duplicate data in the db.
Upvotes: 1
Views: 3027
Reputation: 57
This video may answer my question. ... at 13:31: "this would probably be duplicate data that would live both in the top-level user object and in this individual review and we'll talk in future videos about the best strategies to keep these kinds of things consistent."
This leads me to believe that I should duplicate the needed group data under the user and not do the joining bit in the client. https://www.youtube.com/watch?v=v_hR4K4auoQ
Or, maybe not: From this video: https://www.youtube.com/watch?v=jm66TSlVtcc
... at 7:17 "If you have a sql background, this is very similar to using an intermediate table for joins"
More details here: https://angularfirebase.com/lessons/firestore-nosql-data-modeling-by-example/#Subcollection-Many-to-Many-Relationships
In the structure above, how would you get all the tweets that a user has liked(hearted) using the hearts table that has the foreign keys with a single query?
Upvotes: 1
Reputation: 599591
You're printing console.log(groups)
outside of the callback where the groups get loaded. That won't work, as data is loaded from Firestore asynchronously, and your log statement runs before any data is loaded.
This is easiest to see if you place a few simple log statements:
console.log("Start loading documents");
group_ids.forEach(function (group_id) {
this.groupRef = firebase.firestore().collection('Group').doc(group_id);
this.groupRef.get().then((groupDoc) => {
console.log("Loaded document");
});
});
console.log("Started loading documents");
When you run this code, it outputs:
Start loading documents
Started loading documents
Loaded document
Loaded document
...
This is probably not the order that you expected, but it perfectly explains why the groups
array is empty when you print it: none of the groups has been loaded yet. In fact, if you log groups
insode the then()
callback, you'll see it getting populated one document at a time.
Any code that needs the documents, needs to either be inside the callback, or wait for the document(s) to be loaded by using a promise. Since you're waiting for multiple documents, use Promise.all()
:
var promises = [];
group_ids.forEach(function (group_id) {
promises.push(firebase.firestore().collection('Group').doc(group_id).get())
});
Promise.all(promises).then(function(docs) {
docs.forEach((groupDoc) => {
group.name = groupDoc._data['name'];
group.city = groupDoc._data['city'];
group.state = groupDoc._data['state'];
groups.push(group);
});
console.log(groups);
});
Upvotes: 2