Fast Project Rollbacks

 

Fast Project Rollbacks

The projects that I work on require both deployment and rollback scripts. Using a database backup as a rollback strategy is not acceptable for a number of reasons.

  • Downtime needs to be measured in minutes and the restore would take hours.
  • The project normally affects a portion of the databases so rollbacks are intended to affect the objects and data that have been deployed and not any other objects or data.
  • Visibility of the rollback steps is needed as an audit log.

I have a particular approach to scripting up the rollback of database projects that I should like to share with you.

Goals for creating rollback scripts

Code readability

All code has to be peer-reviewed and the effort involved in a thorough peer review is not always appreciated. Indeed Cisco used to publish recommendations for peer reviews that include recommendations on the following

  • The amount of code that could be expected to be peer reviewed per session.
  • The amount of time in minutes allowed in any one peer review session before a break was needed.

The figures for both the volume of code and the time spent are likely to be far lower than you would expect therefore anything you can do to make the code easier to read is going to aid the peer review process.

Remember a peer review should not be regarded as an obstacle to overcome but as an insurance policy to ensure that it won't be your code that is to blame for a production incident.

If your code is readable then the peer review can be achieved quicker and any bugs should be easier to spot.

Code readability leads to my second goal

Code Maintainability

I don't code on the fly. I put quite a bit of time and effort into my design but if I can use the analogy of the battle plan then no battle plan survives engagement with the enemy.

Things that made perfect sense in the design stage might not be practical in the real world. My code is probably going to change so it must be easy to change.

Code Durability

If a rollback script runs more than once then it should do no harm and produce no error messages. I am only concerned with rollback scripts here but the same comment also applies to deployment scripts. Ideally the both deployment and rollback scripts should produce status messages to act as a log of what has taken place.

Most of my deployments start in the very early hours of the morning when I'm fully decaffeinated so any unexpected error messages are unwelcome.

Speed of execution

Time is money. If your database is unavailable then it is costing you money. In some cases using a backup as a rollback strategy simply isn't financially feasible except as a method of last resort.

Having a script that can execute in seconds, or at the worst minutes is far better than a restore of a terabyte database that may take hours.

Script headers

I use script headers for three purposes.

  • To provider brief notes on what the script is supposed to do, prerequisites, special actions
  • To generate dynamic status messages showing the server, time of deployment, the account running the script.
  • To set session settings such as XACT_ABORT and NOCOUNT ON.

Consider the code below.


AW_TARGET
GO

 

	@CRLF  ,
	@Deployer , 
	@DeploymentDate , 
	@DBName SYSNAME 
 @CRLF=(13)+(10)
 @Deployer = ()
 @DeploymentDate = ((20),(),120)
 @DBName = ()
(,10,1,,@CRLF,@Deployer,@CRLF, @DeploymentDate,@CRLF, @DBName)
 

The first line simply switches the following two session options on.

Session option

Action when ON

XACT_ABORT

Any action resulting in an error will rollback the transaction in which the error occurred.

NOCOUNT

Suppresses the (n row(s) affected) message and makes the audit messages easier to read.

In this case the AW_TARGET database is simply a copy of AdventureWorks

The remaining line cause the following message to be displayed

---------------------------------------------------
DEPLOYMENT SERVER: DBDevelopment01
DEPLOYER: Development01/David
Deployment Date: 2008-09-07 15:54:33 
Database: AW_Target
---------------------------------------------------

Removing views

The following code may look familiar

  EXISTS (  TABLE_SCHEMA= AND TABLE_NAME=)
 Production.vProductAndDescription
GO

Mechanically there is nothing wrong with the above but for removing multiple objects the repetitive nature of the code makes it easy to miss errors.

The following code shows an alternative method of removing a set list of views

@SQL () , @CRLF (2)
 @CRLF = (13)+(10)
 @SQL=(@SQL++@CRLF,) 
+ 'DROP VIEW '
+ (TABLE_SCHEMA)
+ '.'
+ (TABLE_NAME)
 
 
	TABLE_SCHEMA=
AND TABLE_NAME IN (

)
 @SQL

GO

Although it may seem like overkill for three simple views the process comes into its own when the view list gets longer. The view list acts as a manifest that can be checked against the deployment script or release notes.

To prevent accidents while experimenting I have commented out the EXEC statement but when run the script will produce the following.

---------------------------------------------------
DEPLOYMENT SERVER: D_POOLE
DEPLOYER: D_POOLE/David
Deployment Date: 2008-09-07 16:05:05 
Database: AW_Target
---------------------------------------------------
DROP VIEW [Production].[vProductAndDescription];
DROP VIEW [Production].[vProductModelCatalogDescription];
DROP VIEW [Production].[vProductModelInstructions]

Removing Stored Procedures

The method for removing stored procedures is almost identical differing only in the INFORMATION_SCHEMA view and associated field names.

@SQL () , @CRLF (2)
 @CRLF = (13)+(10)
 @SQL=(@SQL++@CRLF,) 
+	'DROP '
+	ROUTINE_TYPE
+	(ROUTINE_SCHEMA)
+	'.'
+	(ROUTINE_NAME)
 
 ROUTINE_NAME IN(

)
PRINT @SQL
--
GO

Removing tables

Removing tables is very similar but in this case we have to consider the dependencies between the tables.

If you try and delete a table where referenced by a foreign key constraint then the delete will fail. This means that we must first remove any foreign key constraints referencing the tables that we want to remove.

The first stage is to assemble a table containing the list of database tables we want to drop.

 @TableList  (ID  NOT NULL, TableName  NOT NULL)
 @TableList (ID,TableName)  
 (TABLE_SCHEMA++TABLE_NAME),TABLE_NAME
 
 TABLE_TYPE='BASE TABLE'
AND TABLE_NAME IN (

)

The next stage is to use this table to establish a list of foreign key constraints and generate the SQL to remove them.

 @SQL () , @CRLF (2)
 @CRLF = (13)+(10)

 @SQL=(@SQL++@CRLF,) 
+	
+	(OBJECT_NAME(fkeyid))
+	
+	(OBJECT_NAME(constid))
 
 rkeyid IN ( id  @TableList)

Finally we must append the SQL to remove the tables themselves.

 @SQL=(@SQL++@CRLF,) 
+	
+	(TABLE_SCHEMA)
+	
+	(TABLE_NAME)
 
 TABLE_TYPE
AND TABLE_NAME IN ( TableName  @TableList)
 @SQL

GO

Our code above will produce the following output

ALTER TABLE [ProductDocument] DROP CONSTRAINT [FK_ProductDocument_Document_DocumentID];
ALTER TABLE [ProductInventory] DROP CONSTRAINT [FK_ProductInventory_Location_LocationID];
ALTER TABLE [ProductModelIllustration] DROP CONSTRAINT [FK_ProductModelIllustration_Illustration_IllustrationID];
ALTER TABLE [ProductModelProductDescriptionCulture] DROP CONSTRAINT [FK_ProductModelProductDescriptionCulture_Culture_CultureID];
ALTER TABLE [WorkOrderRouting] DROP CONSTRAINT [FK_WorkOrderRouting_Location_LocationID];
DROP TABLE [Production].[BillOfMaterials];
DROP TABLE [Production].[Culture];
DROP TABLE [Production].[Document];
DROP TABLE [Production].[Illustration];
DROP TABLE [Production].[Location]

The advantage to this method is that you don't have to worry about the order in which tables are deleted or the names of the foreign key constraints.

Removing Replication

If our tables are replicated then any attempt to delete them will still fail in which case we need to consider the code below.

 @TABLELIST (TableName  )
 @TABLELIST
 TABLE_NAME
 
 TABLE_TYPE=
 TABLE_NAME  (

)

Once again we build up a table containing a list of the database tables for which we wish to drop replication.

I use the presence of the sysarticles table to detect whether or not the database is replicated but I could just as well use IF DATABASEPROPERTYEX('AdventureWorks','IsPublished')=1

 ( 1   [name]=)
 @SQL () , @CRLF (2)
		 @CRLF = (13)+(10)
 @SQL=(@SQL++@CRLF,) 
		+	
		+	P.[name]
		+	
		+ A.[name]
		+ 
		+ @CRLF
		+	
		+ p.[name]
		+	
		+ A.[name]
		+    
		   A
			INNER JOIN   P
			ON A.pubid = P.pubid
		AND A.name IN ( TableName  @TableList
		)
		 @SQL

	

The output from this code is as follows

exec sp_dropsubscription @publication = N'AdventureWorks_Reporting',@article = N'BillOfMaterials', @subscriber = N'all', @destination_db = N'all'
exec sp_droparticle @publication = N'AdventureWorks_Reporting',@article = N'BillOfMaterials', @force_invalidate_snapshot = 1;
exec sp_dropsubscription @publication = N'AdventureWorks_Reporting',@article = N'Culture', @subscriber = N'all', @destination_db = N'all'
exec sp_droparticle @publication = N'AdventureWorks_Reporting',@article = N'Culture', @force_invalidate_snapshot = 1;
exec sp_dropsubscription @publication = N'AdventureWorks_Reporting',@article = N'Document', @subscriber = N'all', @destination_db = N'all'
exec sp_droparticle @publication = N'AdventureWorks_Reporting',@article = N'Document', @force_invalidate_snapshot = 1;
exec sp_dropsubscription @publication = N'AdventureWorks_Reporting',@article = N'Illustration', @subscriber = N'all', @destination_db = N'all'
exec sp_droparticle @publication = N'AdventureWorks_Reporting',@article = N'Illustration', @force_invalidate_snapshot = 1;
exec sp_dropsubscription @publication = N'AdventureWorks_Reporting',@article = N'Location', @subscriber = N'all', @destination_db = N'all'
exec sp_droparticle @publication = N'AdventureWorks_Reporting',@article = N'Location', @force_invalidate_snapshot = 1

Removing Columns

Just as we cannot remove tables if they are referenced by foreign key constraints we are also prevented from removing columns if they are referenced by check or default constraints.

The following code removes any column constraints on ModifiedDate in the HumanResource.EmployeeAddress table.

@SQL () , @CRLF (2)
 @CRLF = (13)+(10)
 @SQL=(@SQL++@CRLF,) 
+	
+	(COL.id)
+	
+	(COL.id)
+	
+	(C.ConstID)
   C
	INNER JOIN   COL
	ON C.Id = COL.Id
	AND C.ColID = COL.ColID
 COL.id IN(
	()
)
AND COL.Name = 

Once the constraints have been removed we can remove the column itself.

 @SQL=(@SQL++@CRLF,) 
+	
+	(COL.id)
+	
+	(COL.id)
+	
+	COL.Name
   COL
 COL.id IN(
	()
)
AND COL.Name IN( 
	 -- Add the list of fields that you want to remove.
)
 @SQL

SQL2000 considerations

In SQL2005 and SQL2008 we have the VARCHAR(MAX) data type.

In SQL2000 and earlier we have an 8000 character limit to a VARCHAR. If your rollback has a large number of objects of a specific type then you have to consider whether your dynamic rollback commands will exceed the 8000 character limit.

Obviously you could repeat the scripts for a different block of objects but there is an alternative approach.

The example below uses our "Removing Columns" example

 () IS NOT NULL
 #RollbackCommands
GO

 #RollbackCommands (
	CommandID  NOT NULL (1,1) 
		 PK_RollbackCommands ,
	 CommandText varchar(1024) 
)

 #RollbackCommands(CommandText)

	
+	(COL.id)
+	
+	(COL.id)
+	
+	OBJECT_NAME(C.ConstID)
   C
	INNER JOIN   COL
	ON C.Id = COL.Id
	AND C.ColID = COL.ColID
 COL.id IN(
	()
)
AND COL.Name = 

 #RollbackCommands(CommandText)

	
+	(COL.id)
+	
+	(COL.id)
+	
+	COL.Name
   COL
 COL.id IN(
	()
)
AND COL.Name = 

Having assembled our commands we now have to execute them in order

 
	@CommandID ,
	@CommandText (1024)
 @CommandID = 0
 @CommandID IS NOT NULL
 @CommandID = (CommandID)
			#RollbackCommands
		 CommandID>@CommandID
		 @CommandID IS NOT NULL
			
				 @CommandText = CommandText
				 #RollbackCommands
				 CommandID = @CommandID
				 @CommandText

			 #RollbackCommands
 PK_RollbackCommands

IF () IS NOT NULL
	 #RollbackCommands
GO

Conclusion

Although these scripts may look a little verbose initially, the more objects you have to remove from a database the more useful these code snippets become. In fact it is easy to set these up as templates then whoever peer reviews the code knows that they have to verify the list of objects in the WHERE clause.

Three advantages of this approach to rollback are as follows

  • You don' t have to worry about the order in which objects are destroyed, simply maintain the list of the objects
  • If constraint names are SQL server assigned names then these scripts derive those names automatically. You don't need to know the names of the constraints.
  • If you accidentally run the code for a 2nd or subsequent time it will do no harm. As the system is assembling the list of commands from system tables there is no chance of errors occurring when trying to delete an object that no-longer exists.

By David Poole

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章