(NOTE: I'm still learning since all of this stuff is very new to me, so everything I say here should be taken with a grain of salt. Better solutions/approaches than what I'm doing are likely available and I'd definitely appreciate good tips from anyone who reads this.)
I started working on a breakable toy, and the first thing I wanted to get working was storing my documents in MongoDB and getting them back out again. I first read Karl Seguin's The Little MongoDB Book because it's a great introduction to MongoDB and to working with document databases in general (btw: it's free, only 33 pages and very well written => highly recommended!).
So I looked around and found Mongoose. It presents itself as an ORM-like layer on top of MongoDB which it kinda is, though I prefer to think of it as an Object Document Mapper instead of an Object Relational Mapper since they definitely differ in significant ways. Anyways, let's get to some code. As mentioned in the first post about my toy project, the goal is to generate invoices and timesheets for the work I do for my customers. So I started off with 2 entities (obviously, I'll need more, but this is enough to get started):
There's quite a bit going on in this piece of code already. This defines the schema of our entities. Some of you might be thinking "wait a sec, I thought document databases were schema-less?". They are indeed, but Mongoose uses these 'Schema' instances to generate constructor functions for your entity objects and to give them some interesting behavior out of the box. Those schema-definitions in the code are still meaningless as far as MongoDB is concerned. But for Mongoose and our code, they certainly are important.
As you can see, we define a Customer type which has some properties, as well as embedded Address and Contact objects. When properties are made required or given default values, it only has an influence on Mongoose. I could still connect to MongoDB through its shell (which is awesome btw, check Karl Seguin's book for some interesting examples) and insert whatever I want in the collections (similar to tables in a relational database, though there is no schema that is upheld for the elements in the collection). We also have a PerformedWork type, though there won't be a collection in the database for those instances. You can see in the schema of Activity that its performedWork property holds an array of PerformedWork instances. We just mapped a one-to-many without requiring a separate MongoDB Collection (or table, if you prefer to think of it that way). If you're using MongoDB directly, you could just put whatever you want in an array-property of a document. For Mongoose, it's important to know the structure of the data, so you have to define a schema for embedded documents in arrays. Notice also that I can easily define min and max values for the hours property of PerformedWork. You can go a lot further with validation in your entity objects, but I haven't looked further into that yet. Also interesting to note is that Activity has a customer property, in which we'll store an ObjectId. It means that the customer property will refer to a customer through the id value that it holds, but it is not an actual Customer reference. I'll show you how the actual documents are stored in the database later on in this post.
Another thing that you'll probably find weird is this:
Suppose I want to test some of the behavior related to saving a customer, I'd start with something like this:
Within the function that is passed to the describe method, I can start adding some tests. For instance, here's one that tests whether or not Mongoose applies the validation rules I specified on my CustomerSchema:
This test tries to save an empty customer object to the database, but our CustomerSchema specifies that a couple of its properties are required. Our customer object has a save method (created by Mongoose), and we need to pass it a callback which will be executed after the customer has been inserted. On Node, all I/O calls are asynchronous so you have to tell Jasmine-node to wait for the callback to executed, which is what the call to asyncSpecWait() does. When we get in our callback, we assert that the passed in error object (typically named 'err') is not null, and then we use the toHaveRequiredValidationErrorFor method to assert whether the expected validation error messages are present. The toHaveRequiredValidationErrorFor method doesn't come with Jasmine, it's a custom matcher which we make available before each test:
We pass an object containing 2 functions to the addMatchers function, which will in turn make those 2 methods available to our expectations.
Let's take a look at a more interesting example, saving an Activity object with an array of PerformedWork instances:
The ActivityBuilder constructor constructs a typical builder object. I won't go into the details of this pattern, and I won't list the code since this post is already getting a bit too long but you can look at the code here if you're interested. Anyways, back to the test. We're creating an activity object and using the addPerformedWork function (which we added to ActivitySchema.methods in the first code snippet) to add some performed hours to the activity. We expect the save function to not cause errors, and then we launch a simple query: finding an activity by its id value. Note how we use the findById function through the Activity variable. That Activity variable points to the constructor function which creates activity instances when invoked directly, but as I mentioned earlier it can have properties, which can hold functions, of its own. Notice the syntactic similarity with calling a static method in a static language. Behind the scenes it's entirely different, but from a conceptual point of view, it's sorta the same. As you can see, in this test the save operation works, so what does the activity object, or better yet, document look like in the database? Here it is:
It doesn't actually store it with all that whitespace, I just formatted it to increase readability. Anyways, what's interesting here is that we have our array of PerformedWork instances embedded right here in our document. So whenever we retrieve this activity instance, we automatically get its PerformedWork instances as well. Also notice the _id properties. We never defined id properties in our schemas, so MongoDB automatically adds an _id property. The id value is filled in by the MongoDB driver before the document is sent to the database. And as you can see, our customer property simply holds an ObjectId, not an actual customer document. Customers are stored in the customers collection, and an instance of a customer in MongoDB looks like this:
Pretty self-explanatory I think.
That's enough for this post. We've seen how we defined our objects in Mongoose, got a glimpse of Jasmine and covered some very basic interactions with MongoDB. I'm going to post more on this stuff as I continue working on my toy project, though I won't make any promises on how long it'll take before new posts will show up :)
This code will likely evolve significantly in the next couple of weeks/months, and if you're interested you can always follow its evolution on github.