【问题标题】:SQL Server Multiple DDLs Ignoring Order And in a Single TransactionSQL Server 多个 DDL 忽略顺序并在单个事务中
【发布时间】:2013-06-20 10:50:05
【问题描述】:

我正在尝试在 SQL Server 上运行多个 DDL(大约 90 个)。

DDL 不包含对表的任何更改,仅包含视图、存储过程和函数。 DDL 之间可能存在相互依赖关系,例如,一个 STP 调用另一个 STP。

我不想开始以正确的顺序组织文件,因为这会花费很长时间,并且如果任何一个脚本出现错误,我希望整个操作都失败。

我怎样才能做到这一点?

到目前为止,我的想法是启动事务,告诉 SQL 忽略错误(我不知道该怎么做)运行一次所有脚本,告诉 SQL 再次开始抛出错误,运行所有脚本再次提交,如果一切成功则提交。

  1. 这是个好主意吗?
  2. 我如何CREATE \ ALTER 存储过程或视图,即使它有错误?

澄清和解决一些问题...

这不适用于生产。我只是不想让我正在测试的数据库损坏。

我想要实现的是:在服务器上运行一大组脚本,而无需花时间订购它们。但是如果任何脚本中有错误,我想回滚整个操作。

我不关心隔离,我只希望操作作为单个事务发生。

【问题讨论】:

    标签: sql sql-server tsql ddl


    【解决方案1】:

    以正确的顺序组织文件,在测试环境中测试过程,进行验证和验收测试,然后在生产中运行它。

    虽然在事务中运行 DDL 似乎可行,但实际上并非如此。有许多 DDL 语句不能很好地与事务混合。您必须使应用程序脱机,在架构更改之前进行数据库备份(或创建快照),运行经过测试和验证的升级过程(您的脚本),通过验收测试验证结果,然后将应用程序重新联机。如果出现故障,请恢复到最初创建的备份(对任何下游日志使用者的所有影响,如复制、日志传送或镜像)。

    这是正确的方法,就我而言,这是唯一的方法。我知道您会找到很多关于如何以错误方式执行此操作的建议。

    【讨论】:

    • 绝对的;另一件事是,如果您的对象不是模式绑定的,即使它引用不存在的列,SQL Server 也不会在您的 DDL 中引发错误。这不是与 SQL Server 配置相关的东西。参见http://msdn.microsoft.com/en-us/library/ms187956.aspx,schemabindnig 段落。
    • 这是用于测试环境的...我只是在确保脚本正常的同时尽量不破坏环境...
    • 我也知道 DDL 不适用于快照隔离,并且所有 ALTER DATABASE 语句都不适用于事务。如果更改失败,我只想回滚更改...
    • @OzrenTkalčecKrznarić 如果我查看视图并尝试使用不存在的列更改它,则命令失败并显示“Msg 207”
    • @AK_ 这是一般情况。 CREATE/ALTER 视图将在缺少引用的列时失败。 CREATE/ALTER SP 在整个引用表丢失时不会失败。事实上,您可以在表上执行 DROP COLUMN 并将引用它的视图保持在“损坏”状态,而对此一无所知。只要对象不是模式绑定的。
    【解决方案2】:

    我们实际上做了这样的事情来将我们的数据库脚本部署到生产环境中。我们在连接到我们数据库的应用程序中执行此操作。更复杂的是,我们还有 600 个数据库应该具有相同的模式,但实际上并没有。这是我们的方法:

    1. 将我们所有的脚本合并到一个大文件中。在每个文件之间注入 go's。这使它看起来像是一个很长的脚本。我们根据编码员的要求进行简单的订购。
    2. 将所有内容拆分为“go blocks”。由于 go 不是合法的 sql,我们将它们分成多个块,一次执行一个。
    3. 打开数据库连接。
    4. 开始交易。
    5. 对于每个 go 块:
      1. 确保事务仍处于活动状态。 (这非常重要。我稍后会解释原因。)
      2. 运行代码,记录错误。
    6. 如果有任何错误,回滚。否则,提交。

    在我们的多数据库设置中,我们将整个事情做了两次。对每个数据库运行一次,“测试”代码以确保任何数据库都没有错误,然后返回并再次“真正地”运行它们。

    现在说明您需要确保交易仍然有效的原因。有一些命令会在出错时回滚您的事务!想象一下我们第一次发现这一点时的惊讶......错误之前的所有内容都被回滚,但之后的所有内容都已提交。但是,如果出现错误,则同一个块中的任何内容都不会被提交,所以一切都很好。

    下面是我们执行代码的核心。我们在 SqlClient 周围使用了一个包装器,但它看起来应该与 SqlClient 非常相似。

    Dim T = New DBTransaction(client)
    For Each block In scriptBlocks
        If Not T.RestartIfNecessary Then
            exceptionCount += 1
            Log("Could not (re)start the transaction for {0}. Not executing the rest of the script.", scriptName)
            Exit For
        End If
        Debug.Assert(T.IsInTransaction)
        Try
            client.Text = block
            client.ExecNonQuery()
        Catch ex As Exception
            exceptionCount += 1
            Log(ex.Message + " on {0} executing: '{1}'", client.Connection.Database, block.Replace(vbNewLine, ""))
        End Try
    Next
    If exceptionCount > 0 Then Log("There were {0} exceptions while executing {1}.", exceptionCount, scriptName)
    If testing OrElse
        exceptionCount > 0 Then
        Try
            T.Rollback()
            Log("Rolled back all changes for {0} on {1}.", scriptName, client.Connection.Database)
        Catch ex As Exception
            Log("Could not roll back {0} on {1}: {2}", scriptName, client.Connection.Database, ex.Message)
            If Debugger.IsAttached Then
                Debugger.Break()
            End If
        End Try
    
    Else
        T.Commit()
        Log("Successfully committed all changes for {0} on {1}.", scriptName, client.Connection.Database)
    End If
    Return exceptionCount
    
    
    
    
    Class DBTransaction
        Private _tName As String
        Public ReadOnly Property name() As String
            Get
                Return _tName
            End Get
        End Property
        Private _client As OB.Core2.DB.Client
        Public Sub New(client As OB.Core2.DB.Client, Optional name As String = Nothing)
            If name Is Nothing Then
                name = "T" & Guid.NewGuid.ToString.Replace("-", "").Substring(0, 30)
            End If
            _tName = name
            _client = client
        End Sub
        Public Function Begin() As Boolean
            Return RestartIfNecessary()
        End Function
        Public Function RestartIfNecessary() As Boolean
            Try
                _client.Text = "IF NOT EXISTS (Select transaction_id From sys.dm_tran_active_transactions where name = '" & name & "') BEGIN BEGIN TRANSACTION " & name & " END"
                _client.ExecNonQuery()
                Return IsInTransaction()
            Catch ex As Exception
                Return False
            End Try
        End Function
        Public Function IsInTransaction() As Boolean
                _client.Text = "Select transaction_id From sys.dm_tran_active_transactions where name = '" & name & "'"
                Dim scalar As String = _client.ExecScalar
                Return scalar <> ""
        End Function
        Public Sub Rollback()
            _client.Text = "ROLLBACK TRANSACTION " & name
            _client.ExecNonQuery()
        End Sub
        Public Sub Commit()
            _client.Text = "COMMIT TRANSACTION " & name
            _client.ExecNonQuery()
        End Sub
    End Class
    

    【讨论】:

    • 我已经开始编写一个 F# 脚本来做类似的事情......我可能会在某个时候发布源代码......
    【解决方案3】:

    你有一个很好的答案,这里是“hack”答案。对于“你不能这样做,但如果你非常想要,那就继续”的情况。我很有信心你不会实现你的想法,因此

    做完整备份!

    假设这些文件中没有COMMITGO 语句(显式或!隐式!),您唯一需要做的就是在单个事务中运行它们。将它们合并到一个文件中,包装在一个事务中,然后运行。

    如何将90个文件合并为一个文件:

    如果按名称排序使它们按正确的顺序排列,则在命令提示符下从包含文件的文件夹中运行此命令:

    FOR /F "tokens=1" %G IN ('dir /b /-d /o:n *.sql') DO (
        type %G >> Big_SQL_Script.sql && echo. >> Big_SQL_Script.sql 
        )
    

    如果顺序是随机的,则创建一个文件列表dir /b /-d *.sql &gt; File_Name_List.txt 并手动对其进行排序。然后运行:

    FOR /F "tokens=1" %G IN (File_Name_List.txt) DO (
        type %G >> Big_SQL_Script.sql && echo. >> Big_SQL_Script.sql 
        )
    

    通过这种方式,您可以按自动顺序连接 90 个文件。运行看看会发生什么。

    祝你好运!

    【讨论】:

      猜你喜欢
      • 2021-03-14
      • 1970-01-01
      • 2019-06-09
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-11-27
      • 2023-03-27
      • 1970-01-01
      相关资源
      最近更新 更多