SQL Server 管理对象 (SMO) 的默认约束不一致

Posted

技术标签:

【中文标题】SQL Server 管理对象 (SMO) 的默认约束不一致【英文标题】:Inconsistent default constraints from SQL Server Management Objects (SMO) 【发布时间】:2014-12-16 06:29:12 【问题描述】:

我的程序使用 SQL Server 管理对象 (SMO) 为 Microsoft SQL Server 数据库生成 DDL 脚本。但是,根据服务器和数据库,我收到的表的默认约束输出不一致。有时它们与CREATE TABLE 语句内联,有时它们是独立的ALTER TABLE 语句。我意识到两者都是有效且正确的 SQL 语句,但如果没有一致性,它会阻止多个数据库的输出之间的自动比较,并阻止将输出添加到源代码控制以跟踪数据库模式的更改。 如何确保默认约束的脚本输出的一致性?

示例程序

代码应该是直截了当的。打开服务器和数据库,然后为每个数据库对象生成单独的脚本文件以及一个包含整个数据库脚本的文件。我已经省略了很多错误检查和似乎已经生成一致输出的数据库对象。

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.SqlServer.Management.Smo;
using Microsoft.SqlServer.Management.Common;
using System.Data.SqlClient;
using System.IO;
using System.Configuration;
using System.Runtime.Serialization;
using System.Data;

namespace ***.Sample

    class Program
    
        public static void CreateScripts(SqlConnectionStringBuilder source, string destination)
        
            Server sv = new Server(source.DataSource);
            sv.ConnectionContext.LoginSecure = false;
            sv.ConnectionContext.Login = source.UserID;
            sv.ConnectionContext.Password = source.Password;
            sv.ConnectionContext.ConnectionString = source.ConnectionString;

            Database db = sv.Databases[source.InitialCatalog];

            ScriptingOptions options = new ScriptingOptions();
            options.ScriptData = false;
            options.ScriptDrops = false;
            options.ScriptSchema = true;
            options.EnforceScriptingOptions = true;
            options.Indexes = true;
            options.IncludeHeaders = true;
            options.ClusteredIndexes = true;
            options.WithDependencies = false;
            options.IncludeHeaders = false;
            options.DriAll = true;

            StringBuilder sbAll = new StringBuilder();

            Dictionary<string, TriggerCollection> tableTriggers = new Dictionary<string, TriggerCollection>();
            Dictionary<string, TriggerCollection> viewTriggers = new Dictionary<string, TriggerCollection>();

            // Code omitted for Functions

            // Tables
            foreach (Table table in db.Tables)
            
                StringBuilder sbTable = new StringBuilder();
                foreach (string line in db.Tables[table.Name].Script(options))
                
                    sbAll.Append(line + "\r\n");
                    sbTable.Append(line + "\r\n");
                    Console.WriteLine(line);
                
                // Write file with DDL of individual object
                File.WriteAllText(Path.Combine(destination, table.Name + ".sql"), sbTable.ToString());

                if (table.Triggers.Count > 0)
                    tableTriggers.Add(table.Name, table.Triggers);
            

            // Code omitted for Views, Stored Procedures, Table Triggers, View Triggers, Database Triggers, etc

            // Write file with full DDL of everything above
            string[] statements = sbAll.ToString().Split(new string[]  "\r\nGO\r\n" , StringSplitOptions.RemoveEmptyEntries);
            File.WriteAllLines(Path.Combine(destination, "Full.sql"), statements);
        
    

内联语句的示例输出

当 SMO 生成带有默认约束的内联语句的脚本时的输出样例。

SET ANSI_NULLS ON
SET QUOTED_IDENTIFIER ON
CREATE TABLE [dbo].[Products](
    [ID] [bigint] IDENTITY(1,1) NOT NULL,
    [StartDate] [date] NOT NULL,
    [EndDate] [date] NULL,
    [Name_En] [nvarchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
    [Name_Fr] [nvarchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
    [Type] [int] NOT NULL CONSTRAINT [DF_Products_Type]  DEFAULT ((0)),
    [ManagedType] [int] NOT NULL CONSTRAINT [DF_Products_ManagedType]  DEFAULT ((0)),
    [ProductFamilyID] [bigint] NOT NULL,
    [ImplementationID] [bigint] NOT NULL,
 CONSTRAINT [PK_Products] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

ALTER TABLE [dbo].[Products]  WITH CHECK ADD  CONSTRAINT [FK_Products_Implementations] FOREIGN KEY([ImplementationID])
REFERENCES [dbo].[Implementations] ([ID])
ALTER TABLE [dbo].[Products] CHECK CONSTRAINT [FK_Products_Implementations]
ALTER TABLE [dbo].[Products]  WITH CHECK ADD  CONSTRAINT [FK_Products_ProductFamilies] FOREIGN KEY([ProductFamilyID])
REFERENCES [dbo].[ProductFamilies] ([ID])
ALTER TABLE [dbo].[Products] CHECK CONSTRAINT [FK_Products_ProductFamilies]

独立语句的示例输出

当 SMO 生成带有默认约束的独立语句的脚本时的输出样例。

SET ANSI_NULLS ON
SET QUOTED_IDENTIFIER ON
CREATE TABLE [dbo].[Products](
    [ID] [bigint] IDENTITY(1,1) NOT NULL,
    [StartDate] [date] NOT NULL,
    [EndDate] [date] NULL,
    [Name_En] [nvarchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
    [Name_Fr] [nvarchar](50) COLLATE SQL_Latin1_General_CP1_CI_AS NOT NULL,
    [Type] [int] NOT NULL,
    [ManagedType] [int] NOT NULL,
    [ProductFamilyID] [bigint] NOT NULL,
    [ImplementationID] [bigint] NOT NULL,
 CONSTRAINT [PK_Products] PRIMARY KEY CLUSTERED 
(
    [ID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]

ALTER TABLE [dbo].[Products] ADD  CONSTRAINT [DF_Products_Type]  DEFAULT ((0)) FOR [Type]
ALTER TABLE [dbo].[Products] ADD  CONSTRAINT [DF_Products_ManagedType]  DEFAULT ((0)) FOR [ManagedType]
ALTER TABLE [dbo].[Products]  WITH CHECK ADD  CONSTRAINT [FK_Products_Implementations] FOREIGN KEY([ImplementationID])
REFERENCES [dbo].[Implementations] ([ID])
ALTER TABLE [dbo].[Products] CHECK CONSTRAINT [FK_Products_Implementations]
ALTER TABLE [dbo].[Products]  WITH CHECK ADD  CONSTRAINT [FK_Products_ProductFamilies] FOREIGN KEY([ProductFamilyID])
REFERENCES [dbo].[ProductFamilies] ([ID])
ALTER TABLE [dbo].[Products] CHECK CONSTRAINT [FK_Products_ProductFamilies]

似乎从不会在单个数据库中混合使用,但可以在单个服务器上为每个数据库获得不同的输出样式。没有注意到它随着时间的推移而改变数据库,但也许我只是没有尝试在足够长的时间内为数据库生成脚本。我已经将数据库备份并恢复到另一台服务器和不同名称的同一台服务器上,它似乎随机决定选择一种输出样式。因此,当单个数据库恢复可能表现出随机行为时,这似乎不是数据库设置。

目前用于测试的所有服务器都安装了 SQL Server 2012,并且始终在安装了 SQL Server Management Studio 2012 的同一工作站上运行代码。我在 MSDN 上查看了 ScriptingOptions 的属性,但没有发现任何突出的解决方案。

【问题讨论】:

内联表单只能在同时添加列的情况下使用。如果稍后应用默认值,则必须由独立表单完成。也许您所看到的只是反映了每个数据库如何最终处于当前状态的现实。 我不是在说恶作剧,@Damien_The_Unbeliever,但是元数据(即 sys.default_constraints)中的差异如何体现?也就是说,该工具如何知道差异并采取相应措施? @Damien_The_Unbeliever 有趣的想法。我和 Ben 有同样的问题要问你,当前状态的差异将存储在哪里?我在 sys.default_constraints 中检查了 [DF_Products_Type] 和 [DF_Products_ManagedType],在我的问题中生成示例输出的数据库之间的唯一区别是 object_id、parent_object_id、create_date 和 modify_date。我还检查了 sys.tables 中的 [Products],唯一的区别是 object_id、create_date 和 modify_date。在每一行中,create_date 等于 modify_date,并且任何行之间没有 create_date 匹配。 在使用“任务 > 生成脚本”时,此问题似乎也会影响 Sql Server Management Studio (ssms)。我猜 ssms 在后台使用 smo,但在网上找不到确认。 【参考方案1】:

经过进一步调查,我发现这是 SQL Server 管理对象 (SMO) 及其在 2012 及更高版本中处理默认约束的问题。其他人报告了相关问题,例如以下 Microsoft Connect 问题:https://connect.microsoft.com/SQLServer/Feedback/Details/895113

虽然这回答了为什么来自 SQL Server 管理对象 (SMO) 的默认约束不一致,但这并不是一个解决方案。在微软解决问题之前,有人可能会确定一种解决方法以确保输出的一致性。因此,如果您能找到解决方法,这个问题仍然可以接受其他答案。

【讨论】:

记录的连接问题标题“SMO 2014 脚本表的默认值取决于行数 = 0 与否” 我的测试证实,向原本为空的表中添加一行会导致在表定义中指定默认值,而不是在单独的 alter 语句中指定。烦人。【参考方案2】:

此解决方法通过删除单个 ALTER TABLE ... ADD CONSTRAINT ... DEFAULT 脚本并将定义放入 CREATE TABLE 脚本来修改生成的脚本。它获得了“在我的机器上工作”徽章。

Table table = GetTable();

List<string> scripts = table.Script(new ScriptingOptions

    DriAll = true,
    FullTextCatalogs = true,
    FullTextIndexes = true,
    Indexes = true,
    SchemaQualify = true
).Cast<string>().ToList();

// There is a bug in the SQL SMO libraries that changes the scripting of the
// default constraints depending on whether or not the table has any rows.
// This hack gets around the issue by modifying the scripts to always include
// the constaints in the CREATE TABLE definition. 
// https://connect.microsoft.com/SQLServer/Feedback/Details/895113
//
// First, get the CREATE TABLE script to modify.
string originalCreateTableScript = scripts.Single(s => s.StartsWith("CREATE TABLE"));
string modifiedCreateTableScript = originalCreateTableScript;
bool modificationsMade = false;

// This pattern will match all ALTER TABLE scripts that define a default constraint.
Regex defineDefaultConstraintPattern = new Regex(@"^ALTER TABLE .+ ADD\s+CONSTRAINT \[(?<constraint_name>[^\]]+)]  DEFAULT (?<constraint_def>.+) FOR \[(?<column>.+)]$");

// Find all the matching scripts.
foreach (string script in scripts)

    Match defaultConstraintMatch = defineDefaultConstraintPattern.Match(script);

    if (defaultConstraintMatch.Success)
    
        // We have found a default constraint script. The following pattern
        // will match the line in the CREATE TABLE script that defines the
        // column on which the constraint is defined.
        Regex columnPattern = new Regex(@"^(?<def1>\s*\[" + Regex.Escape(defaultConstraintMatch.Groups["column"].Value) + @"].+?)(?<def2>,?\r)$", RegexOptions.Multiline);

        // Replace the column definition with a definition that includes the constraint.
        modifiedCreateTableScript = columnPattern.Replace(modifiedCreateTableScript, delegate (Match columnMatch)
        
            modificationsMade = true;
            return string.Format(
                "0 CONSTRAINT [1]  DEFAULT 23",
                columnMatch.Groups["def1"].Value,
                defaultConstraintMatch.Groups["constraint_name"].Value,
                defaultConstraintMatch.Groups["constraint_def"].Value,
                columnMatch.Groups["def2"].Value);
        );
    


if (modificationsMade)

    int ix = scripts.IndexOf(originalCreateTableScript);
    scripts[ix] = modifiedCreateTableScript;
    scripts.RemoveAll(s => defineDefaultConstraintPattern.IsMatch(s));

【讨论】:

【参考方案3】:

我想我找到了解决方法。我们唯一要做的就是将DefaultConstraint 类的内部字段forceEmbedDefaultConstraint 设置为true。为此,我们必须使用一些反射。请在您希望编写脚本的每个表上执行以下代码,默认约束定义将根据行数独立添加到列创建语句中。

    private void ForceScriptDefaultConstraint(Table table)
    
        foreach (Column column in table.Columns)
        
            if (column.DefaultConstraint != null)
            
                FieldInfo info = column.DefaultConstraint.GetType().GetField("forceEmbedDefaultConstraint", BindingFlags.NonPublic | BindingFlags.GetField | BindingFlags.Instance);
                info.SetValue(column.DefaultConstraint, true);
            
        
    

对于要求解释为什么我认为它应该起作用的人:使用 dotPeek 我在 Microsoft.SqlServer.SMO.Column 类中找到了一个方法:

private void ScriptDefaultConstraint(StringBuilder sb, ScriptingPreferences sp)

  if (this.DefaultConstraint == null || this.DefaultConstraint.IgnoreForScripting && !sp.ForDirectExecution || (!this.EmbedDefaultConstraints() && !this.DefaultConstraint.forceEmbedDefaultConstraint || sb.Length <= 0))
    return;
  this.DefaultConstraint.forceEmbedDefaultConstraint = false;
  sb.Append(this.DefaultConstraint.ScriptDdl(sp));

上面的代码说服我将forceEmbedDefaultConstraint 的值更改为true。就我而言,它有效,但创建数据库对象的顺序可能会影响最终结果。

【讨论】:

从概念上看,这看起来很有希望;但是,在测试代码后,它未能对生成默认约束的 ALTER TABLE 样式的表产生任何影响。而且由于这试图操纵我没有源代码的私有字段,我无法调试并找出它失败的地方,我也不能确定它会做任何事情(例如:据我所知SMO 中的潜在错误可能是该字段被忽略)。您能否提供更多材料说明您认为这应该可行的原因? 嗨,Sybeus。我在之前的帖子中添加了一些解释,以解释我是如何在我的场景中解决这个问题的。 我可以确认,很遗憾,这种解决方法不起作用。 另一个确认内部结构从一个版本到另一个版本的变化。 Microsoft.SqlServer.Smo v11 版本的 Microsoft.SqlServer.Management.Smo.Column 有其私有变量名为“scriptDefaultConstraintEmbedded”,ScriptDefaultConstratin() 的方法体明显不同。

以上是关于SQL Server 管理对象 (SMO) 的默认约束不一致的主要内容,如果未能解决你的问题,请参考以下文章

[转]SQL Server编程:SMO介绍

使用 SMO 重启 SQL Server 实例

sql 使用SMO重新启动Sql Server代理服务

如何使用 SMO 和 SQL Server 生成脚本以删除列?

SQL Server 无效版本:15 (Microsoft.SqlServer.Smo)

从 SQL DMO 迁移到 SQL SMO (SQL Server 2012)