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.