К основному контенту

Контроль версий базы данных [Version control of database]

Разработка приложения - это постоянное изменение. Изменение кода, реквайрементов, и естественно, изменение базы данных, как структуры так и данных.
Проблема
Каждый разработчик сталкивается с данной проблемой чуть ли не каждый день. Что бы понять проблему рассмотрим примеры:
  • Работая в команде, иногда нескольким разработчика нужно менять БД одновременно, как изменять базу данных, что бы каждый мог менять БД независимо друг от друга?
  • Какая гарантия, что версия базы данных соответствует коду приложения? Как узнать Ивану Дураку, что Павлик Морозов поменял БД? 
  • Как менять БД, которая уже в продакшине?

Решение
Идеальным решением является апдейт скрипты. Предлагаю Вам рассмотреть следующую версию решения, которое работает на MS SQL 2005+.  
Программист1 добавляет таблицу [Client]. Как результат получаем скрипт 1_0_0_x.sql (ВАЖНО! первая строка в файле скрипта должна быть SET XACT_ABORT ON, эта инструкция говорит, что бы сиквел сервер откатывал транзакцию при любой ошибке):
SET XACT_ABORT ON


-- BEGIN UPDATE: 1.0.0.x PROJ-1 Create Client
DECLARE @UpdateId NVARCHAR(MAX);
SET @UpdateId = '1.0.0.x PROJ-1';
IF [dbo].[IsUpdateExist](@UpdateId) = 0
BEGIN
 BEGIN TRAN @UpdateId;

 EXEC dbo.sp_executesql @statement = N'
CREATE TABLE [dbo].[Client](
 [Id] [int] IDENTITY(1,1) NOT NULL,
 [Name] [nvarchar](255) NOT NULL
)'
 
 EXEC [dbo].CommitUpdate @UpdateId;
 COMMIT TRAN @UpdateId;
END
GO
-- END UPDATE: 1.0.0.x PROJ-1 Create Client

Программист1 добавляет колонку [Description] в таблицу [Client] и одновременно Программист2 добавляет колонку [CreatedDate] в таблицу [Client]. После мержа (merge) в скрипте 1_0_0_x.sql добавится 2 апдейта:
-- BEGIN UPDATE: 1.0.0.x PROJ-2 Add Client Description 
DECLARE @PrevUpdateId NVARCHAR(MAX);
DECLARE @UpdateId NVARCHAR(MAX);
SET @PrevUpdateId = '1.0.0.x PROJ-1'; 
SET @UpdateId = '1.0.0.x PROJ-2';
IF [dbo].[IsUpdateExist](@PrevUpdateId) = 1 
   AND [dbo].[IsUpdateExist](@UpdateId) = 0
BEGIN
 BEGIN TRAN @UpdateId;

 EXEC dbo.sp_executesql @statement = N'
ALTER TABLE dbo.Client ADD
 Description nvarchar(MAX) NULL'
 
 EXEC [dbo].CommitUpdate @UpdateId;
 COMMIT TRAN @UpdateId;
END
GO
-- END UPDATE: 1.0.0.x PROJ-2 Add Client Description 


-- BEGIN UPDATE: 1.0.0.x PROJ-3 Add Client Creation Date 
DECLARE @PrevUpdateId NVARCHAR(MAX);
DECLARE @UpdateId NVARCHAR(MAX);
SET @PrevUpdateId = '1.0.0.x PROJ-1'; 
SET @UpdateId = '1.0.0.x PROJ-3';
IF [dbo].[IsUpdateExist](@PrevUpdateId) = 1 
   AND [dbo].[IsUpdateExist](@UpdateId) = 0
BEGIN
 BEGIN TRAN @UpdateId;

 EXEC dbo.sp_executesql @statement = N'
ALTER TABLE dbo.Client ADD
 CreatedDate datetime NULL'
 
 EXEC [dbo].CommitUpdate @UpdateId;
 COMMIT TRAN @UpdateId;
END
GO
-- END UPDATE: 1.0.0.x PROJ-3 Add Client Creation Date 
Как результат, получаем цепочку зависимых апдейтов, в случае не прохождения предыдущего апдейта, последующие не будут выполнены. Если же апдейт прошел успешно, повторно он выполняться уже не будет. Значит скрипт 1_0_0_x.sql можно выполнять сколько угодно раз.

ВАЖНО! Внутри апдейта нельзя использовать GO, вместо этого необходимо использовать хранимку EXEC dbo.sp_executesql @statement = N'...'
НО, что бы это все заработало нам нужен следующий скрипт SetupDatabaseVersioning.sql, который содержит [dbo].[IsUpdateExist] и [dbo].CommitUpdate.
-- BEGIN: Sql updates management
IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[UpdateLog]') AND type in (N'U'))
 CREATE TABLE [dbo].[UpdateLog](
  [Key] [nvarchar](255) NOT NULL,
  [Timestamp] [datetime] NOT NULL CONSTRAINT [DF_UpdateLog_Timestamp]  DEFAULT (getdate()),
  CONSTRAINT [PK_UpdateLog] PRIMARY KEY CLUSTERED 
 (
  [Key] ASC
 )WITH (PAD_INDEX  = OFF, STATISTICS_NORECOMPUTE  = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS  = ON, ALLOW_PAGE_LOCKS  = ON) ON [PRIMARY]
 ) ON [PRIMARY]
GO

IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[IsUpdateExist]') AND type in (N'FN'))
EXEC dbo.sp_executesql @statement = N'
 CREATE FUNCTION [dbo].[IsUpdateExist] (@UpdateKey NVARCHAR(MAX))
 RETURNS BIT
 AS
 BEGIN
  DECLARE @Count INT
  SELECT @Count = COUNT(*) FROM UpdateLog WHERE [Key] = @UpdateKey
  RETURN CAST(@Count AS BIT)
 END
' 
GO

IF NOT EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[CommitUpdate]') AND type in (N'P', N'PC'))
EXEC dbo.sp_executesql @statement = N'
 CREATE PROCEDURE [dbo].[CommitUpdate]
  @UpdateKey NVARCHAR(MAX)
 AS
 BEGIN
  SET NOCOUNT ON;

  IF [dbo].[IsUpdateExist](@UpdateKey) = 0
   INSERT
    INTO [UpdateLog]([Key]) 
    VALUES(@UpdateKey)

 END
' 
GO
-- END: Sql updates management

Пойдем далее, сочиним пакетный файл ApplyAllUpdates.bat, что бы применить все апдейты.
cd /d %~dp0

set server=%1%
set databaseName=%2%

if "%databaseName%" == "" set databaseName=MyDatabase
if "%server%" == "" set server=(local)

sqlcmd -S %server% -Q "USE %databaseName% EXEC sp_changedbowner 'sa'"
sqlcmd -S %server% -d %databaseName% -i "SetupDatabaseVersioning.sql" -v database=%databaseName%
sqlcmd -S %server% -d %databaseName% -i "1_0_0_x.sql" -v database=%databaseName%
sqlcmd -S %server% -d %databaseName% -i "1_1_0_x.sql" -v database=%databaseName%
sqlcmd -S %server% -d %databaseName% -i "1_2_0_x.sql" -v database=%databaseName%
В итоге, получаем самодостаточное решение, никакой зависимости от внешних систем контроля версий, простой SQL и ничего более, вот почему мне это по душе. Этот подход используется в нескольких проектах, проверенно работоспособность продакшином.
P.S.: Версию базы данных можно вычислить, она соответсвует количеству записей в таблице [UpdateLog]. Данная таблица содержит список всех апдейтов, которые были выполнены. Данное число можно хранить в коде приложения, и в случае несоответсвия версии БД переводить приложение в нерабочее состояние.

Комментарии