Co-locating your app's database structure in your app
How to make changes to your database schema from CommandBox
An introduction to qb's SchemaBuilder
Using cfmigrations in testing
Migrating things other than database schema
🏡 Utah
🏬 Ortus
📦 Quick, qb, ColdBox Elixir
👨👩👦👦 1 wife, 3 kids
component {
function up() {
// logic to migrate up goes here
}
function down() {
// logic to roll back goes here
}
}
component {
function up() {
queryExecute( "
CREATE TABLE `users` (
`id` INTEGER UNSIGNED NOT NULL AUTO_INCREMENT,
`username` NVARCHAR(255) NOT NULL,
`first_name` NVARCHAR(255) NOT NULL,
`last_name` NVARCHAR(255) NOT NULL,
`password` NVARCHAR(100) NOT NULL,
`country_id` INTEGER UNSIGNED NOT NULL,
`created_date` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`modified_date` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT `pk_users_id` PRIMARY KEY (`id`),
CONSTRAINT `fk_users_country_id` FOREIGN KEY (`country_id`)
REFERENCES `countries` (`id`) ON UPDATE NO ACTION ON DELETE CASCADE
)
" );
}
function down() {
queryExecute( "
DROP TABLE `users`
" );
}
}
var migrationService = getWireBox().getInstance( "MigrationService@cfmigrations" );
migrationService.setMigrationsDirectory( "/resources/database/migrations" );
migrationService.setDefaultGrammar( "PostgresGrammar" );
migrationService.setDatasource( getUtil().getSystemSetting( "DB_DATABASE" ) );
migrationService.setTable( "cfmigrations" );
migrationService.install();
migrationService.runAllMigrations( "up" );
install commandbox-migrations
migrate init
migrate install
migrate up
2017_09_03_043150_create_users_table.cfc
{
"cfmigrations": {
"connectionInfo": {
"class": "org.gjt.mm.mysql.Driver",
"connectionString": "jdbc:mysql://localhost:3306/my_db",
"username": "root",
"password": "root"
},
"defaultGrammar": "MySQLGrammar",
"schema": "my_db"
}
}
In box.json
{
"cfmigrations": {
"connectionInfo": {
"class": "${DB_CLASS}",
"connectionString": "${DB_CONNECTIONSTRING}",
"username": "${DB_USER}",
"password": "${DB_PASSWORD}"
},
"defaultGrammar": "AutoDiscover",
"schema": "${DB_SCHEMA}"
}
}
Perfect for local development
# .env
DB_CLASS=org.gjt.mm.mysql.Driver
DB_CONNECTIONSTRING=jdbc:mysql://localhost:3306/my_db
DB_USER=root
DB_PASSWORD=root
DB_SCHEMA=my_db
But create a .env.example file with blank values that isn't ignored
# .env.example
DB_CLASS=
DB_CONNECTIONSTRING=
DB_USER=
DB_PASSWORD=
DB_SCHEMA=
This signals to other team members which environment variable are needed
component {
function up( schema, query ) {
// An pre-configured instance
// of `SchemaBuilder` and `QueryBuilder`
// are passed to each migration function
}
function down( schema, query ) {
// feel free to ignore them
// if they aren't your thing
}
}
schema.create( "users", function( table ) {
table.increments( "id" );
table.string( "email" );
table.string( "password" );
table.timestamp( "created_date" );
table.timestamp( "modified_date" );
table.timestamp( "last_logged_in" ).nullable();
} );
create
table.string( "email" );
table.integer( "age", 3 );
columns
All Columns
bigIncrements | bigInteger | bit | boolean |
char | date | datetime | decimal |
enum | float | increments | integer |
json | longText | mediumIncrements | mediumInteger |
mediumText | morphs | nullableMorphs | raw |
smallIncrements | smallInteger | string | text |
time | timestamp | tinyIncrements | tinyInteger |
unsignedBigInteger | unsignedInteger | unsignedMediumInteger | unsignedSmallInteger |
unsignedTinyInteger | uuid |
table.integer( "age" ).nullable();
table.boolean( "is_active" ).default( false );
column modifiers
table.string( "id" ).primaryKey();
table.string( "username" ).unique();
table.string( "country_id" )
.references( "id" )
.on( "countries" );
column indexes
table.primaryKey( [ "post_id", "tag_id" ] );
table.index( "created_date" );
table indexes
schema.alter( "users", function( table ) {
table.addColumn( table.boolean( "is_active" ) );
table.modifyColumn( "age", table.integer( "age", 3 ) );
table.renameColumn( "name", table.string( "username" ) );
table.dropColumn( "last_logged_in" );
table.addConstraint( table.unique( "username" ) );
table.dropConstraint( "full_constraint_name" );
table.dropConstraint( table.foreignKey( "country_id" ) );
table.renameConstraint(
"unq_users_first_name_last_name",
"unq_users_full_name"
);
} );
alter
schema.drop( "user_logins" );
schema.dropIfExists( "user_profiles" );
drop
component {
property name="indexBuilder" inject="provider:IndexBuilder@cbElasticsearch";
property name="aliasBuilder" inject="provider:AliasBuilder@cbElasticsearch";
property name="client" inject="Client@cbElasticsearch";
variables.aliasName = "sailings";
variables.oldIndexName = "sailings_2019_08_01_141730";
variables.newIndexName = "sailings_2019_09_04_091730";
function up() {
// create new index with new settings
variables.indexBuilder.new(
name = variables.newIndexName,
properties = getMappingConfig(),
settings = {
"index.mapping.total_fields.limit": "10000",
"index.max_result_window": "1000000",
"number_of_shards": 8,
"number_of_replicas": 2
}
).save();
// reindex data from old index to new index
variables.client.reindex( variables.oldIndexName, variables.newIndexName );
// swap the alias from the old index to the new index
variables.client.applyAliases( [
aliasBuilder.add( variables.newIndexName, variables.aliasName ),
aliasBuilder.remove( variables.oldIndexName, variables.aliasName )
] );
}
function down() {
// swap the alias from the new index back to the old index
variables.client.applyAliases( [
aliasBuilder.add( variables.oldIndexName, variables.aliasName ),
aliasBuilder.remove( variables.newIndexName, variables.aliasName )
] );
// delete the new index
variables.client.deleteIndex( variables.newIndexName );
}
function getMappingConfig() {
return { /* ... */ };
}
}
component extends="coldbox.system.testing.BaseTestCase" {
property name="migrationService" inject="MigrationService@cfmigrations";
this.loadColdBox = true;
this.unloadColdBox = false;
/**
* Run Before all tests
*/
function beforeAll() {
super.beforeAll();
// Wire up this object
application.wirebox.autowire( this );
// Check if migrations ran before all tests
if ( ! request.keyExists( "migrationsRan" ) ) {
migrationService.setMigrationsDirectory( "/root/resources/database/migrations" );
migrationService.setDefaultGrammar( "MySQLGrammar" );
migrationService.setDatasource( "testing" );
migrationService.runAllMigrations( "down" );
migrationService.runAllMigrations( "up" );
request.migrationsRan = true;
}
}
/**
* This function is tagged as an around each handler. All the integration tests we build
* will be automatically rolledbacked
*
* @aroundEach
*/
function wrapInTransaction( spec ) {
transaction action="begin" {
try {
arguments.spec.body();
} catch ( any e ) {
rethrow;
} finally {
transaction action="rollback";
}
}
}
}
// box.json
{
"githooks": {
"postCheckout": "migrate up"
}
}