Introduction
Vitamin provides a simple and easy to use Data Mapper implementation for working with your relational database.
Based on knex, it supports Postgres, MySQL, MariaDB, SQLite3, and Oracle databases, featuring both promise based and traditional callback interfaces, providing lazy and eager relationships loading, and support for one-to-one, one-to-many, and many-to-many relations.
Installation
$ npm install --save vitamin
# Then add one of the supported database drivers
$ npm install pg
$ npm install sqlite3
$ npm install mysql
$ npm install mysql2
$ npm install mariasql
$ npm install strong-oracle
$ npm install oracle
Vitamin is initialized by passing an initialized Knex client instance. The knex documentation provides a number of examples for different use cases.
var knex = require('knex')({
client: 'mysql',
connection: {
host : '127.0.0.1',
user : 'your_database_user',
password : 'your_database_password',
database : 'your_database_name',
charset : 'utf8'
}
})
module.exports = require('vitamin')(knex)
Getting started
Defining data mappers
To get started, let's define a user data mapper by specifying both, the primary key
name, and the table
name
// using the previous initialized vitamin object,
// we define a mapper called `user` using `mapper` method of vitamin object
module.exports = vitamin.model('user', {
// the primary key, default to `id`
primaryKey: 'user_id',
// the table name
tableName: 'users',
// add default attributes
defaults: {
active: true,
verified: false
}
})
model()
also accepts a mapper instance passed as a second argument instead of a config object
CRUD : Reading and writing data
You can use the standard Node.js style callbacks by calling
.asCallback(function (error, result) {})
on any promise method
1 - Create
To create a new record in the database, simply create a new model instance, set its attributes, then call the save
method
// access the model generated by the mapper via vitamin's `model` method
var User = vitamin.model('user')
// we create a new instance of User model with `make` method
var user = User.make({ name: "John", occupation: "Developer" })
// or simply with the new operator
var user = new User({ name: "John", occupation: "Developer" })
// then we save it
user.save()
.then(function (result) {
assert.equal(result, user)
assert.instanceOf(result, User)
})
.catch(function (error) {
...
})
Another shorthand to create and save a new user is the create
static method
var data = { name: "John", occupation: "Developer" }
User.create(data).then(function (result) {
assert.instanceOf(result, User)
})
2 - Read
Below a few examples of different data access methods provided by Vitamin.
- Retrieving multiple models
// get a collection of all users
User.query().fetch().then(
function (result) {
assert.instanceOf(result, Collection)
},
function (error) {
...
}
)
The fetch
method will return all the rows in the users
table as a collection of User
models.
But, if you may also add constraints to queries, you can use the where
method, which returns a query builder
instance
User.query().where('role', "guest").offset(10).limit(15).fetch().then(
function (result) {
assert.instanceOf(result, Collection)
},
function (error) {
...
}
)
- Retrieving single model
Of course, in addition to retrieving all of the records for a given table, you may also retrieve single records using
find
andfirst
. Instead of returning a collection of models, these methods return only a single model instance
// find a user by its primary key
User.query().find(123).then(
function (result) {
assert.instanceOf(result, User)
},
function (error) {
...
}
)
To retrieve the first model matching the query constraints, use first
// fetch the `id` and `email` of the first admin user
User.query().where('is_admin', true).first('id', 'email').then(
function (result) {
assert.instanceOf(result, User)
},
function (error) {
...
}
)
3 - Update
The save
method may also be used to update a single model that already exists in the database.
To update a model, you should retrieve it, set any attributes you wish to update, and then call the save
method.
// post model is retrieved from `posts` table
// then we modify the status attribute and save it
var Post = vitamin.model('post')
Post.query().find(1).then(function (post) {
return post.set('status', "draft").save()
})
In case you have many attributes to edit, you may use the update
method directly:
var data = { 'status': "published", 'published_at': new Date }
post.update(data).then(
function (result) {
...
},
function (error) {
...
}
)
4 - Delete
Likewise, once retrieved, a model can be destroyed which removes it from the database.
To delete a model, call the destroy
method on an existing model instance:
Post.make({ id : 45 }).destroy().then(
function (result) {
assert.equal(result, post)
},
function (error) {
...
}
)
Of course, you may also run a delete query on a set of models.
// we will delete all posts that are marked as draft
Post.query().where('status', 'draft').destroy().then(
function (result) {
...
},
function (error) {
...
}
)
Events
Model events allow you to attach code to certain events in the lifecycle of yours models.
This enables you to add behaviors to your models when those built-in events
ready
, creating
, created
, saving
, saved
, updating
, updated
, deleting
or deleted
occur.
Events can be defined when you register the model
vitamin.model('user', {
...
events: {
'creating': _.noop,
'saved': [
handler1,
handler2,
]
}
})
Or, later with the static method on
// attach a listener for `created` event
User.on('created', function (user) {
assert.instanceOf(user, User)
})
// Events `saving - creating - created - saved` are fired in order when we create a new model
>>> User.create({ name: "John", occupation: "Developer" })
You can also attach the same handler for many events separated by a white space
Post.on('creating updating', updateTimestamps)
The built-in events are fired automatically by the mapper, but you can trigger manually those events, or any custom ones
Post.make().emit('saving')
Order.make().emit('purchased', ...arguments)
Associations
Vitamin makes managing and working with relationships easy, and supports several types of relations:
- One To One
- One To Many
- Many To Many
Defining relations
One to One
Let's define a relation one to one between Person
and Phone
.
var Phone = vitamin.model('phone', {
tableName: 'phones',
relations: {
owner: function () {
// we refer to`Person` mapper by its name
// `owner_id` is the foreign key of `users` in `phones` table
return this.belongsTo('person', 'owner_id', 'id')
}
}
})
var Person = vitamin.model('person', {
tableName: 'people',
relations: {
phone: function () {
// the first argument is the target mapper name
// the second is the foreign key in phones table
// the third parameter is optional, it corresponds to the primary key of person model
return this.hasOne('phone', 'owner_id', 'id')
}
}
})
One To Many
An example for this type, is the relation between blog post
and its author
var User = vitamin.model('user', {
tableName: 'users',
relations: {
posts: function () {
// if the foreign key is not provided,
// vitamin will use the parent mapper name suffixed by '_id',
// as a foreign key in the `posts` table, in this case `author_id`
return this.hasMany('post')
}
}
})
var Post = vitamin.model('post', {
tableName: 'posts',
relations: {
author: function () {
return this.belongsTo('author', 'author_id')
}
}
})
Many To Many
This relation is more complicated than the previous.
An example of that is the relation between Product
and Category
, when a product has many categories, and the same category is assigned to many products.
A pivot table product_categories
is used and contains the relative keys product_id
and category_id
vitamin.model('product', {
tableName: 'products',
relations: {
categories: function () {
return belongsToMany('category', 'product_categories', 'category_id', 'product_id')
}
}
})
vitamin.model('category', {
tableName: 'categories',
relations: {
products: function () {
return belongsToMany('product', 'product_categories', 'product_id', 'category_id')
}
}
})
Querying relations
Lazy loading
We will use the relations defined below, to lazy load the related models
// load the related phone model of the person with the id 123
// we access the relation via `phone()` which return a HasOne relation instance
var person = Person.make({ id: 123 })
person.load(['posts']).then(function (model) {
assert.equal(model, person)
assert.instanceOf(person.getRelated('posts'), Collection)
})
Eager loading
To load a model and its relationships in one call, you can use the query method withRelated
// fetch the first article and its author
Post.query().withRelated('author').first().then(function (post) {
assert.instanceOf(post.getRelated('author'), User)
})
// load all authors with their posts
User.query().withRelated('posts').fetch().asCallback((error, authors) => {
assert.instanceOf(authors, Collection)
authors.forEach(function (author) {
assert.instanceOf(author.getRelated('posts'), Collection)
assert.instanceOf(author.getRelated('posts').first(), Post)
})
})
Saving related models
Instead of manually setting the foreign keys, Vitamin provides many methods to save the related models.
save()
and saveMany()
var comment = Comment.make({ body: "Hello World !!" })
// saving and attach one comment
post.comments().save(comment).then(
function (model) {
assert.equal(model, comment)
},
function (error) {
...
}
)
// saving many events
post.comments().saveMany([
Comment.make({ body: "first comment" }),
Comment.make({ body: "second comment" })
]).then(function (result) {
assert.instanceOf(result, Collection)
assert.instanceOf(result.first(), Comment)
})
create()
and createMany()
In addition to the save
and saveMany
methods, you may also use the create
method,
which accepts an array of attributes, creates a model, and inserts it into the database.
// create and attach a post comment
post.comments().create({ body: "Hello World !!" }).then(function (error, model) {
assert.instanceOf(model, Comment)
})
// create and attach the post comments
post.comments().createMany([
{ body: "first comment" }, { body: "second comment" }
]).then(function (result) {
assert.instanceOf(result, Collection)
assert.instanceOf(result.first(), Comment)
})
associate()
and dissociate()
When updating a belongsTo
relationship, you may use the associate
method.
var john = Person.make({ id: 123 })
// set the foreign key `owner_id` and save the phone model
phone.owner().associate(john).save().then(...)
// unset the foreign key, then save
phone.owner().dissociate().save().then(...)
attach()
, detach()
and updatePivot()
When working with many-to-many relationships, Vitamin provides a few additional helper methods to make working with related models more convenient.
// to attach a role to a user by inserting a record in the joining table
user.roles().attach(roleId).then(
function (result) {
...
},
function (error) {
...
}
)
To remove a many-to-many relationship record, use the detach method.
// detach all roles of the loaded user
user.roles().detach().asCallback(function (error, result) {
...
})
// detach only the role with the given id
user.roles().detach(roleId).then(...)
// detach all roles with the given ids
user.roles().detach([1, 2, 3]).then(...)
If you need to update an existing row in your pivot table, you may use updatePivot
method
user.roles().updatePivot(roleId, pivotAttributes).then(...)
sync()
You may also use the sync
method to construct many-to-many associations.
The sync method accepts an array of IDs to place on the intermediate table.
Any IDs that are not in the given array will be removed from the intermediate table.
So, after this operation is complete, only the IDs in the array will exist in the intermediate table:
// using callbacks
user.roles().sync([1, 2, 3], function (error, result) {
...
})
// using promises
user.roles().sync([1, 2, 3]).then(...)
You may also pass additional intermediate table values with the IDs:
user.roles().sync([[1, { 'expires': true }], 2]).then(...)