I am trying to model a database for a fitness app. Currently the 4 main entities are as follows:
Exercise
User
Workout
UserWorkout
id
id
id
id
name
email
name
userId (fk)
body_part
name
description
workoutId (fk)
category
password
level
date
age
exerciseIds (fk)
time_taken
The app will have default workouts as well as default exercises.
I would like the user to be able to add their own custom workouts/exercises that only they can see (in addition to the default ones) but I'm not sure on how to best structure the data?
Kris, MongoDB is a schemaless database, what makes it really flexible when it comes down to data modelling. There are different ways of achieving what you described, the one I would recommend would be adding nested documents to the user document if they belong to it. You would have something like this:
User {
firstName: ...,
lastName: ...,
age: ...,
weight: ...,
exercises: [
// User's exercise objects
],
workout: [
// User's workout objects
],
}
This way you can easily have access to information related to the user and avoid using expensive operations like $lookup or querying the database multiple times.
To handle the default exercises/workouts you can have a property in the respective objects like isDefault: true.
Related
What is the best way to structure many-to-many models in a mongoose schema?
I have two models that have a many-to-many relationship with eachother. Users can belong to many organistaions and Organisations can have many users.
Options:
Define the relationship in each model by referencing the other model
Define the relationship in one model by referencing the other model
Option 1
const UserSchema = new Schema({
organiations: { type: Schema.Types.ObjectId, ref: "Organiation" }, // references organisation
})
mongoose.model("User", UserSchema)
const OrganiationSchema = new Schema({
users: { type: Schema.Types.ObjectId, ref: "User" }, // references users
})
mongoose.model("Organiation", OrganiationSchema)
This seems like a good idea at first and it means I can query the Organisation model to get all users and I can also query the user model to get all relative organisations.
The only problem with this is I have to maintain 2 sources of truth. If I create a organisation, I must update the user witht the orgs it belongs to and I must update the organisation with the users it has.
This leads me to option 2 which is to have one source of truth by only defining the relationship in one model.
option 2:
const UserSchema = new Schema({
organiations: { type: Schema.Types.ObjectId, ref: "Organiation" }, // references organistion
})
mongoose.model("User", UserSchema)
const OrganiationSchema = new Schema({}) // no referencces
mongoose.model("Organiation", OrganiationSchema)
This means when I create a new organisation I only need to update user with the organisations they belong to. There is no risk of the 2 sources getting out of sync. However, it does mean when it comes to querying data, it makes it more tricky. If I want to get all users that belong to an organisation, I have to query the user document. This would mean my organisation controller has to then be aware of both user and organisation models and as I start adding more relationships and models, I get tight coupling between all of these modules which I want to avoid.
How would you recommend handling many-to-many relationship in a mongoose schema?
There is no fixed solution to this.
If the organization can have orders of magnitudes more users than users can have organizations, option 2 might be a better solution.
Performance wise, populating the referenced data would be about the same as long as the referenced ids are indexed.
Having said that you might still go for option 1, even if you organization collection has the potential to have "huge" arrays. Especially if you want to make simple computation such as number of organizations users or use the "organiztion's current userIds to some other collection". In this case option 1 would be way better.
But if you opt of option 1 and if your array has the potential to become very large, consider bucket design pattern. Basically you limit the max length of your nested array. If the array reaches its max length, you make another document that holds newly added ids(or nested documents). Think of it as pagination.
The structure of the table is:
chats
--> randomId
-->--> participants
-->-->--> 0: 'name1'
-->-->--> 1: 'name2'
-->--> chatItems
etc
What I am trying to do is query the chats table to find all the chats that hold a participant by a passed in username string.
Here is what I have so far:
subscribeChats(username: string) {
return this.af.database.list('chats', {
query: {
orderByChild: 'participants',
equalTo: username, // How to check if participants contain username
}
});
}
Your current data structure is great to look up the participants of a specific chat. It is however not a very good structure for looking up the inverse: the chats that a user participates in.
A few problems here:
you're storing a set as an array
you can only index on fixed paths
Set vs array
A chat can have multiple participants, so you modelled this as an array. But this actually is not the ideal data structure. Likely each participant can only be in the chat once. But by using an array, I could have:
participants: ["puf", "puf"]
That is clearly not what you have in mind, but the data structure allows it. You can try to secure this in code and security rules, but it would be easier if you start with a data structure that implicitly matches your model better.
My rule of thumb: if you find yourself writing array.contains(), you should be using a set.
A set is a structure where each child can be present at most once, so it naturally protects against duplicates. In Firebase you'd model a set as:
participants: {
"puf": true
}
The true here is really just a dummy value: the important thing is that we've moved the name to the key. Now if I'd try to join this chat again, it would be a noop:
participants: {
"puf": true
}
And when you'd join:
participants: {
"john": true,
"puf": true
}
This is the most direct representation of your requirement: a collection that can only contain each participant once.
You can only index known properties
With the above structure, you could query for chats that you are in with:
ref.child("chats").orderByChild("participants/john").equalTo(true)
The problem is that this requires you to define an index on `participants/john":
{
"rules": {
"chats": {
"$chatid": {
"participants": {
".indexOn": ["john", "puf"]
}
}
}
}
}
This will work and perform great. But now each time someone new joins the chat app, you'll need to add another index. That's clearly not a scaleable model. We'll need to change our data structure to allow the query you want.
Invert the index - pull categories up, flattening the tree
Second rule of thumb: model your data to reflect what you show in your app.
Since you are looking to show a list of chat rooms for a user, store the chat rooms for each user:
userChatrooms: {
john: {
chatRoom1: true,
chatRoom2: true
},
puf: {
chatRoom1: true,
chatRoom3: true
}
}
Now you can simply determine your list of chat rooms with:
ref.child("userChatrooms").child("john")
And then loop over the keys to get each room.
You'll like have two relevant lists in your app:
the list of chat rooms for a specific user
the list of participants in a specific chat room
In that case you'll also have both lists in the database.
chatroomUsers
chatroom1
user1: true
user2: true
chatroom2
user1: true
user3: true
userChatrooms
user1:
chatroom1: true
chatroom2: true
user2:
chatroom1: true
user2:
chatroom2: true
I've pulled both lists to the top-level of the tree, since Firebase recommends against nesting data.
Having both lists is completely normal in NoSQL solutions. In the example above we'd refer to userChatrooms as the inverted index of chatroomsUsers.
Cloud Firestore
This is one of the cases where Cloud Firestore has better support for this type of query. Its array-contains operator allows filter documents that have a certain value in an array, while arrayRemove allows you to treat an array as a set. For more on this, see Better Arrays in Cloud Firestore.
The structure of the table is:
chats
--> randomId
-->--> participants
-->-->--> 0: 'name1'
-->-->--> 1: 'name2'
-->--> chatItems
etc
What I am trying to do is query the chats table to find all the chats that hold a participant by a passed in username string.
Here is what I have so far:
subscribeChats(username: string) {
return this.af.database.list('chats', {
query: {
orderByChild: 'participants',
equalTo: username, // How to check if participants contain username
}
});
}
Your current data structure is great to look up the participants of a specific chat. It is however not a very good structure for looking up the inverse: the chats that a user participates in.
A few problems here:
you're storing a set as an array
you can only index on fixed paths
Set vs array
A chat can have multiple participants, so you modelled this as an array. But this actually is not the ideal data structure. Likely each participant can only be in the chat once. But by using an array, I could have:
participants: ["puf", "puf"]
That is clearly not what you have in mind, but the data structure allows it. You can try to secure this in code and security rules, but it would be easier if you start with a data structure that implicitly matches your model better.
My rule of thumb: if you find yourself writing array.contains(), you should be using a set.
A set is a structure where each child can be present at most once, so it naturally protects against duplicates. In Firebase you'd model a set as:
participants: {
"puf": true
}
The true here is really just a dummy value: the important thing is that we've moved the name to the key. Now if I'd try to join this chat again, it would be a noop:
participants: {
"puf": true
}
And when you'd join:
participants: {
"john": true,
"puf": true
}
This is the most direct representation of your requirement: a collection that can only contain each participant once.
You can only index known properties
With the above structure, you could query for chats that you are in with:
ref.child("chats").orderByChild("participants/john").equalTo(true)
The problem is that this requires you to define an index on `participants/john":
{
"rules": {
"chats": {
"$chatid": {
"participants": {
".indexOn": ["john", "puf"]
}
}
}
}
}
This will work and perform great. But now each time someone new joins the chat app, you'll need to add another index. That's clearly not a scaleable model. We'll need to change our data structure to allow the query you want.
Invert the index - pull categories up, flattening the tree
Second rule of thumb: model your data to reflect what you show in your app.
Since you are looking to show a list of chat rooms for a user, store the chat rooms for each user:
userChatrooms: {
john: {
chatRoom1: true,
chatRoom2: true
},
puf: {
chatRoom1: true,
chatRoom3: true
}
}
Now you can simply determine your list of chat rooms with:
ref.child("userChatrooms").child("john")
And then loop over the keys to get each room.
You'll like have two relevant lists in your app:
the list of chat rooms for a specific user
the list of participants in a specific chat room
In that case you'll also have both lists in the database.
chatroomUsers
chatroom1
user1: true
user2: true
chatroom2
user1: true
user3: true
userChatrooms
user1:
chatroom1: true
chatroom2: true
user2:
chatroom1: true
user2:
chatroom2: true
I've pulled both lists to the top-level of the tree, since Firebase recommends against nesting data.
Having both lists is completely normal in NoSQL solutions. In the example above we'd refer to userChatrooms as the inverted index of chatroomsUsers.
Cloud Firestore
This is one of the cases where Cloud Firestore has better support for this type of query. Its array-contains operator allows filter documents that have a certain value in an array, while arrayRemove allows you to treat an array as a set. For more on this, see Better Arrays in Cloud Firestore.
I am trying to learn how to use MongoDB and am really confused how to do this. What I have are two collections, one which has a number of users and another collection which has a number of items. For example:
users:
{
"_id" : ObjectId("56dba03438e1a255b97e82b6"),
"name" : "john",
"age" : 25
}
items:
{
"_id" : ObjectId("56dba0db38e1a255b97e82b7"),
"name" : "pencil"
}
Now what I want to do in my app is to allow users to select an item but multiple users can select the same item. So I need to keep track of which users clicked which items. I thought about doing this using another collection which keeps track of the user id and item id (a user can only select an item once). Is this the correct approach? I created this collection:
useritems:
{
"_id" : ObjectId("56dba0db38e1a255b97e82b7"),
"userid" : "56db9fb038e1a255b97e82b5",
"itemid" : "56dba03438e1a255b97e82b6"
}
If this is the right approach, then I want to be able to click on an item in my app and for it to display a list of all the users who selected that item. How can I do this? I got as far as to display only the useritems collection documents where the itemid = itemid selected on the app...but now how would I display all of the users from the users collection based on the ones in the useritems collection?
router.get('/userlist/:id', function(req, res) {
var db = req.db;
var collection = db.get('useritems');
collection.find({'itemid' : '_id'},{},function(e,docs){
res.json(docs);
});
});
Thanks for the help, I'm really having a hard time understanding how this would work.
The idea of creating a third collection is a solution that mirrors how you would solve this problem in a relational database. With MongoDB, it often pays off to think about different patterns based on how you access your data.
In your case, I would not create another collection, but track which user has selected which item within the user document, within the item document, or within both documents. Which way you do this depends on your data access patterns.
Adding Selected Item to User Document
{
"_id": ObjectId("56dba03438e1a255b97e82b6"),
"name": "john",
"age": 25,
"selectedItemId": "56dba0db38e1a255b97e82b7"
}
If you will often want to see the item each user has selected, it makes sense to store the item inside the user document. When you retrieve a user, you would only have to do one extra call to the items collection to retrieve the item for that user. (If you decide to use Mongoose as an object-document mapper (ODM), then you can achieve this extra call by using Mongoose's populate method).
Adding User to the Item Document
{
"_id": ObjectId("56dba03438e1a255b97e82b7"),
"name": "pencil",
"selectedBy": [
"56dba0db38e1a255b97e82b4",
"56dba0db38e1a255b97e82b5",
"56dba0db38e1a255b97e82b6"
],
}
If you will often want to see which users have selected a given item, it makes sense to store an array of users inside the item document. When you retrieve an item, you would then have the IDs of the users that selected that item, which you could then retrieve from the database. (Again, if you decide to use Mongoose you can do this by using its populate method).
Adding Both Solutions
The reason why you would prefer one solution over another is that given your access pattern, you will be spared from iterating through the whole collection to get the data you need. For example, in the case were you add the array of users to an item, if you wanted to find the item a given user has selected, you would have to iterate though all the items and look in the array of user IDs until you found the user you wanted. Something similar would occur if you only stored the item ID inside a user document and suddenly needed to look at all the users for a given item. If both of these calls are made often, then it pays off having the data in both places. Indeed this "denormalises" your data and you will have to make sure that when you insert, update, and delete the data you do so in both places, but it's a far more scalable solution if you're making both types of queries often.
Embedding the Whole Item Document inside the User Document
{
"_id": ObjectId("56dba03438e1a255b97e82b6"),
"name": "john",
"age": 25,
"selectedItem": {
"name": "pencil"
}
}
Following the OP's comment, I'll address this scenario too. This is also a possible solution and can be very useful in simplifying the query needed to access the data. Just by querying the user document, you will be able to access what item he/she has selected without the extra query to a collection of items. The limitation of this approach is that if for whatever reason you want to update the name of the item from say "pencil" to "Pencil", you will have to ensure that you update it across all of the user documents, otherwise your data will be inconsistent. This gets more complicated when your embedded documents are more complex. Nonetheless, it is a widely used solution. If you're rarely updating your data, but reading it very often, especially if you are more interested in seeing the item picked by a given user, then it definitely speeds up your most frequent data access patterns.
You are right, only you need populate the userid to get all atributes of that collection. I suggest you use (if your are not) Mongoose
With mongoose:
UserItems
.find({'itemid' : '_id'})
.populate('userid')
.then( useritems => {
// here you have all users with their data for a specific item
return res.json(useritems);
});
You can add an array in the item document that keeps track of the IDs of the users who clicked that item.
This is assuming the ID is stored in a active session.
docs.user_who_clicked.push(req.user._id);
docs.save()
I wouldn't create a separate collection just for that unless you have a good reason. Just add a selectedBy to each Item document. Also, I find it simpler to just use my own unique names or IDs rather than looking things up with the internal Mongo IDs. Something like this:
var items = db.collection('items');
items.updateOne({itemname:'nuts'},{$push:{selectedBy:'johns'}});
//...
items.find({itemname:'nuts'}).toArray(function(err,items) {
console.log(items[0].selectedBy);
db.close();
});
First of all excuse me since I don't know how it is called in computer since:
For each of my document types in my mongo app I want to define a structure, with every field defined with its constraints, validation patterns and, generally, roles that can view modify and delete this document.
For example: Book:
{
name: "Book",
viewRoles: ["Admin","User"],
createRoles: ["Admin"],
modifyRoles: ["Admin", "User"],
fields: [
{
id:"title",
name:"Book Title",
validation: "",
maxLength: 50,
minLength: 3,
required: true
},
{
id:"authorEmail",
name:"Email of the Author",
validation: "email",
maxLength: 50,
minLength: 3,
required: false
}
]
}
Then if I have this "schema" for all of my documents, I can have one view for creating modifying and showing this "entities".
I also want to have the ability to create new document types, modify their fields through admin panel of my application.
When I google "mongo dynamic schema", "mongo document meta design" I get useless information.
My question is how it is called -- when I want to have predefined schema of my documents and have the ability to modify it. Where I can get more information about how to design such systems?
Since you tagged this as having a Meteor connection, I'll point you to Simple Schema: https://github.com/aldeed/meteor-simple-schema/. I use it, along with the related collection2 package. I find it's a nice way to document and enforce schema design. When used with the autoform package, it also provides a way to create validated forms directly from your schema.
I think you are looking for how to model your data. The below link might be helpful:
http://docs.mongodb.org/manual/data-modeling/
I also want to have the ability to create new document types, modify
their fields through admin panel of my application.
For Administrative activities you may look into the options given in:
http://docs.mongodb.org/ecosystem/tools/administration-interfaces/
And once you are done, you might want to read this as a kick off:
https://blog.serverdensity.com/mongodb-schema-design-pitfalls/
In Mongo DB you don't create collections. You just start using them. So you can't define schemas before hand. The collection is created on the first insert you make to the collection. Just make sure to ensure Index on the collection before inserting documents into it:
db.collection.ensureIndex({keyField: 1})
So it all depends on maintaining the structure of the documents inserted to the collection rather than defining the collection.