|
Database Under Version Control Once you have scripted out the database, the individual parts will be version controlled in the subversion repository as a complete database set. In order to effect any change to a database’s set of objects, the developer or DBA will modify some part of the set of database’s objects using a traditional edit-and-commit development process.
Database Object Structure in Subversion (or Any Source-code Provider) \database_name\ \Functions \Post-deploy \Pre-deploy \Stored Procedures \Synonyms \Tables \Views The \Post-deploy and \Pre-deploy folders are for custom scripts that alter or add data to the database. Pre-deploy scripts are those changes that need to happen before the schema is changed. This might be to format data before its type is changed or to create a temporary table to store data before a table is changed or dropped. The Post-deploy folder is for all other data changes. Ninety-nine percent of all data changes will consist of scripts in the post-deploy folder.
Database Version CREATE TABLE [dbo].[_DBVersion]( [BuildVersion] [varchar](50) NOT NULL, [AppliedDate] [datetime] NOT NULL, [Application] [varchar](50) NOT NULL, [DBName] [varchar](50) NOT NULL ) ON [PRIMARY] GO SET ANSI_PADDING OFF GO ALTER TABLE [dbo].[_DBVersion] ADD CONSTRAINT [DF__DBVersion_AppliedDate] DEFAULT (getdate()) FOR [AppliedDate] GO
CI Update of Databases In our case, we are going to assume that an initial database already is set up for the website application. A DBA or developer may have created this, and it might be complete with data and schema for that moment in time. In any case, the level of the first database deployed in INT should be reflected in the scripted source that is checked in. In other words, a comparison of the database that is deployed and the checked-in version should be the same. Now, similar to the way we build websites continuously using tools like TeamCity, we also can set up database CI projects to call NAnt scripts that will compare an existing database to a checked-in version of database scripts. If the scripts detect differences, then the process will update the target database to a version that reflects what is checked in. We implement data changes by grouping them into two groups, those that need to happen before the schema update and those that happen after. The general step-by-step process for the CI database update would then be: · TeamCity detects any database source code changes. · Check out the database source for the database we are updating. · Remove the prior build’s pre- and post-build data scripts from the execution path. · Run pre-deploy scripts for pre-schema update data changes. · Run SQLCompare.exe to perform a comparison of the script source to the existing database, and sync the existing database. · Run post-deploy data scripts in alphabetical order. · On success, write the version number to the _DBVersion table.
Syncdb.build script An example of the NAnt parameters used to call Syncdb.build from TeamCity is: "-D:deploy.environment=INT" "-D:build.dir=%teamcity.build.checkoutDir%" "-D:sql.scriptsourcepath=%teamcity.build.checkoutDir%\dbname" "-D:runsql=true" "-D:sql.database=dbname" "-D:sql.password=keepItOutOfTheNantScript" "-D:sql.scriptfilepath=e:\syncDbfolder" "-D:build.version=%env.BUILD_NUMBER%" When TeamCity makes the call to the syncdb.build script, the target it calls is syncdbs. This target essentially executes the other necessary targets in the proper sequence. <target name="syncdbs" depends = "runpresql, movePrePostDeployFoldersOut, comparesyncdbs, movePrePostDeployFoldersBack, runpostsql" > <call target="updateversion" /> </target> In this scenario, updateversion is called after successful execution of runpresql, movePrePostDeployFoldersOut, comparesyncdbs, movePrePostDeployFoldersBack, and runpostsql in that order. The targets of importance in syncdb.build are runpresql, comparesyncdbs, runpostsql, and updateversion.
Runpresql <target name="runpresql" > <!-- run pre-deploys --> <foreach item="File" property="sqlscriptname" > <in> <items basedir="${sql.scriptsourcepath}"> <include name="**\Pre**.sql" /> <exclude name="**\Post-**" /> <exclude name="**\archived\**" /> </items> </in> <do failonerror="true"> <if test="${runsql=='true'}" > <echo message="Running Script ${sqlscriptname}" /> <echo message="sqlcmd -S ${sql.server} -d ${sql.database} -U ${sql.user} -P ${sql.password} -i ${sqlscriptname} -I" /> <exec failonerror="true" program="${sqlcmd.exe}" commandline='-b -S ${sql.server} -d ${sql.database} -U ${sql.user} -P ${sql.password} -i "${sqlscriptname}" -I' /> <property name="HasSQLScriptRun" value="true" /> </if> </do> </foreach> </target>
Comparesyncdbs <target name="comparesyncdbs"> <echo message="Start the update db process" /> <delete file="${sql.syncfilename}" if="${file::exists(sql.syncfilename)}" /> <delete file="${sql.reportfilename}" if="${file::exists(sql.reportfilename)}" /> <echo message="Calling sqlcompare.exe to sync db's " /> <choose> <when test="${runsql=='true'}" > <echo message="comparing and syncing schema" /> <exec program="${sqlcompare.path}\sqlcompare.exe" failonerror="false" resultproperty="returncode" verbose="true" > <arg value="/scripts1:${sql.scriptsourcepath}" /> <arg value="/database2:${sql.database}" /> <arg value="/server2:${sql.server}" /> <arg value="/username2:${sql.user}" /> <arg value="/password2:${sql.password}" /> <arg value="/sync" /> <arg value="/ScriptFile:${sql.syncfilename}" /> <arg value="/report:${sql.reportfilename}" /> <arg value="/ReportType:Simple" /> <arg value="/options:ForceColumnOrder,IgnoreUsers,IgnorePermissions,IgnoreWhiteSpace,IgnoreUserProperties,IgnoreCollations" /> <arg value="/exclude:User" /> <arg value="/exclude:Role" /> <arg value="/exclude:Schema" /> <arg value="/exclude:Synonym" /> </exec> <echo message="error return code = ${returncode}" /> <fail if="${returncode !='63' and returncode !='0'}">Sync failed </fail> </when> <otherwise> (Do not sync) </otherwise> </choose> <if test="${file::exists(sql.syncfilename)}" > <property name="HasSQLScriptRun" value="true" /> </if> </target>
Runpostsql <target name="runpostsql" > <!-- run post-deploys --> <foreach item="File" property="sqlscriptname" > <in> <items basedir="${sql.scriptsourcepath}"> <include name="**\Post**.sql" /> <exclude name="**\Pre-**" /> </items> </in> <do failonerror="true"> <if test="${runsql=='true'}" > <echo message="Running Script ${sqlscriptname}" /> <echo message="sqlcmd -S ${sql.server} -d ${sql.database} -U ${sql.user} -P ${sql.password} -i ${sqlscriptname} -I" /> <exec failonerror="true" program="${sqlcmd.exe}" commandline='-b -S ${sql.server} -d ${sql.database} -U ${sql.user} -P ${sql.password} -i "${sqlscriptname}" -I' /> <property name="HasSQLScriptRun" value="true" /> </if> </do> </foreach> </target>
Updateversion <target name="updateversion" > <if test="${runsql=='true' and HasSQLScriptRun=='true' }" > <sql connstring="Provider=SQLOLEDB;Data Source=${sql.server}; Initial Catalog=${sql.database};User Id=${sql.user}; Password=${sql.password};" transaction="true" delimiter=";" delimstyle="Normal" print="true" output="${sql.scriptfilepath}\results.txt" > insert into _dbversion values ('${build.version}', getdate(), 'CIUPDATE','${sql.database}'); </sql> </if> </target>
Final Word Complete NAnt Syncdb.build Script <?xml version="1.0"?> <!-- Generated by NAntBuilder v2.0--> <!-- ================================= Project: syncdb.build Author : Lance Lyons Onlife Health Solutions This script will create the blank model database on the blank model database server as well as create the migration script from the prior build to the current build. Required inputs: sql.database sql.user sql.password sql.scriptsourcepath sql.scriptfilepathname sql.reportfilepathname ================================= --> <project name="syncdb" default="donothing"> <target name="donothing"> </target> <!-- ================================= --> <!-- Add your includes in this area --> <include buildfile="publishing.include.build" /> <!-- ================================= --> <loadtasks assembly="C:\nantcontrib-0.85\bin\NAnt.Contrib.Tasks.dll" /> <property name="sqlcmd.exe" value="C:\Program Files\Microsoft SQL Server\100\Tools\Binn\sqlcmd.exe" /> <property name="sql.database" value="" readonly="false" /> <property name="sql.user" value="app_AutomatedBuild" readonly="false" /> <property name="sql.password" value="" readonly="false" /> <property name="sql.server" value="int-sql01" readonly="false" /> <if test="${deploy.environment == 'DEV'}" > <property name="sql.server" value="dev-sql02,54552" readonly="false" /> </if> <if test="${deploy.environment == 'DEV2'}" > <property name="sql.server" value="dev-sql02,54552" readonly="false" /> </if> <property name="HasSQLScriptRun" value="false" /> <property name="sqlcompare.path" value="C:\Program Files\Red Gate\SQL Compare 8" readonly="false" /> <property name="sql.scriptsourcepath" value="" readonly="false" /> <property name="sql.scriptfilepath" value="" readonly="false" /> <property name="sql.reportfilepath" value="" readonly="false" /> <property name="build.version" value="" readonly="false" /> <property name="sql.scriptsourcepath.postdeploy" value="${sql.scriptsourcepath}\Post-Deploy" readonly="false" /> <property name="sql.scriptsourcepath.predeploy" value="${sql.scriptsourcepath}\Pre-Deploy" readonly="false" /> <property name="sql.syncfilename" value="${sql.scriptfilepath}\${sql.database}\sync${sql.database}${build.version}.sql" /> <property name="sql.reportfilename" value="${sql.scriptfilepath}\${sql.database}\report${sql.database}${build.version}.html" /> <property name="sql.tempdir.postdeploy" value="e:\tempdir\${sql.database}\Post-Deploy" readonly="false" /> <property name="sql.tempdir.predeploy" value="e:\tempdir\${sql.database}\Pre-Deploy" readonly="false" /> <target name="updateversion" > <if test="${runsql=='true' and HasSQLScriptRun=='true' }" > <sql connstring="Provider=SQLOLEDB;Data Source=${sql.server}; Initial Catalog=${sql.database};User Id=${sql.user}; Password=${sql.password};" transaction="true" delimiter=";" delimstyle="Normal" print="true" output="${sql.scriptfilepath}\results.txt" > insert into _dbversion values ('${build.version}',getdate(),'CIUPDATE','${sql.database}'); </sql> </if> </target> <target name="movePrePostDeployFoldersOut"> <echo message="deleting temp directory : ${sql.tempdir.postdeploy}" /> <echo message="deleting temp directory : ${sql.tempdir.predeploy}" /> <delete> <fileset basedir="${sql.tempdir.postdeploy}"> <include name="**"/> </fileset> </delete> <delete> <fileset basedir="${sql.tempdir.predeploy}"> <include name="**"/> </fileset> </delete> <echo message="deleting temp directory : ${sql.tempdir.predeploy}" /> <move todir="e:\tempdir\${sql.database}\Post-Deploy"> <fileset basedir="${sql.scriptsourcepath.postdeploy}"> <include name="**" /> <exclude name="archived/**" /> </fileset> </move> <move todir="e:\tempdir\${sql.database}\Pre-Deploy"> <fileset basedir="${sql.scriptsourcepath.predeploy}"> <include name="**" /> <exclude name="archived/**" /> </fileset> </move> <delete> <fileset basedir="${sql.scriptsourcepath.postdeploy}"> <include name="**"/> </fileset> </delete> <delete> <fileset basedir="${sql.scriptsourcepath.predeploy}"> <include name="**"/> </fileset> </delete> </target> <target name="movePrePostDeployFoldersBack"> <move todir="${sql.scriptsourcepath.postdeploy}"> <fileset basedir="e:\tempdir\${sql.database}\Post-Deploy"> <include name="**" /> <exclude name="archived/**" /> </fileset> </move> <move todir="${sql.scriptsourcepath.predeploy}"> <fileset basedir="e:\tempdir\${sql.database}\Pre-Deploy"> <include name="**" /> <exclude name="archived/**" /> </fileset> </move> <delete dir="e:\tempdir\${sql.database}\Post-Deploy" /> <delete dir="e:\tempdir\${sql.database}\Pre-Deploy" /> </target> <target name="comparesyncdbs"> <echo message="Start the update db process" /> <delete file="${sql.syncfilename}" if="${file::exists(sql.syncfilename)}" /> <delete file="${sql.reportfilename}" if="${file::exists(sql.reportfilename)}" /> <echo message="Calling sqlcompare.exe to sync db's " /> <choose> <when test="${runsql=='true'}" > <echo message="comparing and syncing schema" /> <exec program="${sqlcompare.path}\sqlcompare.exe" failonerror="false" resultproperty="returncode" verbose="true" > <arg value="/scripts1:${sql.scriptsourcepath}" /> <arg value="/database2:${sql.database}" /> <arg value="/server2:${sql.server}" /> <arg value="/username2:${sql.user}" /> <arg value="/password2:${sql.password}" /> <arg value="/sync" /> <arg value="/ScriptFile:${sql.syncfilename}" /> <arg value="/report:${sql.reportfilename}" /> <arg value="/ReportType:Simple" /> <arg value="/options:ForceColumnOrder,IgnoreUsers,IgnorePermissions,IgnoreWhiteSpace,IgnoreUserProperties,IgnoreCollations" /> <arg value="/exclude:User" /> <arg value="/exclude:Role" /> <arg value="/exclude:Schema" /> <arg value="/exclude:Synonym" /> </exec> <echo message="error return code = ${returncode}" /> <fail if="${returncode !='63' and returncode !='0'}">Sync failed </fail> </when> <otherwise> <echo message="comparing schema no sync" /> <exec program="${sqlcompare.path}\sqlcompare.exe" failonerror="false" resultproperty="returncode" verbose="true" >
<arg value="/scripts1:${sql.scriptsourcepath}" /> <arg value="/database2:${sql.database}" /> <arg value="/server2:${sql.server}" /> <arg value="/username2:${sql.user}" /> <arg value="/password2:${sql.password}" /> <!-- <arg value="/sync" /> --> <arg value="/ScriptFile:${sql.syncfilename}" /> <arg value="/report:${sql.reportfilename}" /> <arg value="/ReportType:Simple" /> <arg value="/options:ForceColumnOrder,IgnoreUsers,IgnorePermissions,IgnoreWhiteSpace,IgnoreUserProperties,IgnoreCollations" /> <arg value="/exclude:User" /> <arg value="/exclude:Role" /> <arg value="/exclude:Schema" /> <arg value="/exclude:Synonym" /> </exec> <echo message="error return code = ${returncode}" /> <fail if="${returncode !='63' and returncode !='0'}">Compare failed </fail> </otherwise> </choose> <if test="${file::exists(sql.syncfilename)}" > <property name="HasSQLScriptRun" value="true" /> </if> </target> <target name="runpresql" > <!-- run pre-deploys --> <foreach item="File" property="sqlscriptname" > <in> <items basedir="${sql.scriptsourcepath}"> <include name="**\Pre**.sql" /> <exclude name="**\Post-**" /> <exclude name="**\archived\**" /> </items> </in> <do failonerror="true"> <if test="${runsql=='true'}" > <echo message="Running Script ${sqlscriptname}" /> <echo message="sqlcmd -S ${sql.server} -d ${sql.database} -U ${sql.user} -P ${sql.password} -i ${sqlscriptname} -I" /> <exec failonerror="true" program="${sqlcmd.exe}" commandline='-b -S ${sql.server} -d ${sql.database} -U ${sql.user} -P ${sql.password} -i "${sqlscriptname}" -I' /> <property name="HasSQLScriptRun" value="true" /> </if> </do> </foreach> </target> <target name="runpostsql" > <!-- run post-deploys --> <foreach item="File" property="sqlscriptname" > <in> <items basedir="${sql.scriptsourcepath}"> <include name="**\Post**.sql" /> <exclude name="**\Pre-**" /> </items> </in> <do failonerror="true"> <if test="${runsql=='true'}" > <echo message="Running Script ${sqlscriptname}" /> <echo message="sqlcmd -S ${sql.server} -d ${sql.database} -U ${sql.user} -P ${sql.password} -i ${sqlscriptname} -I" /> <exec failonerror="true" program="${sqlcmd.exe}" commandline='-b -S ${sql.server} -d ${sql.database} -U ${sql.user} -P ${sql.password} -i "${sqlscriptname}" -I' /> <property name="HasSQLScriptRun" value="true" /> </if> </do> </foreach> </target> <target name="syncdbs" depends="runpresql,movePrePostDeployFoldersOut,comparesyncdbs,movePrePostDeployFoldersBack,runpostsql" > <!-- This will only set the sequence of target execution through the depends --> <call target="updateversion" /> </target> </project>
About the Author Lance has spent the past ten years working primarily as a software configuration management expert. He has spent time at several companies improving, automating, and streamlining processes from software development through software release. Lance currently works for Onlife Health of Brentwood, TN as a Senior Configuration Manager. Write him at Lance_Lyons@onlifehealth.com.
Set as favorite
Bookmark
Email this
Hits: 1254 Trackback(0)Comments (0)
|




Developers must have good feedback to ensure productivity. Most shops have a CI build process that allows the developer to quickly know if a build failed, and some shops have an automatic deploy of the CI build to provide website feedback. However, most shops don’t have an automated update for databases in the CI environment. Without this database automation, the website CI is incomplete or requires a database administrator (DBA) to constantly update the database as developers hand over new changes. In this article, I write about an approach to automating the update of databases in a CI environment that we currently use at Onlife Health Inc., a Brentwood, TN based subsidiary of Blue Cross Blue Shield of Tennessee who specializes in personal health coaching.
