OnionRM is an ORM for node.js targeting Postgres. OnionRM owes it's existence to node-orm2 from which OnionRM forked.
Aren't ORMs supposed to be database agnostic?
Many, and perhaps most, are. But, at the end of the day you've chosen Postgres for a reason. Postgres has many awesome features, and shouldn't your application make use of them?
Features like JSON, Arrays are natively supported.
To get started, initialize a connection to Postgres;
var orm = require('onionrm');
orm.connect("postgres://user@localhost/database", function(err, db){
//define models
//perform database operations here
db.close() //close connection
};
Like all other ORMs, OnionRM lets your define models. Models can be defined after connecting to Postgres and acquiring a database object;
var Customer = db.define("customers", {
created_at : Date,
updated_at : Date,
username : String,
password_hash : String,
name : String
});
//class level methods can be defined like this;
Customer.withUsernameAndPassword = function(username, password, callback){
var hash = calculateAHash(password);
var filter = {
username: username,
password_hash: hash
};
this.find(filter, callback);
};
var Order = db.define("orders", {
created_at : Date,
updated_at : Date,
number : String,
status_code : Number,
customer_id : String
}, {
methods: {
//define instance methods here
statusDescription: function () {
switch(this.status_code){
case 1:
return "Processing";
case 2:
return "Shipped";
case 3:
return "Cancelled";
default:
return "Pending";
}
}
}
});
Order.hasOne "customer", Customer
To create/save an instance of a method, pass a hash into the create
method;
var details = {
number: "123-abc",
customer_id: "435cb549-8d10-4137-8e60-67c6f6204c7e"
};
Order.create(details, function(err, order){
//other tasks
});
Upon successful persistance of the created instance, a model instance will be returned.
With a reference to an OnionRM model, you can then perform an update. To update an model, mutate it's state, and then call save
;
order.status_code = 2;
order.save(function(err){
});
Models expose an isDirty
method for checking if anything has actually changed since the last time it was saved.
order.isDirty(); //false
order.status_code = 2;
order.isDirty(); //true (assuming status_code previously was not 2, false otherwise)
order.save(function(err){
});
Deletes can be performed in a similar fashion. Simply call remove
on an OnionRM model instance;
order.remove(function(err){
});
With the basics of how to create, update, or delete models out of the way, let's look at how to fetch existing models.
Fetching data starts with ChainFind
, a chainable querying builder. The simplest query is find
;
Order.find({number: "456-zyx"}, function(err, orders){
};
The above is similar to writing the following SQL:
select id, created_at, updated_at, number, status_code, customer_id
from orders
where number='456-zyx';
Note: Any SQL shown in this document is simply for illustration purposes. The actual SQL generated by OnionRM is slightly different, and can even vary based upon which ChainFind operators are performed (e.g. to help resolve ambiguous columns, etc).
The first parameters to find
is a hash of properties to "filter" by. As illustrated above they're simple equalities, as as we'll see later many more operators are available.
Note: It's worth pointing out that if only one item is expected, the one
method can be used instead. It behaves exactly like find
, and is chainable, but only returns the first matching record.
Additional keys/values in the filter hash are ANDed together. So, this;
Order.find({customer_id: "435cb549-8d10-4137-8e60-67c6f6204c7e", status_code: 3}, function(err, orders){
});
Is similar to;
select id, created_at, updated_at, number, status_code, customer_id
from orders
where customer_id='435cb549-8d10-4137-8e60-67c6f6204c7e'
and status_code=3;
Since this is a chainable query builder, we don't have to stop here. Omitting the callback parameter to find
allows for almost limitless queries. Since all* chainable actions are valid for find
as well as one
, for the remainder of this section we'll just used the variable chain
to represent either. Similar to;
var chain = Order.find();
//or
var chain = Order.one();
When working against large datasets, it's useful to paginate through records;
Order.find().limit(100).offset(1000).run(function(err, orders){
});
select id, created_at, updated_at, number, status_code, customer_id
from orders
limit 100
offset 1000;
Of course, offset is required to use limit. If you only cared about the first 500 orders in a particular status, you could;
Order.find({status_code: 1}).limit(500).fun(function(err, orders){
});
select id, created_at, updated_at, number, status_code, customer_id
from orders
where status_code=1
limit 500;
Continuing with the above example, let's say you wanted to find the 500 most recent orders in a status.
Order.find({status_code: 1}).limit(500).order("-created_at").run(function(err, orders){
});
select id, created_at, updated_at, number, status_code, customer_id
from orders
where status_code=1
order by created_at desc
limit 500;
As in you can see in the above example, the order of limit and order is irrelevant. The generated SQL will be syntactically correct, since the SQL isn't generated (or ran) until the fun
function is called.
To find the 500 oldest orders in a specific status, remove the negative sign in front of the sort property;
Order.find({status_code: 1}).limit(500).order("created_at").run(function(err, orders){
});
select id, created_at, updated_at, number, status_code, customer_id
from orders
where status_code=1
order by created_at
limit 500;
Filtering on the values of a specific model can only get you so far, however many times we want to find items based on their relationship to other items. This is where join
and with
come in.
By itself, join simply joins one model to another;
LineItem.find({product_code: 'ZZZ123'}).join("orders", "id", "order_id").run(function(err, lineItems){
});
select --properties in line item model
from line_items
join orders on order_id=orders.id
where line_items.product_code = 'ZZZ123';
To specify filters on a joined model, use with
;
LineItem.find({product_code: 'ZZZ123'}).join("orders", "id", "order_id").with({status_code: 3}).run(function(err, lineItems){
});
select --properties in line item model
from line_items
join orders on order_id=orders.id
where line_items.product_code = 'ZZZ123'
and orders.status_code = 3;
The with
operator is slightly different from other ChainFind
methods in that it's applied only to the immediately preceding join
. This permits multiple models to be joined, without ambiguity of model property names. For example;
LineItem.find({product_code: 'ZZZ123'}).join("orders", "id", "order_id").with({created_at: orm.gt(thisYear)}).join("customers", "id", "customer_id").with({created_at: orm.lt(thisYear)}).run(function(err, lineItems){
});
select --properties in line item model
from line_items
join orders on order_id=orders.id
join customers on customer_id=customers.id
where line_items.product_code = 'ZZZ123'
and orders.created_at > '2015-01-01T00:00:00Z'
and customers.created_at < '2015-01-01T00:00:00Z';
The orm.gt
and orm.lt
operators will be discussed in detail in the section below.
By default OnionRM retrieves all (and only) the properties specified in a model's definition when querying Postgres. In the example "order" model, all our SQL has explicitly selected specific fields;
select id, created_at, updated_at, number, status_code, customer_id
from orders
--etc
This allows for database columns to exist, that aren't mapped into a model (and thusly aren't fetched). But, it also allows for fields to be blacklisted/whitelisted on demand using only
and omit
.
Perhaps you're exposing orders via an API, and you'd rather not expose status_code
. You could blacklist it using omit
;
Order.find(filter).omit("status_code").run//....
Alternatively you could whitelist which properties you do want expose using only
;
Order.find(filter).only(["id", "created_at", "number", "customer_id"]).run//....
All the examples so far have executed the ChainFind
by invoking run
. The run
method instantiates models, and returns an array of OnionRM models.
In scenarios where you're just fetching models to turn right around and serialize them to JSON, it might make sense to have Postgres directly handle the serialization for you;
Order.find().join("customers", "customer_id", "id").with({name: "Fred"}).omit("status_code").limit(100).asJson(function(err, json){
});
select array_to_json(array_agg(t))::text as json from (
select
orders.id,
orders.created_at,
orders.updated_at,
orders.number, orders.
customer_id
from orders
join customers on orders.customer_id=customers.id
where customers.name='Fred'
limit 100
) t;
The asJson
method can be used anywhere run
would have been used, and it's second parameters (called json
in the above example) is string containing JSON - useful for directly handing off to Express (or other).
So far we've seen filters that query on equality. However, many more operators are available.
var filter = {
status_code: orm.ne(1)
};
Order.find(filter, function(err, orders){
});
Is similar to;
select id, created_at, updated_at, number, status_code, customer_id
from orders
where status_code <> 1;
To keep the remainder of these comparators concise, only the relevant section of code will be illustrated.
created_at: orm.gt(aDate)
created_at > '2015-01-01T06:00:00Z'
created_at: orm.gte(aDate)
where created_at >= '2015-01-01T06:00:00Z'
created_at: orm.lt(aDate)
where created_at < '2015-01-01T06:00:00Z'
created_at: orm.lte(aDate)
where created_at <= '2015-01-01T06:00:00Z'
status_code: [1, 2, 3]
where status_code in (1, 2, 3)
status_code: orm.not_in([1, 2, 3])
where status_code not in (1, 2, 3)
created_at: orm.between(aDate, anotherDate)
where created_at between '2015-01-01T06:00:00Z' and '2015-02-01T06:00:00Z'
The ANY
operator compares a literal value for equality to any element in an array.
favorite_foods: orm.any('cookies')
where favorite_foods =ANY('cookies')
age: orm.mod(4)
where age % 4
You can reconstitute an OnionRM model by calling the reconstitute
class method with a hash of the instance's properties;
Customer.reconstitute({name: "Sue"}, function(err, model){
});
It's like soup!
Sure, there are tons of awesome features OnionRM inherited from node-orm2. However, unless it's outlined in this readme, it's subject to removal as existing code is refactored and Postgres specific functionality is added.
Get in and explore! If there's anything super awesome, submit a pull request, even if it's just to document said feature.
:squirrel: