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

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

ztj100 2025-04-07 21:25 11 浏览 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的基本功能无疑为为更大的团队和数据库提供持续集成提供了很大的帮助。

相关推荐

30天学会Python编程:16. Python常用标准库使用教程

16.1collections模块16.1.1高级数据结构16.1.2示例...

强烈推荐!Python 这个宝藏库 re 正则匹配

Python的re模块(RegularExpression正则表达式)提供各种正则表达式的匹配操作。...

Python爬虫中正则表达式的用法,只讲如何应用,不讲原理

Python爬虫:正则的用法(非原理)。大家好,这节课给大家讲正则的实际用法,不讲原理,通俗易懂的讲如何用正则抓取内容。·导入re库,这里是需要从html这段字符串中提取出中间的那几个文字。实例一个对...

Python数据分析实战-正则提取文本的URL网址和邮箱(源码和效果)

实现功能:Python数据分析实战-利用正则表达式提取文本中的URL网址和邮箱...

python爬虫教程之爬取当当网 Top 500 本五星好评书籍

我们使用requests和re来写一个爬虫作为一个爱看书的你(说的跟真的似的)怎么能发现好书呢?所以我们爬取当当网的前500本好五星评书籍怎么样?ok接下来就是学习python的正确姿...

深入理解re模块:Python中的正则表达式神器解析

在Python中,"re"是一个强大的模块,用于处理正则表达式(regularexpressions)。正则表达式是一种强大的文本模式匹配工具,用于在字符串中查找、替换或提取特定模式...

如何使用正则表达式和 Python 匹配不以模式开头的字符串

需要在Python中使用正则表达式来匹配不以给定模式开头的字符串吗?如果是这样,你可以使用下面的语法来查找所有的字符串,除了那些不以https开始的字符串。r"^(?!https).*&...

先Mark后用!8分钟读懂 Python 性能优化

从本文总结了Python开发时,遇到的性能优化问题的定位和解决。概述:性能优化的原则——优化需要优化的部分。性能优化的一般步骤:首先,让你的程序跑起来结果一切正常。然后,运行这个结果正常的代码,看看它...

Python“三步”即可爬取,毋庸置疑

声明:本实例仅供学习,切忌遵守robots协议,请不要使用多线程等方式频繁访问网站。#第一步导入模块importreimportrequests#第二步获取你想爬取的网页地址,发送请求,获取网页内...

简单学Python——re库(正则表达式)2(split、findall、和sub)

1、split():分割字符串,返回列表语法:re.split('分隔符','目标字符串')例如:importrere.split(',','...

Lavazza拉瓦萨再度牵手上海大师赛

阅读此文前,麻烦您点击一下“关注”,方便您进行讨论和分享。Lavazza拉瓦萨再度牵手上海大师赛标题:2024上海大师赛:网球与咖啡的浪漫邂逅在2024年的上海劳力士大师赛上,拉瓦萨咖啡再次成为官...

ArkUI-X构建Android平台AAR及使用

本教程主要讲述如何利用ArkUI-XSDK完成AndroidAAR开发,实现基于ArkTS的声明式开发范式在android平台显示。包括:1.跨平台Library工程开发介绍...

Deepseek写歌详细教程(怎样用deepseek写歌功能)

以下为结合DeepSeek及相关工具实现AI写歌的详细教程,涵盖作词、作曲、演唱全流程:一、核心流程三步法1.AI生成歌词-打开DeepSeek(网页/APP/API),使用结构化提示词生成歌词:...

“AI说唱解说影视”走红,“零基础入行”靠谱吗?本报记者实测

“手里翻找冻鱼,精心的布局;老漠却不言语,脸上带笑意……”《狂飙》剧情被写成歌词,再配上“科目三”背景音乐的演唱,这段1分钟30秒的视频受到了无数网友的点赞。最近一段时间随着AI技术的发展,说唱解说影...

AI音乐制作神器揭秘!3款工具让你秒变高手

在音乐创作的领域里,每个人都有一颗想要成为大师的心。但是面对复杂的乐理知识和繁复的制作过程,许多人的热情被一点点消磨。...

取消回复欢迎 发表评论: