Use Case: Simple versioning and build management

Abstract

The most basic task you can perform with DataClass is using it to capture, upgrade, and manage version upgrade scripts. DataClass does this by allowing you to document the construction steps required to upgrade to any given version from its immediate predecessor. The output of DataClass will manage choosing which scripts to execute and executing them in the right order.

Learning Objectives

  • How databases should be built
  • How DataClass allows you to manage version
  • How DataClass applies upgrade scripts

Body

There may be many right ways to build a database. However, the only one we've found that works every time is to define your database build scripts in terms of the deltas from each version to its immediate successors and then always apply those exact scripts in a precise order every time you build a database.

This is the foundation of what we call "creating a class of databases," which helps guarantee that all database instances are built the exact same way and, thus, have the exact same behaviors. This, in turn, allows us to get a lot more confidence from our testing because we know that what we are testing is representative of what we will have in production.

We'll start off simple, defining a simple class of database that can store credits and debits associated with account information. For the purpose of this exercise, we will start off using an email address as an account identifier.

To do this using DataClass, we must define a class of database, assign that class a name, declare that it has a new version, specify that version's starting point as being an initialized, empty database, and add the scripts required to build said database up to the new version.

You can click on keywords and concepts in blue.
// this is the name of the class of database
handdatabase CustomerAccounting
{
  /*
  we will start off with just one version
  this version is built on top of an
  bare-bones database instance
  */
  version 1.0 : initialized
  {
    /*
    databases have complex construction
    logic, unlike middle-tier objects
    that logic is specified as "steps" to
    get to a particular version from its
    immediate predecessor
    */
    construction
    {
      step sql
      {
        CREATE TABLE CustomerTransactions(
          AccountEmail NVARCHAR(500), 
          Debit INT, 
          Credit INT);
      }
    }
  }
}

Running that code through a compiler will produce a class library with a class named CustomerAccounting. That class will know how to inspect a database to ensure it is in the right starting state, update a database to version 1.0, and record in that database the new version number.

Assuming we are satisfied with that design, we would at some point want to create a production database instance. We would do this using the same compiled class of database we used to create our test databases. Once this has been done, version 1.0 ceases to be a design artifact and becomes an historical document. That is: version 1.0 is what already happened instead of what we would like to happen.

Prudence requires us to keep our records of what happened to a production database straight. Unlike a middle-tier object, databases have deep, rich, long histories that heavily influence what they are in the present.

On the other hand, business drivers push us to change the design of the database. In this case, there's a fairly obvious issue with our database design: the details of an account are mixed up with its transaction history. So once we've released, we decide to refactor the database. This is accomplished by creating a new version of our database, setting version 1.0 as its predecessor, and adding construction steps to get the new structure we want.

You can click on keywords and concepts in blue.
// old comments deleted for brevity.
handdatabase CustomerAccounting
{
  version 1.0 : initialized
  {
    construction
    {
      step sql
      {
        CREATE TABLE CustomerTransactions(
          AccountEmail NVARCHAR(500), 
          Debit INT,
          Credit INT);
      }
    }
  }
  
  // create a new version and "inherit" it from version 1.0
  version 1.1 : 1.0
  {
    /*
    Since a database will always be
    at version 1.0 before we upgrade to v1.1
    these construction steps need only
    differentiate those two versions.
    */
    construction
    {
      step sql
      {
        CREATE TABLE CustomerAccounts(
          [ID] INT IDENTITY(1,1) PRIMARY KEY,
          Email NVARCHAR(500));
        
        INSERT INTO CustomerAccounts(Email)
          SELECT DISTINCT AccountEmail
          FROM CustomerTransactions;
        
        ALTER TABLE CustomerTransactions
          ADD CustomerAccountID INT;
          
        UPDATE CustomerTransactions SET
          CustomerAccountID = accounts.ID
        FROM
          CustomerTransactions transactions
        INNER JOIN
          CustomerAccounts accounts
        ON
          transactions.CustomerAccountID = accounts.[ID]
          
        -- and so on...
      }
    }
  }
}

Compiling that will produce a library that knows how to build an empty database up to version 1.1. It will also know how to upgrade an existing v1.0 database to v1.1. Most importantly, however, all databases upgraded to v1.1 are built exactly the same way, regardless of whether or not they were upgraded to version v1.0 at some point in the past.

Related Concepts

Related Keywords

Other Actions

documentation | all examples | use cases | concepts | keywords