在这里,我提供了一个PowerShell脚本,您可以使用它来安全地删除所有克隆,然后准备好父图像,以便使用最新版本的数据库刷新所有开发和测试实例。至于回滚过程,此脚本旨在管理删除过程,以确保工作不会丢失。通过将删除脚本与安装脚本相结合,您可以在更新映像时刷新所有克隆,以反映原始数据库中的更改。这些脚本一起用于管理典型的配置周期,以使克隆与当前版本的数据库保持同步。

在数据库开发和测试期间安全地删除克隆和图像_数据库

将克隆更新到最新版本

在测试或开发中,确保使用已知版本的数据库(通常是最新版本)非常重要。使用SQL Clone,您可以从代表该构建的数据库中获取一个图像,再加上数据,并克隆它的相同副本。每当版本更改时,都需要将这些克隆迁移到新版本。当您使用测试和开发服务器的单元时,这可能会带来挑战。

您当然可以应用同步脚本以使每个克隆达到最新版本,但这并不总是实用的,因为每个克隆可能处于不同的状态,因此每个克隆都需要自己的同步脚本。此外,由于更改将保留在服务器上本地存储上的差异磁盘上,因此您将逐渐失去SQL Clone的一大优势,即磁盘空间的巨大节省。此外,在这样做的过程中,您将解决一个已不复存在的问题,即复制数据库所需的时间。使用SQL Clone,只需几秒钟。

使用克隆时,它会好得多 - 一旦成功制作了新的构建并存储了适当的数据并进行了测试 - 删除所有克隆,然后删除它们使用的图像。完成后,您可以使用相同的名称从成功构建中创建新图像,并以其先前的名称重新创建新克隆。作为对此的改进,使用大型开发或测试数据库,您可以在删除旧克隆并使用新图像重新创建它们然后最终删除旧图像之前,首先以不同的名称创建新图像。

实际上,通过使用Remove-SqlCloneRemove-SqlCloneImagecmdlet ,在GUI或PowerShell中删除旧克隆和父图像是一个非常简单的过程。


<#现在我们非常简单地删除每个克隆#>
 
   $ image  =  Get-SqlCloneImage  - 命名 $ data 。图片。名称
 
   #with图像对象,我们现在可以删除克隆
 
   Get-SqlClone  - 图片 $ image  |  foreach  {
 
     $ _  |  Remove-SqlClone  |  等待,SqlCloneOperation
 
   } ;
 
 $ null  =  Remove-SqlCloneImage  - Image  $ Image


这有可能满足您的需求吗?如果是这样,请继续阅读。

但是,开发人员面临的主要问题是,在克隆上执行某些脚本非常容易 - 例如创建例程或重新设计某些表 - 并且忘记将工作保存到源代码控制中。更糟糕的是,有人可能仍在使用他们的本地克隆,突然它消失了!是的,除非您先检查上次读取或写入的时间,或其他一些活动度量,否则在数据库中存在活动用户时完全可以删除克隆。一旦删除克隆,删除图像也非常容易,但这不太可能导致附带损害。

这意味着为了安全起见,我们可能希望在删除克隆之前运行一些检查。该Remove-SqlClonecmdlet不支持使用克隆的模板,这样的克隆被破坏之前,我们无法通过GUI运行使用这些cmdlet或任何SQL查询。

如果您感到幸运或者没有严格的测试时间表,这很好。但是,我将展示如何解决这些问题并安全地删除克隆。

删除旧克隆和图像之前的安全检查

删除克隆可能需要执行其他SQL脚本,例如导出自动测试的结果。您可以在配置文件中将这些指定为BeforeDeleteScript。您可能还需要使用外部工具来保存最近的架构更改。例如,我们将使用SQL Compare保存当前克隆和图像之间的所有差异。我们无法直接与图像进行比较,也不愿意。相反,我们创建一个“引用”克隆,我将其称为“原始”,将其设置为只读,并与原始克隆进行比较并保存生成的同步脚本。这与我在脚本中用于将克隆恢复到其原始状态的技术相同,如第一篇文章中所述。

在这里,我们还需要确定是否有人仍在积极使用克隆。仅检查打开的SPID是没有用的; 我们需要找出他们最近如何使用系统,我们可以使用这样的查询来做(当然,你需要提供数据库的名称!):


使用大师


SELECT Coalesce(Min(DateDiff(MINUTE,last_read,GetDate())),20000)AS MinsSinceLastRead,
 
        合并(Min(DateDiff(MINUTE,last_write,GetDate())),20000)AS MinsSinceLastwrite
 
      从
 
    sys .dm_exec_connections A.
 
         INNER JOIN sys .dm_exec_sessions B ON
 
             A .session_id = B .session_id
 
 WHERE database_id = Db_Id('<NameOfDatabase>')


我们从RemoveClonesandImage PowerShell脚本(稍后介绍)运行此查询,并在初始检查后终止该脚本,如果查询返回的结果表明任何克隆中有最近的活动。我使用最后四十分钟作为阈值,但您可以在PowerShell脚本中更改此值。

RemoveClonesandImage脚本

如果您的需求是针对可以自动执行更复杂的配置任务的常规流程,则使用此脚本。如果您只是通过基本的PowerShell脚本进行简单的删除过程,那么请查看SQL Clone文档,其中有几个示例。

此脚本使用我们在前两篇文章中使用的相同PowerShell配置数据文件。这将允许您从使用SQL Compare的“逆止器比较过程”中选择克隆来保存任何可能未保存的工作。配置文件的结构如下:

在数据库开发和测试期间安全地删除克隆和图像_sql_02

您可以访问RemoveClonesandImage.ps1脚本,如下所示,以及本系列文章中有关使用克隆进行开发和测试工作的所有其他脚本:https://github.com/Phil-Factor/SQLCloneFamily。


 


$ VerbosePreference  =  “继续”
 
<#
 
此powershell脚本删除图像,首先删除所有克隆并备份所有克隆
 
如果需要,可以更改元数据。它还会在删除之前检查克隆
 
确保没有当前活动* /它允许您指定一个或多个
 
在删除克隆之前要使用的其他SQL脚本。
 
#>
 
#set“Option Explicit”来捕捉细微的错误
 
set-psdebug  - 严格
 
$ ErrorActionPreference  =  “停止”
 
 
 
<#first,找出我们的执行地点,以便我们确保获取数据#>
 
尝试
 
{  $ executablepath  =  [ 系统。IO 。路径] ::GetDirectoryName ($ myInvocation 。mycommand的。定义) }
 
抓住
 
{
 
 $ executablepath  =  “ $(如果 ($ psISE )
 
   {  Split-Path  - Path  $ psISE 。CurrentFile 。FullPath  }
 
   否则 {  $ global:PSScriptRoot  } )“
 
}
 
 
 
<#只是为了让它更容易理解,各种参数值的结构都是一个
 
层次结构。我们在制作或更新它们时迭代克隆#>
 
$ Errors  =  @()
 
#First我们从文件中读取配置(这样做我们也可以使用ISE)
 
尝试 {
 
   $ Data  =  &“ $ executablePath \ CloneConfig.ps1”
 
   }
 
抓住
 
   {
 
   $ Errors  + = “无法访问$ executablePath \ CloneConfig.ps1中的配置文件”
 
   }
 
 
 
<#我们将数据作为结构读入。#>
 
 
 
<#现在我们需要找出我们需要用来与克隆进行比较的克隆
 
我们希望恢复以保存任何差异。#>
 
$ originalClone  =  @()
 
$ data 。克隆 |  foreach  {
 
 如果 ($ _ 。IsOriginal  当量 $真实)
 
 {  $ originalClone  =  $ _  } ;
 
}
 
<#检查我们是否已正确使用#>
 
如果 ($ originalClone 。IsOriginal  -ne  $真)
 
{
 
 $ errors  + =  '您还没有定义哪个克隆代表原始'
 
}
 
 
 
Connect-SQLClone  - ServerUrl  $ data 。图片。SERVERURL    `
 
        - ErrorAction  silentlyContinue    `
 
        - ErrorVariable  + 错误
 
如果 ($错误。计数 -eq  0 )
 
{
 
 $ image  =  Get-SqlCloneImage  - 命名 $ data 。图片。姓名    `
 
                - ErrorAction  默默地继续    `
 
                - ErrorVariable  + 错误
 
 
 
 如果 ($错误。计数 -gt  0 )
 
 {  Write-Warning  “ 无法找到图像$ data .Image.Name”  }
 
}
 
 
 
#我们需要获取具有用户标识的任何连接的密码并附加它
 
#将每个克隆对象作为凭证供以后使用。
 
#我们将这些凭据保存在用户区域内的文件中,依赖于NTFS安全性和
 
#encryption(gulp)
 
#我们只为每台服务器要求输入一次密码。如果您更改密码
 
#您需要删除用户区域中的相应文件。
 
如果 ($错误。计数 -eq  0 )
 
{
 
 $ data 。克隆 |  foreach  {
 
   if  ($ _ 。用户名 -ine  '' )
 
   {
 
     #create连接对象以管理凭据
 
     $ encryptedPasswordFile  =  “ $ ENV:USERPROFILE \ $($ _ 。用户名)- $($ _ 。NETNAME ).TXT”
 
     #test以查看我们是否知道密码而不是存储在用户区域中的安全字符串
 
     if  (Test-Path  - path  $ encryptedPasswordFile  - PathType  leaf )
 
     {
 
       #has已经为此登录设置了此设置,因此获取它
 
       $ encrypted  =  Get-Content  $ encryptedPasswordFile  |  的ConvertTo-SecureString的
 
       $ _ 。凭证 =  新对象 系统。管理。自动化。PsCredential ($ _ 。用户名, $加密)
 
     }
 
     否则 #then我们必须要求用户
 
     {
 
       #还没有为此登录设置此设置
 
       $ _ 。凭据 =  get-credential  - 凭据 $用户名
 
       $ _ 。凭证。密码 |  ConvertFrom-SecureString  |
 
       Set-Content  “ $ env:USERPROFILE \ $ SourceLogin-$ SourceServerName .txt“
 
     }
 
 
 
   }
 
 }
 
}
 
如果 ($的数据。工具。SQLCompare  -ne  $空)
 
<#we定义SQLCompare别名以使其更容易调用。如果用户没有定义的位置
 
工具se根本就不做比较#>
 
{
 
 Set-Alias  SQLCompare  $ data 。工具。SQLCompare  - 范围 脚本;
 
 $ NoSQLCompare  =  $ false
 
}
 
其他
 
{  $ NoSQLCompare  =  $ true  }
 
 
 
<#now现在我们遍历原始克隆以外的克隆,如果需要SQL Compare,我们就这样做
 
我们还检查以确保没有使用任何克隆。最后,我们运行任何或所有脚本
 
指定在销毁克隆之前运行。#>
 
如果 ($错误。计数 -eq  0 )
 
{
 
 $ data 。克隆 |
 
 当 {   (-不是 (($ _ 。数据库 -eq  $ originalClone 。数据库) -and  ($ _ 。NETNAME  -eq  $ originalclone 。NETNAME ))) }  |
 
 #不要做原件因为无论如何都不能写。
 
   foreach  {
 
       #我们使用这个字符串非常值得计算一次
 
       $ OurDB = “ $($ _ 。数据库)在$($ _ 。网络名)”
 
       write-verbose  “Checking   $ OurDB ”
 
   $ CloneIsThere  =  $ True ;  #assume yes,除非另有证明
 
   $ sqlServerInstance  =  (GET-SqlCloneSqlServerInstance  - ErrorAction  SilentlyContinue  |
 
                                当 服务器 -ieq  $ _ 。NetName );  #test如果它在那里
 
   if  ($ sqlServerInstance  -eq  $ null )
 
   {
 
     write-verbose  “ 找不到克隆$ OurDB ” ;  $ CloneIsThere  =  $ false
 
   }
 
       其他
 
       {
 
   $克隆 =  GET-SqlClone   `
 
           - ErrorAction  silentlyContinue   `
 
           - 名称 “ $($ _ 。数据库)”   `
 
           - 位置 $ sqlServerInstance
 
   if  ($ clone  -eq  $ null )
 
           {   #因为它不在那里
 
           write-verbose  “Clone $ OurDB 不存在” ;
 
           $ CloneIsThere  =  $ false  } ;
 
           }
 
       #We只做我们能做的比较,它在数据中指定,如果需要这个克隆
 
   如果 ($ _ 。NOCHECK  -ne  $真 -and  $ CloneIsThere  当量 $真实的 -和 $ NoSQLCompare  -eq  $假)
 
   {
 
     write-verbose  “检查克隆$ OurDB 与$($ OriginalClone 。网络名称)相比   是否有任何变化$($ OriginalClone 。数据库)”
 
           <#我们检查以确保工作目录#>存在路径
 
     如果 (-not  (测试的路径 - PathType  集装箱 “ $($的数据。WorkDirectory )” ))
 
     {  #if路径不存在,我们创建它
 
       新建项目 - ItemType的 目录 - 力 - 路径 “ $($的数据。WorkDirectory )”  `
 
            - ErrorAction  silentlycontinue  - ErrorVariable  + 错误;
 
     }
 
           #我们计算文件的名称,在哪里放置显示更改的脚本
 
     $ OutputMigrationScript  =  “ $($的数据。WorkDirectory )\ $($ _ 。数据库)- $($ OriginalClone 。数据库)”
 
     #如果那里已有脚本文件,我们重命名它
 
     if  (Test-Path  - PathType  Leaf  “ $ OutputMigrationScript .sql” )
 
     {
 
       rename-item  - literalpath  “ $ OutputMigrationScript .sql”  - NewName  “ $ OutputMigrationScript$(Get-Date  - format  FileDateTime ).sql“  - 强制 `
 
             - ErrorAction  silentlycontinue  - ErrorVariable  + 错误;
 
     }
 
<#我们汇编了SQL Compare#>所需的所有命令行参数
 
     $ AllArgs  =  @(“/服务器1:$($ OriginalClone 。NETNAME )” , #源服务器
 
       “/ database1:$($ OriginalClone 。数据库)” , #源服务器上源数据库的名称
 
       “/服务器2:$($ _ 。网络名)” , #the克隆
 
       “/ database2:$($ _ 。数据库)” , #克隆服务器上数据库的名称
 
       “/ scriptfile:$($ OutputMigrationScript ).sql” ,
 
       “/ include:相同” )
 
<#如果需要,我们添加额外的参数来处理sql server authentication#>
 
     如果 ($ OriginalClone 。用户名 -ne  '' )
 
     {
 
       $ AllArgs  + =  “/密码1:$($ OriginalClone 。凭证。GetNetworkCredential ()。密码)”
 
       $ AllArgs  + =  “/ USERNAME1:$($ OriginalClone 。用户名)”
 
     }
 
     如果 ($ _ 。用户名 -ne  '' )# 它必须是SQL Server身份验证
 
     {
 
       $ AllArgs  + =  “/密码2:$($ _ 。凭证。GetNetworkCredential ()。密码)”
 
       $ AllArgs  + =  “/ username2:$($ _ 。用户名)”
 
     }
 
<#now现在我们可以在最后运行SQL Compare来保存脚本更改以防万一#>
 
     SQLCompare  @AllArgs   >  “ $($ OutputMigrationScript ).txt”  #save输出
 
     if  ($?) {  “现在比较了克隆(参见$($ OutputMigrationScript ).txt)”  }
 
     其他
 
     {
 
       if  ($ LASTEXITCODE  -eq  63 ) {  '数据库相同'  }
 
       else  {  $ errors  + =  “我们有比较错误!(代码$ LASTEXITCODE )”  }
 
     }
 
   }
 
<#now现在我们在删除之前运行任何必要的脚本,如数据文件#>中指定的那样    
 
   if  ($ CloneIsThere  -eq  $ true ) #we只在克隆仍在那里时才这样做
 
   { #我们创建一个连接字符串来运行一些SQL
 
     $的ConnectionString  =  “数据源= $($ _ 。NETNAME );初始目录= $($ _ 。数据库);”
 
     if  ($ _ 。用户名 -ieq  '' ) #no 用户名。Windows身份验证
 
     {
 
       $ ConnectionString  + =  ';集成安全性= SSPI;'
 
     }
 
     否则 #我们需要获得该密码。
 
     {
 
       $的ConnectionString  + =  “UID = $($ _ 。用户名); PWD =” “ ($ $ _ 。凭证。GetNetworkCredential ()。密码)”“;”
 
     }
 
     $ SqlConnection  =  新对象 系统。数据。SqlClient 。SqlConnection ($ connectionString )
 
     #打开连接
 
     $ SqlConnection 。打开()
 
           #创建一个命令
 
     $ sqlCommand  =  $ sqlConnection 。CreateCommand ()
 
           #首先,我们查询最近查看此日期库中的活动
 
     $ sqlCommand 。CommandText  =  “USE master
 
           SELECT  Coalesce (Min  (DateDiff (MINUTE ,last_read , GetDate ())), 20000 )
 
                    AS  MinsSinceLastRead ,
 
                  合并(Min  (DateDiff (MINUTE ,last_write , GetDate ())), 20000 )
 
                    AS  MinsSinceLastwrite
 
                来自 sys 。dm_exec_connections  A.
 
                   INNER  JOIN  系统。dm_exec_sessions  B  ON
 
                       一。SESSION_ID  =  乙。SESSION_ID
 
           WHERE  database_id  = Db_Id ('$($ _。数据库)' )“
 
     $ reader = $ sqlCommand 。ExecuteReader ()
 
           如果 ($读者。HasRows ) #WE读返回的数据。
 
                {
 
                while  ($ reader 。阅读())
 
                   {
 
                       $ MinsSinceLastRead = $读者。GetInt32 (0 );
 
                       if  ($ MinsSinceLastRead  -lt  30 )
 
                         { $错误+ = “A用户读取数据仅$ MinsSinceLastRead 分钟前在$ OurDB ” }
 
                       $ MinsSinceLastWrite = $读者。GetInt32 (1 );
 
                       if  ($ MinsSinceLastWrite  -lt  30 )
 
                         { $ errors + = “用户仅在$ OurDB 上写了$ MinsSinceLastWrite 分钟前的数据” }
 
                   }
 
                }
 
       }
 
       <#now now now执行数据#>指定的任何额外SQL脚本
 
   如果 ($ _ 。BeforeDeleteScripts  -ne  $空)
 
   {
 
     $ _ 。BeforeDeleteScripts 。GetEnumerator () |  foreach  {  #do each script
 
       $ sqlCommand 。的CommandText  =  ([ IO ,文件] ::ReadAllText ($ _ ))
 
       $ sqlCommand 。ExecuteNonQuery ()
 
     }
 
   }
 
 }
 
}
 
<#现在我们删除克隆和图像#>
 
 
 
如果 ($错误。计数 -eq  0 )
 
{ <#现在我们非常简单地删除每个克隆#>
 
 $ image  =  Get-SqlCloneImage  - 命名 $ data 。图片。名称
 
 #with图像对象,我们现在可以删除克隆
 
 Get-SqlClone  - 图片 $ image  |  foreach  {
 
   写冗长 “现在删去$($ _ 。名称)在$( (GET-SqlCloneSqlServerInstance  |  其中 编号 当量 $ _ 。LocationId )。ServerAddress )”
 
   $ _  |  Remove-SqlClone  |  等待,SqlCloneOperation
 
 } ;
 
 写冗长 “现在移除图像$($图片。名称)取自$($图像。OriginServerName )。$($图像。OriginDatabaseName )”
 
 $ null  =  Remove-SqlCloneImage  - Image  $ Image
 
} ;
 
<#我们收集所有的软错误并在这里处理它们。#>
 
如果 ($错误。计数 -gt  0 )
 
{
 
 $ errors  |  foreach  {
 
   写错误 $ _ ;  “ $( (获取最新)。的ToString ()):$($ _ )的图像删除已中止” > > “ $($的数据。WorkDirectory )\ Errors.log” ;
 
 
 
   写入错误(“ $($ _ )” )
 
 }
 
} ;


重新创建新图像和克隆

删除所有克隆,确保不中断正在进行的开发工作,也不丢失任何未保存的更改,该过程的下一部分是使用最新构建的最新图像并使用以下方法创建一批新的克隆CreateOrRenew.ps1脚本(在此处描述)。

每个新克隆很可能需要根据该克隆用户的需求和访问控制要求进行自定义,因此使用共享配置文件可确保在创建新克隆时保持这些差异。运行此“刷新”过程中,每一次,CreateOrRenew脚本将使用在配置数据文件中指定的图像修改和克隆的模板,如所描述这里。

当您需要将克隆恢复或回滚到其原始状态时(例如,在影响架构或数据的测试运行之后),您可以使用RollbackClone.ps1脚本,它将重新应用任何现有的克隆模板。

结论

SQL Clone可以通过提供的GUI轻松管理,PowerShell脚本的功能通常保持同步,这样无论您在GU中可以做什么,它都可以在PowerShell中实现。这意味着任何常规流程一旦稳定下来就可以轻松实现自动化。

使用共享数据结构而不是使用基于函数的过程方法生成一系列脚本的方法对于应用程序开发人员来说可能是一种文化冲击。从我自己与Ops人的工作以及当我在Ops团队工作时,我发现这是首选的方法,这就是为什么Bash和DOS脚本的艺术至今仍然处于健康状态。这意味着可以安排并(希望)保留脚本,允许团队完全根据数据结构和检查日志进行任何更改。

在PowerShell中所有这些都是自动化的方式有很多选择,在这里,我只是说明了一种方法,但我应该很容易将我所做的作为一个例子或一个起点。