Оптимизация работы серверов баз данных Microsoft SQL Server 2005

Динамическое построение запросов

Разбить на страницы
Показывать лекцию целиком

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

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

Интерфейс пользователя для построения запросов

Среда SQL Server Management Studio включает сложный интерфейс для построения запросов. Давайте изучим этот интерфейс, чтобы у вас сформировалось представление о том, как можно создавать запросы динамически. Вашему приложению не понадобятся все элементы управления, которые предоставляет среда SQL Server Management Studio. По сути, нужно тщательно продумать, как наилучшим образом ограничить пользователям возможности выбора.

Создаем запрос при помощи Конструктора запросов SQL Server Management Studio

  • В SQL Server Management Studio, если нужно, присоедините базу данных Совет. О том, как присоединить базу данных, рассказывается в лекциях 6-7 курса "Разработка и защита баз данных в Microsoft SQL Server 2005", которая называется "Перенос базы данных на другие системы".
  • Разверните узел Tables (Таблицы) в Object Explorer (Обозревателе объектов).
  • Найдите таблицу
  • В SQL Server Management Studio появилась панель инструментов Query Designer (Конструктор запросов), показанная на рисунке. Первые три кнопки этой панели инструментов отображают таблицы вашего запроса, список выбранных столбцов и действительный код SQL, который был сгенерирован.
  • Нажмите первую кнопку Show Diagram Pane (Показать область схемы), затем выберите столбцы CustomerID и AccountNumber.
  • Нажмите вторую кнопку - Show Criteria Pane (Показать область условий). Снимите флажок Output (Вывод) в строке *. Щелкните мышью в столбце Sort Type (Тип сортировки) в строке CustomerID и выберите из раскрывающегося списка пункт Ascending (По возрастанию).
  • При необходимости переместите видимую область окна вправо, чтобы отобразить столбец Filter (Фильтр). В той же строке CustomerID введите <10 и нажмите клавишу Enter.
  • Нажмите третью кнопку - Show SQL Pane (Показать область SQL кода). Окно программы примет следующий вид:
  • Обратите внимание на строку запроса, которую программа SQL Server Management Studio сгенерировала в области SQL кода. Нажмите на панели инструментов кнопку Execute SQL (Выполнить SQL код). В панель Results (Результаты) будет возвращено 9 записей.

    Пользователям ваших приложений, как правило, не нужно ничего делать со строкой запроса. Они ничего не знают об SQL. Ваша обязанность - правильно построить строку запроса, либо на этапе проектирования, либо через код приложения в процессе выполнения рабочего цикла. Динамическое построение запроса во время рабочего цикла позволяет ввести в строку запроса пользовательский ввод.

  • Извлечение информации о таблицах базы данных

    Чтобы предоставить пользователю список параметров, приложению, вероятно, придется извлечь информацию о таблицах базы данных. Существует несколько способов получить эту информацию. Самый важный из этих методов - использование схемы INFORMATION_SCHEMA. Эта схема является стандартной в любой базе данных.

    Применение INFORMATION_SCHEMA

    Схема INFORMATION_SCHEMA - это особая схема, которая есть в каждой базе данных. Она содержит определения некоторых объектов базы данных.

    INFORMATION_SCHEMA соответствует стандарту ANSI, который предназначен для извлечения информации от любого ANSI-совмести-мого ядра базы данных. В SQL Server INFORMATION_SCHEMA состоит из набора представлений, которые запрашивают таблицы базы данных sys*, содержащие информацию о структуре базы данных. Запрос к этим таблицам можно выполнить напрямую, точно так же, как к любым таблицам базы данных. Однако в большинстве случаев для того, чтобы извлечь информацию из таблиц *sys, лучше использовать представления схемы INFORMATION_SCHEMA.

    Примечание. Схема INFORMATION_SCHEMA иногда запрашивает таблицы, которые не нужны, что наносит ущерб производительности. В следующем примере данной лекции это не особенно важно, потому что приложение уже ожидало пользовательского ввода. Однако это следует учитывать, если скорость является важным аспектом для вашего приложения.

    Вот базовый код T-SQL, который используется для получения информации о столбцах, входящих в таблицу:

    SELECT TABLE_SCHEMA,
           TABLE_NAME,
           COLUMN_NAME,
           ORDINAL_POSITION,
           DATA_TYPE 
      FROM INFORMATION_SCHEMA.COLUMNS 
      WHERE TABLE_NAME = "<TABLE_NAME>")

    Обратите внимание на то, что для получения схемы для таблицы нужно выбрать поле TABLE_SCHEMA. Это может иметь значение для создания аналогичных запросов в дальнейшем. Чтобы экспериментировать с методами, описанными в данной лекции, создайте новый проект в Visual Studio.

    Создаем новый проект Visual Studio

  • Выберите из меню Start (Пуск) команды All Programs, Microsoft Visual Studiio 2005, Microsoft Visual Studio 2005.
  • В меню Visual Studio выберите команды File, New, Project (Файл, Создать, Проект).
  • В панели Project Types (Типы проектов) разверните узел Visual Basic (Решения Visual Basic) и выберите в панели Templates (Шаблоны) шаблон Application (Приложение). Дайте проекту имя Chapter7 и нажмите кнопку ОК,
  • Приложение для этого примера можно найти в файлах примеров в папке \Chapter7\DynQuery. Вы можете вырезать и вставлять код для следующих процедур из файла Form1.vb.
  • Получение списка таблиц и представлений

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

    SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE 
       FROM INFORMATION_SCHEMA.TABLES

    В приложении этот запрос можно использовать следующим образом.

    Получаем список таблиц

  • Дважды щелкните форму Form1, сгенерированную Visual Studio. На экране появится процедура Form1_Load. Объявите две глобальные переменные и добавьте вызов процедуры RetrieveTables в процедуру Form1_Load, чтобы она выглядела примерно так:
    Public Class Form1
     Dim SchemaName As String = "", 
          TableName As String = ""
     Private Sub Form1_Load(ByVal sender As System.Object, _
                            ByVal e As System.EventArgs) 
        Handles 
        MyBase.Load
        RetrieveTables() 
     End Sub
  • После этого добавьте показанную здесь процедуру RetrieveTables после процедуры Form1_Load. Чтобы эта программа могла выполняться, необходимо, чтобы к экземпляру сервера SQLExpress была присоединена база данных Adventure Work. Узнать о том, как присоединить базу данных, можно в лекции 6-7 курса "Разработка и защита баз данных в Microsoft SQL Server 2005".
    Sub RetrieveTables()
      
      Dim FieldName As String
      Dim MyConnection As New SqlClient.SqlConnection( _ 
                              "Data Source=.\SQLExpress;"  _
                              "Initial Catalog=AdventureWorks;Trusted_
                              Connection=Yes;") 
      Dim com As New SqlClient.SqlCommand( _
                              "SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE "  _ 
                              "FROM INFORMATION_SCHEMA.TABLES", _ 
                              MyConnection) MyConnection.Open()
      Dim dr As SqlClient.SqlDataReader = com.ExecuteReader With dr
      Do While .Read
         "Следует сохранить эту информацию для использования в 
         "форме или на странице
         SchemaName = .GetString(0)
         TableName = .GetString(1)
         FieldName = .GetString(2)
         Console.WriteLine("{0} {1} {2}", _
         SchemaName, TableName, FieldName) Loop .Close() End With
         "Предположим, что пользователь выбрал следующие схему и таблицу: 
         SchemaName = "Sales" 
         TableName = "Customer" 
    End Sub
    Примечание. В реальном приложении строка соединения и строка SQL могут обслуживаться ресурсами приложения или конфигурационным файлом приложения.
  • Выберите из меню Debug (Отладка) команду Start Debugging (Начать отладку), чтобы создать и выполнить проект. Появится пустое окно формы, а в панели Output (Вывод) в окне Visual Studio будет записана информация схемы.
  • Закройте форму, чтобы завершить работу приложения.
  • Приведенный выше код на Visual Basic инициализирует объект SqlCommand с именем com со строкой SQL, которую нужно выполнить, а затем выполняет объект SqlCommand. Это самый простой способ выполнить предложение T-SQL из приложения.

    В качестве упражнения можно разместить схемы и таблицы, полученные в процессе выполнения процедуры Load нашей формы, в пользовательском интерфейсе приложения в виде формы, чтобы пользователь мог выбрать схему и таблицу для работы. Для примера в этой лекции мы предполагаем, что пользователь выбрал схему Sales и таблицу Customer.

    После того, как пользователь выбрал таблицу, можно извлечь список столбцов для этой таблицы при помощи того же метода, используя пользовательский ввод в качестве имени таблицы в запросе. Для этого в строку запроса следует ввести заместитель, а затем заменить этот заместитель вызовом String.Format. В приведенном ниже коде заместитель в строке запроса - (0).

    Получаем список столбцов

  • Добавьте следующую процедуру RetrieveColumns в код ниже процедуры RetrieveTables:
    Sub RetrieveColumns(ByVal TableName As String)
      MyConnection As New SqlClient.SqlConnection( _ 
                  "Data Source=.\SQLExpress;"  _
                  "Initial Catalog=AdventureWorks;Trusted_Connection=Yes;") 
      Dim sqlStr As String
      sqlStr = "SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, " + _
               "ORDINAL_POSITION, DATA_TYPE " + _ 
               "FROM INFORMATION_SCHEMA.COLUMNS " + _ 
               "WHERE (TABLE_NAME = "{0}")" 
      Dim tableColumns As New DataTable Dim da As New SqlClient.SqlDataAdapter( _
                 String.Format(sqlStr, TableName), MyConnection) da.Fill(tableColumns)
      For i As Integer = 0 To tableColumns.Rows.Count - 1 
        With tableColumns.Rows.Item(i) 
           Console.WriteLine("{0} {1} {2}", _ 
                             .Item(1), .Item(2), .Item(3)) 
        End With 
      Next 
    End Sub
  • В процедуру Form1_Load добавьте следующий вызов процедуры RetrieveColumns после процедуры RetrieveTables:
    RetrieveColumns(TableName)
  • Выберите из меню Debug (Отладка) команду Start Debugging (Начать отладку), чтобы создать и выполнить проект. Появится пустое окно формы, а в панели Output (Вывод) в окне Visual Studio будет записана информация о столбцах и таблице.
  • Закройте форму, чтобы завершить работу приложения.
  • Объект типа DataTable в процедуре RetrieveColumns может быть использован для заполнения элементов управления CheckListBox или ListView с включенными элементами управления CheckBoxes, чтобы пользователь мог выбрать нужные поля.

    Добавляем в форму элемент управления ListView

  • В окне Visual Studio перейдите на вкладку Form1.vb [Design].
  • Выберите из меню View (Вид) команду Toolbox (Панель элементов).
  • Перетащите мышью элемент управления Label из панели элементов в форму. Щелкните правой кнопкой мыши на label и выберите из контекстного меню команду Properties (Свойства). В окне Properties (Свойства) измените текст в строке Label на Столбцы.
  • Теперь перетащите мышью элемент управления
  • Перетащите правую границу ListView (Список), чтобы увеличить ширину списка. Щелкните правой кнопкой мыши на ListView (Список) и выберите из контекстного меню команду Properties (Свойства). В окне Properties (Свойства) задайте для свойства CheckBoxes значение True, а свойство View на List.
  • Щелкните правой кнопкой мыши на форме и выберите из контекстного меню команду View Code (Перейти к коду), чтобы вернуться к нашему коду. В процедуре RetrieveColumns замените предложение Console.WriteLine следующим кодом:
    ListView1.Items.Add(.Item(2))
  • Выполните построение и запустите проект. Вы увидите в форме список столбцов с полями для установки флажков, как показано на рисунке:
  • Закройте форму, чтобы завершить работу приложения.
  • После того, как пользователь выберет таблицу и столбцы для просмотра, приложение может создать запрос. Базовая структура запроса на выборку включает список столбцов через запятую:

    SELECT <Column1>, <Column2>, <Column3> 
       FROM <Table_Name>

    Вы легко можете создать список столбцов из элемента управления ListView.

    Форматирование пользовательского ввода для динамического запроса

  • В окне Visual Studio перейдите на вкладку Form1.vb [Design].
  • Перетащите элемент управления Button (Кнопка) под элемент управления ListView (Список). В окне Properties (Свойства) измените текст на "Выполнить запрос".
  • Дважды щелкните на элементе Button (Кнопка). Visual Studio создаст процедуру с именем Button1_Click. Добавьте в эту процедуру следующий код:
    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) 
       Handles Button1.Click 
       Dim baseSQL As String = "SELECT {0} FROM {1}" 
       Dim sbFields As New System.Text.StringBuilder 
       Dim numChecked As Integer = 0 
       With sbFields
         For Each el As ListViewItem In ListView1.Items 
           If el.Checked Then
             numChecked = numChecked + 1 
             If .Length <> 0 Then
                .Append(",") 
             End If
             Console.WriteLine(el.Text) 
             AppendFormat("[{0}]", el.Text) 
           End If 
         Next 
       End With
       Console.WriteLine(sbFields) 
    End Sub
    Примечание. Если вам нужно выполнить со строкой более двух манипуляций, лучше использовать объект StringBuilder, чтобы получить результат, а затем использовать соединение строк.
  • Постройте и запустите проект. Выберите несколько столбцов в элементе управления ListView, нажмите кнопку "Выполнить запрос" и изучите список столбцов с разделителем-запятой, который отображается в панели Output (Вывод).
  • Закройте форму, чтобы завершить работу приложения.
  • В завершение можно завершить создание динамической инструкции, добавив список столбцов и имя таблицы. Поскольку таблица может принадлежать схеме, в запросе лучше использовать полное уточненное имя таблицы.
  • Совет. В нашем приложении, если безопасность не является большой проблемой, вы, возможно, захотите отобразить строку запроса в элементе управления TextBox и разрешить пользователю изменять эту строку перед выполнением запроса. Прочитайте раздел "Параметры и безопасность динамических запросов", в котором приводится информация о риске для системы безопасности, который возникает, если разрешить пользователям напрямую изменять строку запроса.

    Строим и выполняем динамический запрос

  • Замените предложение Console.WriteLine в конце процедуры Button1_Click следующим кодом, который создает строку динамического запроса:
    Dim tblsql As String 
      Dim txtsql As String
      tblsql = String.Format("{0}.{1}", SchemaName, TableName)
      txtsql = String.Format(baseSQL, _ 
               sbFields.ToString, tblsql)
  • После этого добавьте в конце процедуры Button1_Click следующий код для выполнения динамического запроса. Для этого примера первые 100 строк результата отображаются в панели Output (Вывод). В реальном приложении следует использовать в качестве DataSource для элемента управления DataGridView значение DataTable. Элементы управления DataGridView идеально подходят для отображения результатов запроса для пользователя.
    Dim MyConnection As New SqlClient.SqlConnection( _
               "Data Source=.\SQLExpress;"  _
               "Initial Catalog=AdventureWorks;Trusted_Connection=Yes;") 
      Dim com As New SqlClient.SqlCommand(txtsql, MyConnection) 
      Dim tableResults As New DataTable tableResults.Clear()
      Dim da As New SqlClient.SqlDataAdapter(com) 
      
      da.Fill(tableResults) 
      For i As Integer = 0 To tableResults.Rows.Count - 1
        With tableResults.Rows.Item(i)
          Dim rowstr As New System.Text.StringBuilder 
          For j As Integer = 0 To numChecked - 1 
            rowstr.Append(.Item(j)) 
            rowstr.Append(" ") 
          Next
          Console.WriteLine(rowstr) 
        End With
        If i > 100 Then Exit For 
      Next
  • Постройте и запустите проект. Выберите несколько столбцов в элементе управления ListView и нажмите кнопку Выполнить запрос. Результаты динамического запроса отображаются в панели Output (Вывод). Вы можете выбрать другие столбцы и нажать кнопку еще раз, чтобы построить и выполнить другой динамический запрос.
  • Закройте форму, чтобы завершить работу приложения.
  • Выполнение динамического запроса от имени другой учетной записи

    Одной из лучших методик обеспечения безопасности считается запрещение разрешения READ для таблиц с целью предотвращения несанкционированного доступа, поэтому иногда приложению приходится выполнять запрос, динамически сгенерированный пользователем с использованием других учетных данных.

    В SQL Server 2005 для того, чтобы действовать от имени другого пользователя в рамках определенной команды, можно выполнить команду EXECUTE. Синтаксис команды EXECUTE:

    EXECUTE ("<dynamic query>") as 
            USER='<User Name>'
    Предупреждение. Это очень опасная идея. Как вы увидите далее в этой лекции, она подвергает безопасность базы данных потенциальному риску. Чтобы не допустить нарушений безопасности, приложение должно точно определить вид запроса, который оно должно выполнить.

    Динамическая сортировка и фильтрация

    Когда запрос возвращает большой набор записей, результаты будут полезнее, если пользователь сможет задать порядок возвращения строк, а также то, какие строки следует отфильтровать.

    Добавление порядка сортировки в динамический запрос

    Чтобы выполнить сортировку динамического запроса, просто добавьте предложение ORDER BY, указав после него список столбцов в порядке, выбранном пользователем. Если пользователь хочет получить информацию в порядке убывания, добавьте также ключевое слово DESC.

    В следующем коде упорядоченные выборки сохраняются в специальной коллекции, которая называется SortInfo ; это особый пользовательский класс, предназначенный для хранения всей информации, необходимой для каждой упорядоченной выборки.

    Public Class SortInfo
    Public SchemaName As String
    Public TableName As String
    Public ColumnName As String
    Public SortOrder As SortOrder
    Public SortPosition As Integer End Class

    Чтобы создать упорядоченный список, код проходит по этому списку, добавляя определение в StringBuilder, и в случае, если задано какое-либо упорядочение, добавляет предложение ORDER TO в начале содержимого StringBuilder.

    For Each o As System.Collections.Generic.KeyValuePair( _ 
               Of Integer, SortInfo) In OrderList 
      With sbOrderBy
        If .Length <> 0 Then
           .Append(",") 
        End If
        .AppendFormat("[{0}].[{1}]", o.Value.TableName, _
                      o.Value.ColumnName) 
        If o.Value.SortOrder = SortOrder.Descending Then
           .Append(" DESC") 
        End If 
      End With 
    Next 
    If sbOrderBy.Length > 0 Then
      sbOrderBy.Insert(0, " ORDER BY ") 
    End If

    Фильтрация динамического запроса

    Добавить фильтр в запрос немного сложнее, чем добавить порядок сортировки - приходится иметь в виду много различных операторов и типов данных. Но результат стоит затраченных усилий. Как и раньше, нашей исходной точкой будет базовый синтаксис предложения WHERE, задающего фильтрацию в инструкции SELECT.

    SELECT <Field List> 
       FROM <Table Name> 
       WHERE <Condition List> 
       ORDER BY <Order List>

    Список условий следует проектировать с учетом следующих рекомендаций:

  • Используйте шаблон <ColumnName> <compare operator> <value>.
  • Сопоставьте значение datatype столбцу datatype.
  • Добавьте дополнительные фильтры в Condition_List с помощью ключевых слов AND или OR.
  • Необходимо реализовать для пользователей интерфейс, позволяющий указать значения и операторы сравнения для каждого фильтра. Цель -получить пользовательский ввод и создать строку с условиями фильтрации, которую можно будет вставить в динамический запрос.

    Законченный пример - приложение Dynamic Query

    Следующие примеры основаны на примере приложения Dynamic Queries, которое находится в папке Filters в файлах примеров к 3 лекции. Пример приложения Dynamic Queries - это расширенная версия приложения, которое мы создали в этой лекции. Это приложение включает элемент управления ListView, который показывает все настройки, сделанные пользователем для создания динамического запроса, как показано на рис. 3.1. Чтобы попробовать сделать то же самое, выполните действия, описанные ниже.

    (рис 3.1) Элемент управления ListView с информацией о запросе, который нужно создать пользователю

    Запускаем пример приложения Dynamic Queries

  • Дважды щелкните файл Ch07.sln в папке Chapter07\Filter, чтобы открыть проект в Visual Studio.
  • Выберите команду Start Debugging (Начать отладку) из меню Debug (Отладка). Выберите сервер и нажмите кнопку ОК, Visual Studio построит и запустит проект, отобразив начальную форму, как показано ниже.
  • В форме frmPpal щелкните раскрывающийся список Data в панели инструментов, а затем выберите Table Sales, Customer.
  • Выберите элементы CustomerID, TerritoryID и AccountNumber в поле со списком и нажмите кнопку "Стрелка вправо" в центре формы.
  • Нажмите кнопку Build Query на панели инструментов. Теперь окно программы вверху справа отображает поля, которые вы выбрали, а внизу справа - соответствующий запрос SQL, как показано на рисунке.
  • Щелкните правой кнопкой мыши в строке CustomerID и выберите из контекстного меню команды Order, Ascending.
  • Щелкните правой кнопкой мыши в строке CustomerID и выберите из контекстного меню команды Order, Ascending. В диалоговом окне Filter выберите в раскрывающемся списке Less_Than и введите в текстовое поле 20. Нажмите кнопку ОК.
  • Снова нажмите кнопку Build Query на панели инструментов. Теперь динамический запрос выглядит следующим образом:
    SELECT [CustomerID],[TerritoryID],[AccountNumber] 
       FROM Sales.Customer WHERE [CustomerID] < 20 
       ORDER BY [Customer].[CustomerID]
  • Нажмите кнопку Execute Query, чтобы выполнить этот динамический запрос. В окне программы отобразятся следующие результаты:
  • Как пример приложения строит строку фильтрации

    Это приложение хранит различные значения, показанные в правой верхней части формы, в элементе управления ListView с именем lvUseFields, показанном в табл. 3.1.

    Поля в элементе управления ListView lvUseFields
    СвойствоСодержит
    Text Имя схемы
    SubItems(1) Имя таблицы
    SubItems(2) Имя поля
    SubItems(3) Имя (Псевдоним для результирующего столбца)
    SubItems(4) Состояние сортировки
    SubItems(5) Оператор фильтрации
    SubItems(6) Значение фильтра

    Когда вы нажимаете кнопку Build Query на панели инструментов, следующий код выполняет построение динамического запроса, используя содержимое элемента управления ListView lvUseFields. Обратите особое внимание на ту часть кода, которая показана полужирным шрифтом и демонстрирует построение предложения WHERE для фильтрации динамического запроса.

    Private Sub tsbGen_Click(ByVal sender As System.Object, _ 
                             ByVal e As System.EventArgs) 
      Handles tsbGen.Click 
      Dim baseSQL As String = "SELECT {0} from {1}" 
      "Список отображаемых полей 
      Dim sbFields As New System.Text.StringBuilder           
      "Порядок списка столбцов 
      Dim sbOrderBy As New System.Text.StringBuilder("")
      "Фильтры
      Dim sbFilter As New System.Text.StringBuilder("")
      
      With sbFields
        For Each el As ListViewItem In lvUseFields.Items 
          If .Length <> 0 Then
            .Append(",") 
          End If
          "Добавляем в список полей 
          Column .AppendFormat("[{0}]", _
                           el.SubItems(UseFieldsColumnsEnum.Field).Text) 
          "Если Name не совпадает с именем столбца, добавляем Alias 
          If el.SubItems(UseFieldsColumnsEnum.Name).Text <> _
                         el.SubItems(UseFieldsColumnsEnum.Field).Text Then
            .AppendFormat(" AS [{0}]", _
            el.SubItems(UseFieldsColumnsEnum.Name).Text) 
          End If 
          "Если существует фильтр...
          If el.SubItems(UseFieldsColumnsEnum.Filter).Text <> "" Then 
             With sbFilter
               If .Length > 0 Then
                 .Append(" AND ") End If
                 "Добавляем имя столбца в список фильтров 
                 .AppendFormat("[{0}]", el.SubItems( _
                               UseFieldsColumnsEnum.Field).Text) 
                 "Добавляем оператор 
                 Select Case CType([Enum].Parse( _
                             GetType(FilterTypeEnum), _
                             el.SubItems(UseFieldsColumnsEnum.Filter _
                             ).Text.Replace(" ", "_")), FilterTypeEnum)
                   Case FilterTypeEnum.Equal 
                      .Append(" = ")
                   Case FilterTypeEnum.Not_Equal 
                      .Append(" <> ")
                   Case FilterTypeEnum.Greather_Than 
                      .Append(" >")
                   Case FilterTypeEnum.Less_Than 
                      .Append(" < ")
                   Case FilterTypeEnum.Like 
                      .Append(" LIKE ")
                   Case FilterTypeEnum.Between 
                      .Append(" BETWEEN ") 
                 End Select
                 "Получаем тип данных из определений столбцов 
                 Dim Typename As String = _
                     tableColumns.Select(String.Format("COLUMN_NAME='{0}'", _
                     el.SubItems(UseFieldsColumnsEnum.Field).Text))(0).Item( _
                     "DATA_TYPE").ToString 
                 "Если тип данных принадлежит к типам с символьными значениями,
                 "значение следует заключить в апострофы 
                 If Typename.ToUpper.IndexOf("CHAR") > -1 _
                    OrElse Typename.ToUpper.IndexOf("TEXT") > -1 Then
                   .Append(""") 
                 End If
                 .Append(el.SubItems(UseFieldsColumnsEnum.Filter_Value).Text) 
                 "Если оператор с Like, добавляем групповой символ "%" 
                 If CType([Enum].Parse(GetType(FilterTypeEnum), _
                          el.SubItems(UseFieldsColumnsEnum.Filter).Text.Replace( _ 
                          " ", "_")), FilterTypeEnum) = FilterTypeEnum.Like Then 
                  .Append("%") 
                 End If
                 If Typename.ToUpper.IndexOf("CHAR") > -1 _ 
                     OrElse Typename.ToUpper.IndexOf("TEXT") > -1 Then .Append(""") 
               End If 
             End With 
           End If 
         Next 
      End With
      "Добавляем порядок
      For Each o As System.Collections.Generic.KeyValuePair( _ 
                  Of Integer, SortInfo) In OrderList 
        With sbOrderBy
           If .Length <> 0 Then
             .Append(",") 
           End If
           .AppendFormat("[{0}].[{1}]", o.Value.TableName, o.Value.ColumnName) 
           If o.Value.SortOrder = SortOrder.Descending Then
             .Append(" DESC") 
           End If 
         End With 
      Next 
      If sbOrderBy.Length > 0 Then
        sbOrderBy.Insert(0, " ORDER BY ") 
      End If
      If sbFilter.Length > 0 Then
        sbFilter.Insert(0, " WHERE ") 
      End If
      "Строка запроса должна выглядеть так: SELECT columns FROM table, 
      "затем WHERE и в завершение ORDER 
      txtsql.Text = _ 
             String.Format(baseSQL, _
                          sbFields.ToString, _
                           String.Format("{0}.{1}", SchemaName, TableName))  _
                           sbFilter.ToString  " "  sbOrderBy.ToString 
    End Sub

    Разбор форматирования строки фильтрации

    При построении фильтра для каждого столбца, тип данных которого включает символы ( char, nchar, varchar, nvarchar, text, or ntext ), значения, сравниваемые с этим столбцом, должны быть заключены в символы апострофа (" ).

    При фильтрации типов данных smalldatetime или datetime код зависит от региональных и языковых параметров компьютера пользователя. Можете ли вы сказать, что дата " 11/10/05 " означает 10 ноября 2005 года или 11 октября 2005 года или, может быть, 5 октября 2011 года? Это может стать настоящей проблемой для любого приложения, но задача еще усложняется, если вы создаете приложение для интернета. В этом случае следует учитывать пользователей по всему миру.

    Самым простым решением может быть инструкция для пользователя по поводу того, в каком формате следует вводить дату. Но было бы лучше иметь решение, которое не требовало бы обучения пользователя. SQL Server использует форматы даты и времени, определяемые региональными и языковыми настройками операционной системы, на которой выполняется сервер. Тем не менее, он всегда распознает формат ISO, который соответствует следующему шаблону: гггг-мм-дд.

    Если вы примените на компьютере пользователя параметр CultureInfo, то можете получить значение DateTime. Используйте это значение для создания адекватного сравниваемого значения в вашем фильтре. Кроме того, просто для того, чтобы быть уверенным, что SQL Server была отправлена правильная строка, можно использовать функцию CONVERT. Вот как это делается:

    Dim theDate As Date = DateTime.Parse(el.SubItems(1).Text, _
                          System.Globalization.CultureInfo.CurrentCulture.DateTimeFormat) 
    sparam = String.Format("Convert(datetime,'{0}-{1}-{2}',120)", _
                           theDate.Year, _
                           theDate.Month, _
                           theDate.Day)

    Процедура tsbGen_Click генерирует следующий динамический запрос с использованием полей, показанных на рис. 3.1.

    SELECT [Name], [ProductNumber], [Color], [ListPrice], [Size], [Weight], [Style] 
      FROM Production.Product WHERE [ListPrice] >100 ORDER BY [Product].[Name]

    Параметры и безопасность динамических запросов

    Создание запросов, использующих вводимые пользователем значения, подвергает риску безопасность системы, особенно если приложение является открытым веб-сайтом; в этом случае вы не можете знать, кто ваши пользователи и какими знаниями они обладают. Если пользователь знает синтаксис SQL, он может проникнуть в вашу базу данных с помощью метода, который получил говорящее название - атака "SQL-injection" (инъекция SQL).

    Принцип атаки SQL-injection

    Давайте рассмотрим очень простой пример. Представьте себе открытый веб-сайт, который разрешает пользователям выполнять поиск продукции через интернет. На этом сайте приложение дает возможность пользователю выполнять поиск по части названия, поэтому оно создает и использует простые динамические запросы, подобные приведенному ниже:

    SqlDataSource1.SelectCommand = _
            "SELECT ProductNumber, Name, ListPrice " _
            "FROM Production.Product WHERE Name LIKE "" _
             TextBox1.Text  "%'" Me.GridView1.DataBind()

    Пользователь может ввести первые символы имени в текстовое поле Search By (Поиск по), и код будет искать все изделия, названия которых начинаются с этих символов, как показано на рис. 3.2.

    Итак, как эксперт по SQL, даже если вы ничего не знаете о приложении, вы можете представить себе, что незаметно для пользователя выполняется код, аналогичный тому, о котором мы говорили в этой лекции. Можно проверить, правы ли вы. Что произойдет, если вы введете в текстовое поле Search By следующую строку?

    a' UNION select @@Version, @@SERVERNAME, 0;-

    На самом деле вы получите результат, показанный на рис. 3.3, потому что простодушное приложение вставит пользовательский ввод в следующую инструкцию SQL:

    (рис 3.2) Простое приложение, которое использует текст из текстового поля для создания динамического запроса

    SELECT ProductNumber,
           Name,
           ListPrice 
      FROM Production.Product 
      WHERE Name LIKE "a" 
      UNION select @@Version,@@SERVERNAME,0;-%'

    Обратите внимание на символ закрывающей кавычки ( " ) и символы комментариев ( - - ), которые хитроумный пользователь использовал для трансформации динамического запроса в несколько корректных инструкций SQL.

    (рис 3.3) Пользователь ввел команду SQL в текстовое поле и, тем самым, выполнил атаку SQL-injection

    Теперь вам известно, какое ядро базы данных работает на сервере, знаете имя сервера, но что еще важнее, вы знаете, что сервер может выполнить ваши команды. Если вы захотите предоставить себе максимальные привилегия на этом сервере, вы можете создать своего пользователя!

    Совет. Если вам трудно запомнить точный синтаксис определенной команды, можно воспользоваться SQL Server Management Studio и создать сценарий, а затем использовать его, чтобы освежить память.

    Следующий код создает пользователя BadBoy и добавляет его в серверную роль sysadmin!

    USE [master]
    CREATE LOGIN [BadBoy] WITH 
    PASSWORD='Mad', 
    DEFAULT_DATABASE=[master], CHECK_EXPIRATION=OFF, 
    CHECK_POLICY=OFF EXEC master.sp_addsrvrolemember 
    @loginame = "BadBoy", @rolename = "sysadmin"

    Если ввести его в текстовое поле с закрывающей кавычкой ( " ) в конце предложения WHERE и символами комментария для нейтрализации закрывающей кавычки, вставленной приложением, приложение будет вынуждено сгенерировать и выполнить следующий код для добавления нового пользователя.

    SELECT
    ProductNumber,
    Name,
    ListPrice FROM Production.Product WHERE Name LIKE "a"; 
    USE [master]; CREATE LOGIN [BadBoy] WITH
    PASSWORD='Mad',
    DEFAULT_DATABASE=[master],
    CHECK_EXPIRATION=OFF, CHECK_POLICY=OFF; EXEC master..sp_addsrvrolemember
    @loginame = "BadBoy",
    @rolename = "sysadmin"; -%'

    Кроме того, можно проверить, был ли создан этот пользователь, если ввести в текстовое поле следующий код:

    a'
    UNION
    SELECT name,
    CONVERT(nvarchar(1), sysadmin) AS IsAdmin,
    0 AS Expr1 FROM sys.syslogins WHERE (name = "BadBoy")-%' "
    В результате получается следующая инструкция SQL:
    SELECT ProductNumber,
    Name,
    ListPrice FROM Production.Product WHERE Name LIKE "a" UNION SELECT name,
    CONVERT(nvarchar(1), sysadmin) AS IsAdmin,
    0 AS Expr1 FROM sys.syslogins WHERE (name = "BadBoy")-%'

    Как предотвратить атаку типа SQL-injection

    Теперь вы хорошо понимаете, что может произойти, поэтому потратьте немного усилий и времени на то, чтобы защитить вашу базу данных от этого вида атак; для этого выполните следующие рекомендации:

  • Старайтесь избегать построения инструкций SQL, используйте вместо этого указание отдельных параметров. В приложениях, подобных тому, что мы только что рассмотрели, используйте следующую инструкцию SQL, которая имеет параметр, и выполните привязку этого параметра к текстовому полю, чтобы текст в нем расценивался только как строка. Любая команда в этом поле не будет выполняться.
    SELECT ProductNumber,
    Name,
    ListPrice FROM Production.Product WHERE (Name LIKE @Param1 + "%")
  • По возможности храните запросы в хранимых процедурах.
  • Чтобы передать запрос SQL Server, используйте процедуру sp_ExecuteSql. Об этой системной хранимой процедуре рассказывается в следующем разделе.
  • Как использовать процедуру spExecuteSql

    Системная хранимая процедура sp_executeSql позволяет выполнять динамически определяемые инструкции T-SQL по тому же принципу, который использует команда EXECUTE. Однако sp_executeSql требует, чтобы вы указали параметры и типы данных, а также значения для этих параметров.

    При первом выполнении запроса системная хранимая процедура sp_executeSql создает и сохраняет план выполнения. Поскольку необходимо определить параметры, при каждом последующем выполнении запроса sp_executeSql просто обновляет значения параметров и использует один и тот же план выполнения.

    Поскольку системная хранимая процедура нуждается в определении параметров, можно создавать сложные запросы не только для инструкций SELECT, но и для любых действий в отношении базы данных, которые требуются вашему приложению. Ваши инструкции могут содержать даже параметры вывода.

    Предположим, что нужно указать новую причину задержки выпуска изделий в базе данных Adventure Works, при этом новую причину следует указать для всех заказов, ожидаемая дата поставки которых больше, чем конечная дата выполнения заказа. Для выполнения двух этих задач в одном пакете можно использовать следующий сценарий T-SQL. Этот код можно найти в файлах примеров под именем newReason.sql.

    -Переменная для инструкции T-SQL 
    DECLARE @sql nvarchar(300);
    SET @sql='INSERT INTO [AdventureWorks].[Production].[ScrapReason] 
         ([Name]
         ,[ModifiedDate]) 
      VALUES 
         (@NewNameSQL ,GetDate());" 
    SET @sql=@sql + "SET @NewIdSQL=(SELECT ScrapReasonID " + 
             "FROM [AdventureWorks].[Production].[ScrapReason] " + 
             "WHERE Name=@NewNameSQL)" 
    -Переменная для получения нового идентификатора 
    DECLARE @NewId int;
    -Объявляем параметры 
    DECLARE @Params nvarchar(200);
    SET @Params='@NewNameSQL nvarchar(100), @NewIdSQL int OUTPUT';
    -Добавляем новое имя 
    DECLARE @NewName nvarchar(100); 
    SET @NewName='Delayed 
    Production';
    -Выполнение вставки в ScrapReason
    EXEC sp_executeSql @sql,@params,@NewNameSQL=@NewName, 
       @NewIdSQL=@NewId OUTPUT;
    /*
    Заменяем инструкцию T-SQL для обновления столбца ScrapReasonID
    на указанную новую инструкцию для всех записей, в которой конечная дата
    выполнения заказа превышает ожидаемую дату
    */
    SET @sql="UPDATE [AdventureWorks].[Production].[WorkOrder]" + 
             "SET ScrapReasonID=@NewIdSQL " + 
             "WHERE (EndDate > DueDate) AND (ScrapReasonID IS NULL)";
    -Определяем параметр для этого нового предложения 
    SET @Params='@NewIdSQL int ";
    EXEC sp_executeSql @sql, @params, @NewIdSQL=@NewId; 
    GO

    Как видите, системная хранимая процедура sp_executeSql позволяет применить параметры к динамическим инструкциям SQL, не являющимся простыми запросами SELECT.

    Заключение

    Прочитав эту лекцию, вы узнали, как собирать пользовательский ввод и форматировать его для динамического создания запросов. Вы получили информацию о некоторых недостатках построения динамических запросов, а также о потенциальном риске для безопасности, который с ними связан.

    Чтобы сохранить в безопасности базу данных, используйте параметризованные запросы и выполняйте их через хранимую процедуру sp_executeSql. Эту методику можно использовать не только для задач, связанных с простым возвращением данных пользователю.

    Чтобы Выполните следующие действия
    Создать запрос в Конструкторе запросов SQL Server Management Studio Щелкните правой кнопкой мыши на таблице в Object Explorer (Обозревателе объектов), затем выберите из контекстного меню команду Open Table (Открыть таблицу). Для построения запросов используйте кнопки на панели инструментов Конструктора запросов и соответствующие панели
    Получить список представлений, имеющихся в базе данных Выполните инструкцию SQL
    SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE
       FROM INFORMATION_SCHEMA.TABLES
    Создавать запросы динамически Соедините ключевое слово SELECT со списком имен столбцов и ключевое слово FROM, после которого указано имя таблицы
    Выполнить сортировку результатов Добавьте в инструкцию предложение ORDER BY со списком имен столбцов. Добавьте инструкцию DESC, чтобы упорядочить информацию в порядке убывания.
    Выполнить фильтрацию результатов Добавьте предложение WHERE (перед предложением ORDER BY, если оно используется) с условиями фильтрации.
    Предотвратить атаку типа SQL-injection Всегда используйте параметризованные запросы. Попробуйте выполнять все операции в соответствующем контексте безопасности.
    Ускорить выполнение динамических запросов Используйте хранимую процедуру sp_executeSql для кэширования плана запроса.
    Страницы:

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

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

    Интерфейс пользователя для построения запросов

    Среда SQL Server Management Studio включает сложный интерфейс для построения запросов. Давайте изучим этот интерфейс, чтобы у вас сформировалось представление о том, как можно создавать запросы динамически. Вашему приложению не понадобятся все элементы управления, которые предоставляет среда SQL Server Management Studio. По сути, нужно тщательно продумать, как наилучшим образом ограничить пользователям возможности выбора.

    Создаем запрос при помощи Конструктора запросов SQL Server Management Studio

  • В SQL Server Management Studio, если нужно, присоедините базу данных Совет. О том, как присоединить базу данных, рассказывается в лекциях 6-7 курса "Разработка и защита баз данных в Microsoft SQL Server 2005", которая называется "Перенос базы данных на другие системы".
  • Разверните узел Tables (Таблицы) в Object Explorer (Обозревателе объектов).
  • Найдите таблицу
  • В SQL Server Management Studio появилась панель инструментов Query Designer (Конструктор запросов), показанная на рисунке. Первые три кнопки этой панели инструментов отображают таблицы вашего запроса, список выбранных столбцов и действительный код SQL, который был сгенерирован.
  • Нажмите первую кнопку Show Diagram Pane (Показать область схемы), затем выберите столбцы CustomerID и AccountNumber.
  • Нажмите вторую кнопку - Show Criteria Pane (Показать область условий). Снимите флажок Output (Вывод) в строке *. Щелкните мышью в столбце Sort Type (Тип сортировки) в строке CustomerID и выберите из раскрывающегося списка пункт Ascending (По возрастанию).
  • При необходимости переместите видимую область окна вправо, чтобы отобразить столбец Filter (Фильтр). В той же строке CustomerID введите <10 и нажмите клавишу Enter.
  • Нажмите третью кнопку - Show SQL Pane (Показать область SQL кода). Окно программы примет следующий вид:
  • Обратите внимание на строку запроса, которую программа SQL Server Management Studio сгенерировала в области SQL кода. Нажмите на панели инструментов кнопку Execute SQL (Выполнить SQL код). В панель Results (Результаты) будет возвращено 9 записей.

    Пользователям ваших приложений, как правило, не нужно ничего делать со строкой запроса. Они ничего не знают об SQL. Ваша обязанность - правильно построить строку запроса, либо на этапе проектирования, либо через код приложения в процессе выполнения рабочего цикла. Динамическое построение запроса во время рабочего цикла позволяет ввести в строку запроса пользовательский ввод.

  • Извлечение информации о таблицах базы данных

    Чтобы предоставить пользователю список параметров, приложению, вероятно, придется извлечь информацию о таблицах базы данных. Существует несколько способов получить эту информацию. Самый важный из этих методов - использование схемы INFORMATION_SCHEMA. Эта схема является стандартной в любой базе данных.

    Применение INFORMATION_SCHEMA

    Схема INFORMATION_SCHEMA - это особая схема, которая есть в каждой базе данных. Она содержит определения некоторых объектов базы данных.

    INFORMATION_SCHEMA соответствует стандарту ANSI, который предназначен для извлечения информации от любого ANSI-совмести-мого ядра базы данных. В SQL Server INFORMATION_SCHEMA состоит из набора представлений, которые запрашивают таблицы базы данных sys*, содержащие информацию о структуре базы данных. Запрос к этим таблицам можно выполнить напрямую, точно так же, как к любым таблицам базы данных. Однако в большинстве случаев для того, чтобы извлечь информацию из таблиц *sys, лучше использовать представления схемы INFORMATION_SCHEMA.

    Примечание. Схема INFORMATION_SCHEMA иногда запрашивает таблицы, которые не нужны, что наносит ущерб производительности. В следующем примере данной лекции это не особенно важно, потому что приложение уже ожидало пользовательского ввода. Однако это следует учитывать, если скорость является важным аспектом для вашего приложения.

    Вот базовый код T-SQL, который используется для получения информации о столбцах, входящих в таблицу:

    SELECT TABLE_SCHEMA,
           TABLE_NAME,
           COLUMN_NAME,
           ORDINAL_POSITION,
           DATA_TYPE 
      FROM INFORMATION_SCHEMA.COLUMNS 
      WHERE TABLE_NAME = "<TABLE_NAME>")

    Обратите внимание на то, что для получения схемы для таблицы нужно выбрать поле TABLE_SCHEMA. Это может иметь значение для создания аналогичных запросов в дальнейшем. Чтобы экспериментировать с методами, описанными в данной лекции, создайте новый проект в Visual Studio.

    Создаем новый проект Visual Studio

  • Выберите из меню Start (Пуск) команды All Programs, Microsoft Visual Studiio 2005, Microsoft Visual Studio 2005.
  • В меню Visual Studio выберите команды File, New, Project (Файл, Создать, Проект).
  • В панели Project Types (Типы проектов) разверните узел Visual Basic (Решения Visual Basic) и выберите в панели Templates (Шаблоны) шаблон Application (Приложение). Дайте проекту имя Chapter7 и нажмите кнопку ОК,
  • Приложение для этого примера можно найти в файлах примеров в папке \Chapter7\DynQuery. Вы можете вырезать и вставлять код для следующих процедур из файла Form1.vb.
  • Получение списка таблиц и представлений

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

    SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE 
       FROM INFORMATION_SCHEMA.TABLES

    В приложении этот запрос можно использовать следующим образом.

    Получаем список таблиц

  • Дважды щелкните форму Form1, сгенерированную Visual Studio. На экране появится процедура Form1_Load. Объявите две глобальные переменные и добавьте вызов процедуры RetrieveTables в процедуру Form1_Load, чтобы она выглядела примерно так:
    Public Class Form1
     Dim SchemaName As String = "", 
          TableName As String = ""
     Private Sub Form1_Load(ByVal sender As System.Object, _
                            ByVal e As System.EventArgs) 
        Handles 
        MyBase.Load
        RetrieveTables() 
     End Sub
  • После этого добавьте показанную здесь процедуру RetrieveTables после процедуры Form1_Load. Чтобы эта программа могла выполняться, необходимо, чтобы к экземпляру сервера SQLExpress была присоединена база данных Adventure Work. Узнать о том, как присоединить базу данных, можно в лекции 6-7 курса "Разработка и защита баз данных в Microsoft SQL Server 2005".
    Sub RetrieveTables()
      
      Dim FieldName As String
      Dim MyConnection As New SqlClient.SqlConnection( _ 
                              "Data Source=.\SQLExpress;"  _
                              "Initial Catalog=AdventureWorks;Trusted_
                              Connection=Yes;") 
      Dim com As New SqlClient.SqlCommand( _
                              "SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE "  _ 
                              "FROM INFORMATION_SCHEMA.TABLES", _ 
                              MyConnection) MyConnection.Open()
      Dim dr As SqlClient.SqlDataReader = com.ExecuteReader With dr
      Do While .Read
         "Следует сохранить эту информацию для использования в 
         "форме или на странице
         SchemaName = .GetString(0)
         TableName = .GetString(1)
         FieldName = .GetString(2)
         Console.WriteLine("{0} {1} {2}", _
         SchemaName, TableName, FieldName) Loop .Close() End With
         "Предположим, что пользователь выбрал следующие схему и таблицу: 
         SchemaName = "Sales" 
         TableName = "Customer" 
    End Sub
    Примечание. В реальном приложении строка соединения и строка SQL могут обслуживаться ресурсами приложения или конфигурационным файлом приложения.
  • Выберите из меню Debug (Отладка) команду Start Debugging (Начать отладку), чтобы создать и выполнить проект. Появится пустое окно формы, а в панели Output (Вывод) в окне Visual Studio будет записана информация схемы.
  • Закройте форму, чтобы завершить работу приложения.
  • Приведенный выше код на Visual Basic инициализирует объект SqlCommand с именем com со строкой SQL, которую нужно выполнить, а затем выполняет объект SqlCommand. Это самый простой способ выполнить предложение T-SQL из приложения.

    В качестве упражнения можно разместить схемы и таблицы, полученные в процессе выполнения процедуры Load нашей формы, в пользовательском интерфейсе приложения в виде формы, чтобы пользователь мог выбрать схему и таблицу для работы. Для примера в этой лекции мы предполагаем, что пользователь выбрал схему Sales и таблицу Customer.

    После того, как пользователь выбрал таблицу, можно извлечь список столбцов для этой таблицы при помощи того же метода, используя пользовательский ввод в качестве имени таблицы в запросе. Для этого в строку запроса следует ввести заместитель, а затем заменить этот заместитель вызовом String.Format. В приведенном ниже коде заместитель в строке запроса - (0).

    Получаем список столбцов

  • Добавьте следующую процедуру RetrieveColumns в код ниже процедуры RetrieveTables:
    Sub RetrieveColumns(ByVal TableName As String)
      MyConnection As New SqlClient.SqlConnection( _ 
                  "Data Source=.\SQLExpress;"  _
                  "Initial Catalog=AdventureWorks;Trusted_Connection=Yes;") 
      Dim sqlStr As String
      sqlStr = "SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, " + _
               "ORDINAL_POSITION, DATA_TYPE " + _ 
               "FROM INFORMATION_SCHEMA.COLUMNS " + _ 
               "WHERE (TABLE_NAME = "{0}")" 
      Dim tableColumns As New DataTable Dim da As New SqlClient.SqlDataAdapter( _
                 String.Format(sqlStr, TableName), MyConnection) da.Fill(tableColumns)
      For i As Integer = 0 To tableColumns.Rows.Count - 1 
        With tableColumns.Rows.Item(i) 
           Console.WriteLine("{0} {1} {2}", _ 
                             .Item(1), .Item(2), .Item(3)) 
        End With 
      Next 
    End Sub
  • В процедуру Form1_Load добавьте следующий вызов процедуры RetrieveColumns после процедуры RetrieveTables:
    RetrieveColumns(TableName)
  • Выберите из меню Debug (Отладка) команду Start Debugging (Начать отладку), чтобы создать и выполнить проект. Появится пустое окно формы, а в панели Output (Вывод) в окне Visual Studio будет записана информация о столбцах и таблице.
  • Закройте форму, чтобы завершить работу приложения.
  • Объект типа DataTable в процедуре RetrieveColumns может быть использован для заполнения элементов управления CheckListBox или ListView с включенными элементами управления CheckBoxes, чтобы пользователь мог выбрать нужные поля.

    Добавляем в форму элемент управления ListView

  • В окне Visual Studio перейдите на вкладку Form1.vb [Design].
  • Выберите из меню View (Вид) команду Toolbox (Панель элементов).
  • Перетащите мышью элемент управления Label из панели элементов в форму. Щелкните правой кнопкой мыши на label и выберите из контекстного меню команду Properties (Свойства). В окне Properties (Свойства) измените текст в строке Label на Столбцы.
  • Теперь перетащите мышью элемент управления
  • Перетащите правую границу ListView (Список), чтобы увеличить ширину списка. Щелкните правой кнопкой мыши на ListView (Список) и выберите из контекстного меню команду Properties (Свойства). В окне Properties (Свойства) задайте для свойства CheckBoxes значение True, а свойство View на List.
  • Щелкните правой кнопкой мыши на форме и выберите из контекстного меню команду View Code (Перейти к коду), чтобы вернуться к нашему коду. В процедуре RetrieveColumns замените предложение Console.WriteLine следующим кодом:
    ListView1.Items.Add(.Item(2))
  • Выполните построение и запустите проект. Вы увидите в форме список столбцов с полями для установки флажков, как показано на рисунке:
  • Закройте форму, чтобы завершить работу приложения.
  • После того, как пользователь выберет таблицу и столбцы для просмотра, приложение может создать запрос. Базовая структура запроса на выборку включает список столбцов через запятую:

    SELECT <Column1>, <Column2>, <Column3> 
       FROM <Table_Name>

    Вы легко можете создать список столбцов из элемента управления ListView.

    Форматирование пользовательского ввода для динамического запроса

  • В окне Visual Studio перейдите на вкладку Form1.vb [Design].
  • Перетащите элемент управления Button (Кнопка) под элемент управления ListView (Список). В окне Properties (Свойства) измените текст на "Выполнить запрос".
  • Дважды щелкните на элементе Button (Кнопка). Visual Studio создаст процедуру с именем Button1_Click. Добавьте в эту процедуру следующий код:
    Private Sub Form1_Load(ByVal sender As System.Object, ByVal e As System.EventArgs) 
       Handles Button1.Click 
       Dim baseSQL As String = "SELECT {0} FROM {1}" 
       Dim sbFields As New System.Text.StringBuilder 
       Dim numChecked As Integer = 0 
       With sbFields
         For Each el As ListViewItem In ListView1.Items 
           If el.Checked Then
             numChecked = numChecked + 1 
             If .Length <> 0 Then
                .Append(",") 
             End If
             Console.WriteLine(el.Text) 
             AppendFormat("[{0}]", el.Text) 
           End If 
         Next 
       End With
       Console.WriteLine(sbFields) 
    End Sub
    Примечание. Если вам нужно выполнить со строкой более двух манипуляций, лучше использовать объект StringBuilder, чтобы получить результат, а затем использовать соединение строк.
  • Постройте и запустите проект. Выберите несколько столбцов в элементе управления ListView, нажмите кнопку "Выполнить запрос" и изучите список столбцов с разделителем-запятой, который отображается в панели Output (Вывод).
  • Закройте форму, чтобы завершить работу приложения.
  • В завершение можно завершить создание динамической инструкции, добавив список столбцов и имя таблицы. Поскольку таблица может принадлежать схеме, в запросе лучше использовать полное уточненное имя таблицы.
  • Совет. В нашем приложении, если безопасность не является большой проблемой, вы, возможно, захотите отобразить строку запроса в элементе управления TextBox и разрешить пользователю изменять эту строку перед выполнением запроса. Прочитайте раздел "Параметры и безопасность динамических запросов", в котором приводится информация о риске для системы безопасности, который возникает, если разрешить пользователям напрямую изменять строку запроса.

    Строим и выполняем динамический запрос

  • Замените предложение Console.WriteLine в конце процедуры Button1_Click следующим кодом, который создает строку динамического запроса:
    Dim tblsql As String 
      Dim txtsql As String
      tblsql = String.Format("{0}.{1}", SchemaName, TableName)
      txtsql = String.Format(baseSQL, _ 
               sbFields.ToString, tblsql)
  • После этого добавьте в конце процедуры Button1_Click следующий код для выполнения динамического запроса. Для этого примера первые 100 строк результата отображаются в панели Output (Вывод). В реальном приложении следует использовать в качестве DataSource для элемента управления DataGridView значение DataTable. Элементы управления DataGridView идеально подходят для отображения результатов запроса для пользователя.
    Dim MyConnection As New SqlClient.SqlConnection( _
               "Data Source=.\SQLExpress;"  _
               "Initial Catalog=AdventureWorks;Trusted_Connection=Yes;") 
      Dim com As New SqlClient.SqlCommand(txtsql, MyConnection) 
      Dim tableResults As New DataTable tableResults.Clear()
      Dim da As New SqlClient.SqlDataAdapter(com) 
      
      da.Fill(tableResults) 
      For i As Integer = 0 To tableResults.Rows.Count - 1
        With tableResults.Rows.Item(i)
          Dim rowstr As New System.Text.StringBuilder 
          For j As Integer = 0 To numChecked - 1 
            rowstr.Append(.Item(j)) 
            rowstr.Append(" ") 
          Next
          Console.WriteLine(rowstr) 
        End With
        If i > 100 Then Exit For 
      Next
  • Постройте и запустите проект. Выберите несколько столбцов в элементе управления ListView и нажмите кнопку Выполнить запрос. Результаты динамического запроса отображаются в панели Output (Вывод). Вы можете выбрать другие столбцы и нажать кнопку еще раз, чтобы построить и выполнить другой динамический запрос.
  • Закройте форму, чтобы завершить работу приложения.
  • Выполнение динамического запроса от имени другой учетной записи

    Одной из лучших методик обеспечения безопасности считается запрещение разрешения READ для таблиц с целью предотвращения несанкционированного доступа, поэтому иногда приложению приходится выполнять запрос, динамически сгенерированный пользователем с использованием других учетных данных.

    В SQL Server 2005 для того, чтобы действовать от имени другого пользователя в рамках определенной команды, можно выполнить команду EXECUTE. Синтаксис команды EXECUTE:

    EXECUTE ("<dynamic query>") as 
            USER='<User Name>'
    Предупреждение. Это очень опасная идея. Как вы увидите далее в этой лекции, она подвергает безопасность базы данных потенциальному риску. Чтобы не допустить нарушений безопасности, приложение должно точно определить вид запроса, который оно должно выполнить.

    Динамическая сортировка и фильтрация

    Когда запрос возвращает большой набор записей, результаты будут полезнее, если пользователь сможет задать порядок возвращения строк, а также то, какие строки следует отфильтровать.

    Добавление порядка сортировки в динамический запрос

    Чтобы выполнить сортировку динамического запроса, просто добавьте предложение ORDER BY, указав после него список столбцов в порядке, выбранном пользователем. Если пользователь хочет получить информацию в порядке убывания, добавьте также ключевое слово DESC.

    В следующем коде упорядоченные выборки сохраняются в специальной коллекции, которая называется SortInfo ; это особый пользовательский класс, предназначенный для хранения всей информации, необходимой для каждой упорядоченной выборки.

    Public Class SortInfo
    Public SchemaName As String
    Public TableName As String
    Public ColumnName As String
    Public SortOrder As SortOrder
    Public SortPosition As Integer End Class

    Чтобы создать упорядоченный список, код проходит по этому списку, добавляя определение в StringBuilder, и в случае, если задано какое-либо упорядочение, добавляет предложение ORDER TO в начале содержимого StringBuilder.

    For Each o As System.Collections.Generic.KeyValuePair( _ 
               Of Integer, SortInfo) In OrderList 
      With sbOrderBy
        If .Length <> 0 Then
           .Append(",") 
        End If
        .AppendFormat("[{0}].[{1}]", o.Value.TableName, _
                      o.Value.ColumnName) 
        If o.Value.SortOrder = SortOrder.Descending Then
           .Append(" DESC") 
        End If 
      End With 
    Next 
    If sbOrderBy.Length > 0 Then
      sbOrderBy.Insert(0, " ORDER BY ") 
    End If

    Фильтрация динамического запроса

    Добавить фильтр в запрос немного сложнее, чем добавить порядок сортировки - приходится иметь в виду много различных операторов и типов данных. Но результат стоит затраченных усилий. Как и раньше, нашей исходной точкой будет базовый синтаксис предложения WHERE, задающего фильтрацию в инструкции SELECT.

    SELECT <Field List> 
       FROM <Table Name> 
       WHERE <Condition List> 
       ORDER BY <Order List>

    Список условий следует проектировать с учетом следующих рекомендаций:

  • Используйте шаблон <ColumnName> <compare operator> <value>.
  • Сопоставьте значение datatype столбцу datatype.
  • Добавьте дополнительные фильтры в Condition_List с помощью ключевых слов AND или OR.
  • Необходимо реализовать для пользователей интерфейс, позволяющий указать значения и операторы сравнения для каждого фильтра. Цель -получить пользовательский ввод и создать строку с условиями фильтрации, которую можно будет вставить в динамический запрос.

    Законченный пример - приложение Dynamic Query

    Следующие примеры основаны на примере приложения Dynamic Queries, которое находится в папке Filters в файлах примеров к 3 лекции. Пример приложения Dynamic Queries - это расширенная версия приложения, которое мы создали в этой лекции. Это приложение включает элемент управления ListView, который показывает все настройки, сделанные пользователем для создания динамического запроса, как показано на рис. 3.1. Чтобы попробовать сделать то же самое, выполните действия, описанные ниже.

    (рис 3.1) Элемент управления ListView с информацией о запросе, который нужно создать пользователю

    Запускаем пример приложения Dynamic Queries

  • Дважды щелкните файл Ch07.sln в папке Chapter07\Filter, чтобы открыть проект в Visual Studio.
  • Выберите команду Start Debugging (Начать отладку) из меню Debug (Отладка). Выберите сервер и нажмите кнопку ОК, Visual Studio построит и запустит проект, отобразив начальную форму, как показано ниже.
  • В форме frmPpal щелкните раскрывающийся список Data в панели инструментов, а затем выберите Table Sales, Customer.
  • Выберите элементы CustomerID, TerritoryID и AccountNumber в поле со списком и нажмите кнопку "Стрелка вправо" в центре формы.
  • Нажмите кнопку Build Query на панели инструментов. Теперь окно программы вверху справа отображает поля, которые вы выбрали, а внизу справа - соответствующий запрос SQL, как показано на рисунке.
  • Щелкните правой кнопкой мыши в строке CustomerID и выберите из контекстного меню команды Order, Ascending.
  • Щелкните правой кнопкой мыши в строке CustomerID и выберите из контекстного меню команды Order, Ascending. В диалоговом окне Filter выберите в раскрывающемся списке Less_Than и введите в текстовое поле 20. Нажмите кнопку ОК.
  • Снова нажмите кнопку Build Query на панели инструментов. Теперь динамический запрос выглядит следующим образом:
    SELECT [CustomerID],[TerritoryID],[AccountNumber] 
       FROM Sales.Customer WHERE [CustomerID] < 20 
       ORDER BY [Customer].[CustomerID]
  • Нажмите кнопку Execute Query, чтобы выполнить этот динамический запрос. В окне программы отобразятся следующие результаты:
  • Как пример приложения строит строку фильтрации

    Это приложение хранит различные значения, показанные в правой верхней части формы, в элементе управления ListView с именем lvUseFields, показанном в табл. 3.1.

    Поля в элементе управления ListView lvUseFields
    СвойствоСодержит
    Text Имя схемы
    SubItems(1) Имя таблицы
    SubItems(2) Имя поля
    SubItems(3) Имя (Псевдоним для результирующего столбца)
    SubItems(4) Состояние сортировки
    SubItems(5) Оператор фильтрации
    SubItems(6) Значение фильтра

    Когда вы нажимаете кнопку Build Query на панели инструментов, следующий код выполняет построение динамического запроса, используя содержимое элемента управления ListView lvUseFields. Обратите особое внимание на ту часть кода, которая показана полужирным шрифтом и демонстрирует построение предложения WHERE для фильтрации динамического запроса.

    Private Sub tsbGen_Click(ByVal sender As System.Object, _ 
                             ByVal e As System.EventArgs) 
      Handles tsbGen.Click 
      Dim baseSQL As String = "SELECT {0} from {1}" 
      "Список отображаемых полей 
      Dim sbFields As New System.Text.StringBuilder           
      "Порядок списка столбцов 
      Dim sbOrderBy As New System.Text.StringBuilder("")
      "Фильтры
      Dim sbFilter As New System.Text.StringBuilder("")
      
      With sbFields
        For Each el As ListViewItem In lvUseFields.Items 
          If .Length <> 0 Then
            .Append(",") 
          End If
          "Добавляем в список полей 
          Column .AppendFormat("[{0}]", _
                           el.SubItems(UseFieldsColumnsEnum.Field).Text) 
          "Если Name не совпадает с именем столбца, добавляем Alias 
          If el.SubItems(UseFieldsColumnsEnum.Name).Text <> _
                         el.SubItems(UseFieldsColumnsEnum.Field).Text Then
            .AppendFormat(" AS [{0}]", _
            el.SubItems(UseFieldsColumnsEnum.Name).Text) 
          End If 
          "Если существует фильтр...
          If el.SubItems(UseFieldsColumnsEnum.Filter).Text <> "" Then 
             With sbFilter
               If .Length > 0 Then
                 .Append(" AND ") End If
                 "Добавляем имя столбца в список фильтров 
                 .AppendFormat("[{0}]", el.SubItems( _
                               UseFieldsColumnsEnum.Field).Text) 
                 "Добавляем оператор 
                 Select Case CType([Enum].Parse( _
                             GetType(FilterTypeEnum), _
                             el.SubItems(UseFieldsColumnsEnum.Filter _
                             ).Text.Replace(" ", "_")), FilterTypeEnum)
                   Case FilterTypeEnum.Equal 
                      .Append(" = ")
                   Case FilterTypeEnum.Not_Equal 
                      .Append(" <> ")
                   Case FilterTypeEnum.Greather_Than 
                      .Append(" >")
                   Case FilterTypeEnum.Less_Than 
                      .Append(" < ")
                   Case FilterTypeEnum.Like 
                      .Append(" LIKE ")
                   Case FilterTypeEnum.Between 
                      .Append(" BETWEEN ") 
                 End Select
                 "Получаем тип данных из определений столбцов 
                 Dim Typename As String = _
                     tableColumns.Select(String.Format("COLUMN_NAME='{0}'", _
                     el.SubItems(UseFieldsColumnsEnum.Field).Text))(0).Item( _
                     "DATA_TYPE").ToString 
                 "Если тип данных принадлежит к типам с символьными значениями,
                 "значение следует заключить в апострофы 
                 If Typename.ToUpper.IndexOf("CHAR") > -1 _
                    OrElse Typename.ToUpper.IndexOf("TEXT") > -1 Then
                   .Append(""") 
                 End If
                 .Append(el.SubItems(UseFieldsColumnsEnum.Filter_Value).Text) 
                 "Если оператор с Like, добавляем групповой символ "%" 
                 If CType([Enum].Parse(GetType(FilterTypeEnum), _
                          el.SubItems(UseFieldsColumnsEnum.Filter).Text.Replace( _ 
                          " ", "_")), FilterTypeEnum) = FilterTypeEnum.Like Then 
                  .Append("%") 
                 End If
                 If Typename.ToUpper.IndexOf("CHAR") > -1 _ 
                     OrElse Typename.ToUpper.IndexOf("TEXT") > -1 Then .Append(""") 
               End If 
             End With 
           End If 
         Next 
      End With
      "Добавляем порядок
      For Each o As System.Collections.Generic.KeyValuePair( _ 
                  Of Integer, SortInfo) In OrderList 
        With sbOrderBy
           If .Length <> 0 Then
             .Append(",") 
           End If
           .AppendFormat("[{0}].[{1}]", o.Value.TableName, o.Value.ColumnName) 
           If o.Value.SortOrder = SortOrder.Descending Then
             .Append(" DESC") 
           End If 
         End With 
      Next 
      If sbOrderBy.Length > 0 Then
        sbOrderBy.Insert(0, " ORDER BY ") 
      End If
      If sbFilter.Length > 0 Then
        sbFilter.Insert(0, " WHERE ") 
      End If
      "Строка запроса должна выглядеть так: SELECT columns FROM table, 
      "затем WHERE и в завершение ORDER 
      txtsql.Text = _ 
             String.Format(baseSQL, _
                          sbFields.ToString, _
                           String.Format("{0}.{1}", SchemaName, TableName))  _
                           sbFilter.ToString  " "  sbOrderBy.ToString 
    End Sub

    Разбор форматирования строки фильтрации

    При построении фильтра для каждого столбца, тип данных которого включает символы ( char, nchar, varchar, nvarchar, text, or ntext ), значения, сравниваемые с этим столбцом, должны быть заключены в символы апострофа (" ).

    При фильтрации типов данных smalldatetime или datetime код зависит от региональных и языковых параметров компьютера пользователя. Можете ли вы сказать, что дата " 11/10/05 " означает 10 ноября 2005 года или 11 октября 2005 года или, может быть, 5 октября 2011 года? Это может стать настоящей проблемой для любого приложения, но задача еще усложняется, если вы создаете приложение для интернета. В этом случае следует учитывать пользователей по всему миру.

    Самым простым решением может быть инструкция для пользователя по поводу того, в каком формате следует вводить дату. Но было бы лучше иметь решение, которое не требовало бы обучения пользователя. SQL Server использует форматы даты и времени, определяемые региональными и языковыми настройками операционной системы, на которой выполняется сервер. Тем не менее, он всегда распознает формат ISO, который соответствует следующему шаблону: гггг-мм-дд.

    Если вы примените на компьютере пользователя параметр CultureInfo, то можете получить значение DateTime. Используйте это значение для создания адекватного сравниваемого значения в вашем фильтре. Кроме того, просто для того, чтобы быть уверенным, что SQL Server была отправлена правильная строка, можно использовать функцию CONVERT. Вот как это делается:

    Dim theDate As Date = DateTime.Parse(el.SubItems(1).Text, _
                          System.Globalization.CultureInfo.CurrentCulture.DateTimeFormat) 
    sparam = String.Format("Convert(datetime,'{0}-{1}-{2}',120)", _
                           theDate.Year, _
                           theDate.Month, _
                           theDate.Day)

    Процедура tsbGen_Click генерирует следующий динамический запрос с использованием полей, показанных на рис. 3.1.

    SELECT [Name], [ProductNumber], [Color], [ListPrice], [Size], [Weight], [Style] 
      FROM Production.Product WHERE [ListPrice] >100 ORDER BY [Product].[Name]

    Параметры и безопасность динамических запросов

    Создание запросов, использующих вводимые пользователем значения, подвергает риску безопасность системы, особенно если приложение является открытым веб-сайтом; в этом случае вы не можете знать, кто ваши пользователи и какими знаниями они обладают. Если пользователь знает синтаксис SQL, он может проникнуть в вашу базу данных с помощью метода, который получил говорящее название - атака "SQL-injection" (инъекция SQL).

    Принцип атаки SQL-injection

    Давайте рассмотрим очень простой пример. Представьте себе открытый веб-сайт, который разрешает пользователям выполнять поиск продукции через интернет. На этом сайте приложение дает возможность пользователю выполнять поиск по части названия, поэтому оно создает и использует простые динамические запросы, подобные приведенному ниже:

    SqlDataSource1.SelectCommand = _
            "SELECT ProductNumber, Name, ListPrice " _
            "FROM Production.Product WHERE Name LIKE "" _
             TextBox1.Text  "%'" Me.GridView1.DataBind()

    Пользователь может ввести первые символы имени в текстовое поле Search By (Поиск по), и код будет искать все изделия, названия которых начинаются с этих символов, как показано на рис. 3.2.

    Итак, как эксперт по SQL, даже если вы ничего не знаете о приложении, вы можете представить себе, что незаметно для пользователя выполняется код, аналогичный тому, о котором мы говорили в этой лекции. Можно проверить, правы ли вы. Что произойдет, если вы введете в текстовое поле Search By следующую строку?

    a' UNION select @@Version, @@SERVERNAME, 0;-

    На самом деле вы получите результат, показанный на рис. 3.3, потому что простодушное приложение вставит пользовательский ввод в следующую инструкцию SQL:

    (рис 3.2) Простое приложение, которое использует текст из текстового поля для создания динамического запроса

    SELECT ProductNumber,
           Name,
           ListPrice 
      FROM Production.Product 
      WHERE Name LIKE "a" 
      UNION select @@Version,@@SERVERNAME,0;-%'

    Обратите внимание на символ закрывающей кавычки ( " ) и символы комментариев ( - - ), которые хитроумный пользователь использовал для трансформации динамического запроса в несколько корректных инструкций SQL.

    (рис 3.3) Пользователь ввел команду SQL в текстовое поле и, тем самым, выполнил атаку SQL-injection

    Теперь вам известно, какое ядро базы данных работает на сервере, знаете имя сервера, но что еще важнее, вы знаете, что сервер может выполнить ваши команды. Если вы захотите предоставить себе максимальные привилегия на этом сервере, вы можете создать своего пользователя!

    Совет. Если вам трудно запомнить точный синтаксис определенной команды, можно воспользоваться SQL Server Management Studio и создать сценарий, а затем использовать его, чтобы освежить память.

    Следующий код создает пользователя BadBoy и добавляет его в серверную роль sysadmin!

    USE [master]
    CREATE LOGIN [BadBoy] WITH 
    PASSWORD='Mad', 
    DEFAULT_DATABASE=[master], CHECK_EXPIRATION=OFF, 
    CHECK_POLICY=OFF EXEC master.sp_addsrvrolemember 
    @loginame = "BadBoy", @rolename = "sysadmin"

    Если ввести его в текстовое поле с закрывающей кавычкой ( " ) в конце предложения WHERE и символами комментария для нейтрализации закрывающей кавычки, вставленной приложением, приложение будет вынуждено сгенерировать и выполнить следующий код для добавления нового пользователя.

    SELECT
    ProductNumber,
    Name,
    ListPrice FROM Production.Product WHERE Name LIKE "a"; 
    USE [master]; CREATE LOGIN [BadBoy] WITH
    PASSWORD='Mad',
    DEFAULT_DATABASE=[master],
    CHECK_EXPIRATION=OFF, CHECK_POLICY=OFF; EXEC master..sp_addsrvrolemember
    @loginame = "BadBoy",
    @rolename = "sysadmin"; -%'

    Кроме того, можно проверить, был ли создан этот пользователь, если ввести в текстовое поле следующий код:

    a'
    UNION
    SELECT name,
    CONVERT(nvarchar(1), sysadmin) AS IsAdmin,
    0 AS Expr1 FROM sys.syslogins WHERE (name = "BadBoy")-%' "
    В результате получается следующая инструкция SQL:
    SELECT ProductNumber,
    Name,
    ListPrice FROM Production.Product WHERE Name LIKE "a" UNION SELECT name,
    CONVERT(nvarchar(1), sysadmin) AS IsAdmin,
    0 AS Expr1 FROM sys.syslogins WHERE (name = "BadBoy")-%'

    Как предотвратить атаку типа SQL-injection

    Теперь вы хорошо понимаете, что может произойти, поэтому потратьте немного усилий и времени на то, чтобы защитить вашу базу данных от этого вида атак; для этого выполните следующие рекомендации:

  • Старайтесь избегать построения инструкций SQL, используйте вместо этого указание отдельных параметров. В приложениях, подобных тому, что мы только что рассмотрели, используйте следующую инструкцию SQL, которая имеет параметр, и выполните привязку этого параметра к текстовому полю, чтобы текст в нем расценивался только как строка. Любая команда в этом поле не будет выполняться.
    SELECT ProductNumber,
    Name,
    ListPrice FROM Production.Product WHERE (Name LIKE @Param1 + "%")
  • По возможности храните запросы в хранимых процедурах.
  • Чтобы передать запрос SQL Server, используйте процедуру sp_ExecuteSql. Об этой системной хранимой процедуре рассказывается в следующем разделе.
  • Как использовать процедуру spExecuteSql

    Системная хранимая процедура sp_executeSql позволяет выполнять динамически определяемые инструкции T-SQL по тому же принципу, который использует команда EXECUTE. Однако sp_executeSql требует, чтобы вы указали параметры и типы данных, а также значения для этих параметров.

    При первом выполнении запроса системная хранимая процедура sp_executeSql создает и сохраняет план выполнения. Поскольку необходимо определить параметры, при каждом последующем выполнении запроса sp_executeSql просто обновляет значения параметров и использует один и тот же план выполнения.

    Поскольку системная хранимая процедура нуждается в определении параметров, можно создавать сложные запросы не только для инструкций SELECT, но и для любых действий в отношении базы данных, которые требуются вашему приложению. Ваши инструкции могут содержать даже параметры вывода.

    Предположим, что нужно указать новую причину задержки выпуска изделий в базе данных Adventure Works, при этом новую причину следует указать для всех заказов, ожидаемая дата поставки которых больше, чем конечная дата выполнения заказа. Для выполнения двух этих задач в одном пакете можно использовать следующий сценарий T-SQL. Этот код можно найти в файлах примеров под именем newReason.sql.

    -Переменная для инструкции T-SQL 
    DECLARE @sql nvarchar(300);
    SET @sql='INSERT INTO [AdventureWorks].[Production].[ScrapReason] 
         ([Name]
         ,[ModifiedDate]) 
      VALUES 
         (@NewNameSQL ,GetDate());" 
    SET @sql=@sql + "SET @NewIdSQL=(SELECT ScrapReasonID " + 
             "FROM [AdventureWorks].[Production].[ScrapReason] " + 
             "WHERE Name=@NewNameSQL)" 
    -Переменная для получения нового идентификатора 
    DECLARE @NewId int;
    -Объявляем параметры 
    DECLARE @Params nvarchar(200);
    SET @Params='@NewNameSQL nvarchar(100), @NewIdSQL int OUTPUT';
    -Добавляем новое имя 
    DECLARE @NewName nvarchar(100); 
    SET @NewName='Delayed 
    Production';
    -Выполнение вставки в ScrapReason
    EXEC sp_executeSql @sql,@params,@NewNameSQL=@NewName, 
       @NewIdSQL=@NewId OUTPUT;
    /*
    Заменяем инструкцию T-SQL для обновления столбца ScrapReasonID
    на указанную новую инструкцию для всех записей, в которой конечная дата
    выполнения заказа превышает ожидаемую дату
    */
    SET @sql="UPDATE [AdventureWorks].[Production].[WorkOrder]" + 
             "SET ScrapReasonID=@NewIdSQL " + 
             "WHERE (EndDate > DueDate) AND (ScrapReasonID IS NULL)";
    -Определяем параметр для этого нового предложения 
    SET @Params='@NewIdSQL int ";
    EXEC sp_executeSql @sql, @params, @NewIdSQL=@NewId; 
    GO

    Как видите, системная хранимая процедура sp_executeSql позволяет применить параметры к динамическим инструкциям SQL, не являющимся простыми запросами SELECT.

    Заключение

    Прочитав эту лекцию, вы узнали, как собирать пользовательский ввод и форматировать его для динамического создания запросов. Вы получили информацию о некоторых недостатках построения динамических запросов, а также о потенциальном риске для безопасности, который с ними связан.

    Чтобы сохранить в безопасности базу данных, используйте параметризованные запросы и выполняйте их через хранимую процедуру sp_executeSql. Эту методику можно использовать не только для задач, связанных с простым возвращением данных пользователю.

    Чтобы Выполните следующие действия
    Создать запрос в Конструкторе запросов SQL Server Management Studio Щелкните правой кнопкой мыши на таблице в Object Explorer (Обозревателе объектов), затем выберите из контекстного меню команду Open Table (Открыть таблицу). Для построения запросов используйте кнопки на панели инструментов Конструктора запросов и соответствующие панели
    Получить список представлений, имеющихся в базе данных Выполните инструкцию SQL
    SELECT TABLE_SCHEMA, TABLE_NAME, TABLE_TYPE
       FROM INFORMATION_SCHEMA.TABLES
    Создавать запросы динамически Соедините ключевое слово SELECT со списком имен столбцов и ключевое слово FROM, после которого указано имя таблицы
    Выполнить сортировку результатов Добавьте в инструкцию предложение ORDER BY со списком имен столбцов. Добавьте инструкцию DESC, чтобы упорядочить информацию в порядке убывания.
    Выполнить фильтрацию результатов Добавьте предложение WHERE (перед предложением ORDER BY, если оно используется) с условиями фильтрации.
    Предотвратить атаку типа SQL-injection Всегда используйте параметризованные запросы. Попробуйте выполнять все операции в соответствующем контексте безопасности.
    Ускорить выполнение динамических запросов Используйте хранимую процедуру sp_executeSql для кэширования плана запроса.
    Вернуться к учебному плану