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

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

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

相关推荐

Vue 技术栈(全家桶)(vue technology)

Vue技术栈(全家桶)尚硅谷前端研究院第1章:Vue核心Vue简介官网英文官网:https://vuejs.org/中文官网:https://cn.vuejs.org/...

vue 基础- nextTick 的使用场景(vue的nexttick这个方法有什么用)

前言《vue基础》系列是再次回炉vue记的笔记,除了官网那部分知识点外,还会加入自己的一些理解。(里面会有部分和官网相同的文案,有经验的同学择感兴趣的阅读)在开发时,是不是遇到过这样的场景,响应...

vue3 组件初始化流程(vue组件初始化顺序)

学习完成响应式系统后,咋们来看看vue3组件的初始化流程既然是看vue组件的初始化流程,咋们先来创建基本的代码,跑跑流程(在app.vue中写入以下内容,来跑流程)...

vue3优雅的设置element-plus的table自动滚动到底部

场景我是需要在table最后添加一行数据,然后把滚动条滚动到最后。查网上的解决方案都是读取html结构,暴力的去获取,虽能解决问题,但是不喜欢这种打补丁的解决方案,我想着官方应该有相关的定义,于是就去...

Vue3为什么推荐使用ref而不是reactive

为什么推荐使用ref而不是reactivereactive本身具有很大局限性导致使用过程需要额外注意,如果忽视这些问题将对开发造成不小的麻烦;ref更像是vue2时代optionapi的data的替...

9、echarts 在 vue 中怎么引用?(必会)

首先我们初始化一个vue项目,执行vueinitwebpackechart,接着我们进入初始化的项目下。安装echarts,npminstallecharts-S//或...

无所不能,将 Vue 渲染到嵌入式液晶屏

该文章转载自公众号@前端时刻,https://mp.weixin.qq.com/s/WDHW36zhfNFVFVv4jO2vrA前言...

vue-element-admin 增删改查(五)(vue-element-admin怎么用)

此篇幅比较长,涉及到的小知识点也比较多,一定要耐心看完,记住学东西没有耐心可不行!!!一、添加和修改注:添加和编辑用到了同一个组件,也就是此篇文章你能学会如何封装组件及引用组件;第二能学会async和...

最全的 Vue 面试题+详解答案(vue面试题知识点大全)

前言本文整理了...

基于 vue3.0 桌面端朋友圈/登录验证+60s倒计时

今天给大家分享的是Vue3聊天实例中的朋友圈的实现及登录验证和倒计时操作。先上效果图这个是最新开发的vue3.x网页端聊天项目中的朋友圈模块。用到了ElementPlus...

不来看看这些 VUE 的生命周期钩子函数?| 原力计划

作者|huangfuyk责编|王晓曼出品|CSDN博客VUE的生命周期钩子函数:就是指在一个组件从创建到销毁的过程自动执行的函数,包含组件的变化。可以分为:创建、挂载、更新、销毁四个模块...

Vue3.5正式上线,父传子props用法更丝滑简洁

前言Vue3.5在2024-09-03正式上线,目前在Vue官网显最新版本已经是Vue3.5,其中主要包含了几个小改动,我留意到日常最常用的改动就是props了,肯定是用Vue3的人必用的,所以针对性...

Vue 3 生命周期完整指南(vue生命周期及使用)

Vue2和Vue3中的生命周期钩子的工作方式非常相似,我们仍然可以访问相同的钩子,也希望将它们能用于相同的场景。...

救命!这 10 个 Vue3 技巧藏太深了!性能翻倍 + 摸鱼神器全揭秘

前端打工人集合!是不是经常遇到这些崩溃瞬间:Vue3项目越写越卡,组件通信像走迷宫,复杂逻辑写得脑壳疼?别慌!作为在一线摸爬滚打多年的老前端,今天直接甩出10个超实用的Vue3实战技巧,手把...

怎么在 vue 中使用 form 清除校验状态?

在Vue中使用表单验证时,经常需要清除表单的校验状态。下面我将介绍一些方法来清除表单的校验状态。1.使用this.$refs...

取消回复欢迎 发表评论: