For the past week and a half, I have been working on and off on refactoring my node-activerecord project. For those of you who aren’t familiar with the concept of “active record”, it’s a way to map an object directly to a database entry. It exposes an object-relational mapping (ORM) that makes it incredibly simple to work with the database.
Since a lot goes into developing a full ORM (and because I’m in the middle of developing node-activerecord as I write this), I decided to break down these posts into multiple parts. In this first part, I want to share a cool trick that involves implementing the attribute getters and setters.
If you’ve ever used Rails, then you are likely familiar with that ActiveRecord implementation. It works something like this:
Pretty straightforward stuff, but how do we implement something similar to that in Coffeescript? We want to be able to do this:
Your first thought might be that you can simply set the
name property on
user since it’s an object. While true, this isn’t very helpful. In order to know what attributes have been altered, and in order to do some other important stuff such as trigger Observer methods, we need
user.name to really be a method.
Lucky for us, there is a nifty method that was introduced with ECMAScript 5 (back in 2009!) that lets us do exactly that called
Object.defineProperty. It lets us implement actual getter and setter methods for any property on an object. Here’s how it is implemented in node-activerecord:
Let’s walk through this line by line.
First, we loop over every defined field for the model. The primary key is always created and automatically defined as
id unless specified otherwise (code not shown). The special thing about this loop, however, is that it actually executes a function for each iteration in order to create a closure. The
do keyword in Coffeescript is used for exactly this. Because we’re defining the properties in a loop, we need to make sure that
Next, we call our
Object.defineProperty method. The first argument tells it that we want to bind the getter/setter functions to this object (the Model). The next argument is an object of options. Setting
enumerable: true and
configurable: false guarantees that the property will show up during enumeration (looping over object properties) and that the property cannot be deleted from the Model.
Finally the getter and setter functions are defined. The getter is extremely simple. It returns the value from the
@data object (via
@readAttribute), which is where we actually store all attributes of the model.
The setter, however, does some important operations. First, we don’t allow the primary key to be manually set. By convention, the primary key is an automatically generated and monotonically increasing number. Next, if the value of the attribute has changed, we apply a filter function from the Observer, if it exists. The Observer is simply an object that contains callback methods, which are named by convention. Updating the
name attribute calls the
filterName function in the Observer. This gives us an opportunity to perform operations on the value of the attribute before it is set. A good example of this would be to strip invalid characters. Finally, we use the setter method to easily track what attributes have changed and need to be persisted to the database by updating a
Object.defineProperty for every field, we can define a proxy that intercepts the reading and writing of any attribute on the model. It would look sometihng like this:
Posted February 21, 2013