express-cassandra
This is one of the steps needed to transform expressjs into a complete MVC framework, by adding Models-support. No more hassling with code in your models. express-cassandra automatically loads your models and provides you with object oriented mapping with your cassandra tables like a standard ORM.
This module uses datastax cassandra-driver for node and many of the orm features are wrapper over a modified version of apollo-cassandra module. The modifications made to the orm library was necessary to support missing features in the orm and to make it compatible with requirements of this module.
Installation
$ npm install express-cassandra
Usage
var models = require('express-cassandra');
models.setDirectory( __dirname + '/models').bind(
{
clientOptions: {
contactPoints: ['127.0.0.1'],
keyspace: 'mykeyspace',
queryOptions: {consistency: models.consistencies.one}
},
ormOptions: {
defaultReplicationStrategy : {
class: 'SimpleStrategy',
replication_factor: 1
},
dropTableOnSchemaChange: false
}
},
function(err) {
if(err) console.log(err.message);
else console.log(models.timeuuid());
}
);
Write a Model named PersonModel.js
inside models directory
module.exports = {
fields:{
name : "text",
surname : "text",
age : "int"
},
key:["name"]
}
Note that a model class name should contain the word Model
in it,
otherwise it won't be treated as a model class.
Let's insert some data into PersonModel
var alex = new models.instance.Person({name: "Alex", surname: "Rubiks", age: 32});
alex.save(function(err){
if(err) console.log(err);
else console.log('Yuppiie!');
});
Now let's find it
models.instance.Person.find({name: 'jhon'}, function(err, john){
if(err) throw err;
console.log('Found ' + john.name + ' to be ' + john.age + ' years old!');
});
Model Schema in detail
module.exports = {
"fields": {
"id" : { "type": "uuid", "default": {"$db_function": "uuid()"} },
"name" : { "type": "varchar", "default": "no name provided"},
"surname" : { "type": "varchar", "default": "no surname provided"},
"complete_name" : { "type": "varchar", "default": function(){ return this.name + ' ' + this.surname;}},
"age" : { "type": "int" },
"created" : {"type": "timestamp", "default" : {"$db_function": "now()"} }
},
"key" : [["id"],"created"],
"indexes": ["name"],
"custom_index": {
on: '...',
using: '...',
options: {
option1 : '...',
option2: '...'
}
}
}
What does the above code means?
fields
are the columns of your table. For each column name the value can be a string representing the type or an object containing more specific informations. i.e.
"id" : { "type": "uuid", "default": {"$db_function": "uuid()"} },
in this example id type is uuid
and the default value is a cassandra function (so it will be executed from the database)."name" : { "type": "varchar", "default": "no name provided"},
in this case name is a varchar and, if no value will be provided, it will have a default value of no name provided
. The same goes for surname
.complete_name
the default values is calculated from others field. When apollo processes you model instances, the complete_name
will be the result of the function you defined. In the function this
is bound to the current model instance.age
no default is provided and we could write it just as "age": "int"
.created
, like uuid(), will be evaluated from cassandra using the now()
function.
key
: here is where you define the key of your table. As you can imagine, the first value of the array is the partition key
and the others are the clustering keys
. The partition key
can be an array defining a compound key
. Read more about keys on the documentationindexes
are the index of your table. It's always an array of field names. You can read more on the documentationcustom_index
provides the ability to define custom indexes with cassandra. Cassandra upto version 2.1.x supports only one custom index per table.
When you instantiate a model, every field you defined in schema is automatically a property of your instances. So, you can write:
john.age = 25;
console.log(john.name);
console.log(john.complete_name);
note: john.complete_name
is undefined in the newly created instance but will be populated when the instance is saved because it has a default value in schema definition
Ok, we are done with John, let's delete it:
john.delete(function(err){
});
A few handy tools for your model
Express cassandra exposes some node driver methods for convenience. To generate uuids e.g. in field defaults:
models.uuid()
returns a type 3 (random) uuid, suitable for Cassandra uuid
fields, as a stringmodels.timeuuid()
returns a type 1 (time-based) uuid, suitable for Cassandra timeuuid
fields, as a string
Support for Composite Data Types
Cassandra composite data types (map
, list
& set
) are supported in model schema definitions. An additional typeDef
attribute is used to define the composite type.
module.exports = {
"fields": {
info: {
type: "map",
typeDef: "<varchar, text>"
}
}
}
When saving or updating composite types, use the string representation of the value like the following:
var person = new models.instance.Person({
info: "{'key1':'val1','key2': 'val2'}"
});
person.save(function(err){
});
You may also use composite expressions supported by cassandra like the following:
models.instance.Dictionary.update({},{
info: "info + {'hello':'world'}"
},{},function(err){});
Virtual fields
Your model could have some fields which are not saved on database. You can define them as virtual
module.exports = {
"fields": {
"id" : { "type": "uuid", "default": {"$db_function": "uuid()"} },
"name" : { "type": "varchar", "default": "no name provided"},
"surname" : { "type": "varchar", "default": "no surname provided"},
"complete_name" : {
"type": "varchar",
"virtual" : {
get: function(){return this.name + ' ' +this.surname;},
set: function(value){
value = value.split(' ');
this.name = value[0];
this.surname = value[1];
}
}
}
}
}
A virtual field is simply defined adding a virtual
key in field description. Virtuals can have a get
and a set
function, both optional (you should define at least one of them!).
this
inside get and set functions is bound to current instance of your model.
Validators
Every time you set a property for an instance of your model, an internal type validator checks that the value is valid. If not an error is thrown. But how to add a custom validator? You need to provide your custom validator in the schema definition. For example, if you want to check age to be a number greater than zero:
module.exports = {
age: {
type : "int",
rule : function(value){ return value > 0; }
}
}
your validator must return a boolean. If someone will try to assign john.age = -15;
an error will be thrown.
You can also provide a message for validation error in this way
module.exports = {
age: {
type : "int",
rule : {
validator : function(value){ return value > 0; },
message : 'Age must be greater than 0'
}
}
}
then the error will have your message. Message can also be a function; in that case it must return a string:
module.exports = {
age: {
type : "int",
rule : {
validator : function(value){ return value > 0; },
message : function(value){ return 'Age must be greater than 0. You provided '+ value; }
}
}
}
The error message will be Age must be greater than 0. You provided -15
Note that default values are validated if defined either by value or as a javascript function. Defaults defined as DB functions, on the other hand, are never validated in the model as they are retrieved after the corresponding data has entered the DB.
If you need to exclude defaults from being checked you can pass an extra flag:
module.exports = {
email: {
type : "text",
default : "<enter your email here>",
rule : {
validator : function(value){ },
ignore_default: true
}
}
}
Querying your data
Ok, now you have a bunch of people on db. How do I retrieve them?
Find
models.instance.Person.find({name: 'John'}, function(err, people){
if(err) throw err;
console.log('Found ', people);
});
In the above example it will perform the query SELECT * FROM person WHERE name='john'
but find()
allows you to perform even more complex queries on cassandra. You should be aware of how to query cassandra. Every error will be reported to you in the err
argument, while in people
you'll find instances of Person
.
If you don't want apollo to cast results to instances of your model you can use the raw
option as in the following example:
models.instance.Person.find({name: 'John'}, { raw: true }, function(err, people){
});
You can also select particular columns using the select key in the options object like the following example:
models.instance.Person.find({name: 'John'}, { raw: true, select: ['name','age'] }, function(err, people){
});
Let's see a complex query
var query = {
name: 'John',
age : { '$gt':10 },
surname : { '$in': ['Doe','Smith'] },
$orderby:{'$asc' :'age'} },
$limit: 10
}
models.instance.Person.find(query, {raw: true}, function(err, people){
});
If you want to set allow filtering option, you may do that like this:
models.instance.Person.find(query, {raw:true, allow_filtering: true}, function(err, people){
});
Note that all query clauses must be Cassandra compliant. You cannot, for example, use $in operator for a key which is not the partition key. Querying in Cassandra is very basic but could be confusing at first. Take a look at this post and, obvsiouly, at the documentation
Create / Update / Delete
Create
var alex = new models.instance.Person({name: "Alex", surname: "Rubiks", age: 32});
alex.save(function(err){
if(err) console.log(err);
else console.log('Yuppiie!');
});
The save function also takes optional parameters. By default cassandra will update the row if the primary key
already exists. If you want to avoid on duplicate key updates, you may set if_not_exist:true.
alex.save({if_not_exist: true}, function(err){
if(err) console.log(err);
else console.log('Yuppiie!');
});
You can also set an expiry ttl for the saved row if you want. In that case the row will be removed by cassandra
automatically after the time to live has expired.
alex.save({ttl: 86400}, function(err){
if(err) console.log(err);
else console.log('Yuppiie!');
});
Update
The update function takes the following forms, (options are optional):
var query_object = {username: 'abc'};
var update_values_object = {email: 'abc@gmail.com'};
var options = {ttl: 86400, if_exists: true};
models.instance.Person.update(query_object, update_values_object, options, function(err){
if(err) console.log(err);
else console.log('Yuppiie!');
});
var query_object = {username: 'abc'};
var update_values_object = {email: 'abc@gmail.com'};
var options = {conditions: {email: 'typo@gmail.com'}};
models.instance.Person.update(query_object, update_values_object, options, function(err){
if(err) console.log(err);
else console.log('Yuppiie!');
});
Delete
The delete function takes the following form:
var query_object = {username: 'abc'};
models.instance.Person.delete(query_object, function(err){
if(err) console.log(err);
else console.log('Yuppiie!');
});
Raw Query
You can get the raw query interface from cassandra nodejs-driver using the execute_query
method.
var query = "Select * from user where gender=? and age > ? limit ?";
var params = ['male', 18, 10];
models.instance.Person.execute_query(query, params, function(err, people){
});
Batch Query
You can get the batch query interface from cassandra nodejs-driver using the execute_batch
method.
var queries = [
{
query: "...",
params: [...]
},
{
query: "...",
params: [...]
}
];
models.instance.Person.execute_batch(queries, function(err){
});
Note
All queries except schema definition related queries (i.e. create table etc.) are prepared by default. So you don't
need to set prepare=true
, the orm takes care of it automatically.