npm install --save pure-orm
PureORM is a pure ORM sql toolkit library for node (on top of pg-promise). It allows you to write regular native SQL and receive back properly structured (nested) pure business objects.
This contrasts against traditional ("stateful") ORMs which use query builders (rather than raw SQL) to return database-aware (rather than pure) objects.
The name pureORM reflects both that it is pure ORM (there is no query builder dimension) as well as the purity of the mapped Objects.
A Business Object (BO) is a pure javascript object corresponding to a table.
A Data Access Object (DAO) is a database-aware abstraction layer where native SQL is written.
Our data access layer where SQL is written.
class PersonDAO extends BaseDAO {
Bo = Person;
// example code from below...
}
Lets start with a basic example which just uses the BaseBO.createOneFromDatabase method to map the column names to our desired javascript properties.
getRandom() {
const query = `
SELECT person.id, person.first_name, person.last_name, person.created_date, person.employer_id
FROM person
ORDER BY random()
LIMIT 1;
`;
return db.one(query).then(Person.createOneFromDatabase)
}
// OUTPUT: Person {id, firstName, lastName, createdDate, employerId}
We can use BaseDAO.one to create our business object for us.
getRandom() {
const query = `
SELECT person.id, person.first_name, person.last_name, person.created_date, person.employer_id
FROM person
ORDER BY random()
LIMIT 1;
`;
- return db.one(query).then(Person.createOneFromDatabase)
+ return this.one(query);
}
// OUTPUT: Person {id, firstName, lastName, createdDate, employerId}
Specifying all the columns is tedious; lets use BaseBo.getSQLSelectClause() to get them for free.
getRandom() {
const query = `
- SELECT person.id, person.first_name, person.last_name, person.created_date, person.employer_id
+ SELECT ${Person.getSQLSelectClause()}
FROM person
ORDER BY random()
LIMIT 1;
`;
return this.one(query);
}
// OUTPUT: Person {id, firstName, lastName, createdDate, employerId}
More important than saving the tedium, though, is how BaseBo.getSQLSelectClause() namespaces each select expression name under the hood, and which BaseBo.createOneFromDatabase knows how to handle. This means that when joining, not only is the select expression easy, select expression names won't collide:
getRandom() {
const query = `
- SELECT ${Person.getSQLSelectClause()}
+ SELECT ${Person.getSQLSelectClause()}, ${Employer.getSQLSelectClause()}
FROM person
+ JOIN employer on person.employer_id = employer.id
ORDER BY random()
LIMIT 1;
`;
return this.one(query);
}
// OUTPUT: Person {id, firstName, lastName, createdDate, employer: Employer}
Rather than being flat, with the employer id and createdDate colliding with person's id and createDate, the result is a nice Person BO with a nested Employer BO.
Lets move to a different example to show off another aspect of BaseBo.createOneFromDatabase: how it handles flattening data. Lets say there are three tags for article being retrieved, rather than the data being an array of 3 results with article repeated, the result is a nice Article BO with the tags nested in it.
getBySlug(slug) {
const query = `
SELECT
${Article.getSQLSelectClause()},
${Person.getSQLSelectClause()},
${ArticleTag.getSQLSelectClause()},
${Tag.getSQLSelectClause()}
FROM article
JOIN person
ON article.author_id = person.id
LEFT JOIN article_tags
ON article.id = article_tags.article_id
LEFT JOIN tag
ON article_tags.tag_id = tag.id
WHERE article.slug = $(slug);
`;
return this.one(query, { slug });
}
// OUTPUT: Article { person: Person, articleTags: Array<ArticleTag> }
Notice that we're using this.one, which is what we want. The DAO methods for one, oneOrNone, many, any ensure their count against the number of generated top level business objects - not the number of rows the sql expression returns!
Lets say we want to get more than one article. We can make slug an array, and BaseBo.createFromDatabase handles it seemlessly, giving us an Articles collections
-getBySlug(slug) {
+getBySlugs(slugs) {
const query = `
SELECT
${Article.getSQLSelectClause()},
${Person.getSQLSelectClause()},
${ArticleTag.getSQLSelectClause()},
${Tag.getSQLSelectClause()}
FROM article
JOIN person
ON article.author_id = person.id
LEFT JOIN article_tags
ON article.id = article_tags.article_id
LEFT JOIN tag
ON article_tags.tag_id = tag.id
- WHERE article.slug = $(slug);
+ WHERE article.slug in ($(slugs:csv));
`;
- return this.one(query, { slugs });
+ return this.many(query, { slugs });
}
-// OUTPUT: Article { person: Person, articleTags: Array<ArticleTag> }
+// OUTPUT: Articles[
+// Article { person: Person, articleTags: Array<ArticleTag> }
+// Article { person: Person, articleTags: Array<ArticleTag> }
+// ]
Lastly, lets switch gears one more time to see how meta data can be intertwined. Prefix the value as meta_ and it will be passed through to the business object.
getBloggerPayout(id, startDate, endDate) {
const query = `
SELECT
${Person.getSQLSelectClause()},
COALESCE(SUM(article.blogger_payout), 0) as meta_amount,
FROM
person
LEFT JOIN article
ON article.author_id = person.id
AND (article.created_date BETWEEN $(startDate) AND $(endDate))
WHERE
person.id = $(id)
GROUP BY person.id, person.slug, person.email,
person.first_name, person.last_name, person.last_paid_date,
person.pay_frequency
ORDER BY meta_amount DESC NULLS LAST;
`;
return this.one(query, { id, startDate, endDate });
}
Now lets look at our business logic layer where we use the DAO to get/persist pure data. (This example uses the few included common DAO methods in order to show something. However, in practice you'll mainly be using your own custom functions with your own SQL to do your own interesting things; vs this contrived and basic example.)
let raw = new Person({
email: 'foobar@gmail.com',
firstName: 'craig',
lastName: 'martin'
});
const personDAO = new PersonDAO({ db });
// Returns a person business object with the persisted data
let person = await personDAO.create(raw);
person.email = 'craigmartin@gmail.com';
// Returns a person business object with the updated persisted data
person = await personDAO.update(person);
// Gets or creates a person business object
same = await personDAO.getOrCreate(raw);
same.id === person.id; // true
// Returns the person business object which matches this data
same = await personDAO.getMatching(
new Person({ email: 'craigmartin@gmail.com' })
);
same.id === person.id; // true
// Deletes the person data form the database
await personDAO.delete(person);
To see everything in action, check out the examples directory and the tests.
Low Level Abstractions
Stateful ORMs (comprised of two portions)
Query Builders (eg knex) - These (built on database drivers) offer a dialetic-generic, chainable object api for expressing underlying SQL - thus solving for database "lock-in" as well the inability to compose SQL queriers as strings. pure-orm takes the approach that the tradeoff of developers having to learn the huge surface area of dialetic-generic api, and having to map the complexity and nuance of SQL to it, are simply not worth the cost, and so does not use a query building library. With pure-orm you just write SQL. The tradeoff on pure-orms side that is indeed being tied to a sql dialect and in the inability to compose sql expressions (strings don't compose nicely). Yet all this considered, pure-orm sees writing straight SQL heaviliy as a feature, not a defect needing solved, and not eclipsed by the composibility of a query builder.
Stateful, Database Aware Objects (eg sequelize, waterline, bookshelf, typeorm) - These stateful, database-aware object libraries are the full embrace of "Stateful ORMs". Contrary to this these is pure-orm which yields pure, un-attached, structured objects.
PureORM
An abstract class which is the base class your BO classes to extend.
Abstract Methods to be implemented
Optional
Public Methods
An abstract class which is the base class your Bo Collection classes extend.
Abstract Methods to be implemented
Optional
Public Methods
The base class your DAO classes extend.
Abstract Methods to be implemented
Public Methods
Abstractions over pg-promise's query methods:
(Note, these methods assert the correct number on the created BO's - not the raw postgres sql result. Thus, for example, one understands that there may be multiple result rows (which pg-promise's one would throw at) but which could correctly nest into one BO.)
Built-in "basic" / generic functions which your extending DAO class instance gets for free
These are just provided because they are so common and straight-forward. However, the point of this library specifically contrasts against having a large surface area of pre-built functions to learn. The idea is to add a DAO class, and add your own custom functions with your own SQL.
Parameters
Return Value
Parameters
Return Value
It is in production at www.kujo.com - powering the marketing pages and blog, as well as the customer, affiliate, and admin platforms (behind login). When considering for your case, note the Current Limitations and TODOs sections above.
#node #pureorm #sql #database #nodejs