0

我正在实现“检查”约束,它只是为每个受约束的列调用一个 CLR 函数。

每个 CLR 函数都是一两行代码,它们尝试构造与该列关联的用户定义 C# 数据类的实例。例如,“Score”类有一个构造函数,它在构造失败时(即,当分数超出有效范围时)抛出有意义的错误消息。

首先,您如何看待这种方法? 对我来说,它将我的数据类型集中在 C# 中,使它们在我的应用程序中可用,同时还在数据库中强制执行相同的约束,因此它可以防止非程序员可能尝试在管理工作室中进行无效的手动编辑。到目前为止它运行良好,尽管更新程序集会导致约束被禁用,需要重新检查所有约束(这是完全合理的)。我DBCC CHECKCONSTRAINTS WITH ALL_CONSTRAINTS用来确保所有表中的数据对于启用和禁用约束仍然有效,并根据需要进行更正,直到没有错误为止。然后我通过 重新启用所有表的约束ALTER TABLE [tablename] WITH CHECK CHECK CONSTRAINT ALL是否有一个 T-SQL 语句可以重新启用检查所有表上的所有检查约束,还是我必须逐个表重新启用它们?

最后,对于检查约束中使用的 CLR 函数,我可以:

  1. 在每个函数中包含一个 try/catch 以捕获数据构造错误,错误时返回 false,成功时返回 true,这样 CLR 就不会在数据库引擎中引发错误,或者...
  2. 省略 try/catch,只需构造实例并返回 true,从而允许在数据库引擎中引发上述“有意义的”错误消息。

我更喜欢 2,因为我的函数更简单,没有错误代码,并且当使用管理工作室的人进行无效的列编辑时,他们会从 CLR 获得有意义的消息,"Value for type X didn't match regular expression '^p[1-9]\d?$'"而不是像“违反约束”这样的通用 SQL 错误。 允许 CLR 错误通过 SQL Server 是否有任何严重的负面后果,或者它是否就像任何其他因违反约束而导致的插入/更新失败一样?

4

2 回答 2

2

例如,“Score”类有一个构造函数,它在构造失败时(即,当分数超出有效范围时)抛出有意义的错误消息。首先,您如何看待这种方法?

这让我有点担心,因为调用 ctor 需要分配内存,这是相对昂贵的。对于插入的每一行,您都在调用一个 ctor——而且只是为了它的副作用。

也昂贵的是例外。当您需要它们时它们很棒,但在这种情况下,您可以在 ctor 上下文中使用它们,而不是在检查上下文中使用它们。

通过将检查作为类静态或自由函数存在,重构可以降低这两种成本,然后检查约束和 ctor 都可以调用:

class Score {

  private:
   int score;

  public:
   static bool valid( int score ) { 
    return score > 0  ; 
   }
 
    Score( int s ) {
    if( ! valid( s ) ) { 
      throw InvalidParameter();
    }

    score = s;
   }
}   

检查约束调用 Score::valid(),不需要构造或异常。

当然,对于每一行,CLR 调用仍然会产生开销。这是否可以接受是您必须决定的事情。

是否有一个 T-SQL 语句可以重新启用检查所有表上的所有检查约束,还是我必须逐个表重新启用它们?

不,但您可以这样做来生成命令:

select 'ALTER TABLE ' || name || ' WITH CHECK CHECK CONSTRAINT ALL;'
 from sys.tables ;

然后针对数据库运行结果集。

OP的评论:

我对所有数据类型使用名为 ConstrainedNumber 和 RegexConstrainedString 的基类。按照您的建议,我可以轻松地将这两个类的简单构造函数代码移动到单独的公共布尔 IsValueValid 方法中,并且可能会这样做。

CLR 开销(和内存分配)只会发生在插入和更新中。鉴于方法的简单性以及表更新发生的速率,我认为性能影响不会对我的系统产生任何影响。

我仍然真的很想为他们将提供给管理工作室用户的信息提出例外。我喜欢 IsValueValid 方法,因为它给了我不抛出错误的“选项”。在使用我的数据类型的应用程序中,我仍然可以通过构造一个实例来获得异常:)

我不确定我是否同意抛出异常,但同样,“带回家的信息”是通过将问题分解为多个部分,您可以选择您愿意支付的部分,而无需支付您不支付的部分不使用。您不使用的 ctor,因为您只是调用它来获得副作用。所以我们分解了创建和检查。我们可以进一步分解投掷:

class Score {

  private:
   int score;

  public:
   static bool IsValid( int score ) { 
    return score > 0  ; 
   }

   static checkValid( int score ) {
    if( ! isValid( s ) ) { 
      throw InvalidParameter();
    }

 
    Score( int s ) {
    checkValid( s ) ;    
    score = s;
   }
}   

现在用户可以调用 ctor,获取检查和可能的异常和构造,调用 checkValid 并获取检查和异常,或者 isValid 只获取有效性,只为他需要的东西支付运行时成本。

于 2009-04-11T19:47:02.173 回答
-1

一些澄清。这些数据类在基本类型之上设置了一层,限制数据使其有意义。

RegexConstrainedString实际上,它们位于和类之上ConstrainedNumber<T>,这就是我们正在讨论将构造函数的验证代码重构为单独的方法的地方。

重构验证代码的问题在于,验证所需的正则表达式仅存在于 的子类中RegexConstrainedString,因为每个子类都有不同的Regex. 这意味着验证数据仅可用于RegexConstrainedString的构造函数,而不是它的任何方法。因此,如果我排除验证代码,调用者将需要访问Regex.

public class Password: RegexConstrainedString
{
    internal static readonly Regex regex = CreateRegex_CS_SL_EC( @"^[\w!""#\$%&'\(\)\*\+,-\./:;<=>\?@\[\\\]\^_`{}~]{3,20}$" );

    public Password( string value ): base( value.TrimEnd(), regex ) {} //length enforced by regex, so no min/max values specified
    public Password( Password original ): base( original ) {}
    public static explicit operator Password( string value ) {return new Password( value );}
}

因此,当从数据库读取值或读取用户输入时,Password 构造函数将其转发Regex给基类以处理验证。另一个技巧是它会自动修剪结束字符,以防数据库类型是 char 而不是 varchar,所以我不必记住这样做。无论如何,RegexConstrainedString 的主要构造函数如下所示:

protected RegexConstrainedString( string value, Regex subclasses_static_regex, int? min_length, int? max_length )
{
    _value      = (value ?? String.Empty);
    if (min_length != null)
        if (_value.Length < min_length)
            throw new Exception( "Value doesn't meet minimum length of " + min_length + " characters." );
    if (max_length != null)
        if (_value.Length > max_length)
            throw new Exception( "Value exceeds maximum length of " + max_length + " characters." );
    value_match = subclasses_static_regex.Match( _value ); //Match.Synchronized( subclasses_static_regex.Match( _value ) );
    if (!value_match.Success)
        throw new Exception( "Invalid value specified (" + _value + "). \nValue must match regex:" + subclasses_static_regex.ToString() );
}

由于调用者需要访问子类的Regex,我认为我最好的选择是IsValueValid在子类中实现一个方法,它将数据转发到基类IsValueValid中的方法。RegexConstrainedString换句话说,我会将这一行添加到 Password 类中:

public static bool IsValueValid( string value ) {return IsValueValid( value.TrimEnd(), regex, min_length, max_length );}

但是我不喜欢这样,因为我正在复制子类构造函数代码,必须记住再次修剪字符串并在必要时传递相同的最小/最大长度。这个要求将被强加于 RegexConstrainedString 的所有子类,这不是我想做的事情。像 Password 这样的数据类非常简单,因为 RegexConstrainedString 处理了大部分工作,实现了运算符、比较、克隆等。

此外,分解代码还有其他复杂性。验证涉及在实例中运行和存储正则表达式匹配,因为某些数据类型可能具有报告字符串特定元素的属性。例如,我的 SessionID 类包含像 TimeStamp 这样的属性,它从存储在数据类实例中的 Match 返回匹配的组。底线是这个静态方法是一个完全不同的上下文。由于它本质上与构造函数上下文不兼容,构造函数不能使用它,所以我最终会再次复制代码。

所以...我可以通过复制验证代码并将其调整为静态上下文并对子类施加要求,或者我可以让事情变得更简单,只执行对象构造。分配的相对额外内存将是最小的,因为实例中只存储了一个字符串和匹配引用。无论如何,其他所有内容,例如 Match 和字符串本身仍将由验证生成,因此无法解决。 我可以整天担心性能,但我的经验是正确的更重要的是,因为正确性通常会导致许多其他优化。例如,我不必担心流经我的应用程序的数据格式或大小不正确,因为只使用有意义的数据类型,这会强制验证从其他层进入应用程序的入口点,无论是数据库或用户界面。我 99% 的验证代码作为不再需要的工件被删除,我发现自己现在只检查空值。顺便说一句,已经到了这一点,我现在明白为什么包含空值是十亿美元的错误. 似乎是我唯一需要检查的东西了,即使它们在我的系统中基本上不存在。将这些数据类型作为字段的复杂对象不能用空值构造,但我必须在属性设置器中强制执行这一点,这很烦人,因为否则它们永远不需要验证代码......只有响应值变化而运行的代码.

更新:我模拟了CLR函数的双向调用,发现当所有数据都有效时,性能差异仅为每千次调用的几分之一毫秒,可以忽略不计。但是,当大约一半的密码无效时,在“实例化”版本中抛出异常,它会慢三个数量级,这相当于每 1000 次调用大约多 1 秒。差异的大小当然会成倍增加,因为对表中的多个列进行了多次 CLR 调用,但这对于我的项目来说是 3 到 5 倍。那么,我是否可以接受每 1000 次更新额外 3 到 5 秒,作为保持我的代码非常简单和干净的权衡? 嗯,这取决于更新率。如果我的应用程序每秒获得 1000 次更新,那么 3 - 5 秒的延迟将是毁灭性的。另一方面,如果我每分钟或每小时获得 1000 次更新,那可能是完全可以接受的。 在我的情况下,我现在可以告诉你它是完全可以接受的,所以我想我会使用实例化方法,并允许错误通过。当然,在这个测试中,我处理了 CLR 中的错​​误,而不是让 SQL Server 处理它们。将错误信息编组到 SQL Server,然后可能会返回到应用程序,这肯定会使事情变得更慢。我想我必须完全实现它才能获得真正的测试,但从这个初步测试来看,我很确定结果会是什么。

于 2009-04-12T05:20:33.417 回答