Реализация проверочных ограничений с интеграцией SQL CLR - PullRequest
0 голосов
/ 11 апреля 2009

Я реализую ограничения 'check', которые просто вызывают функцию 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, потому что мои функции проще без кода ошибки, и когда кто-то, использующий Management Studio, делает неверное редактирование столбца, он получит осмысленное сообщение из CLR, например "Value for type X didn't match regular expression '^p[1-9]\d?$'", вместо какой-то общей ошибки SQL, например "ограничение нарушено". Существуют ли какие-либо серьезные негативные последствия от передачи ошибок CLR на SQL Server, или это похоже на любую другую ошибку вставки / обновления, вызванную нарушением ограничения?

Ответы [ 2 ]

2 голосов
/ 11 апреля 2009

Например, у класса «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 ;

и затем запустите набор результатов для базы данных.

Комментарии от ОП:

Я использую базовые классы с именами ConstrainedNumber и RegexConstrainedString для всех моих типов данных. Я мог бы легко переместить простой код конструктора этих двух классов в отдельный общедоступный логический метод IsValueValid, как вы предложили и, вероятно, сделаете.

Затраты CLR (и выделение памяти) будут происходить только для вставок и обновлений. Учитывая простоту методов и скорость, с которой будут происходить обновления таблиц, я не думаю, что влияние на производительность будет беспокоить мою систему.

Я все еще очень хочу поднять исключения для информации, которую они будут предоставлять пользователям студии управления. Мне нравится метод IsValueValid, потому что он дает мне возможность не выдавать ошибки. В приложениях, использующих мои типы данных, я все еще могу получить исключение, создав экземпляр:)

Я не уверен, что согласен с выдачей исключений, но, опять же, «сообщение домой» состоит в том, что, разбивая проблему на части, вы можете выбрать, за какие части вы готовы платить, не платя за части, которые вы не используете. Котор, который вы не используете, потому что вы только вызывали его, чтобы получить побочный эффект. Итак, мы разложили создание и проверку. Мы можем далее разложить метание:

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, чтобы просто получить действительность, оплачивая затраты времени выполнения только за то, что ему нужно.

0 голосов
/ 12 апреля 2009

Некоторые уточнения. Эти классы данных устанавливают один уровень выше типов примитивов, ограничивая данные, чтобы сделать их осмысленными.

На самом деле они располагаются чуть выше классов RegexConstrainedString и ConstrainedNumber<T>, и именно здесь мы говорим о рефакторинге кода проверки конструктора в отдельный метод.

Проблема с рефакторингом кода валидации заключается в том, что Regex, необходимый для валидации, существует только в подклассах 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 выполняет большую часть работы, реализуя операторы, сравнения, клонирование и т. Д.

Кроме того, существуют другие сложности с выделением кода. Проверка включает запуск и сохранение соответствия Regex в экземпляре, поскольку некоторые типы данных могут иметь свойства, которые сообщают о конкретных элементах строки. Например, мой класс SessionID содержит свойства, такие как TimeStamp, которые возвращают сопоставленную группу из Match, хранящегося в экземпляре класса данных. Суть в том, что этот статический метод является совершенно другим контекстом. Поскольку он по существу несовместим с контекстом конструктора, конструктор не может его использовать, поэтому я бы в итоге повторил код еще раз.

Итак ... Я мог бы выделить код валидации, реплицировав его и настроив его для статического контекста и наложив требования на подклассы, или я мог бы упростить задачу и просто выполнить конструирование объекта. Относительная дополнительная выделенная память будет минимальной, так как в экземпляре хранятся только строка и ссылка Match. Все остальное, такое как Match и сама строка, все равно будет сгенерировано проверкой, поэтому нет никакого пути к этому. Я мог бы беспокоиться о производительности весь день, но мой опыт показывает, что правильность важнее, потому что правильность часто приводит к множеству других оптимизаций. Например, мне никогда не нужно беспокоиться о неправильно отформатированных или измеренных данных, проходящих через мое приложение, потому что используются только значимые типы данных, что вынуждает проверять точку входа в приложение с других уровней, будь то база данных или пользовательский интерфейс. 99% моего кода проверки были удалены как ненужный артефакт, и в настоящее время я проверяю только нулевые значения. Кстати, достигнув этой точки, я теперь понимаю, почему включение нулей было ошибкой в ​​миллиард долларов . Кажется, это единственное, что я должен проверять больше, даже если они по существу не существуют в моей системе. Сложные объекты, имеющие эти типы данных в виде полей, не могут быть построены с пустыми значениями, но я должен обеспечить это в установщиках свойств, что раздражает, потому что в противном случае им никогда бы не понадобился код проверки ... только код, который выполняется в ответ на изменения значений .

UPDATE: Я смоделировал вызовы функций CLR в обоих направлениях и обнаружил, что, когда все данные верны, разница в производительности составляет лишь доли миллисекунды на тысячу вызовов, что незначительно. Однако, когда примерно половина паролей недействительна, исключая исключения в версии «создания экземпляров», это на три порядка медленнее, что соответствует примерно 1 дополнительной секунде на 1000 вызовов. Разница в размерах, конечно, будет многократной, поскольку для нескольких столбцов в таблице выполняется несколько вызовов CLR, но для моего проекта это в 3-5 раз. Итак, приемлемы ли для меня дополнительные 3–5 секунд на 1000 обновлений в качестве компромисса за простоту и чистоту моего кода? Ну, это зависит от частоты обновления. Если бы мое приложение получало 1000 обновлений в секунду, задержка в 3 - 5 секунд была бы разрушительной. Если, с другой стороны, я получаю 1000 обновлений в минуту или час, это может быть вполне приемлемо. В моей ситуации я могу сказать вам, что это вполне приемлемо, поэтому я думаю, что я просто пойду с метод создания экземпляров и разрешить ошибки до конца. Конечно, в этом тесте я обрабатывал ошибки в CLR, а не позволял SQL Server их обрабатывать. Распространение информации об ошибках в SQL Server, а затем, возможно, обратно в приложение, может значительно замедлить процесс. Я предполагаю, что мне придется полностью реализовать это, чтобы получить реальный тест, но из этого предварительного теста, я почти уверен, что результаты будут.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...