I\'m still not sure how to do my migrations with knex. Here is what I have so far. It works on up
, but down
gives me FK constraint error even though fo
inTable()
should be placed after references()
:
inTablecolumn.inTable(table)
Sets the
table
where the foreign key column is located after callingcolumn.references
.
Documentation.
Figured out that it wasn't working because of connection pooling. It would use a different connection to run each migration task which caused foreign key checks not to be set properly. setting
pool:{
max:1
}
in the migration config file fixed this.
I thought I'd update this, since there have been some additions to Javascript that make this quite a bit easier. knex
still requires that you return a Promise
but inside that Promise
you can do a lot of things to clean up the code related to the creation/modification of tables. My preference is to use a combination of async
/await
and Promise.all
.
exports.up = function(knex, Promise) {
return new Promise(async (resolve, reject) => {
try {
await Promise.all([
knex.schema.createTable('videos', table => {
table.increments('id');
table.string('title');
table.string('director');
table.json('meta');
}),
knex.schema.createTable('books', table => {
table.increments('id');
table.string('title');
table.string('author');
table.json('meta');
})
]);
console.log('Tables created successfully');
resolve();
} catch(error) {
reject(error);
}
})
}
If you prefer to create each table individually, then I'd just use async
/await
.
exports.up = function(knex, Promise) {
return new Promise(async (resolve, reject) => {
try {
await knex.schema.createTable('videos', table => {
table.increments('id');
table.string('title');
table.string('director');
table.json('meta');
});
console.log('videos table created successfully!');
await knex.schema.createTable('books', table => {
table.increments('id');
table.string('title');
table.string('author');
table.json('meta');
});
console.log('books table created successfully!');
resolve();
} catch(error){
reject(error);
}
})
}
This keeps things a lot cleaner, not requiring you to daisy chain a bunch of then
s or wrap them in surrounding functions. You just await
for each table creation to resolve and then resolve their encapsulating Promise
! Yay for async
/await
!
You can follow this pattern for the dropping of tables in your down
migration, too. Simply replace the knex.schema.createTable
statements with knex.schema.dropTable
statements.
jedd.ahyoung is correct. You don't need to limit your connection pool to 1. You just need to chain your promises so they are not run in parallel.
For example:
exports.up = function(knex, Promise) {
return removeForeignKeyChecks()
.then(createMemberTable)
.then(createAddressTable)
.then(addForeignKeyChecks);
function removeForeignKeyChecks() {
return knex.raw('SET foreign_key_checks = 0;');
}
function addForeignKeyChecks() {
return knex.raw('SET foreign_key_checks = 1;');
}
function createMemberTable() {
return knex.schema.createTable('Member', function (table) {
table.bigIncrements('id').primary().unsigned();
table.string('email',50);
table.string('password');
/* CREATE FKS */
table.bigInteger('ReferralId').unsigned().index();
table.bigInteger('AddressId').unsigned().index().inTable('Address').references('id');
});
}
function createAddressTable() {
return knex.schema.createTable('Address', function (table) {
table.bigIncrements('id').primary().unsigned();
table.index(['city','state','zip']);
table.string('city',50).notNullable();
table.string('state',2).notNullable();
table.integer('zip',5).unsigned().notNullable();
});
}
};
Also I may be missing something but it looks like you won't need to remove and then reinstate the foreign key checks if you create the address table before the member table.
Here's how the final code would look:
exports.up = function(knex, Promise) {
return createAddressTable()
.then(createMemberTable);
function createMemberTable() {
return knex.schema.createTable('Member', function (table) {
table.bigIncrements('id').primary().unsigned();
table.string('email',50);
table.string('password');
/* CREATE FKS */
table.bigInteger('ReferralId').unsigned().index();
table.bigInteger('AddressId').unsigned().index().inTable('Address').references('id');
});
}
function createAddressTable() {
return knex.schema.createTable('Address', function (table) {
table.bigIncrements('id').primary().unsigned();
table.index(['city','state','zip']);
table.string('city',50).notNullable();
table.string('state',2).notNullable();
table.integer('zip',5).unsigned().notNullable();
});
}
};
I solved this problem by using a transaction
transation.js
module.exports = function transaction(fn) {
return function _transaction(knex, Promise) {
return knex.transaction(function(trx) {
return trx
.raw('SET foreign_key_checks = 0;')
.then(function() {
return fn(trx, Promise);
})
.finally(function() {
return trx.raw('SET foreign_key_checks = 1;');
});
});
};
}
Migration file
var transaction = require('../transaction');
function up(trx, Promise) {
return trx.schema
.createTable('contract', function(table) {
table.boolean('active').notNullable();
table.integer('defaultPriority').unsigned().references('priority.id');
table.integer('defaultIssueStatus').unsigned().references('issueStatus.id');
table.integer('owner').notNullable().unsigned().references('user.id');
})
.createTable('user', function (table) {
table.increments('id').primary();
table.datetime('createdAt');
table.datetime('updatedAt');
table.string('phoneNumber').notNullable().unique();
table.string('password').notNullable();
table.string('name').notNullable().unique();
table.string('email');
table.string('status');
table.string('roles').defaultTo('user');
table.integer('contract').unsigned().references('contract.id');
});
}
function down(trx, Promise) {
return trx.schema
.dropTable('contract')
.dropTable('user');
}
exports.up = transaction(up);
exports.down = transaction(down);
Subjectively as the most clean way to do it I would suggest including in your migration file something like:
exports.up = function (knex) {
return Promise.all([
knex.schema.createTable('users', function (table) {
table.increments('id')
table.string('username').notNullable()
table.string('password').notNullable()
table.string('service').notNullable()
table.text('cookies')
table.enu('status', ['active', 'disabled', 'need_login', 'failed']).defaultTo('need_login').notNullable()
table.datetime('last_checked')
table.timestamps()
}),
knex.schema.createTable('products', function (table) {
table.increments()
table.integer('user_id').unsigned().notNullable()
table.string('title')
table.decimal('price', 8, 2)
table.text('notes')
table.enu('status', ['to_publish', 'published', 'hidden', 'not_found']).notNullable()
table.timestamps()
table.foreign('user_id').references('id').inTable('users')
}),
knex.schema.createTable('messages', function (table) {
table.increments()
table.integer('user_id').unsigned().notNullable()
table.integer('product_id').unsigned().notNullable()
table.boolean('incoming')
table.boolean('unread')
table.text('text')
table.timestamps()
table.foreign('user_id').references('id').inTable('users')
table.foreign('product_id').references('id').inTable('products')
})
])
}
exports.down = function (knex) {
return Promise.all([
knex.schema.dropTable('messages'),
knex.schema.dropTable('products'),
knex.schema.dropTable('users')
])
}