百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术分类 > 正文

探索与SQL Clone和SQL Change Automation的数据库持续集成

ztj100 2025-04-07 21:25 19 浏览 0 评论

当您作为应用程序开发团队的成员时,值得建立一个例程,该例程从源代码控制开始进行日常构建,然后供应数据库的测试和开发实例。一个主要的优点是可以尽早检查代码和数据库,并且您可以证明可以完全从代码构建工作数据库。这意味着管理层可以确信资产全部在源代码控制中。这也意味着测试可以尽早发现问题。

这需要自动化。最困难的部分是按照依赖关系的顺序组装单个对象脚本,或者在迁移方法的情况下,按照清单指示的顺序组装单个对象脚本。最繁琐的部分是将数据加载到数据库中。当然,耗时的部分是准备数据,但这超出了本文的范围。我们假设您有一个用于测试的标准数据集,并且所有服务器都将共享它。

我使用SQL Change Automation(SCA)来构建数据库。微软的SMO扮演次要的整合角色,我也使用它来创建数据库。我使用SQL Clone(SQL Provisioning的一个组件)将完成的数据库分发到需要最新版本的各种基于团队的服务器。将流程调整为符合您的要求后,您可以在每次有人提交数据库更改时运行该流程。

SQL克隆简介

第一次处理SQL Clone时,总是会有轻微的文化冲击,因此,我将作简要说明(有关更多信息,请参见Clone的工作原理)。SQL Clone使用x64 Windows中的虚拟磁盘服务来允许将相同的字节(“数据映像”)作为“克隆”数据库重复使用,并根据需要多次在SQL Server实例上使用。SQL Server完全不知道Windows操作系统正在从事任何“烟雾和镜像”活动。数据映像仅在网络上的单个位置保存一次,并且永不更改。每个克隆上进行的任何更改都存储在本地克隆服务器上的差异磁盘中。

设置克隆所需的时间很少,这是设置虚拟硬盘(.vhdx)并将其装入所需的时间。无论实际数据库的大小如何,从该映像创建的初始克隆仅需要大约40MB的磁盘空间。

您将需要一个中央服务器,即SQL Clone服务器,在其上安装SQL Clone Web应用程序,该应用程序必须在网络上对所有其他计算机可见。您的服务器不必是Windows域的一部分即可运行SQL Clone,但是如果您在混合开发环境中运行,最简单的方法是在与Clone相同的计算机上为映像提供共享目录。服务器。克隆可以在任何SQL Server上运行,在该SQL Server上,分配给克隆代理的用户可以通过网络访问该共享映像目录。

网络中任何SQL Server实例都可以用来承载克隆。为此,他们需要在其上安装SQL Clone代理,这些代理被分配了具有sysadmin权限的本地用户。

一旦数据库开始需要大量数据来进行开发和测试工作,那么用当前版本的数据库和数据来配置所有必需的数据库实例的麻烦就会变得一发不可收拾。如果可以确定克隆具有正确的数据,则可以通过模式同步来实现,但这是不现实的。任何测试运行都必然会更改数据。要进行一系列集成测试,您需要在每次测试后将数据库还原到已知状态。传统上,这是通过备份/还原或重复分离和附加来完成的。

取而代之的是,我将使用SQL Clone,这使得拆卸和恢复克隆非常迅速,容易,这是拆卸的一部分,并且可以在PowerShell的一行中轻松地自动完成此过程。

PowerShell自动化的CI

接下来我将介绍的PowerShell脚本将允许您使用源控制程序的最新版本的架构以及一个或多个测试或开发的数据版本来维护测试和开发所需的各种数据库。我提供了一个AdventureBuild zip文件,其中包含AdventureWorks数据库的源以及开发和测试所需的数据(作为bcp文件)。如果您从包含敏感数据或个人数据的数据集开始,则在配置开发和测试服务器之前,需要屏蔽数据。

我假设这种情况下的原始数据库是正在开发的数据库,并且其源保存在“源代码管理”中,因此我不备份现有克隆,也不在删除克隆之前保存更改,并且不检查是否任何人仍然都有与克隆一起运行的会话。

该脚本通过同步到使用SMO创建的空数据库来使用SCA进行构建(但是您也可以使用SQL Compare)。您需要在运行SQL Server版本(等于或低于您需要将克隆部署到的最旧版本)的实例上构建数据库。无法将克隆运行在比其父代更早的SQL Server版本上。

我们使用BCP将数据导入到空版本中,然后使用SQL Clone创建数据库映像并将克隆部署到一台或多台服务器上的“克隆”数据库列表中。将使用创建过程创建的数据库副本来创建克隆或刷新现有克隆。最后应该是所有具有相同元数据和数据的数据库。您需要安装最新版本的Clone PowerShell cmdlet(Clone应用程序中的Settings | PowerShell)。

如果您有SCA但没有SQL Clone,则仍然可以执行此操作,但是它会慢得多,并且会占用更多空间。

该解决方案省略了一些更详细的细节,您可以在其他文章中找到大多数细节。例如,我不会为数据库生成文档网站,也不会使用SQL Code Guard 对代码进行检查,也不会发送有关构建进度的信息。我没有显示如何将脚本和模板应用于每个单独的克隆以自定义安装或使数据库符合服务器的设置。

配置文件

为简单起见,我将所有设置放在单独的脚本中。其中包含要创建或更新的克隆列表,必须构建的数据库的详细信息,要在其中找到数据和源代码的位置以及克隆服务器和共享映像目录的位置。显然,您需要全部填写。

$Database = 'OurBuild'

$Repository= 'OurPathToTheScriptAndData'

@{

"Source" = @{

#specify the various directories you want in order to store files and logs

#The location of the executable SQL data insertion script.

'DataSyncPath' = "$Repository\$Database\data";

#where you want to put the reports for this particular database.

'ReportPath' = "$Repository\$Database";

#where you have (or want to put) the source of the current database.

'DatabasePath' = "$Repository\$Database\Build";

}

"Build" = @{

#now we'll specify where we want the new build. We will clone from this.

'NewBuildServer' = 'OurServer'; #The Server

'NewBuildInstance' = ''; #The SQL Server instance

'NewDatabase' = "New$Database"; #The name of the database

'username' = 'Phil Factor';#leave blank if windows authentication

'SQLCompareOptions' ='NoTransactions'# you sometimes need these for a complex build

}

"Image" = @{

#create an image of what we built

'Name' = "$($database)";

'ServerURL' = 'http://CloneServer:14145';

'ImageDirectoryURL'='\\CloneServer\Clone'

}

"Clones" = @(

@{ "NetName" = "AServer"; "Database" = "$($database)1" },

@{ "NetName" = "AnotherServer"; "Database" = "$($database)2" },

@{ "NetName" = "YetAnotherServer"; "Database" = "$($database)3" },

@{ "NetName" = "StillAnotherServer"; "Database" = "$($database)4" },

@{ "NetName" = "AndYetAnotherServer"; "Database" = "$($database)5" },

@{ "NetName" = "YeGodsAnotherServer"; "Database" = "$($database)6" }

)

}

该目录必须与以下PowerShell脚本位于同一目录中。

CI脚本

这是用于构建和填充数据库以及部署克隆的PowerShell脚本:

$VerbosePreference = "Continue"

#-- just to save typing ----

$MS = 'Microsoft.SQLServer'

$My = "$MS.Management.Smo"

$Mc = "$MS.Management.Common"

$popVerbosity = $VerbosePreference

$VerbosePreference = "Silentlycontinue"

# the import process is very noisy if you are in verbose mode

Import-Module sqlserver -DisableNameChecking #load the SQLPS functionality

$VerbosePreference = $popVerbosity

# set "Option Explicit" to catch subtle errors

set-psdebug -strict

$ErrorActionPreference = "stop"

<# just to make it easier to understand, the various parameter values are structured in a

hierarechy. We iterate over the clones when making or updating them #>

#First we read in the configuration from a file (do it so we can use the ISE as well)

$Data = &"$(If($psISE)

{Split-Path -Path $psISE.CurrentFile.FullPath}

Else {$global:PSScriptRoot})\MySCACloneConfig.ps1"

<# we read in the data as a structure.

Then we do some sanity checking to make sure that the data is reasonably viable.

We apply defaults if possible #>

$Errors = @()

# the fourth value means -1 provide a blank default, 0 = not a directory-must be there,

# 1=create if not exist, 2 = must already exist

@(($data.source.DatabasePath, 'source', 'DatabasePath', 2),

($data.source.DataSyncpath, 'source', 'DataSyncPath', 2),

($data.source.ReportPath, 'source', 'ReportPath', 2),

($data.Build.NewBuildServer, 'Build', 'NewBuildServer', 0),

($data.Build.NewBuildInstance, 'Build', 'NewBuildInstance', 0),

($data.Build.NewDatabase, 'Build', 'NewDatabase', 0),

($data.Build.username, 'Build', 'username', -1),

($data.Build.SQLCompareOptions, 'build', 'SQLCompareOptions', -1)

($data.Image.Name, 'Image', 'Name', 0),

($data.Image.ImageDirectoryURL, 'Image', 'ImageDirectoryURL', 0),

($data.Image.ServerURL, 'Image', 'ServerURL', 0)

) | foreach{

if ($_[0] -eq $null) #if the parameter has'nt been provided

{# we give a default '' else flag up an error

if ($_[3] -eq -1) { $data.$_[1].$_[2] = '' }

else

{ $Errors += "There is no $($_[1]).$($_[2]) defined" }

}

elseif ($_[3] -ge 1) #it is a directory that needs to be tested

{

if (-not (Test-Path -PathType Container $_[0]))

{

if ($_[3] -eq 2)

{

New-Item -ItemType Directory -Force -Path $_[0] `

-ErrorAction silentlycontinue -ErrorVariable +Errors;

}

else { $Errors += "the path '$($_[0])'in $($_[1]).$($_[2]) does not exist" }

}

}

}

$TheLogfile="$($data.source.ReportPath)\ReportFile.txt"

if ($data.build.NewBuildInstance -eq '')

{$NewBuildServerInstance= "$($data.build.NewBuildServer)"}

else

{$NewBuildServerInstance= "$($data.build.NewBuildServer)\$($data.build.NewBuildInstance)"}

if ($NewBuildServerInstance -eq '') {$errors += 'No build server specified'}

if ($errors.Count -eq 0) #any errors are displayed at the end

{

#first make sure we can connect

$conn = new-object "$Mc.ServerConnection" $NewBuildServerInstance `

-ErrorAction silentlycontinue -ErrorVariable +Errors;

if ($data.build.username -ieq '')

{

# Crikey, this is easy, windows Passwords. Dont you love 'em?

$conn.LoginSecure = $true;

}

else

{

<# This is more elaborate a process than you might expect because we can't assume

that we can use Windows authentication, because of Azure, remote servers outside

the domain, and other such complications. We can't ever keep passwords for SQL

Server authentication as part of the static script data. At this stage, we ask

for passwords if they aren't known, and otherwise store them as secure strings

on file in the user area, protected by the workstation security.

#>

#create a connection object to manage credentials

$conn = new-object "$Mc.ServerConnection"

$conn.ServerInstance = $NewBuildServerInstance

$encryptedPasswordFile = `

"$env:USERPROFILE\$($data.build.Username)-$($conn.ServerInstance).txt"

# test to see if we know about the password in a secure string stored in the user area

if (Test-Path -path $encryptedPasswordFile -PathType leaf)

{

#has already got this set for this login so fetch it

$encrypted = Get-Content $encryptedPasswordFile | ConvertTo-SecureString

$Credentials = `

New-Object System.Management.Automation.PsCredential($data.build.Username, $encrypted)

}

else #then we have to ask the user for it

{

#hasn't got this set for this login

$Credentials = get-credential -Credential $data.build.username

$Credentials.Password | ConvertFrom-SecureString |

Set-Content $encryptedPasswordFile

}

$conn.LoginSecure = $false;

$conn.Login = $Credentials.UserName;

$conn.SecurePassword = $Credentials.Password;

}

$data.build.ServerConnection = $conn; #this sets our server connection for the build database

}

<# now that weve established all the connections we need we now build the database>

if ($errors.Count -eq 0)

{

#if the database already exists, then kill it

$TheBuildDatabase = $data.build.NewDatabase

$BuildServer = new-object ("$My.Server") $data.build.ServerConnection

if ($BuildServer.Databases[$TheBuildDatabase] -ne $null)

{ $BuildServer.KillDatabase($TheBuildDatabase) }

$BuildDatabase = `

New-Object ("$My.Database") ($BuildServer, $TheBuildDatabase)

$BuildDatabase.Create()

if ($BuildDatabase.name -ne $TheBuildDatabase)

{

$Errors += "Can't create the database '$($TheBuildDatabase)' in '$($data.build.ServerInstance)"

};

if ($data.build.ServerConnection.LoginSecure)

{

$data.build.Connection = New-DatabaseConnection `

-ServerInstance $NewBuildServerInstance `

-Database $TheBuildDatabase `

-ErrorAction silentlycontinue -ErrorVariable +Errors;

}

else

{

$data.build.Connection = New-DatabaseConnection `

-ServerInstance $NewBuildServerInstance `

-Database $TheBuildDatabase `

-Username $data.build.ServerConnection.Login `

-Password $data.build.ServerConnection.Password `

-ErrorAction silentlycontinue `

-ErrorVariable +Errors;

}

$syncResult = Sync-DatabaseSchema `

-Source $data.source.DatabasePath `

-Target $data.build.Connection `

-AbortOnWarningLevel None `

-SQLCompareOptions $data.build.SQLCompareOptions `

-ErrorAction silentlycontinue `

-ErrorVariable +Errors

}

if ($errors.Count -eq 0) # we can put the data in the database

{

<# Now we BCP all the table data in. As we are using native mode the utility

disables constraints for the table before doing the import #>

$BuildDatabase.Tables | #for every table

foreach {

$filename = "$($_.schema)_$($_.Name)" -replace '[\\\/\:\.]', '-';

$TheScriptPath = "$($data.source. DataSyncPath)\$($filename).bcp";

if (-not ($data.build.ServerConnection.LoginSecure))

{

$whatHappened = "`"$TheBuildDatabase`".`"$($_.Schema)`".`"$($_.Name)`""

$WhatHappened +=

BCP "`"$TheBuildDatabase`".`"$($_.Schema)`".`"$($_.Name)`"" `

in "`"$TheScriptPath`"" -q -n -N -E "-U$($data.build.ServerConnection.Login)" `

"-P$($data.build.ServerConnection.Password)" "-S$($data.build.ServerConnection.ServerInstance)";

}

else

{

$WhatHappened =

BCP "`"$TheBuildDatabase`".`"$($_.Schema)`".`"$($_.Name)`"" `

in "`"$TheScriptPath`"" -q -N -T -E "-S$($data.build.ServerConnection.ServerInstance)";

}

if ($WhatHappened -like '*Error *')

{ throw ("$whatHappened adding data to $TheBuildDatabase.$filename on $TheScriptPath") };

}

$result = $BuildServer.ConnectionContext.ExecuteNonQuery(

"EXEC sp_msforeachtable 'ALTER TABLE ? WITH CHECK CHECK CONSTRAINT all'");

"$($data.build.ServerConnection.ServerInstance) $TheBuildDatabase has been stocked with data"

}

<# we get through to clone>

if ($Errors.Count -eq 0)

{

#initialise SQL Clone by attempting to initate a connection with a SQL Clone Server.

Connect-SqlClone -ServerUrl $data.image.ServerURL -ErrorAction SilentlyContinue `

-ErrorVariable +Errors

}

<# now we slaughter all existing versions of the database. its so great that youve put everything worthwhile in source control and have sql prompt tab magic as well.>

if ($Errors.Count -eq 0)

{

$image = Get-SqlCloneImage `

-Name $data.image.Name -ErrorAction SilentlyContinue

if ($image -ne $null) #if it already exists

{

$clones = Get-SqlClone `

-Image $image -ErrorAction SilentlyContinue

if (!($clones -eq $null)) #delete all existing clones (normally, you'd save changes

{

$clones | Remove-SqlClone | Wait-SqlCloneOperation

}

Remove-SqlCloneImage -Image $image | Wait-SqlCloneOperation

}

}

if ($Errors.Count -eq 0)

{

Write-Verbose " Creating new image of $NewBuildServerInstance.$($data.build.NewDatabase)"

#create an image of what we built. We name it whatever we have specified

$SqlServerInstance = [RedGate.SqlClone.Client.Api.Objects.SqlServerInstanceResource](Get-SqlCloneSqlServerInstance |

Where-Object{ ($_.Serveraddress -ieq $data.build.NewBuildServer.Trim()) -and ($_.Instance.Trim() -ieq $data.build.NewBuildInstance.Trim())})

$ImageFileDestination = Get-SqlCloneImageLocation `

-Path $data.Image.ImageDirectoryURL

New-SqlCloneImage -Name $data.image.Name `

-SqlServerInstance $SqlServerInstance `

-DatabaseName $data.build.NewDatabase `

-Destination $ImageFileDestination `

-ErrorAction silentlycontinue `

-ErrorVariable +Errors | Wait-SqlCloneOperation

$ourCloneImage = Get-SqlCloneImage `

-Name $data.image.Name `

-ErrorAction SilentlyContinue

if ($ourCloneImage -eq $null)

{

$Errors += "couldn't find the clone $($data.image.Name) That has just been created"

}

if ($ourCloneImage.State -ne 'Created')

{$Errors += "We hit a problem with the image. It's state is $($ourCloneImage.State)"}

}

#clone it as whatever database is specified to whatever SQL Clone servers are specified

$data.clones | foreach {"$($_.Netname,$_.Database ) is ok"}

if ($Errors.Count -eq 0)

{

$data.clones | foreach {

$clone = $null; $Thedatabase = $_.Database;

#get the correct instance that has an agent installed on it.

$sqlServerInstance = (Get-SqlCloneSqlServerInstance | Where server -ieq $_.NetName);

if ($sqlServerInstance -eq $null) { Throw "Unable to find the clone agent for $($_.NetName)" }

write-verbose "Cloning $($_.Database) on $($_.NetName)"

$clone = Get-SqlClone `

-ErrorAction silentlyContinue `

-Name "$($TheDatabase)" `

-Location $sqlServerInstance

if (($clone) -ne $null)

{

write-warning "Removing Clone $Thedatabase that already existed on $($_.NetName)"

Remove-SqlClone $clone | Wait-SqlCloneOperation

}

Get-SqlCloneImage -Name $data.Image.Name |

New-SqlClone -Name "$($Thedatabase)" -Location $SqlServerInstance |

Wait-SqlCloneOperation

if ($errors.Count -gt 0)

{

break

}

}

}

if ($errors.Count -gt 0)

{

$errors| foreach{Write-error $_; "$((Get-Date).ToString()): $($_) the build process was aborted">>$TheLogFile;};

write-error("$($_)")

}

else

{

"$((Get-Date).ToString()): the build process had no errors">>$TheLogFile

}

结论

使用SQL Server数据库执行自动CI的方法有很多。考虑到丰富的团队方法,这也是一样。对我来说,SQL Clone的最大优势不仅在于自动化的简化和磁盘空间的节省,还在于抛弃正在测试的数据库并将其还原为构建版本的便捷性。数据库的大小越大,能够执行此操作的优势就越大。

我在这里显示的这个脚本是为了说明可能性。您可以添加许多内容,例如数据生成,脚本检查和数据屏蔽,但是SQL Clone的基本功能无疑为为更大的团队和数据库提供持续集成提供了很大的帮助。

相关推荐

sharding-jdbc实现`分库分表`与`读写分离`

一、前言本文将基于以下环境整合...

三分钟了解mysql中主键、外键、非空、唯一、默认约束是什么

在数据库中,数据表是数据库中最重要、最基本的操作对象,是数据存储的基本单位。数据表被定义为列的集合,数据在表中是按照行和列的格式来存储的。每一行代表一条唯一的记录,每一列代表记录中的一个域。...

MySQL8行级锁_mysql如何加行级锁

MySQL8行级锁版本:8.0.34基本概念...

mysql使用小技巧_mysql使用入门

1、MySQL中有许多很实用的函数,好好利用它们可以省去很多时间:group_concat()将取到的值用逗号连接,可以这么用:selectgroup_concat(distinctid)fr...

MySQL/MariaDB中如何支持全部的Unicode?

永远不要在MySQL中使用utf8,并且始终使用utf8mb4。utf8mb4介绍MySQL/MariaDB中,utf8字符集并不是对Unicode的真正实现,即不是真正的UTF-8编码,因...

聊聊 MySQL Server 可执行注释,你懂了吗?

前言MySQLServer当前支持如下3种注释风格:...

MySQL系列-源码编译安装(v5.7.34)

一、系统环境要求...

MySQL的锁就锁住我啦!与腾讯大佬的技术交谈,是我小看它了

对酒当歌,人生几何!朝朝暮暮,唯有己脱。苦苦寻觅找工作之间,殊不知今日之事乃我心之痛,难道是我不配拥有工作嘛。自面试后他所谓的等待都过去一段时日,可惜在下京东上的小金库都要见低啦。每每想到不由心中一...

MySQL字符问题_mysql中字符串的位置

中文写入乱码问题:我输入的中文编码是urf8的,建的库是urf8的,但是插入mysql总是乱码,一堆"???????????????????????"我用的是ibatis,终于找到原因了,我是这么解决...

深圳尚学堂:mysql基本sql语句大全(三)

数据开发-经典1.按姓氏笔画排序:Select*FromTableNameOrderByCustomerNameCollateChinese_PRC_Stroke_ci_as//从少...

MySQL进行行级锁的?一会next-key锁,一会间隙锁,一会记录锁?

大家好,是不是很多人都对MySQL加行级锁的规则搞的迷迷糊糊,一会是next-key锁,一会是间隙锁,一会又是记录锁。坦白说,确实还挺复杂的,但是好在我找点了点规律,也知道如何如何用命令分析加...

一文讲清怎么利用Python Django实现Excel数据表的导入导出功能

摘要:Python作为一门简单易学且功能强大的编程语言,广受程序员、数据分析师和AI工程师的青睐。本文系统讲解了如何使用Python的Django框架结合openpyxl库实现Excel...

用DataX实现两个MySQL实例间的数据同步

DataXDataX使用Java实现。如果可以实现数据库实例之间准实时的...

MySQL数据库知识_mysql数据库基础知识

MySQL是一种关系型数据库管理系统;那废话不多说,直接上自己以前学习整理文档:查看数据库命令:(1).查看存储过程状态:showprocedurestatus;(2).显示系统变量:show...

如何为MySQL中的JSON字段设置索引

背景MySQL在2015年中发布的5.7.8版本中首次引入了JSON数据类型。自此,它成了一种逃离严格列定义的方式,可以存储各种形状和大小的JSON文档,例如审计日志、配置信息、第三方数据包、用户自定...

取消回复欢迎 发表评论: