Database change management
I thought I would get started with database change management and Tarantino, but after spending a day trying to figure it out, I eventually decided that it would go much faster to just roll my own.
What is database change management?
Most developers today are familiar with source version control. Even if some use it only for backups only, the main idea is for you to go back in history when needed or to try out different approaches in other branches. Using SVC (software version control) has become a standard in the industry, but very few apply the same idea on databases even though they also are a major part of most application development.
How not to version control databases
Check in the database to source control
It works as long as you are only one developer to check in the binary database file to your repository. You will however not be able to merge changes from different developers and you will have to do manual updates of your production environment when the time comes.Share database with team members on a central server
The most common way to handle database development is not to version control at all, but to setup the database on a shared central server. This fails when you need to revert to a previous version, but it also makes it harder to work as a team. When you make changes to the database you will affect your team members because they don't have your code to support the database changes. In worst case the whole team will halt production until you've checked in your code to support the database change.
Why should I care?
If you manage to version control your database development, you will not only be able to revert to old revisions, work in branches but also automatically get old databases up to date within your release process. This is much more efficient than manually merging the databases at every release, and also less error prone.
- You bring stability to your release cycle
- You reduce hold ups in the production line for your team
- You bring greater control to your database development process
How do I get started?
If you use NAnt or MsBuild as build script you should probably head over to Tarantino and download their software since it has excellent support for your environment. If not, you may keep on reading. Since I'm using psake at the moment, I decided to implement my database change management in PowerShell v2. The concept is very simple. You keep a directory in your source control where you store database creation/update scripts. The key component is that you never ever change any of these scripts after their first commit. When you need to change something in the database you create a new script. That way you will always be able to create a new database and bring it up to head revision at any time.
The naming convention here is very important. Next thing you need is a database where you have a table and a column that tells you what version this database is. As you've already guessed, this is exactly up to what revision the change scripts has been applied.
All you need now is one function that can look at your database, decide what version it is and apply those changes that has not yet been applied. Actually, I do it in three functions and they look like this.
Update-Database $connection_string $database_directory
Connects to the database and get the version number. Finds all change scripts in the database directory that are above the specific version and applies them to the database. Last it updates the database with the current version number.Build-Database $sql_server $database_name
Connects to the DBMS and creates an empty database. In this database it adds the necessary table and column to keep track of database versions.Drop-Database $sql_server $database_name
Removes the database from the DBMS.
Example usage
In my current project I want to run integration tests on a fresh database every time I check in code. That means I will have the following build process executed.
# Compile
Drop-Database "MAIA\SQLEXPRESS" "IntegrationTests"
Build-Database "MAIA\SQLEXPRESS" "IntegrationTests"
Update-Database "Data Source=MAIA\SQLEXPRESS;Initial Catalog=IntegrationTests;Integrated Security=True;" ".\Database"
# Run integration tests
When I check in code I will rebuild the database and execute my integration tests on it. That way I will not only test my code, but also verify that my change scripts are working. When it is time for release I will be able to run Update-Database on my production database, because I know that those build scripts has been thoroughly tested. Here is my PowerShell script if you're interested. I still see myself as a novice in PowerShell scripting, but it works and is quite minimalistic.
Here comes the complete database change management script.
Script for database change management
Author: Mikael Lundin
Website: http://mint.litemedia.se
E-mail: mikael.lundin@litemedia.se
Drop-Database
Will remove a database from the DBMS
Build-Database
Will create database in the DBMS and the mandatory Settings table
Update-Database
Will bring the database up to the most recent version
Will drop the database if it exists
Function Drop-Database ([string]$sqlserver, [string]$databasename) { try { [System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.SMO') | out-null $s = new-object ('Microsoft.SqlServer.Management.Smo.Server') $sqlServer $s.Refresh(); $s.KillDatabase($databasename); Write-Host "Killed database $databaseName" } catch { Write-Host "Tried to delete database $databaseName but failed, probably because it did not exist" } }
Will update the database to the most current version in the database directory
Function Update-Database ([string]$connectionstring, [string]$databasedirectory) {
$databaseVersion = Get-Database-Version $connection_string
# Get all source files that have higher database version number
$files = Get-ChildItem "$database_directory\*.sql" | Where { [int]::Parse($_.name.Substring(0, 4)) -gt $databaseVersion }
# For each of those files, run query on database
foreach ($file in $files)
{
$fileName = $file.name
Write-Host "Apply update script: $fileName"
# Get-Content returns a string array of all the lines. We join that into a single string
$fileContents = Get-Content "$file"
$sql = [string]::Join([Environment]::NewLine, $fileContents);
Execute-Sql-Query $connectionString $sql
# Get this version number
$version = [int]::Parse($fileName.Substring(0, 4))
# Update the settings database with current version number
Execute-Sql-Query $connectionString "UPDATE [Settings] SET [DatabaseVersion] = $version"
}
}
Will create a new database and add table to manage versions
Function Build-Database ([string]$sqlServer, [string]$databaseName) { # http://sqlblog.com/blogs/allen_white/archive/2008/04/28/create-database-from-powershell.aspx [System.Reflection.Assembly]::LoadWithPartialName('Microsoft.SqlServer.SMO') | out-null $s = new-object ('Microsoft.SqlServer.Management.Smo.Server') $sqlServer $dbname = $databaseName
# Instantiate the database object and create database
$db = new-object ('Microsoft.SqlServer.Management.Smo.Database') ($s, $dbname)
$db.Create()
# Create table and column for handling database version
$db.ExecuteNonQuery("CREATE TABLE [$databaseName].[dbo].[Settings] ([DatabaseVersion] int NOT NULL)");
$db.ExecuteNonQuery("INSERT INTO [$databaseName].[dbo].[Settings] ([DatabaseVersion]) VALUES (0)");
}
Helper functions
Function Get-Database-Version ([string]$connectionString) { [System.Data.SqlClient.SqlConnection]::ClearAllPools()
$sql = "SELECT TOP 1 [DatabaseVersion] FROM [Settings]"
## Connect to the data source and open it
$connection = New-Object System.Data.SqlClient.SqlConnection $connectionString
$connection.Open()
$command = New-Object System.Data.SqlClient.SqlCommand $sql,$connection
$version = $command.ExecuteScalar();
$connection.Close()
$version
}
Function Validate-Connection ([string]$connectionString) { [System.Data.SqlClient.SqlConnection]::ClearAllPools()
try {
## Connect to the data source and open it
$connection = New-Object System.Data.SqlClient.SqlConnection $connectionString
$connection.Open()
$connection.Close()
$TRUE
}
catch {
$FALSE
}
}
Function Execute-Sql-Query ([string]$connectionString, [string]$sql) { [System.Data.SqlClient.SqlConnection]::ClearAllPools()
## Connect to the data source and open it
$connection = New-Object System.Data.SqlClient.SqlConnection $connectionString
$connection.Open()
$command = New-Object System.Data.SqlClient.SqlCommand $sql,$connection
$result = $command.ExecuteNonQuery();
$connection.Close()
}
An update to this database script has been posted here.