Click here to Skip to main content
15,897,371 members
Articles / Programming Languages / XML

DBKeeperNet - Keeps Your DB Schema Up-to-date

Rate me:
Please Sign up or sign in to vote.
4.88/5 (14 votes)
26 Aug 2014BSD4 min read 50.7K   575   89  
An article describing a simple .NET library which simply keeps your database schema up-to-date.
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
using System.Xml.Serialization;
using System.Xml.Schema;
using System.Xml;
using System.Reflection;
using DbKeeperNet.Engine.Resources;
using System.Globalization;

namespace DbKeeperNet.Engine
{
    /// <summary>
    /// Main class responsible for invocation of updates in XML
    /// definition file.
    /// </summary>
    /// <remarks>
    /// <example>
    /// Prepare App.Config file. Following example is for execution of one embeded resource script followed
    /// by a disk fil on MSSQL server.
    /// <code>
    /// <![CDATA[
    /// <?xml version="1.0" encoding="utf-8" ?>
    /// <configuration>
    /// <configSections>
    /// <section name="dbkeeper.net" 
    ///     type="DbKeeperNet.Engine.DbKeeperNetConfigurationSection,DbKeeperNet.Engine"/>
    ///   </configSections>
    ///   <dbkeeper.net loggingService="fx">
    ///     <updateScripts>
    ///       <add provider="asm" location="DbKeeperNet.SimpleDemo.DatabaseSetup.xml,DbKeeperNet.SimpleDemo" />
    ///       <add provider="disk" location="c:\diskpath\DatabaseSetup.xml" />
    ///     </updateScripts>
    ///     <databaseServiceMappings>
    ///       <add connectString="default" databaseService="MsSql" />
    ///     </databaseServiceMappings>
    ///   </dbkeeper.net>
    ///   <connectionStrings>
    ///     <add name="default" 
    ///             connectionString="Data Source=.\SQLEXPRESS;
    ///             AttachDbFilename='|DataDirectory|\DbKeeperNetSimpleDemo.mdf';
    ///             Integrated Security=True;Connect Timeout=30;User Instance=True" 
    ///             providerName="System.Data.SqlClient"/>
    ///   </connectionStrings>
    /// </configuration>
    /// ]]>
    /// </code>
    /// </example>
    /// 
    /// <example>
    /// Add the code which will run the scripts from App.Config (see <see cref="UpdateContext.InitializeDatabaseService(string)"/>):
    /// <code>
    /// using (UpdateContext context = new UpdateContext())
    /// {
    ///     context.LoadExtensions();
    ///     context.InitializeDatabaseService("default");
    ///  
    ///     Updater updater = new Updater(context);
    ///     updater.ExecuteXmlFromConfig();
    /// }
    /// </code>
    /// 
    /// <example>
    /// Alternatively connection in the context can be initialized
    /// manually (see <see cref="UpdateContext.InitializeDatabaseService(DbKeeperNet.Engine.IDatabaseService,bool)"/>):
    /// <code>
    /// using (IUpdateContext context = new UpdateContext())
    /// using (IDatabaseService databaseService = new MsSqlDatabaseService(CreateDatabaseConnection()))
    /// {
    ///    context.LoadExtensions();
    ///    context.InitializeDatabaseService(databaseService, false);
    ///
    ///    using (Updater updater = new Updater(context))
    ///    {
    ///       updater.ExecuteXmlFromConfig();
    ///    }
    /// }
    /// </code>
    /// </example>
    /// 
    /// </example>
    /// 
    /// <example>
    /// Prepare an XML script which can be executed:
    /// <code>
    /// <![CDATA[
    /// <?xml version="1.0" encoding="utf-8"?>
    /// <upd:Updates xmlns:upd="http://code.google.com/p/dbkeepernet/Updates-1.0.xsd" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" AssemblyName="DbKeeperNet.SimpleDemo" xsi:schemaLocation="http://code.google.com/p/dbkeepernet/Updates-1.0.xsd ../DbKeeperNet.Engine/Resources/Updates-1.0.xsd">
    /// 	<!-- Default way how to check whether to execute update step or not -->
    /// 	<DefaultPreconditions>
    /// 		<!-- We will use step information saving strategy -->
    /// 		<Precondition FriendlyName="Update step executed" Precondition="StepNotExecuted"/>
    /// 	</DefaultPreconditions>
    /// 	<Update Version="1.00">
    /// 		<UpdateStep xsi:type="upd:UpdateDbStepType" FriendlyName="Create table DbKeeperNet_SimpleDemo" Id="1">
    /// 			<!-- DbType attribute may be ommited - it will result in default value all
    ///            which means all database types -->
    /// 			<AlternativeStatement DbType="MsSql"><![CDATA[
    ///           CREATE TABLE DbKeeperNet_SimpleDemo
    ///           (
    ///           id int identity(1, 1) not null,
    ///           name nvarchar(32),
    ///           constraint PK_DbKeeperNet_SimpleDemo primary key clustered (id)
    ///           )
    ///         ]]&gt;</AlternativeStatement>
    /// 			<AlternativeStatement DbType="MySql"><![CDATA[
    ///           CREATE TABLE DbKeeperNet_SimpleDemo
    ///           (
    ///           id int not null auto_increment,
    ///           name nvarchar(32),
    ///           constraint PK_DbKeeperNet_SimpleDemo primary key (id)
    ///           )
    ///         ]]&gt;</AlternativeStatement>
    /// 			<AlternativeStatement DbType="PgSql"><![CDATA[
    ///           CREATE TABLE DbKeeperNet_SimpleDemo
    ///           (
    ///           id serial not null,
    ///           name varchar(32),
    ///           constraint PK_DbKeeperNet_SimpleDemo primary key (id)
    ///           )
    ///         ]]&gt;</AlternativeStatement>
    ///         <AlternativeStatement DbType="SQLite">
    ///         <![CDATA[
    ///         CREATE TABLE DbKeeperNet_SimpleDemo
    ///           (
    ///           id integer not null,
    ///           name text,
    ///           constraint PK_DbKeeperNet_SimpleDemo primary key (id)
    ///           )
    ///         ]]&gt;</AlternativeStatement>
    ///       <AlternativeStatement DbType="Oracle">
    ///         <![CDATA[
    ///         CREATE TABLE "DBKEEPERNET_SIMPLEDEMO"
    ///         (
    ///         "ID"         NUMBER(10,0) NOT NULL,
    ///         "NAME"       VARCHAR2(32),
    ///         CONSTRAINT "PK_DBKEEPERNET_SIMPLEDEMO" PRIMARY KEY ("ID")
    ///         )
    ///         ]]&gt;
    ///       </AlternativeStatement>
    /// 		</UpdateStep>
    ///     <UpdateStep xsi:type="upd:UpdateDbStepType" Id="2" FriendlyName="Create sequence generator DBKEEPERNET_SIMPLEDEMO_SEQ">
    ///       <AlternativeStatement DbType="Oracle">
    ///         <![CDATA[CREATE sequence "DBKEEPERNET_SIMPLEDEMO_SEQ" ]]&gt;
    ///       </AlternativeStatement>
    ///     </UpdateStep>
    ///     <UpdateStep xsi:type="upd:UpdateDbStepType" Id="3" FriendlyName="Create identity trigger BI_DBKEEPERNET_SIMPLEDEMO">
    ///       <AlternativeStatement DbType="Oracle">
    ///         <![CDATA[CREATE trigger "BI_DBKEEPERNET_SIMPLEDEMO" 
    /// 		      before insert on "DBKEEPERNET_SIMPLEDEMO"
    /// 		      for each row 
    /// 			    begin  
    /// 				    select "DBKEEPERNET_SIMPLEDEMO_SEQ".nextval into :NEW.ID from dual;
    /// 			    end;]]&gt;
    ///       </AlternativeStatement>
    ///     </UpdateStep>
    /// 		<UpdateStep xsi:type="upd:UpdateDbStepType" FriendlyName="Fill table DbKeeperNet_SimpleDemo" Id="4">
    /// 			<AlternativeStatement><![CDATA[
    ///           insert into DbKeeperNet_SimpleDemo(name) values('First value')
    ///         ]]&gt;</AlternativeStatement>
    /// 		</UpdateStep>
    ///     <UpdateStep xsi:type="upd:UpdateDbStepType" FriendlyName="Fill table DbKeeperNet_SimpleDemo" Id="5">
    ///       <AlternativeStatement>
    ///         <![CDATA[
    ///           insert into DbKeeperNet_SimpleDemo(name) values('Second value')
    ///         ]]&gt;
    ///       </AlternativeStatement>
    ///     </UpdateStep>
    /// 	</Update>
    /// </upd:Updates>
    /// ]]>
    /// </code>
    /// </example>
    /// </remarks>
    public class Updater : IDisposable
    {
        IUpdateContext _context;
        readonly ISqlScriptSplitter _scriptSplitter;

        /// <summary>
        /// Class construction. Requires initialized update context.
        /// </summary>
        /// <param name="context">Update context instance with all required information prepared</param>
        public Updater(IUpdateContext context) : this(context,  new SqlScriptSplitter())
        {
        }

        /// <summary>
        /// Class construction. Requires initialized update context.
        /// </summary>
        /// <param name="context">Update context instance with all required information prepared</param>
        /// <param name="scriptSplitter">Script splitter dependency injection (intended for unit testing)</param>
        public Updater(IUpdateContext context, ISqlScriptSplitter scriptSplitter)
        {
            if (context == null)
                throw new ArgumentNullException(@"context");
            if (scriptSplitter == null)
                throw new ArgumentNullException(@"scriptSplitter");

            _context = context;
            _scriptSplitter = scriptSplitter;
        }

        #region Private methods

        private void ExecuteDatabaseSetupXml()
        {
            _context.Logger.TraceInformation(UpdaterMessages.DatabaseSetupCheck);

            Stream databaseSetup = _context.DatabaseService.DatabaseSetupXml;

            if (databaseSetup != null)
            {
                _context.Logger.TraceInformation(UpdaterMessages.DatabaseSetupToBeExecuted);
                ExecuteXmlInternal(databaseSetup);
                _context.Logger.TraceInformation(UpdaterMessages.DatabaseSetupFinished);
            }
            else
                _context.Logger.TraceInformation(UpdaterMessages.DatabaseSetupNotNecessary);
        }

        private void ExecuteXmlInternal(Stream inputXml)
        {
            XmlSchemaSet schemaSet = new XmlSchemaSet();
            schemaSet.Add(@"http://code.google.com/p/dbkeepernet/Updates-1.0.xsd", XmlReader.Create(Assembly.GetExecutingAssembly().GetManifestResourceStream(@"DbKeeperNet.Engine.Resources.Updates-1.0.xsd")));

            XmlReaderSettings settings = new XmlReaderSettings();
            settings.Schemas.Add(schemaSet);
            settings.IgnoreWhitespace = true;
            settings.ValidationType = ValidationType.Schema;

            Updates updates;

            using (XmlReader xmlReader = XmlReader.Create(inputXml, settings))
            {
                XmlSerializer serializer = new XmlSerializer(typeof(Updates));

                updates = (Updates)serializer.Deserialize(xmlReader);
            }

            _context.CurrentAssemblyName = updates.AssemblyName;

            if (updates.DefaultPreconditions != null)
                _context.DefaultPreconditions = updates.DefaultPreconditions;

            _context.Logger.TraceInformation(UpdaterMessages.ExecutingUpdatesForAssembly, _context.CurrentAssemblyName);

            foreach (UpdateType update in updates.Update)
            {
                _context.CurrentVersion = update.Version;
                ProcessUpdate(update);
            }

            _context.Logger.TraceInformation(UpdaterMessages.ExecutingUpdatesForAssemblyFinished, _context.CurrentAssemblyName);
        }

        bool CheckStepPreconditions(IEnumerable<PreconditionType> preconditions)
        {
            _context.Logger.TraceInformation(UpdaterMessages.CheckingStepPreconditions, _context.CurrentStep);

            bool result = true;


            foreach (PreconditionType precondition in preconditions)
            {
                _context.Logger.TraceInformation(UpdaterMessages.CheckingStepPrecondition, precondition.Precondition, precondition.FriendlyName, DumpParams(precondition.Param));

                bool currentResult = false;

                try
                {
                    currentResult = _context.CheckPrecondition(precondition.Precondition, precondition.Param);

                    _context.Logger.TraceInformation(UpdaterMessages.CheckingPreconditionResult, precondition.Precondition, currentResult);
                }
                catch (NotSupportedException)
                {
                    _context.Logger.TraceWarning(UpdaterMessages.CheckingPreconditionNotSupported, precondition.Precondition, currentResult);
                }

                result &= currentResult;

                if (!result)
                    break;
            }

            _context.Logger.TraceInformation(UpdaterMessages.CheckingStepPreconditionsResult, result);

            return result;
        }
        void ExecuteStepSql(UpdateDbStepType step)
        {
            UpdateDbAlternativeStatementType usableStatement = null;
            UpdateDbAlternativeStatementType commonStatement = null;

            foreach (UpdateDbAlternativeStatementType statement in step.AlternativeStatement)
            {
                if (statement.DbType.Equals(@"all", StringComparison.Ordinal))
                    commonStatement = statement;

                if (_context.DatabaseService.IsDbType(statement.DbType))
                {
                    usableStatement = statement;
                    break;
                }
            }

            if (usableStatement == null)
                usableStatement = commonStatement;

            if (usableStatement != null)
            {
                var stepCount = 0;

                foreach (var statement in _scriptSplitter.SplitScript(usableStatement.Value))
                {
                    _context.Logger.TraceInformation(UpdaterMessages.ExecutingCommandPart, ++stepCount);
                    _context.DatabaseService.ExecuteSql(statement);
                    _context.Logger.TraceInformation(UpdaterMessages.FinishedCommandPart, stepCount);
                }
            }
            else
            {
                _context.Logger.TraceWarning(UpdaterMessages.AlternativeSqlStatementNotFound, _context.DatabaseService.Name);
            }
        }

        void ExecuteStepCustom(CustomUpdateStepType step)
        {
            Type type = Type.GetType(step.Type);

            if (type == null)
                throw new ArgumentException(String.Format(CultureInfo.CurrentCulture, UpdaterMessages.CustomStepTypeNotFound, step.Type));

            ICustomUpdateStep customStep = (ICustomUpdateStep)Activator.CreateInstance(type);
            customStep.ExecuteUpdate(_context, step.Param);
        }

        void ExecuteStepBody(UpdateStepBaseType step)
        {
            bool executed = false;
            UpdateDbStepType dbStep = step as UpdateDbStepType;

            if (dbStep != null)
            {
                ExecuteStepSql(dbStep);
                executed = true;
            }
            if (!executed)
            {
                CustomUpdateStepType customStep = step as CustomUpdateStepType;

                if (customStep != null)
                {
                    ExecuteStepCustom(customStep);
                    executed = true;
                }
            }
            
            if (!executed)
                throw new InvalidOperationException(UpdaterMessages.UnsupportedUpdateStepType);
        }
        void ExecuteStep(UpdateStepBaseType step)
        {
            try
            {
                _context.CurrentStep = step.Id;
                PreconditionType[] preconditions = step.Preconditions;

                if ((preconditions == null) || (preconditions.Length == 0))
                {
                    _context.Logger.TraceInformation(UpdaterMessages.UsingDefaultPreconditions);
                    preconditions = _context.DefaultPreconditions;
                }

                bool preconditionsResult = CheckStepPreconditions(preconditions);

                if (preconditionsResult)
                {
                    _context.Logger.TraceInformation(UpdaterMessages.StartingUpdateStep, step.Id, step.FriendlyName);
                    try
                    {
                        _context.DatabaseService.BeginTransaction();

                        ExecuteStepBody(step);
                        _context.Logger.TraceInformation(UpdaterMessages.FinishedUpdateStep, step.Id, step.FriendlyName);

                        if (step.MarkAsExecuted)
                        {
                            _context.DatabaseService.SetUpdateStepExecuted(_context.CurrentAssemblyName, _context.CurrentVersion, _context.CurrentStep);
                            _context.Logger.TraceInformation(UpdaterMessages.StepMarkedAsExecuted, step.Id, step.FriendlyName);
                        }

                        _context.DatabaseService.CommitTransaction();
                    }
                    catch
                    {
                        _context.Logger.TraceError(UpdaterMessages.StepExceptionRollback);
                        _context.DatabaseService.RollbackTransaction();
                        throw;
                    }
                }
                else
                    _context.Logger.TraceInformation(UpdaterMessages.StepSkipped, step.Id, step.FriendlyName);
            }
            finally
            {
                _context.CurrentStep = 0;
            }
        }

        void ProcessUpdate(UpdateType update)
        {
            _context.Logger.TraceInformation(UpdaterMessages.StartingVersion, update.Version, update.FriendlyName);

            foreach (UpdateStepBaseType step in update.UpdateStep)
                ExecuteStep(step);

            _context.Logger.TraceInformation(UpdaterMessages.FinishedVersion, update.Version, update.FriendlyName);
        }

        private static string DumpParams(IEnumerable<PreconditionParamType> param)
        {
            StringBuilder builder = new StringBuilder();

            if (param != null)
            {
                bool first = true;

                foreach (PreconditionParamType p in param)
                {
                    if (!first)
                        builder.Append(',');

                    builder.Append(p.Value);

                    first = false;
                }
            }
            return builder.ToString();
        }
        #endregion
        #region Public methods
        /// <summary>
        /// Thru this method is executed each XML batch. Single object
        /// instance may be used for invocation of multiple XML update
        /// definitions.
        /// </summary>
        /// <param name="inputXml">Input XML stream, which is during processing validated against the definition schema.</param>
        public void ExecuteXml(Stream inputXml)
        {
            if (inputXml == null)
                throw new ArgumentNullException(@"inputXml");

            try
            {
                ExecuteDatabaseSetupXml();
                ExecuteXmlInternal(inputXml);
            }
            catch (DbKeeperNetException e)
            {
                _context.Logger.TraceError(UpdaterMessages.CaughtException, e.ToString());
                throw;
            }
            catch (Exception e)
            {
                _context.Logger.TraceError(UpdaterMessages.CaughtCommonException, e.ToString());
                throw new DbKeeperNetException(UpdaterMessages.CommonExceptionMessage, e);
            }
        }

        /// <summary>
        /// Executes all XML updates referenced in App.config file.
        /// </summary>
        /// <remarks>
        /// Updates are executed in two phases:
        /// <list type="number">
        /// <item>Database setup for DbKeeperNet</item>
        /// <item>Each configured update in order defined in App.Config</item>
        /// </list>
        /// </remarks>
        /// <see cref="DbKeeperNetConfigurationSection"/>
        /// <exception cref="DbKeeperNetException"/>
        public void ExecuteXmlFromConfig()
        {
            try
            {
                ExecuteDatabaseSetupXml();

                foreach (UpdateScriptConfigurationElement e in _context.ConfigurationSection.UpdateScripts)
                {

                    ExecuteXmlInternal(_context.GetScriptFromStreamLocation(e.Provider, e.Location));
                }
            }
            catch (DbKeeperNetException)
            {
                throw;
            }
            catch (Exception e)
            {
                _context.Logger.TraceError(UpdaterMessages.CaughtCommonException, e.ToString());
                throw new DbKeeperNetException(UpdaterMessages.CommonExceptionMessage, e);
            }
        }
        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            Dispose(true);
            GC.SuppressFinalize(this);
        }

        protected virtual void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (_context != null)
                {
                    _context = null;
                }
            }
        }
        #endregion
    }
}

By viewing downloads associated with this article you agree to the Terms of Service and the article's licence.

If a file you wish to view isn't highlighted, and is a text file (not binary), please let us know and we'll add colourisation support for it.

License

This article, along with any associated source code and files, is licensed under The BSD License


Written By
Team Leader NCR
Czech Republic Czech Republic
I'm software developer since 1996. I started with assembler on Intel 8051 CPUs, during years I was interested in C, C++, Sybase PowerBuilder, PHP, Sybase Anywhere Database, MSSQL server and multiplatform development.

Currently I'm developing in C++ and C# (this is my favorit and I spent some time with MCPD achievement). I'm also interested in design patterns.

Comments and Discussions