Homepage and documentation: http://knexjs.org/
knex
is a Node package that provides:
- a nicer way to build and execute SQL queries
- support for talking to many different SQL database engines (PostgreSQL, MySQL, MSSSQL, ...)
- database migrations - transitioning schema from one version to another
Also:
- it only works for SQL (relational) databases (not Mongo, etc)
- it wraps low-level client libraries like
pg
to talk to database servers, but you can use it without a client library too - comes with a CLI command
knex
(for managing migrations)
To install it for a project:
npm install knex --save
In the docs you'll see knex being required in a form like this:
const knex = require('knex')({ /* configuration */ });
That is, when you require('knex')
, it doesn't really give you the knex
object you want to work with, it gives you a function used to build the knex
object you want. That function takes a configuration (object) as an argument. It's nothing magical, just a shortcut for something like:
const config = { /* configuration here */ };
const makeKnex = require('knex');
const knex = makeKnex(config);
Reference: http://knexjs.org/#Builder
An empty configuration means there's no client library, thus no database server to talk to.
const knex = require('knex')({})
This can be useful if you just want to play with the SQL query builder and see how it works.
In interactive Node:
> const knex = require('knex')({})
undefined
> knex.select()
Builder { ...<some output omitted>... }
> knex.select().toString()
'select *'
> knex.select().from('users')
Builder { ...<some output omitted>... }
> knex.select().from('users').toString()
'select * from "users"'
> knex.select().from('users').limit(1).toString()
'select * from "users" limit 1'
> knex.select().from('users').where('age', '>=', 19).limit(1).toString()
'select * from "users" where "age" > 18 limit 1'
> knex.select().from('users').where({ gender: 'm' }).where('age', '>=', 19).limit(1).toString()
'select * from "users" where "gender" = \'m\' and "age" >= 19 limit 1'
As we see here, the query building methods return a Builder
object. You can keep chaining query building methods to continue building the same query. You can call toString()
on a Builder
(meaning at any point along the chain) to see the SQL it would produce.
(Note: depending on the configured client, some SQL may change slightly, since not every database engine implements SQL exactly the same way.)
(Note 2: knex.select()
is the same as knex.select("*")
)
When you chain methods like this, each method call modifies the query you're building, adding more clauses or modifying existing clauses.
There are several ways to specify where-clauses:
.where('name', 'joe')
producesWHERE "name" = "joe"
.where('age', 20)
producesWHERE "age" = 20
.where('age', '>', 20)
producesWHERE "age" > 20
.where({ age: 20 })
is the same as.where('age', 20)
.where({ age: 20, dollars: 4 }
producesWHERE "age" = 20 and "dollars" = 4
See the docs for more examples, and variations on .where
like .whereNot
. Also see .join
, .having
and others that specify conditions the same way.
Reference: http://knexjs.org/#Schema
Examples:
Create table:
knex.schema.createTable('cats', function (table) {
table.string('name').notNullable();
table.integer('legs_scratched').defaultTo(0)
// etc.
})
Modify table:
knex.schema.table('cats', function (table) {
table.boolean('grumpy').defaultTo(true)
// etc.
})
Drop table:
knex.schema.dropTable('cats');
See more in the docs.
Quick review first:
- most database engines follow a client/server model
- various kinds of clients can connect to the database server, including command-line clients (like
psql
), GUI clients, or any app using a client library - the
pg
package you've been using to interact with PostgreSQL is a client library. It's fairly low-level, meaning more emphasis is on client correctness and completeness than on usability (as you may have noticed...).
Knex can be configured with a "client", meaning a client library. With a client and valid connection settings, knex is able to actually execute queries.
Knex can use the pg
client library (that you were just using) as its "client", as well as many other client libraries supporting MySQL, MSSQL, etc.
An example configuration using pg
is provided further down.
Here's an example configuration, used in this demo project:
const knex = require('knex')({
client: 'pg',
connection: {
database: 'knex_demo_music',
user: 'vaz',
host: 'localhost',
// password: '...'
}
});
Note, you don't actually require("pg")
. But you still have to npm install pg --save
! Knex is going to require it somewhere in its own code.
Reference: http://knexjs.org/#Interfaces
Once you have a configuration with a real backend, you'll want to execute queries, not just build them.
There are multiple ways. I'll describe two here briefly.
See the docs for more.
The following code would not execute anything against the database:
knex.select().from('artists')
Why?
-
Because knex syntax is about chaining together methods. Maybe you want to chain more methods onto this before executing.
-
Because making database queries is an async operation.
As usual, for async we can use callbacks. However, you'll see a different syntax, Promise-based syntax, in most of knex documentation.
Here's how the two look side by side.
Callbacks:
knex.select().from('artists').asCallback(function (err, result) {
// handle err, or
// do something with result
// do a second query
knex.select().from('albums').asCallback(function (err, result) {
// handle err, or
// do something with second result
});
});
Promises:
knex.select().from('artists').then(function (result) {
// do something with result
return knex.select()... // do a second query
}).then(function (result) {
// do something with second result
}).catch(function (err) {
// handle err (from either query)
});
Either way, you're dealing with results or potential errors.
But the way you chain multiple async calls together varies a lot.
Note: we briefly saw jQuery's promise interface for methods
like $.ajax
; it's worth pointing out that for jQuery the methods
were then
/fail
instead of then
/catch
. then
/catch
is more
widely accepted but jQuery doesn't support calling it catch
.
Reference: http://knexjs.org/#Migrations
-
A migration is a description of changes made to a database, to take it from one state (or version) to another.
-
Migrations describe changes required in both directions.
So, a migration describing the 4th version of the database schema will contain a description of changes to make
- to transform the 3rd version to the 4th version (we call this migrating up)
- to transform the 4th version back to the 3rd version (we call this migrating down or rolling back)
-
Migrations usually describe schema changes (like the methods in
knex.schema
, likeknex.schema.createTable(...)
) rather than data changes (inknex
, likeknex.select()...
).(However, sometimes data changes might be described too. Maybe you've added a new column, and want to fill it with some calculated value, for each existing row in the database. Pure "data migrations" are possible too. Both of these are rarely what we mean by "migrations": it's usually all about schema.)
-
Migrations are meant to be committed and pushed (shared with collaborators). The whole point is to be able to keep your database schemas in sync.
-
We typically start using migrations from the very beginning. So version 0 is an empty database, and we migrate up from there.
-
Once you've pushed a migration (shared it with collaborators or the public), don't edit it!
If someone has applied the migration before your edits were made, they will not be able to migrate those changes, because as far as knex is concerned, they already have. Once we've migrated to a version, we can't redo this migrations unless we rollback and migrate again (which usually means loss of data, and it breaks the workflow for everyone). You'll get scolded. So don't do it!
-
To make more changes, make a new migration! This is almost always the correct answer. Even if you haven't pushed it yet, it doesn't hurt to do it this way.
A new migration might add another column to a table you made in an earlier migration. Or it might drop a table you created a while back. Or change a column's name because of a typo-- even that can and usually should be a new migration.
To get the knex
CLI command, install knex globally (this is done in addition to installing it locally for the project):
npm install -g knex
To be really clear: you probably already ran npm install knex --save
. That's good, but installing globally is different and you need to do this too (just once though, not once per project). Only the global install (-g
) gives you the knex
command.
(Technically that's not totally true, you can find a knex
executable command somewhere in the project's node_modules/
, but it's way less handy.)
Now, just like how you pass a configuration to knex when you use it in your Node code, this CLI command version of knex needs a configuration. It gets this from a "knexfile". You can create a default knexfile with:
knex init
Have a look at the knexfile.js
it creates. Multiple
configurations can be set, in case you need to manage
databases in different environments (local development
machine vs live production server, for example). For
development, it's enough to just have a development
config.
Set it up for pg. Here's mine:
module.exports = {
development: {
client: 'pg',
connection: {
database: 'knex_demo_music',
user: 'vaz',
// password: 'password'
},
pool: {
min: 2,
max: 10
},
migrations: {
tableName: 'knex_migrations'
}
}
};
Now when you run the knex
command, it knows what
database to use.
knex help
knex init
- create a defaultknexfile.js
you can then edit with your own settingsknex migrate:make <give it a name>
- creates a new migration file using the current timestamp as the filename prefix in themigrations/
directory (it will create that directory if it doesn't exist).knex migrate:latest
- migrate up to the latest version. You often do this after pulling a project for the first time, during setup; and every time you pull down a new migration thereafter.knex migrate:rollback
- migrate down, or rollback, a single version. Note, this may cause data loss! You are likely dropping tables or columns after all.knex migrate:currentVersion
- show current version (which will be the same as the timestamp prefix of the filename describing it)
A migration file, created with knex migrate:make ...
, originally looks like this:
exports.up = function (knex, Promise) {
};
exports.down = function (knex, Promise) {
};
Notes:
-
up
is a function that returns a knex query describing the changes to make when migration to this version -
down
is a function that returns a knex query describing the changes to make when rolling back this version -
down
should undo the changes described inup
. Ifup
returns a "CREATE TABLE" type query, thendown
should return a "DROP TABLE" type query. -
You don't really need to worry about the Promise arg. In fact you can remove it from the args, if you want.
(
Promise
is actually a global object now anyway if you did want it. It's used to manually make new promises and certain other operations on promises. This is out of scope for now.) -
You need to return the query method chain, excluding the
.asCallback(...)
or.then(...)
. (knex will execute the query for you.)For example:
exports.up = function (knex) { return knex.schema.createTable('tablename', function (table) { // You can use `table` in here to describe changes. // Since this is `createTable`, it describes the // new table's columns: table.increments('id'); table.string('some_field').notNullable(); // etc... }); };
Note the return, and the lack of
asCallback
/then
.