Программирование на ASP.NET

Основы ADO.NET

Разбить на страницы
Показывать лекцию целиком
Файлы к лекции Вы можете скачать здесь

Базы данных - это хранилища структурированных данных, с которыми удобно работать. СУБД - это программное приложение, извлекающее, отображающее в удобном виде и модифицирующее структурированные данные. Чаще всего применяются реляционные базы данных. Библиотека .NET Framework включает свою собственную технологию доступа к данным - ADO.NET. В нее включены классы, обеспечивающие соединение и обработку данных как в локальных, так и в Web-приложениях. Причем код приложения остается почти одинаковым.

Поставщики данных

Важное место в архитектуре ADO.NET занимают поставщики данных ( data provider ). Они являются универсальными посредниками между источником данных и приложением. В состав одного поставщика входят следующие классы ( используются обобщенные имена ):

  • Connection - используется для установки соединения с источником данных
  • Command - используется для выполнения команд SQL и хранимых процедур
  • DataReader - предоставляет быстрый доступ только для чтения к извлеченным данным
  • DataAdapter - этот класс решает две задачи:
  • наполнение DataSet ( набор данных ) информацией, извлеченной из источника данных
  • сохраняет изменения, выполненные в DataSet
  • ADO.NET содержит набор специализированных поставщиков для различных источников данных. Каждый поставщик данных имеет специфическую реализацию классов Connection, Command, DataReader, DataAdapter, оптимизированных для конкретных баз данных. Например, если нужно подключиться к базе данных SQL Server, то используется класс SqlConnection.

    Библиотека .NET Framework содержит четыре типа готовых поставщиков данных

  • Поставщик SQL Server
  • Поставщик OLE DB
  • Поставщик Oracle
  • Поставщик ODBC
  • Для других типов данных разработчики могут либо создавать свои поставщики, либо приобретать их у сторонних организаций. Выбирая класс поставщика, нужно сначала попытаться найти родного поставщика для данного источника данных. Если это невозможно, то можно воспользоваться поставщиком OLE DB при условии, что существует драйвер OLE DB для этого источника данных.

    Технология OLE DB существует уже много лет как часть ADO, поэтому большинство распространенных источников данных ( SQL Server, Oracle, Access, MySQL и т.д.) предусматривают драйверы OLE DB. Наконец, можно использовать поставщик ODBC в сочетании с драйвером ODBC, поскольку это самая старая технология доступа к данным, но производительность такого подключения может быть очень низкой.

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

    Классы ADO.NET группируются в нескольких пространствах имен:

  • System.Data - содержит ключевые контейнерные классы, моделирующие сами данные. Дополнительно содержит ключевые интерфейсы, реализуемые объектами данных, основанными на соединениях
  • System.Data.Common - содержит базовые классы, наследуемые классами поставщиков
  • System.Data.OleDb - включают поставщиков для источников данных OLE DB
  • System.Data.SqlClient - включают поставщиков для источников данных SQL Server
  • System.Data.OracleClient - включают поставщиков для источников данных Oracle
  • System.Data.SqlTypes - содержит структуры, соответствующие родным типам данных SQL Server. Эти классы не являются необходимыми, но в силу их специфичности оптимизируют доступ к данным SQL Server
  • System.Data.Odbc - включают поставщиков для источников данных ODBC. Операционная система Windows имеет драйверы ODBC для всех видов источников данных, которые конфигурируются в панели управления Пуск/Настройка/Панель управления/Администрирование/Источники данных (ODBC)
  • Класс Connection

    Этот класс устанавливает соединение с источником данных, к которому мы хотим подключиться, чтобы выполнить с данными какие-то действия. Свои основные свойства и методы класс Connection реализует от наследуемого интерфейса IDbConnection.

    Строка соединения

    При установке соединения требуется строка соединения, которая представляет собой список пар имя=значение, разделенных точкой с запятой. За исключением небольших различий, строка соединения содержит несколько атрибутов, необходимых почти всегда (порядок следования атрибутов значения не имеет):

  • Сервер, на котором находится база данных. При выполнении упражнений мы будем обоснованно полагать, что наши приложения ASP.NET и сервер базы данных находятся на одном и том же компьютере, поэтому вместо имени компьютера будем использовать псевдоним localhost
  • База данных, к которой надо подключиться. Мы будем использовать готовую учебную БД Northwind, которая инсталируется по умолчанию с установкой SQL Server 2005
  • Атрибут аутентификации (login). Мы не будем использовать имя и пароль, а будем подключаться к БД как текущий пользователь системы
  • Ниже представлены примеры строки соединения для подключения к БД Northwind на текущем компьютере

    Примеры строки соединения
    Источник данных Строка соединения
    SQL Server с использованием интегрированной безопасности string connectionString = "Data Source=localhost;Initial Catalog=Northwind;" + "Integrated Security=SSPI";
    SQL Server с использованием учетной записи ( id=sa - system administrator ) string connectionString = "Data Source=localhost;Initial Catalog=Northwind;" + "user id=sa;password=xxx";
    Используется поставщик OLE DB для подключения к БД Sales типа Oracle через драйвер MSDAORA OLE DB с правами администратора и пустым паролем string connectionString = "Data Source=localhost;Initial Catalog=Sales;" + "user id=sa;password=;Provider=MSDAORA";
    Подключение к БД Access. Символ "@" требует от компилятора интерпретировать строку буквально string connectionString = "Provider=Microsoft.Jet.OLEDB.4.0;" + @"Data Source=C:\DataSources\Northwind.mdb";

    Строку соединения можно указать в конструкторе при создании объекта Connection или в методе Connection.Open(). Строку соединения можно поместить не в код страницы, а в конфигурационный файл web.config

    <?xml version="1.0"?>
    <configuration>
    	<connectionStrings>
    		<add name="Northwind" connectionString="Data Source=localhost;
    			Initial Catalog=Northwind; Integrated Security=SSPI" />
    	</connectionStrings>
    	<system.web>
    	</system.web>
    </configuration>

    В любое время в коде страницы можно извлечь эту строку соединения по ID -имени из коллекции следующим образом

    string connectionString = WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;

    Подключение к БД и тестирование соединения

    После установки SQL Server 2005 нужно убедиться, что служба SQLServerAgent включена в режим Auto. Это можно сделать через Пуск/Настройка/Панель управления/Администрирование/Службы

    Приведем простой пример установки соединения с БД Northwind.

    Способ 1

  • Создайте командой
  • Переведите страницу в режим Design и добавьте из вкладки Standard элемент Label с именем lblInfo
  • В режиме Design двойным щелчком на клиентской области страницы создайте обработчик Page_Load() в блоке скриптов, который заполните так, чтобы общий код страницы был следующим
    (рис ) Код страницы TestConnection.aspx<%@ Page Language="C#" %>
        
    <script runat="server">
        
        protected void Page_Load(object sender, EventArgs e)
        {
            string connectString = System.Web.Configuration.WebConfigurationManager.
                ConnectionStrings["Northwind"].ConnectionString;
            System.Data.SqlClient.SqlConnection con = 
                new System.Data.SqlClient.SqlConnection(connectString);
        
            try
            {
                con.Open();
                lblInfo.Text = "<b>Версия сервера:</b> " + con.ServerVersion;
                lblInfo.Text += "<br /><b>Соединение:</b> " + con.State.ToString();
            }
            catch(Exception err)
            {
                lblInfo.Text = "<b>Ошибка чтения базы данных.</b>";
                lblInfo.Text += err.Message;
            }
            finally
            {
                con.Close();
                lblInfo.Text += "<br /><b>Теперь соединение:</b> ";
                lblInfo.Text += con.State.ToString();
            }
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        <div>
            <asp:Label ID="lblInfo" runat="server" Text="Label"></asp:Label></div>
        </form>
    </body>
    </html>
  • Поместите в файл web.config строку соединения следующим образом
    (рис ) Строка соединения в файле web.config<?xml version="1.0"?>
    <configuration>
    	<connectionStrings>
    		<add name="Northwind" connectionString = 
    			"Data Source=localhost; 
    			Initial Catalog=Northwind; 
    			user id=sa; password=;" />
    	</connectionStrings>
    	<system.web>
    		<compilation debug="true"/>
    	</system.web>
    </configuration>
    Теперь мы будем только извлекать в коде эту строку соединения в наших дальнейших примерах, поэтому не забывайте, что она существует и как выглядит!!!
  • Выполните страницу, сделав ее стартовой, результат должен быть примерно следующим
  • Соединение как объект занимает определенные ресурсы, поэтому открывать соединение нужно как можно позже, и освобождать как можно быстрее. В примере предусмотрен блок finally на тот случай, что даже при возникновении исключения соединение все равно будет закрыто. Если это не предусмотреть, то в случае необработанного исключения соединение останется открытым до тех пор, пока сборщик мусора не уничтожит объект con типа SqlConnection.

    Способ 2

    Другой подход гарантированного закрытия соединения - использовать блок using. Оператор using декларирует, что мы используем уничтожаемый объект в течение краткого периода времени. Как только выполнение блока using завершится, среда выполнения немедленно освободит соответствующий объект, вызвав метод Dispose(). Вызов этого метода эквивалентен вызову метода Close() для объекта Connection.

  • Создайте копию страницы TestConnection.aspx и назначьте ей имя TestConnection1.aspx
  • Назначьте новую страницу стартовой и заполните ее следующим кодом
    (рис ) Код страницы TestConnection1.aspx<%@ Page Language="C#" %>
        
    <script runat="server">
        
        protected void Page_Load(object sender, EventArgs e)
        {
            string connectString = System.Web.Configuration.WebConfigurationManager.
                ConnectionStrings["Northwind"].ConnectionString;
            System.Data.SqlClient.SqlConnection con =
                new System.Data.SqlClient.SqlConnection(connectString);
        
            using (con)
            {
                con.Open();
                lblInfo.Text = "<b>Версия сервера:</b> " + con.ServerVersion;
                lblInfo.Text += "<br /><b>Соединение:</b> " + con.State.ToString();
            }
        
            lblInfo.Text += "<br /><b>Теперь соединение:</b> ";
            lblInfo.Text += con.State.ToString();
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        <div>
            <asp:Label ID="lblInfo" runat="server" Text="Label"></asp:Label></div>
        </form>
    </body>
    </html>
  • Выполните страницу, результат должен быть прежним
  • Организация пула соединений

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

    Пул соединений - это практика хранения приложением постоянного набора открытых соединений, разделяемых между сеансами при использовании одного и того же источника данных. Это позволяет избежать бесконечного создания и уничтожения соединений. При организации пула вызов клиентом метода Open() обслуживается непосредственно из доступного пула без повторного открытия соединения. Когда клиент освобождает соединение методом Close() или Dispose(), оно не закрывается, а возвращается в пул, чтобы обслужить следующий запрос.

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

    Пулы соединений SQL Server и Oracle используют механизм полнотекстового сравнения. Это значит, что любое минимальное изменение в строке соединений нарушает пул, даже если в ней просто изменяется порядок параметров или добавляется простой пробел. Поэтому лучше не помещать строку соединений в код установки соединения, а сохранять в файле web.config, как это мы сделали в примере.

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

    Параметры настройки пулов в строке соединений
    Параметр Описание
    Max Pool Size Максимальное количество соединений, разрешенных для хранения в пуле (по умолчанию 100). Если достигается этот максимальный размер пула, любые последующие попытки соединения становятся в очередь. Если время жизни соединения, установленное параметром Connection.Timeout, истечет раньше, чем подошла очередь в пуле, возникает исключение.
    Min Pool Size Минимальное количество соединений, которое должно оставаться в пуле (по умолчанию 0). Это число соединений будет создано в пуле при открытии первого соединения.
    Pooling По умолчанию true. При значении false отключается механизм пула
    Connection Lifetime Устанавливает время хранения соединения в пуле (в секундах). По умолчанию значение 0 устанавливает неограниченное время жизни

    Некоторые поставщики имеют методы очистки пула соединений. Например, поставщик SqlConnection имеет статические методы SqlConnection.ClearPool(connectionString) - очистка пула конкретного соединения и SqlConnection.ClearAllPools() - очистка пулов всех соединений в текущем домене приложения. Эти методы не удаляют соединения физически, а помечают на удаление для бредущего сзади сборщика мусора.

    Класс Command и DataReader

    Класс Command позволяет выполнить любой SQL-запрос к открытому соединению базы данных. Для того, чтобы использовать команду, нужно выбрать ее тип, установить ее текст и привязать к открытому соединению. Эту работу можно выполнить, установив значения свойств CommandType, CommandText и Connection, либо передать необходимую информацию в аргументах конструктора класса Command.

    Текстом команды может быть SQL-оператор, хранимая процедура ( stored procedure ) или имя таблицы. Это определяется значением перечисления CommandType, присваиваемого свойству Command.CommandType.

    Значения перечисления CommandType
    Значение Описание
    System.Data.CommandType.Text Требование выполнить SQL-оператор, указанный в свойстве Command.CommandText. Установлено по умолчанию в свойстве класса Command.CommandType
    System.Data.CommandType.StoredProcedure Требование выполнить хранимую процедуру, указанную в свойстве Command.CommandText
    System.Data.CommandType.TableDirect Требование извлечь все записи из таблицы, указанной в свойстве Command.CommandText.

    Не поддерживается поставщиком данных SQL Server

    Расширим наш пример, чтобы применить код создания объекта Command.

  • Создайте из копии файла TestConnection1.aspx файл с именем TestConCommand.aspx и установите его стартовым
  • Дополните код страницы TestConCommand.aspx так
    (рис ) Страница TestConCommand.aspx создания объектов соединения и команды<%@ Page Language="C#" %>
        
    <script runat="server">
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Извлекли строку соединения из web.config
            string connectString = System.Web.Configuration.WebConfigurationManager.
                ConnectionStrings["Northwind"].ConnectionString;
            
            // Установили минимальный размер пула соединения равный 10
            connectString += "Min Pool Size=10;";
            
            // Создали объект соединения
            System.Data.SqlClient.SqlConnection con =
                new System.Data.SqlClient.SqlConnection(connectString);
        
            using (con)
            {
                // Получить соединение из пула (если есть)
                // или создать пул с 10 соединениями (если нет)
                con.Open();
                lblInfo.Text = "<b>Соединение:</b> " + con.State.ToString();
                
                // Создали объект Command
                System.Data.SqlClient.SqlCommand cmd = 
                    new System.Data.SqlClient.SqlCommand();
                
                // Настроили объект Command
                cmd.Connection = con;
                cmd.CommandType = System.Data.CommandType.Text;
                cmd.CommandText = "SELECT * FROM Employees";
            }
        
            // Вернуть соединение в пул
            lblInfo.Text += "<br /><b>Соединение:</b> ";
            lblInfo.Text += con.State.ToString();
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        <div>
            <asp:Label ID="lblInfo" runat="server" Text="Label"></asp:Label></div>
        </form>
    </body>
    </html>
  • Исполните страницу, чтобы получить такой результат
  • Учитывая, что по умолчанию установлено CommandType = CommandType.Text, команду можно сразу указать в перегруженном конструкторе при создании объекта команд. Например, код

    // Создали объект Command
    System.Data.SqlClient.SqlCommand cmd = 
        new System.Data.SqlClient.SqlCommand();
    
    // Настроили объект Command
    cmd.Connection = con;
    cmd.CommandType = System.Data.CommandType.Text;
    cmd.CommandText = "SELECT * FROM Employees";

    можно заменить кодом

    // Создали и настроили объект Command
    System.Data.SqlClient.SqlCommand cmd = 
        new System.Data.SqlClient.SqlCommand("SELECT * FROM Employees", con);

    В данном примере мы просто настроили, но не выполнили объект класса Command, поэтому никакой выборки данных из БД не получили. Объект Command имеет три метода выполнения команды, приведенные в таблице на примере класса SqlCommand

    Методы выполнения объекта Command
    Метод Описание
    System.Data.SqlClient.SqlCommand.ExecuteNonQuery() Выполняет команды, отличные от SELECT, такие как SQL-операторы вставки, удаления или обновления записей. Возвращает количество строк, обработанное командой
    System.Data.SqlClient.SqlCommand.ExecuteScalar() Выполняет запрос SELECT и возвращает первое поле первой строки результирующего набора, сгенерированного командой. Обычно применяется в запросах с функциями SUM() или COUNT() для вычисления единственного значения
    System.Data.SqlClient.SqlCommand.ExecuteReader() Выполняет запрос выборки данных SELECT и возвращает объект DataReader - результирующий набор данных, доступный только для чтения

    Метод SqlCommand.ExecuteReader()

    Объект класса DataReader представляет собой результирующий набор данных только для чтения и является самым простым и быстрым средством доступа к данным. Он является выборкой данных по запросу SQL-команды SELECT и состоит из строк и колонок, которые можно анализировать и отображать. Но в нем нет возможностей сортировки, как в более развитых объектах. Ниже приведены несколько методов DataReader на примере класса System.Data.SqlClient.SqlDataReader

    Методы класса DataReader
    Метод Описание
    SqlDataReader.Read() Перемещает курсор строки на следующую строку в потоке. Его нужно также вызывать первым после создания объекта DataReader, поскольку начальный курсор позиционируется перед первой строкой. Возвращает false при чтении последней строки набора данных
    SqlDataReader.GetValue(int) Читает значение поля текущей строки по указанному индексу (индекс начинается с нуля)
    SqlDataReader.GetValues(object[ ]) Сохраняет значение текущей строки в массиве. Количество сохраняемых полей зависит от размера массива, переданного этому методу. Чтобы сохранить все поля текущей строки, нужно создать массив размерности, определяемой свойством SqlDataReader.FieldCount
    SqlDataReader.GetInt32(int) Возвращает значение поля целого типа по индексу столбца
    SqlDataReader.GetDateTime(int) Возвращает значение поля типа даты по индексу столбца
    SqlDataReader.NextResult() Если команда, которая сгенерировала DataReader, возвратила более одного результирующего набора строк, то этот метод перемещает текущий курсор данных на первую строку следующего набора
    SqlDataReader.Close() Закрывает модуль чтения

    Обработка одного результирующего набора данных SqlDataReader

    Приведем пример, в котором создается и выполняется простая команда запроса для выборки всех записей из таблицы Employees учебной базы данных Northwind. Мы помним, что строка соединения была нами задана ранее в конфигурационном файле.

  • Создайте копию страницы TestConCommand.aspx с именем TestDataReader.aspx
  • Заполните страницу приведенным кодом
    (рис ) Код страницы TestDataReader.aspx с чтением из базы Northwind<%@ Page Language="C#" %>
        
    <script runat="server">
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Извлекли строку соединения из web.config
            string connectString = System.Web.Configuration.WebConfigurationManager.
                ConnectionStrings["Northwind"].ConnectionString;
        
            // Создали объект соединения
            System.Data.SqlClient.SqlConnection con =
                new System.Data.SqlClient.SqlConnection(connectString);
        
            // Создали объект Command
            System.Data.SqlClient.SqlCommand cmd =
                new System.Data.SqlClient.SqlCommand();
        
            // Настроили объект Command
            cmd.Connection = con;
            cmd.CommandType = System.Data.CommandType.Text;
            cmd.CommandText = "SELECT * FROM Employees";
        
            // Создаем ссылку на вспомогательную строку
            System.Text.StringBuilder htmlStr = new StringBuilder("");
            
            using (con)
            {
                // Открыли соединение
                con.Open();
        
                // Выполняем команду и получаем результирующий набор данных
                System.Data.SqlClient.SqlDataReader reader =
                    cmd.ExecuteReader();
                
                // Перебираем все записи результирующего набора
                // и строим HTML-строку
                while (reader.Read())
                {
                    htmlStr.Append("<li>Служащий ");
                    htmlStr.Append(reader["TitleOfCourtesy"]);
                    htmlStr.Append(" <b>");
                    htmlStr.Append(reader.GetString(1));
                    htmlStr.Append("</b>, ");
                    htmlStr.Append(reader.GetString(2));
                    htmlStr.Append(" - работает с ");
                    htmlStr.Append(reader.GetDateTime(6).ToString("d"));
                    htmlStr.Append("</li>");
                }
                
                // Закрываем набор данных
                reader.Close();
            }
        
            // Выдаем результаты запроса
            lblInfo.Text = htmlStr.ToString();
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        <div>
            <asp:Label ID="lblInfo" runat="server" Text="Label"></asp:Label></div>
        </form>
    </body>
    </html>
  • Исполните страницу TestDataReader.aspx, должен получиться такой клиентский результат
  • Служащий Ms. Davolio, Nancy - работает с 01.05.1992
  • Служащий Dr. Fuller, Andrew - работает с 14.08.1992
  • Служащий Ms. Leverling, Janet - работает с 01.04.1992
  • Служащий Mrs. Peacock, Margaret - работает с 03.05.1993
  • Служащий Mr. Buchanan, Steven - работает с 17.10.1993
  • Служащий Mr. Suyama, Michael - работает с 17.10.1993
  • Служащий Mr. King, Robert - работает с 02.01.1994
  • Служащий Ms. Callahan, Laura - работает с 05.03.1994
  • Служащий Ms. Dodsworth, Anne - работает с 15.11.1994
  • В этом примере объект класса StringBuilder существенно увеличивает производительность за счет выделения буфера памяти для символов. Если использовать объект класса String, то при каждой операции сложения строк будет создаваться новый объект с копированием результатов из старого объекта, что значительно медленнее.

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

    Обработка множественного результирующего набора данных SqlDataReader

    Команда SqlCommand.ExecuteReader() может возвращать множественный результирующий набор данных в двух случаях:

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

    string sql = "SELECT TOP 5 * FROM Employees;"
    		+ "SELECT TOP 5 * FROM Customers;"
    		+ "SELECT TOP 5 * FROM Suppliers";

    Приведенная строка содержит три запроса, каждый из которых возвращает в результирующий набор данных по пять первых записей из трех таблиц базы Northwind. Ниже приведен код страницы, в котором выполняется последовательная выборка данных из множественного результирующего набора. Переход на новый результирующий набор выполняется методом NextResult(), который возвратит false, когда больше не останется результирующих наборов.

  • Создайте копию страницы TestDataReader.aspx с именем TestMultiDataReader.aspx и определите ее стартовой
  • Заполните страницу приведенным кодом
    (рис ) Код страницы TestMultiDataReader.aspx для нескольких таблиц базы Northwind<%@ Page Language="C#" %>
        
    <script runat="server">
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Извлекли строку соединения из web.config
            string connectString = System.Web.Configuration.WebConfigurationManager.
                ConnectionStrings["Northwind"].ConnectionString;
        
            // Создали объект соединения
            System.Data.SqlClient.SqlConnection con =
                new System.Data.SqlClient.SqlConnection(connectString);
        
            // Создали объект Command
            System.Data.SqlClient.SqlCommand cmd =
                new System.Data.SqlClient.SqlCommand();
        
            // Настроили объект Command
            cmd.Connection = con;
            cmd.CommandType = System.Data.CommandType.Text;
            cmd.CommandText = "SELECT TOP 5 * FROM Employees;"
    		                + "SELECT TOP 5 * FROM Customers;"
    		                + "SELECT TOP 5 * FROM Suppliers";
        
            System.Text.StringBuilder htmlStr = new StringBuilder("");
            int i = 0;
            
            using (con)
            {
                // Открыли соединение
                con.Open();
        
                // Выполняем команду и получаем результирующий набор данных
                System.Data.SqlClient.SqlDataReader reader =
                    cmd.ExecuteReader();
                
                // Перебираем частные наборы в результирующем наборе данных
                do
                {
                    htmlStr.Append("<h2>Набор: ");// Заголовок h2 HTML
                    htmlStr.Append(i.ToString());
                    htmlStr.Append("</h2>");
                    htmlStr.Append("<ul>"); // Начало маркированного списка
                    
                    // Перебираем все записи текущего набора
                    // (их должно по команде быть 5 первых)
                    // в результирующем наборе и строим HTML-строку
                    while (reader.Read())
                    {
                        htmlStr.Append("<li>");// Строка списка HTML
                        // Перебираем все поля строки набора
                        for (int field = 0; field < reader.FieldCount; field++)
                        {
                            if (field >= 3) // Ограничимся тремя первыми полями 
                                continue;
                            htmlStr.Append("<b>");
                            htmlStr.Append(reader.GetName(field).ToString());// Имя поля
                            htmlStr.Append(": </b>");
                            htmlStr.Append(reader.GetValue(field).ToString());// Значение поля
                            htmlStr.Append("nbsp;nbsp;nbsp;nbsp;nbsp;");// Жесткие пробелы HTML
                        }
                        htmlStr.Append("</li>");
                    }
        
                    htmlStr.Append("</ul>"); // Конец маркированного списка
                    i++;
                    
                } while (reader.NextResult());
                
                // Закрываем набор данных
                reader.Close();
            }
        
            // Показываем пользователю результаты запроса
            lblInfo.Text = htmlStr.ToString();
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        <div>
            <asp:Label ID="lblInfo" runat="server" Text="Label"></asp:Label></div>
        </form>
    </body>
    </html>
  • Исполните страницу, должен получиться такой результат
  • Мы искусственно ограничили количество отображаемых полей тремя. Если этот оператор убрать, то на пользователя обрушится поток информации. Нам на данном этапе важен сам принцип извлечения информации из БД.

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

  • Создайте копию страницы TestMultiDataReader.aspx с именем GridViewDataReader.aspx и определите ее стартовой
  • Поместите на страницу элемент управления GridView из вкладки Data панели Toolbox
  • Заполните страницу приведенным кодом
    (рис ) Код страницы GridViewDataReader.aspx с неформатированным просмотром <%@ Page Language="C#" %>
        
    <script runat="server">
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Извлекли строку соединения из web.config
            string connectString = System.Web.Configuration.WebConfigurationManager.
                ConnectionStrings["Northwind"].ConnectionString;
        
            // Создали объект соединения
            System.Data.SqlClient.SqlConnection con =
                new System.Data.SqlClient.SqlConnection(connectString);
        
            // Создали объект Command
            System.Data.SqlClient.SqlCommand cmd =
                new System.Data.SqlClient.SqlCommand();
        
            // Настроили объект Command
            cmd.Connection = con;
            cmd.CommandType = System.Data.CommandType.Text;
            cmd.CommandText = "SELECT TOP 5 * FROM Suppliers";
        
            using (con)
            {
                // Открыли соединение
                con.Open();
        
                // Выполняем команду и получаем результирующий набор данных
                System.Data.SqlClient.SqlDataReader reader =
                    cmd.ExecuteReader();
        
                // Подключаем элемент управления GridView
                // к полученному результирующему набору
                GridView1.DataSource = reader;
        
                // Наполняем элемент управления GridView
                // всеми извлеченными записями DataReader
                GridView1.DataBind();
        
                // Закрываем набор данных
                reader.Close();
            }
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head id="Head1" runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
            <div>
                <asp:GridView ID="GridView1" runat="server">
                </asp:GridView>
            </div>
        </form>
    </body>
    </html>
  • Исполните страницу, должен получиться такой результат
  • SupplierID CompanyName ContactName ContactTitle Address City Region PostalCode Country Phone Fax HomePage
    1 Exotic Liquids Charlotte Cooper Purchasing Manager 49 Gilbert St. London EC1 4SD UK (171) 555-2222
    2 New Orleans Cajun Delights Shelley Burke Order Administrator P.O. Box 78934 New Orleans LA 70117 USA (100) 555-4822 #CAJUN.HTM#
    3 Grandma Kelly's Homestead Regina Murphy Sales Representative 707 Oxford Rd. Ann Arbor MI 48104 USA (313) 555-5735 (313) 555-3349
    4 Tokyo Traders Yoshi Nagase Marketing Manager 9-8 Sekimai Musashino-shi Tokyo 100 Japan (03) 3555-5011
    5 Cooperativa de Quesos 'Las Cabras' Antonio del Valle Saavedra Export Administrator Calle del Rosal 4 Oviedo Asturias 33007 Spain (98) 598 76 54

    Метод SqlCommand.ExecuteScalar()

    Этот метод объекта Command возвращает скалярную величину типа Object, которая является результатом работы агрегатных функций вроде COUNT() или SUM() SQL-запроса. Чтобы получить конкретное значение, к возвращенному объекту нужно применить явное преобразование типов.

  • Создайте копию страницы TestConCommand.aspx с именем TestExecuteScalar.aspx и сделайте ее стартовой
  • Скорректируйте страницу TestExecuteScalar.aspx так
    (рис ) Страница TestExecuteScalar.aspx<%@ Page Language="C#" %>
        
    <script runat="server">
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Извлекли строку соединения из web.config
            string connectString = System.Web.Configuration.WebConfigurationManager.
                ConnectionStrings["Northwind"].ConnectionString;
        
            // Создали объект соединения
            System.Data.SqlClient.SqlConnection con =
                new System.Data.SqlClient.SqlConnection(connectString);
        
            // Создали объект Command
            System.Data.SqlClient.SqlCommand cmd =
                new System.Data.SqlClient.SqlCommand();
        
            // Настроили объект Command
            cmd.Connection = con;
            cmd.CommandType = System.Data.CommandType.Text;
            cmd.CommandText = "SELECT COUNT(*) FROM Employees";
        
            using (con)
            {
                // Открыли соединение
                con.Open();
        
                // Выполнили команду подсчета записей таблицы Employees
                // с применением явного преобразования типа
                int recordCount = (int)cmd.ExecuteScalar();
        
                lblInfo.Text = "<b>Количество записей:</b> " + recordCount.ToString();
            }
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        <div>
            <asp:Label ID="lblInfo" runat="server" Text="Label"></asp:Label></div>
        </form>
    </body>
    </html>
  • Постройте страницу, результат будет таким
  • Метод SqlCommand.ExecuteNonQuery()

    Этот метод напоминает консольную утилиту, которая работает в пакетном режиме, выполняет какие-либо действия, но не возвращает результатов. Метод ExecuteNonQuery() выполняет такие SQL-команды, как INSERT, DELETE, UPDATE, а возвращает только число, равное количеству обработанных записей.

    В качестве примера приведем код страницы, удаляющий запись с определенным значением поля EmployeeID в таблице Employees БД Northwind. Поскольку эта база учебная, не будем ее портить и зададим заведомо несуществующее значение поля EmployeeID.

    <%@ Page Language="C#" %>
        
    <script runat="server">
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Формируем соединение
            string connectString = System.Web.Configuration.WebConfigurationManager.
                ConnectionStrings["Northwind"].ConnectionString;
            System.Data.SqlClient.SqlConnection con = 
                new System.Data.SqlClient.SqlConnection(connectString);
            
            // Формируем команду в конструкторе класса
            int empID = 111; // Заведомо несуществующее значение
            string sql = "DELETE FROM Employees WHERE EmployeeID = " + empID.ToString();
            System.Data.SqlClient.SqlCommand cmd = 
                new System.Data.SqlClient.SqlCommand(sql, con);
        
            try
            {
                con.Open();
                int recCount = cmd.ExecuteNonQuery();
                lblInfo.Text = string.Format("<b>Удалено записей:</b> {0}", recCount);
            }
            catch(System.Data.SqlClient.SqlException err)
            {
                lblInfo.Text = string.Format("<b>Ошибка удаления:</b> {0}", err.Message);
            }
            finally
            {
                con.Close();
            }
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        <div>
            <asp:Label ID="lblInfo" runat="server" Text="Label"></asp:Label></div>
        </form>
    </body>
    </html>

    Атаки на базу данных внедрением SQL

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

    Рассмотрим это на примере.

  • Сделайте копию с именем UserSqlGridData.aspx из файла GridViewDataReader.aspx и назначьте ее стартовой
  • Разместите выше элемента
  • Откорректируйте дескрипторный и встроенный коды страницы так
    (рис ) Код страницы UserSqlGridData.aspx<%@ Page Language="C#" %>
        
    <script runat="server">
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Извлекли строку соединения из web.config
            string connectString = System.Web.Configuration.WebConfigurationManager.
                ConnectionStrings["Northwind"].ConnectionString;
        
            // Создали объект соединения
            System.Data.SqlClient.SqlConnection con =
                new System.Data.SqlClient.SqlConnection(connectString);
            
            // Создали строку SQL-запроса
            string sql =
                "SELECT Orders.CustomerID, Orders.OrderID, COUNT(UnitPrice) AS Items, "
              + "SUM(UnitPrice * Quantity) AS Total FROM Orders "
              + "INNER JOIN[Order Details] "
              + "ON Orders.OrderID = [Order Details].OrderID "
              + "WHERE Orders.CustomerID = '" + TextBox1.Text + "' "
              + "GROUP BY Orders.OrderID, Orders.CustomerID";
        
            // Создали объект Command
            System.Data.SqlClient.SqlCommand cmd =
                new System.Data.SqlClient.SqlCommand(sql, con);
        
            using (con)
            {
                // Открыли соединение
                con.Open();
        
                // Выполняем команду и получаем результирующий набор данных
                System.Data.SqlClient.SqlDataReader reader =
                    cmd.ExecuteReader();
        
                // Подключаем элемент управления GridView
                // к полученному результирующему набору
                GridView1.DataSource = reader;
        
                // Наполняем элемент управления GridView
                // всеми извлеченными записями DataReader
                GridView1.DataBind();
        
                // Закрываем набор данных
                reader.Close();
            }
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head id="Head1" runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
            <div>
                Введите ID пользователя:<br />
                <asp:TextBox ID="TextBox1" runat="server" Width="248px" Font-Bold="True">ALFKI</asp:TextBox> 
                <asp:Button ID="Button1" runat="server" Text="Получить" /><br />
                <br />
                <asp:GridView ID="GridView1" runat="server">
                </asp:GridView>
            </div>
        </form>
    </body>
    </html>
  • Исполните страницу и должен получиться такой результат
  • CustomerID OrderID Items Total
    ALFKI 10643 3 1086,0000
    ALFKI 10692 1 878,0000
    ALFKI 10702 2 330,0000
    ALFKI 10835 2 851,0000
    ALFKI 10952 2 491,2000
    ALFKI 11011 2 960,0000

    При значении текстового поля ALFKI вычисленный SQL-оператор будет иметь такой вид:

    string sql =
        "SELECT Orders.CustomerID, Orders.OrderID, COUNT(UnitPrice) AS Items, "
      + "SUM(UnitPrice * Quantity) AS Total FROM Orders "
      + "INNER JOIN[Order Details] "
      + "ON Orders.OrderID = [Order Details].OrderID "
      + "WHERE Orders.CustomerID = 'ALFKI' "
      + "GROUP BY Orders.OrderID, Orders.CustomerID";

    Злоумышленник может попытаться подделать оператор SQL. Главной целью такой атаки является получение сообщения об ошибке. Если ошибка не обрабатывается правильно, то злоумышленник получит низкоуровневую информацию, которая может быть использована для разработки более изощренной атаки.

    Предположим, что злоумышленник введет в текстовое поле следующий текст

    ALFKI' OR '1' = '1

    В этом случае строка запроса будет такой

    string sql =
        "SELECT Orders.CustomerID, Orders.OrderID, COUNT(UnitPrice) AS Items, "
      + "SUM(UnitPrice * Quantity) AS Total FROM Orders "
      + "INNER JOIN[Order Details] "
      + "ON Orders.OrderID = [Order Details].OrderID "
      + "WHERE Orders.CustomerID = 'ALFKI' OR '1' = '1' "
      + "GROUP BY Orders.OrderID, Orders.CustomerID";

    Этот оператор вернет все записи о заказах, поскольку условие 1=1 истинно для всех строк

  • Исполните страницу UserSqlGridData.aspx и введите как злоумышленник в текстовое поле строку
    ALFKI' OR '1' = '1
  • Должен получиться результат, малая часть которого приведена ниже

    Начальная часть результата, возвращенного по искаженной строке запроса
    CustomerID OrderID Items Total
    VINET 10248 3 440,0000
    TOMSP 10249 2 1863,4000
    HANAR 10250 3 1813,0000
    VICTE 10251 3 670,8000
    SUPRD 10252 3 3730,0000
    HANAR 10253 3 1444,8000
    CHOPS 10254 3 625,2000
    RICSU 10255 4 2490,5000
    WELLI 10256 2 517,8000
    HILAA 10257 3 1119,9000
    ERNSH 10258 3 2018,6000
    CENTC 10259 2 100,8000

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

    Возможны и более сложные атаки. Например, злонамеренный пользователь может просто закомментировать остаток оператора SQL, добавив два тире (--). Эта атака специфична для SQL Server, но аналогичная атака возможна и в MySQL, если использовать символ #, и в Oracle, если задействовать точку с запятой (;).

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

    Вот что злоумышленник может ввести в текстовое поле, чтобы осуществить более изощренную атаку внедрением SQL, удалив все строки из таблицы Customers:

    ALFKI'; DELETE * FROM Customers --

    Не делайте этого с нашей учебной базой!!!

    Для того, чтобы противостоять атаке внедрением, следует помнить несколько правил:

  • Использовать свойство TextBox.MaxLength, чтобы предотвратить длинный ввод, когда в этом нет необходимости
  • Обрабатывать ошибки самому, чтобы ограничить информацию стандартного исключения Exception.Message
  • Выявлять и удалать из строки ввода специальные символы, например, заменять одиночную кавычку парой одиночных кавычек
    string ID = TextBox1.Text.Replace("'", "''");
  • Использовать параметризованные команды или хранимые процедуры, которые предусматривают собственную защиту от атак внедрением SQL
  • Ограничить права учетной записи, от имени которой выполняется доступ к базе данных. Этот способ может предовратить атаки, связанные с удалением таблиц, но не препятствует похищению информации, поскольку права по извлечению информации ограничить нельзя
  • Применение параметризованных команд

    Параметризованная команда, это обычная команда SQL, использующая спецификаторы, обозначающие место, в которое будут динамически подставлены объекту Command определенные параметры-заполнители SQL через коллекцию Parameters. Параметры-заполнители прописываются отдельно и автоматически кодируются.

    Синтаксис параметризованных команд слегка отличается у разных поставщиков данных. Так, поставщик SQL Server предусматривает именованные заполнители с уникальными именами. У поставщика OLE DB спецификаторы являются вопросами, а порядок следования заполнителей должен соответствовать контексту следования спецификаторов.

    Приведем пример, использующий параметризованную команду, в котором исключена возможность атаки внедрением SQL.

  • Сделайте из страницы UserSqlGridData.aspx копию с именем UserSqlParamCommand.aspx и назначьте ее стартовой
  • Откорректируйте страницу UserSqlParamCommand.aspx, чтобы она имела следующий код
    (рис ) Код страницы UserSqlParamCommand.aspx<%@ Page Language="C#" %>
        
    <script runat="server">
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Извлекли строку соединения из web.config
            string connectString = System.Web.Configuration.WebConfigurationManager.
                ConnectionStrings["Northwind"].ConnectionString;
        
            // Создали объект соединения
            System.Data.SqlClient.SqlConnection con =
                new System.Data.SqlClient.SqlConnection(connectString);
            
            // Создали строку SQL-запроса
            string sql =
                "SELECT Orders.CustomerID, Orders.OrderID, COUNT(UnitPrice) AS Items, "
              + "SUM(UnitPrice * Quantity) AS Total FROM Orders "
              + "INNER JOIN[Order Details] "
              + "ON Orders.OrderID = [Order Details].OrderID "
              + "WHERE Orders.CustomerID = @CustomID "
              + "GROUP BY Orders.OrderID, Orders.CustomerID";
        
            // Создали объект Command
            System.Data.SqlClient.SqlCommand cmd =
                new System.Data.SqlClient.SqlCommand(sql, con);
            
            // Дополнили коллекцию Parameters
            cmd.Parameters.Add("@CustomID", TextBox1.Text);
        
            using (con)
            {
                // Открыли соединение
                con.Open();
        
                // Выполняем команду и получаем результирующий набор данных
                System.Data.SqlClient.SqlDataReader reader =
                    cmd.ExecuteReader();
        
                // Подключаем элемент управления GridView
                // к полученному результирующему набору
                GridView1.DataSource = reader;
        
                // Наполняем элемент управления GridView
                // всеми извлеченными записями DataReader
                GridView1.DataBind();
        
                // Закрываем набор данных
                reader.Close();
            }
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head id="Head1" runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
            <div>
                Введите ID пользователя:<br />
                <asp:TextBox ID="TextBox1" runat="server" Width="248px" Font-Bold="True">ALFKI</asp:TextBox> 
                <asp:Button ID="Button1" runat="server" Text="Получить" /><br />
                <br />
                <asp:GridView ID="GridView1" runat="server">
                </asp:GridView>
            </div>
        </form>
    </body>
    </html>
  • Код страницы остался почти таким же, за исключением двух строк, вводящих команду с именованным параметром @CustomID.

  • Попробуйте исполнить атаку внедрением с содержимым поля ввода ALFKI' OR '1' = '1
  • Мы видим, что страница не возвращает никаких данных, поскольку ни одна запись таблицы Orders в поле CustomerID не имеет значение, равное введенной строке ALFKI' OR '1' = '1. Таким простым способом мы защитили данные от злоумышленного внедрения SQL.

    Вызов хранимых процедур

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

    Хранимые процедуры имеют множество достоинств:

  • Их легко сопровождать. Например, можно изменять команды в хранимой процедуре без перекомпиляции приложения, использующего эту процедуру
  • Они позволяют реализовать более безопасный доступ к базе данных. Например, можно позволить учетной записи Windows, запускающей наш код ASP.NET, использовать определенные хранимые процедуры, но ограничить доступ к лежащим в их основе таблицам
  • Они могут повысить производительность. Поскольку хранимые процедуры упаковывают вместе множество SQL-операторов, можно выполнить огромный объем работы за одно обращение к серверу базы данных, особенно, если база расположена на другом компьютере
  • Рассмотрим пример SQL-кода, необходимого при создании хранимой процедуры для вставки отдельной записи в таблицу Employees. Этой хранимой процедуры изначально нет в БД Northwind. Хранимая процедура должна реализовывать следующий код

    CREATE PROCEDURE InsertEmployee
    	@TitleOfCourtesy	varchar(25),
    	@LastName			varchar(20),
    	@FirstName			varchar(10),
    	@EmployeeID			int OUTPUT
    AS
    INSERT INTO Employees
    	(TitleOfCourtesy, LastName, FirstName, HireDate)
    	VALUES(@TitleOfCourtesy, @LastName, @FirstName, GETDATE());
    SET @EmployeeID = @@IDENTITY

    Эту хранимую процедуру можно добавить на этапе проектирования через панель Server Explorer оболочки, предварительно присоединившись к базе данных Nordhwind. Но я не смог это сделать, поскольку оболочка не видит эту базу из-за неправильно установленного пакета SQL Server 2005 (или потому, что у меня руки кривые). Чтобы как-то вывернуться, в приведенном ниже коде мы добавим ее программно.

  • Создайте новую страницу с именем TestStoredProcedure.aspx и назначьте ее стартовой
  • Поместите на страницу из вкладки Standard элементы управления согласно дескрипторному коду, приведенному ниже (устал писать, да и студент Зиборов ругается, но зато все работает!)
  • Настройте код страницы, чтобы в итоге он был таким
  • <%@ Page Language="C#" %>
        
    <script runat="server">
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Извлекли строку соединения из web.config
            string connectString = System.Web.Configuration.WebConfigurationManager.
                ConnectionStrings["Northwind"].ConnectionString;
        
            // Создали объект соединения
            System.Data.SqlClient.SqlConnection con =
                new System.Data.SqlClient.SqlConnection(connectString);
        
            try
            {
                string sql =
                    "CREATE PROCEDURE InsertEmployee "
                  + "@TitleOfCourtesy	varchar(25),"
                  + "@LastName			varchar(20),"
                  + "@FirstName			varchar(10),"
                  + "@EmployeeID		int OUTPUT "
                  + "AS "
                  + "INSERT INTO Employees "
                  + "(TitleOfCourtesy, LastName, FirstName, HireDate) "
                  + "VALUES(@TitleOfCourtesy, @LastName, @FirstName, GETDATE());"
                  + "SET @EmployeeID = @@IDENTITY";
                System.Data.SqlClient.SqlCommand cmd =
                    new System.Data.SqlClient.SqlCommand(sql, con);
                con.Open();
                cmd.ExecuteNonQuery();
            }
            catch (System.Data.SqlClient.SqlException error)
            {
            }
            finally
            {
                con.Close();
            }
        }
        
        protected void AddEmployee_Click(object sender, EventArgs e)
        {
            // Извлекли строку соединения из web.config
            string connectString = System.Web.Configuration.WebConfigurationManager.
                ConnectionStrings["Northwind"].ConnectionString;
        
            // Создали объект соединения
            System.Data.SqlClient.SqlConnection con =
                new System.Data.SqlClient.SqlConnection(connectString);
        
            // Создать и настроить объект Command 
            // для вызова хранимой процедуры InsertEmployee
            System.Data.SqlClient.SqlCommand cmd =
                new System.Data.SqlClient.SqlCommand("InsertEmployee", con);
            cmd.CommandType = System.Data.CommandType.StoredProcedure;
        
            // Добавить входные параметры хранимой процедуры
            // в коллекцию параметров Command.Parameters
            // Добавляем первый входной параметр
            cmd.Parameters.Add(new System.Data.SqlClient.SqlParameter(
                "@TitleOfCourtesy", System.Data.SqlDbType.NVarChar, 25));
            cmd.Parameters["@TitleOfCourtesy"].Value = title.Text;
            // Добавляем второй входной параметр
            cmd.Parameters.Add(new System.Data.SqlClient.SqlParameter(
                "@LastName", System.Data.SqlDbType.NVarChar, 20));
            cmd.Parameters["@LastName"].Value = lastName.Text;
            // Добавляем третий входной параметр
            cmd.Parameters.Add(new System.Data.SqlClient.SqlParameter(
                "@FirstName", System.Data.SqlDbType.NVarChar, 10));
            cmd.Parameters["@FirstName"].Value = firstName.Text;
        
            // Добавить выходной параметр хранимой процедуры
            // в коллекцию параметров Command.Parameters
            cmd.Parameters.Add(new System.Data.SqlClient.SqlParameter(
                "@EmployeeID", System.Data.SqlDbType.Int, 4));
            cmd.Parameters["@EmployeeID"].Direction = System.Data.ParameterDirection.Output;
        
            using (con)
            {
                // Открыли соединение
                con.Open();
        
                // Выполняем команду
                int recCount = cmd.ExecuteNonQuery();
                lblInfo.Text = string.Format("<b>Вставлено записей:</b> {0}<br />", recCount);
                // Получить вновь сгенерированный идентификатор
                int empID = (int)cmd.Parameters["@EmployeeID"].Value;
                lblInfo.Text += "Новый идентификатор: " + empID.ToString();
            }
            // Соединение закрылось автоматически
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head id="Head1" runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
            Введите титул:<asp:TextBox ID="title" runat="server">студент</asp:TextBox><br />
            Введите имя:<asp:TextBox ID="firstName" runat="server">Иван</asp:TextBox><br />
            Введите фамилию:<asp:TextBox ID="lastName" runat="server">Петров</asp:TextBox><br />
            <asp:Label ID="lblInfo" runat="server" />
            <br />
            <br />
            <asp:Button ID="AddEmployee" runat="server" Text="Добавить служащего" 
                OnClick="AddEmployee_Click" />
            <asp:LinkButton ID="btnResult" runat="server" 
                PostBackUrl="~/TestDataReader.aspx">Показать список</asp:LinkButton>
        </form>
    </body>
    </html>

    Интерфейс страницы будет таким

  • Добавьте служащего щелчком по кнопке, после чего выведите список. Результат будет примерно таким
  • Служащий Ms. Davolio, Nancy - работает с 01.05.1992
  • Служащий Dr. Fuller, Andrew - работает с 14.08.1992
  • Служащий Ms. Leverling, Janet - работает с 01.04.1992
  • Служащий Mrs. Peacock, Margaret - работает с 03.05.1993
  • Служащий Mr. Buchanan, Steven - работает с 17.10.1993
  • Служащий Mr. Suyama, Michael - работает с 17.10.1993
  • Служащий Mr. King, Robert - работает с 02.01.1994
  • Служащий Ms. Callahan, Laura - работает с 05.03.1994
  • Служащий Ms. Dodsworth, Anne - работает с 15.11.1994
  • Служащий студент Петров, Иван - работает с 25.04.2007
  • В обработчике Page_Load() мы намеренно применили пустой блок обработки исключений для того, чтобы подавить сообщение, которое выдается при попытке повторного создания хранимой процедуры с тем же именем.

    Транзакции

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

    Поставщики данных включают поддержку транзакций, которые начинаются при вызове метода Connection.BeginTransaction(). Принимается транзакция методом Transaction.Commit(), а отменяется, если возникло исключение, методом Transaction.Rollback().

    Приведем пример, в котором в таблицу Employees БД Nordhwind добавляется две записи через механизм контроля транзакций. Эти записи можно было бы добавить и обычным способом, но в критических случаях делать это нужно под контролем транзакций.

  • Сделайте из страницы TestStoredProcedure.aspx копию с именем TestTransaction.aspx и определите ее стартовой
  • Заполните страницу следующим кодом
    (рис ) Код страницы TestTransaction.aspx<%@ Page Language="C#" %>
        
    <script runat="server">
        
        // Объявляем переменные-ссылки как члены класса страницы
        string sql1 = "INSERT INTO Employees (LastName, FirstName, HireDate) "
            + "VALUES ('Зиборов', 'Алексей', GETDATE())";
        string sql2 = "INSERT INTO Employees (LastName, FirstName, HireDate) "
            + "VALUES ('Погодаева', 'Татьяна', GETDATE())";
        System.Data.SqlClient.SqlConnection con;
        System.Data.SqlClient.SqlCommand cmd1, cmd2;
        System.Data.SqlClient.SqlTransaction tran = null;
        
        /**** Для отладки ****/
        string sql3 = "DELETE FROM Employees WHERE EmployeeID > 9";
        System.Data.SqlClient.SqlCommand cmd3;
        /*********************/
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Извлечь строку соединения из web.config
            string connectString = System.Web.Configuration.WebConfigurationManager.
                ConnectionStrings["Northwind"].ConnectionString;
        
            // Создать объект соединения
            con = new System.Data.SqlClient.SqlConnection(connectString);
            
            // Создать объекты команд
            cmd1 = new System.Data.SqlClient.SqlCommand(sql1, con);
            cmd2 = new System.Data.SqlClient.SqlCommand(sql2, con);
            
            /**** Для отладки ****/
            cmd3 = new System.Data.SqlClient.SqlCommand(sql3, con);
            /*********************/
        }
        
        protected void AddEmployee_Click(object sender, EventArgs e)
        {
            try // Попытка
            {
                // Открыть соединение
                con.Open();
                
                /**** Для отладки ****/
                cmd3.ExecuteNonQuery(); // Удалить последние записи
                /*********************/
                
                // Начать транзакцию
                tran = con.BeginTransaction();
        
                // Пометить команды включенными в транзакцию
                cmd1.Transaction = tran;
                cmd2.Transaction = tran;
        
                // Выполнить обе команды
                // Должны выполняться только команды транзакции!!!
                cmd1.ExecuteNonQuery();
                cmd2.ExecuteNonQuery();
         
                // Искусственная генерация исключения
                // для тестирования отмены транзакции
                //throw new ApplicationException();
       
                // Подтвердить и закончить транзакцию
                tran.Commit();
                
                // Посчитать количество записей таблицы
                // Теперь можно выполнять другие команды
                cmd1.CommandText = "SELECT COUNT(*) FROM Employees";
                string recCount = cmd1.ExecuteScalar().ToString();
                
                // Выдать результат
                lblInfo.Text = "Добавлены две записи!<br />";
                lblInfo.Text += "Общее число записей " + recCount;
            }
            catch // Откат при любом исключении
            {
                lblInfo.Text = "Транзакция отменена!";
                tran.Rollback();
            }
            finally // Обязательное завершение
            {
                // Закрыть соединение по-любому
                con.Close();
            }
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head id="Head1" runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
            <asp:Label ID="lblInfo" runat="server" />
            <br />
            <br />
            <asp:Button ID="AddEmployee" runat="server" 
                Text="Выполнить транзакцию"
                OnClick="AddEmployee_Click" />
            <asp:LinkButton ID="btnResult" runat="server" 
                PostBackUrl="~/TestDataReader.aspx">
                Показать список</asp:LinkButton>
        </form>
    </body>
    </html>
  • Исполните страницу и получите следующий отклик при выполнении транзакции
  • Просмотрите таблицу Employees БД Nordhwind по гиперссылке, результат выполнения транзакции будет примерно таким
  • Служащий Ms. Davolio, Nancy - работает с 01.05.1992
  • Служащий Dr. Fuller, Andrew - работает с 14.08.1992
  • Служащий Ms. Leverling, Janet - работает с 01.04.1992
  • Служащий Mrs. Peacock, Margaret - работает с 03.05.1993
  • Служащий Mr. Buchanan, Steven - работает с 17.10.1993
  • Служащий Mr. Suyama, Michael - работает с 17.10.1993
  • Служащий Mr. King, Robert - работает с 02.01.1994
  • Служащий Ms. Callahan, Laura - работает с 05.03.1994
  • Служащий Ms. Dodsworth, Anne - работает с 15.11.1994
  • Служащий Зиборов, Алексей - работает с 25.04.2007
  • Служащий Погодаева, Татьяна - работает с 25.04.2007
  • Обратите внимание, что пока транзакция находится в процессе работы, должны выполняться только команды транзакции. Область работы транзакции определяется операторными скобками con.BeginTransaction(); ... tran.Commit() ;

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

    throw new ApplicationException();

    Точки сохранения для отката транзакции

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

    Немного изменим предыдущий пример, введя возможность частичного отката.

  • Скопируйте страницу TestTransaction.aspx в TestTransactionSave.aspx и назначьте ее стартовой
  • Измените код новой страницы, чтобы он был таким
    (рис ) Код страницы TestTransactionSave.aspx частичного отката транзакции<%@ Page Language="C#" %>
        
    <script runat="server">
        
        // Объявляем переменные-ссылки как члены класса страницы
        string sql1 = "INSERT INTO Employees (LastName, FirstName, HireDate) "
            + "VALUES ('Зиборов', 'Алексей', GETDATE())";
        string sql2 = "INSERT INTO Employees (LastName, FirstName, HireDate) "
            + "VALUES ('Погодаева', 'Татьяна', GETDATE())";
        System.Data.SqlClient.SqlConnection con;
        System.Data.SqlClient.SqlCommand cmd1, cmd2;
        System.Data.SqlClient.SqlTransaction tran = null;
        
        /**** Для отладки ****/
        string sql3 = "DELETE FROM Employees WHERE EmployeeID > 9";
        System.Data.SqlClient.SqlCommand cmd3;
        /*********************/
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Извлечь строку соединения из web.config
            string connectString = System.Web.Configuration.WebConfigurationManager.
                ConnectionStrings["Northwind"].ConnectionString;
        
            // Создать объект соединения
            con = new System.Data.SqlClient.SqlConnection(connectString);
            
            // Создать объекты команд
            cmd1 = new System.Data.SqlClient.SqlCommand(sql1, con);
            cmd2 = new System.Data.SqlClient.SqlCommand(sql2, con);
            
            /**** Для отладки ****/
            cmd3 = new System.Data.SqlClient.SqlCommand(sql3, con);
            /*********************/
        }
        
        protected void AddEmployee_Click(object sender, EventArgs e)
        {
            try // Попытка
            {
                // Открыть соединение
                con.Open();
                
                /**** Для отладки ****/
                cmd3.ExecuteNonQuery(); // Удалить последние записи
                /*********************/
                
                // Начать транзакцию
                tran = con.BeginTransaction();
        
                // Пометить команды включенными в транзакцию
                cmd1.Transaction = tran;
                cmd2.Transaction = tran;
        
                // Выполнить первую команду транзакции
                cmd1.ExecuteNonQuery();          
                // Пометить точку отката
                tran.Save("Ziborov");
        
                // Выполнить вторую команду транзакции
                cmd2.ExecuteNonQuery();   
                // Выбросить исключение
                throw new ApplicationException();
            }
            catch // Откат
            {
                // Откатить частично
                tran.Rollback("Ziborov");
                
                // Подтвердить и закончить транзакцию
                tran.Commit();
                
                lblInfo.Text = "Транзакция частично отменена!";
        
                // Посчитать количество записей таблицы
                cmd1.CommandText = "SELECT COUNT(*) FROM Employees";
                string recCount = cmd1.ExecuteScalar().ToString();
        
                // Выдать результат
                lblInfo.Text = "Добавлена одна запись!<br />";
                lblInfo.Text += "Общее число записей " + recCount;
            }
            finally // Обязательное завершение
            {
                // Закрыть соединение по-любому
                con.Close();
            }
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head id="Head1" runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
            <asp:Label ID="lblInfo" runat="server" />
            <br />
            <br />
            <asp:Button ID="AddEmployee" runat="server" 
                Text="Выполнить транзакцию"
                OnClick="AddEmployee_Click" />
            <asp:LinkButton ID="btnResult" runat="server" 
                PostBackUrl="~/TestDataReader.aspx">
                Показать список</asp:LinkButton>
        </form>
    </body>
    </html>
  • Выполните страницу и убедитесь, что откат выполняется только до точки сохранения, а принимается только результат выполнения предыдущей команды (или блока команд) транзакции
  • Фабрики поставщиков

    Классы поставщиков оптимизированы под конкретный тип источника данных. Производители, разрабатывающие новую структуру источника данных, должны заботиться и о разработке соответствующего типа поставщика, чтобы этим источником данных могли пользоваться программисты (мы с вами) в своих приложениях. Но традиционный переход на новый тип поставщика требует существенной переделки кода приложения, что не всегда удобно для программистов. Поэтому в ASP.NET 2.0 предусмотрены специальные классы, которые позволяют "подкрутить" настройки только в одном месте, общедоступном для всех страниц приложения или группы страниц без необходимости изменения кода в каждой странице, использующей поставщик. Такой механизм называется фабриками поставщиков.

    Основная идея модели фабрик заключается в том, что можно использовать единственный объект-фабрику для создания всех прочих необходимых для конкретного поставщика объектов. Но это можно выполнить и созданием конкретных объектов поставщика. Самое главное удобство заключается в том, что конкретную фабрику поставщика можно создавать из обобщенного класса фабрик System.Data.Common.DbProviderFactories программным способом, используя информацию, заложенную в файлы web.config (в масштабах конкретного приложения) или machine.config (в масштабах конкретного компьютера-сервера)

    <?xml version="1.0"?>
    <configuration>
      <connectionStrings>
        <add name="Northwind" connectionString="Data Source=localhost; 
          Initial Catalog=Northwind; user id=sa; password=;"/>
       </connectionStrings>
       <system.data>
         <DbProviderFactories>
            <add name="SqlClient Data Provider" invariant="System.Data.SqlClient" 
        type="System.Data.SqlClient.SqlClientFactory" />
            <add name="Odbc Data Provider" invariant="System.Data.Odbc" 
        type="System.Data.Odbc.OdbcFactory" /> 
            <add name="OleDb Data Provider" invariant="System.Data.OleDb" 
        type="System.Data.OleDb.OleDbFactory" /> 
             <add name="OracleClient Data Provider" invariant="System.Data.OracleClient" 
        type="System.Data.OracleClient.OracleClientFactory" /> 
        </DbProviderFactories>
      </system.data>
      <system.web>
        <compilation debug="true"/>
      </system.web>
    </configuration>

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

    Обобщенный класс System.Data.Common.DbProviderFactories имеет статический метод GetFactory(string), который возвращает объект конкретной фабрики, указанной в строке аргумента. Например, для случая явного указания в коде конкретной фабрики это будет выглядеть так

    string factory = "System.Data.SqlClient";
    System.Data.Common.DbProviderFactory provider = 
        System.Data.Common.DbProviderFactories.GetFactory(factory);

    Получив конкретную фабрику из строки аргумента или конфигурационного файла далее с конкретным поставщиком можно работать через объект provider одноименной фабрики, устанавливая соединение с БД и посылая в нее SQL-запросы. Для создания объектов Connection и Command используются методы объекта (например, provider ) фабрики CreateXXX(), которые возвращают эти объекты

    Для создания независимого от поставщика универсального кода мы должны исходить из того, что не знаем заранее тип поставщика. Поэтому взаимодействовать с источником данных нужно только через методы объекта фабрики, возвращающие специфичные объекты Connection, Command, Parameter, DataAdapter базовых классов DbConnection, DbCommand, DbParameter, DbDataAdapter.

    Пример независимого от поставщика кода с использованием фабрик

    Чтобы лучше понять, как все вышесказанное работает, рассмотрим пример SQL-запроса, реализованного на странице TestDataReader.aspx. Первый шаг - настроить в файле web.config строку соединения, имя поставщика и текст SQL-запроса.

  • Откорректируйте файл web.config, чтобы он окончательно выглядел следующим образом
    (рис ) Дополнение файла web.config для применения обобщенного кода<?xml version="1.0"?>
    <configuration>
    	<connectionStrings>
    		<add name="Northwind" connectionString="Data Source=localhost; 
       Initial Catalog=Northwind; user id=sa; password=;"/>
    	</connectionStrings>
    	<appSettings>
    		<add key="factory" value="System.Data.SqlClient" />
    		<add key="employeeQuery" value="SELECT * FROM Employees" />
    	</appSettings>
    	<system.web>
    		<compilation debug="true"/>
    	</system.web>
    </configuration>
  • Создайте из копии TestDataReader.aspx новую страницу с именем TestFactory.aspx, назначьте ее стартовой и заполните следующим кодом
    (рис ) Страница TestFactory.aspx для реализации независимого от поставщика кода<%@ Page Language="C#" %>
        
    <script runat="server">
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Получить по ключу фабрику из файла web.config
            string factory = System.Web.Configuration.
                WebConfigurationManager.AppSettings["factory"];
            System.Data.Common.DbProviderFactory provider =
                System.Data.Common.DbProviderFactories.GetFactory(factory);
            
            // Использовать фабрику для получения объекта соединения
            // и извлечь строку соединения из файла web.config
            System.Data.Common.DbConnection con =
                provider.CreateConnection();
            con.ConnectionString = System.Web.Configuration.
                WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
        
            // Создать базовый объект DbCommand. Для его настройки 
            // и извлечь по ключу команду из файла web.config
            System.Data.Common.DbCommand cmd = provider.CreateCommand();
            cmd.Connection = con;
            cmd.CommandType = System.Data.CommandType.Text;
            cmd.CommandText = System.Web.Configuration.
                WebConfigurationManager.AppSettings["employeeQuery"];
        
            // Создаем ссылку на вспомогательную строку
            System.Text.StringBuilder htmlStr = new StringBuilder("");
            
            using (con)
            {
                // Открыли соединение
                con.Open();
        
                // Выполняем обобщенную команду и получаем результирующий набор данных
                System.Data.Common.DbDataReader reader =
                    cmd.ExecuteReader();
                
                // Перебираем все записи результирующего набора
                // и строим HTML-строку
                while (reader.Read())
                {
                    htmlStr.Append("<li>Служащий ");
                    htmlStr.Append(reader["TitleOfCourtesy"]);
                    htmlStr.Append(" <b>");
                    htmlStr.Append(reader.GetString(1));
                    htmlStr.Append("</b>, ");
                    htmlStr.Append(reader.GetString(2));
                    htmlStr.Append(" - работает с ");
                    htmlStr.Append(reader.GetDateTime(6).ToString("d"));
                    htmlStr.Append("</li>");
                }
                
                // Закрываем набор данных
                reader.Close();
            }
        
            // Выдаем результаты запроса
            lblInfo.Text = htmlStr.ToString();
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        <div>
            <asp:Label ID="lblInfo" runat="server" Text="Label"></asp:Label></div>
        </form>
    </body>
    </html>
  • Выполните страницу TestFactory.aspx. Должен получиться такой же результат, что и на странице TestDataReader.aspx, а именно
  • Служащий Ms. Davolio, Nancy - работает с 01.05.1992
  • Служащий Dr. Fuller, Andrew - работает с 14.08.1992
  • Служащий Ms. Leverling, Janet - работает с 01.04.1992
  • Служащий Mrs. Peacock, Margaret - работает с 03.05.1993
  • Служащий Mr. Buchanan, Steven - работает с 17.10.1993
  • Служащий Mr. Suyama, Michael - работает с 17.10.1993
  • Служащий Mr. King, Robert - работает с 02.01.1994
  • Служащий Ms. Callahan, Laura - работает с 05.03.1994
  • Служащий Ms. Dodsworth, Anne - работает с 15.11.1994
  • Служащий Зиборов, Алексей - работает с 07.05.2007
  • Чтобы убедиться, что код получился действительно универсальным и настройка на конкретного поставщика осуществляется только в файле web.config, поменяем поставщика в этом файле без изменения кода самой страницы.

  • Откорректируйте файл web.config, чтобы он окончательно выглядел следующим образом
    (рис ) Изменение поставщика в файле web.config для проверки обобщенного кода<?xml version="1.0"?>
    <configuration>
    	<connectionStrings>
    		<add name="Northwind" connectionString="Provider=SQLOLEDB; Data Source=localhost; 
      Initial Catalog=Northwind; user id=sa; password=;"/>
    	</connectionStrings>
    	<appSettings>
    		<add key="factory" value="System.Data.OleDb" />
    		<add key="employeeQuery" value="SELECT * FROM Employees" />
    	</appSettings>
    	<system.web>
    		<compilation debug="true"/>
    	</system.web>
    </configuration>
  • Исполните страницу TestFactory.aspx для нового поставщика, указанного в файле web.config, и убедитесь, что прежний код страницы генерирует тот же самый результат, но уже с новым поставщиком
  • Механизм применения фабрик поставщиков не решает всех проблем, связанных с разработкой независимого от поставщика кода. Например, не существует обобщенных объектов исключений базы данных, а только специфичные для конкретных поставщиков. Поставщики могут различаться некоторыми специфическими параметрами или поддерживать специальные средства, недоступные для общих базовых классов. Тем не менее, продемонстрированных возможностей в большинстве практических случаев может быть достаточно для написания независимого кода страниц.

    Страницы:
    Файлы к лекции Вы можете скачать здесь

    Базы данных - это хранилища структурированных данных, с которыми удобно работать. СУБД - это программное приложение, извлекающее, отображающее в удобном виде и модифицирующее структурированные данные. Чаще всего применяются реляционные базы данных. Библиотека .NET Framework включает свою собственную технологию доступа к данным - ADO.NET. В нее включены классы, обеспечивающие соединение и обработку данных как в локальных, так и в Web-приложениях. Причем код приложения остается почти одинаковым.

    Поставщики данных

    Важное место в архитектуре ADO.NET занимают поставщики данных ( data provider ). Они являются универсальными посредниками между источником данных и приложением. В состав одного поставщика входят следующие классы ( используются обобщенные имена ):

  • Connection - используется для установки соединения с источником данных
  • Command - используется для выполнения команд SQL и хранимых процедур
  • DataReader - предоставляет быстрый доступ только для чтения к извлеченным данным
  • DataAdapter - этот класс решает две задачи:
  • наполнение DataSet ( набор данных ) информацией, извлеченной из источника данных
  • сохраняет изменения, выполненные в DataSet
  • ADO.NET содержит набор специализированных поставщиков для различных источников данных. Каждый поставщик данных имеет специфическую реализацию классов Connection, Command, DataReader, DataAdapter, оптимизированных для конкретных баз данных. Например, если нужно подключиться к базе данных SQL Server, то используется класс SqlConnection.

    Библиотека .NET Framework содержит четыре типа готовых поставщиков данных

  • Поставщик SQL Server
  • Поставщик OLE DB
  • Поставщик Oracle
  • Поставщик ODBC
  • Для других типов данных разработчики могут либо создавать свои поставщики, либо приобретать их у сторонних организаций. Выбирая класс поставщика, нужно сначала попытаться найти родного поставщика для данного источника данных. Если это невозможно, то можно воспользоваться поставщиком OLE DB при условии, что существует драйвер OLE DB для этого источника данных.

    Технология OLE DB существует уже много лет как часть ADO, поэтому большинство распространенных источников данных ( SQL Server, Oracle, Access, MySQL и т.д.) предусматривают драйверы OLE DB. Наконец, можно использовать поставщик ODBC в сочетании с драйвером ODBC, поскольку это самая старая технология доступа к данным, но производительность такого подключения может быть очень низкой.

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

    Классы ADO.NET группируются в нескольких пространствах имен:

  • System.Data - содержит ключевые контейнерные классы, моделирующие сами данные. Дополнительно содержит ключевые интерфейсы, реализуемые объектами данных, основанными на соединениях
  • System.Data.Common - содержит базовые классы, наследуемые классами поставщиков
  • System.Data.OleDb - включают поставщиков для источников данных OLE DB
  • System.Data.SqlClient - включают поставщиков для источников данных SQL Server
  • System.Data.OracleClient - включают поставщиков для источников данных Oracle
  • System.Data.SqlTypes - содержит структуры, соответствующие родным типам данных SQL Server. Эти классы не являются необходимыми, но в силу их специфичности оптимизируют доступ к данным SQL Server
  • System.Data.Odbc - включают поставщиков для источников данных ODBC. Операционная система Windows имеет драйверы ODBC для всех видов источников данных, которые конфигурируются в панели управления Пуск/Настройка/Панель управления/Администрирование/Источники данных (ODBC)
  • Класс Connection

    Этот класс устанавливает соединение с источником данных, к которому мы хотим подключиться, чтобы выполнить с данными какие-то действия. Свои основные свойства и методы класс Connection реализует от наследуемого интерфейса IDbConnection.

    Строка соединения

    При установке соединения требуется строка соединения, которая представляет собой список пар имя=значение, разделенных точкой с запятой. За исключением небольших различий, строка соединения содержит несколько атрибутов, необходимых почти всегда (порядок следования атрибутов значения не имеет):

  • Сервер, на котором находится база данных. При выполнении упражнений мы будем обоснованно полагать, что наши приложения ASP.NET и сервер базы данных находятся на одном и том же компьютере, поэтому вместо имени компьютера будем использовать псевдоним localhost
  • База данных, к которой надо подключиться. Мы будем использовать готовую учебную БД Northwind, которая инсталируется по умолчанию с установкой SQL Server 2005
  • Атрибут аутентификации (login). Мы не будем использовать имя и пароль, а будем подключаться к БД как текущий пользователь системы
  • Ниже представлены примеры строки соединения для подключения к БД Northwind на текущем компьютере

    Примеры строки соединения
    Источник данных Строка соединения
    SQL Server с использованием интегрированной безопасности string connectionString = "Data Source=localhost;Initial Catalog=Northwind;" + "Integrated Security=SSPI";
    SQL Server с использованием учетной записи ( id=sa - system administrator ) string connectionString = "Data Source=localhost;Initial Catalog=Northwind;" + "user id=sa;password=xxx";
    Используется поставщик OLE DB для подключения к БД Sales типа Oracle через драйвер MSDAORA OLE DB с правами администратора и пустым паролем string connectionString = "Data Source=localhost;Initial Catalog=Sales;" + "user id=sa;password=;Provider=MSDAORA";
    Подключение к БД Access. Символ "@" требует от компилятора интерпретировать строку буквально string connectionString = "Provider=Microsoft.Jet.OLEDB.4.0;" + @"Data Source=C:\DataSources\Northwind.mdb";

    Строку соединения можно указать в конструкторе при создании объекта Connection или в методе Connection.Open(). Строку соединения можно поместить не в код страницы, а в конфигурационный файл web.config

    <?xml version="1.0"?>
    <configuration>
    	<connectionStrings>
    		<add name="Northwind" connectionString="Data Source=localhost;
    			Initial Catalog=Northwind; Integrated Security=SSPI" />
    	</connectionStrings>
    	<system.web>
    	</system.web>
    </configuration>

    В любое время в коде страницы можно извлечь эту строку соединения по ID -имени из коллекции следующим образом

    string connectionString = WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;

    Подключение к БД и тестирование соединения

    После установки SQL Server 2005 нужно убедиться, что служба SQLServerAgent включена в режим Auto. Это можно сделать через Пуск/Настройка/Панель управления/Администрирование/Службы

    Приведем простой пример установки соединения с БД Northwind.

    Способ 1

  • Создайте командой
  • Переведите страницу в режим Design и добавьте из вкладки Standard элемент Label с именем lblInfo
  • В режиме Design двойным щелчком на клиентской области страницы создайте обработчик Page_Load() в блоке скриптов, который заполните так, чтобы общий код страницы был следующим
    (рис ) Код страницы TestConnection.aspx<%@ Page Language="C#" %>
        
    <script runat="server">
        
        protected void Page_Load(object sender, EventArgs e)
        {
            string connectString = System.Web.Configuration.WebConfigurationManager.
                ConnectionStrings["Northwind"].ConnectionString;
            System.Data.SqlClient.SqlConnection con = 
                new System.Data.SqlClient.SqlConnection(connectString);
        
            try
            {
                con.Open();
                lblInfo.Text = "<b>Версия сервера:</b> " + con.ServerVersion;
                lblInfo.Text += "<br /><b>Соединение:</b> " + con.State.ToString();
            }
            catch(Exception err)
            {
                lblInfo.Text = "<b>Ошибка чтения базы данных.</b>";
                lblInfo.Text += err.Message;
            }
            finally
            {
                con.Close();
                lblInfo.Text += "<br /><b>Теперь соединение:</b> ";
                lblInfo.Text += con.State.ToString();
            }
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        <div>
            <asp:Label ID="lblInfo" runat="server" Text="Label"></asp:Label></div>
        </form>
    </body>
    </html>
  • Поместите в файл web.config строку соединения следующим образом
    (рис ) Строка соединения в файле web.config<?xml version="1.0"?>
    <configuration>
    	<connectionStrings>
    		<add name="Northwind" connectionString = 
    			"Data Source=localhost; 
    			Initial Catalog=Northwind; 
    			user id=sa; password=;" />
    	</connectionStrings>
    	<system.web>
    		<compilation debug="true"/>
    	</system.web>
    </configuration>
    Теперь мы будем только извлекать в коде эту строку соединения в наших дальнейших примерах, поэтому не забывайте, что она существует и как выглядит!!!
  • Выполните страницу, сделав ее стартовой, результат должен быть примерно следующим
  • Соединение как объект занимает определенные ресурсы, поэтому открывать соединение нужно как можно позже, и освобождать как можно быстрее. В примере предусмотрен блок finally на тот случай, что даже при возникновении исключения соединение все равно будет закрыто. Если это не предусмотреть, то в случае необработанного исключения соединение останется открытым до тех пор, пока сборщик мусора не уничтожит объект con типа SqlConnection.

    Способ 2

    Другой подход гарантированного закрытия соединения - использовать блок using. Оператор using декларирует, что мы используем уничтожаемый объект в течение краткого периода времени. Как только выполнение блока using завершится, среда выполнения немедленно освободит соответствующий объект, вызвав метод Dispose(). Вызов этого метода эквивалентен вызову метода Close() для объекта Connection.

  • Создайте копию страницы TestConnection.aspx и назначьте ей имя TestConnection1.aspx
  • Назначьте новую страницу стартовой и заполните ее следующим кодом
    (рис ) Код страницы TestConnection1.aspx<%@ Page Language="C#" %>
        
    <script runat="server">
        
        protected void Page_Load(object sender, EventArgs e)
        {
            string connectString = System.Web.Configuration.WebConfigurationManager.
                ConnectionStrings["Northwind"].ConnectionString;
            System.Data.SqlClient.SqlConnection con =
                new System.Data.SqlClient.SqlConnection(connectString);
        
            using (con)
            {
                con.Open();
                lblInfo.Text = "<b>Версия сервера:</b> " + con.ServerVersion;
                lblInfo.Text += "<br /><b>Соединение:</b> " + con.State.ToString();
            }
        
            lblInfo.Text += "<br /><b>Теперь соединение:</b> ";
            lblInfo.Text += con.State.ToString();
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        <div>
            <asp:Label ID="lblInfo" runat="server" Text="Label"></asp:Label></div>
        </form>
    </body>
    </html>
  • Выполните страницу, результат должен быть прежним
  • Организация пула соединений

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

    Пул соединений - это практика хранения приложением постоянного набора открытых соединений, разделяемых между сеансами при использовании одного и того же источника данных. Это позволяет избежать бесконечного создания и уничтожения соединений. При организации пула вызов клиентом метода Open() обслуживается непосредственно из доступного пула без повторного открытия соединения. Когда клиент освобождает соединение методом Close() или Dispose(), оно не закрывается, а возвращается в пул, чтобы обслужить следующий запрос.

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

    Пулы соединений SQL Server и Oracle используют механизм полнотекстового сравнения. Это значит, что любое минимальное изменение в строке соединений нарушает пул, даже если в ней просто изменяется порядок параметров или добавляется простой пробел. Поэтому лучше не помещать строку соединений в код установки соединения, а сохранять в файле web.config, как это мы сделали в примере.

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

    Параметры настройки пулов в строке соединений
    Параметр Описание
    Max Pool Size Максимальное количество соединений, разрешенных для хранения в пуле (по умолчанию 100). Если достигается этот максимальный размер пула, любые последующие попытки соединения становятся в очередь. Если время жизни соединения, установленное параметром Connection.Timeout, истечет раньше, чем подошла очередь в пуле, возникает исключение.
    Min Pool Size Минимальное количество соединений, которое должно оставаться в пуле (по умолчанию 0). Это число соединений будет создано в пуле при открытии первого соединения.
    Pooling По умолчанию true. При значении false отключается механизм пула
    Connection Lifetime Устанавливает время хранения соединения в пуле (в секундах). По умолчанию значение 0 устанавливает неограниченное время жизни

    Некоторые поставщики имеют методы очистки пула соединений. Например, поставщик SqlConnection имеет статические методы SqlConnection.ClearPool(connectionString) - очистка пула конкретного соединения и SqlConnection.ClearAllPools() - очистка пулов всех соединений в текущем домене приложения. Эти методы не удаляют соединения физически, а помечают на удаление для бредущего сзади сборщика мусора.

    Класс Command и DataReader

    Класс Command позволяет выполнить любой SQL-запрос к открытому соединению базы данных. Для того, чтобы использовать команду, нужно выбрать ее тип, установить ее текст и привязать к открытому соединению. Эту работу можно выполнить, установив значения свойств CommandType, CommandText и Connection, либо передать необходимую информацию в аргументах конструктора класса Command.

    Текстом команды может быть SQL-оператор, хранимая процедура ( stored procedure ) или имя таблицы. Это определяется значением перечисления CommandType, присваиваемого свойству Command.CommandType.

    Значения перечисления CommandType
    Значение Описание
    System.Data.CommandType.Text Требование выполнить SQL-оператор, указанный в свойстве Command.CommandText. Установлено по умолчанию в свойстве класса Command.CommandType
    System.Data.CommandType.StoredProcedure Требование выполнить хранимую процедуру, указанную в свойстве Command.CommandText
    System.Data.CommandType.TableDirect Требование извлечь все записи из таблицы, указанной в свойстве Command.CommandText.

    Не поддерживается поставщиком данных SQL Server

    Расширим наш пример, чтобы применить код создания объекта Command.

  • Создайте из копии файла TestConnection1.aspx файл с именем TestConCommand.aspx и установите его стартовым
  • Дополните код страницы TestConCommand.aspx так
    (рис ) Страница TestConCommand.aspx создания объектов соединения и команды<%@ Page Language="C#" %>
        
    <script runat="server">
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Извлекли строку соединения из web.config
            string connectString = System.Web.Configuration.WebConfigurationManager.
                ConnectionStrings["Northwind"].ConnectionString;
            
            // Установили минимальный размер пула соединения равный 10
            connectString += "Min Pool Size=10;";
            
            // Создали объект соединения
            System.Data.SqlClient.SqlConnection con =
                new System.Data.SqlClient.SqlConnection(connectString);
        
            using (con)
            {
                // Получить соединение из пула (если есть)
                // или создать пул с 10 соединениями (если нет)
                con.Open();
                lblInfo.Text = "<b>Соединение:</b> " + con.State.ToString();
                
                // Создали объект Command
                System.Data.SqlClient.SqlCommand cmd = 
                    new System.Data.SqlClient.SqlCommand();
                
                // Настроили объект Command
                cmd.Connection = con;
                cmd.CommandType = System.Data.CommandType.Text;
                cmd.CommandText = "SELECT * FROM Employees";
            }
        
            // Вернуть соединение в пул
            lblInfo.Text += "<br /><b>Соединение:</b> ";
            lblInfo.Text += con.State.ToString();
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        <div>
            <asp:Label ID="lblInfo" runat="server" Text="Label"></asp:Label></div>
        </form>
    </body>
    </html>
  • Исполните страницу, чтобы получить такой результат
  • Учитывая, что по умолчанию установлено CommandType = CommandType.Text, команду можно сразу указать в перегруженном конструкторе при создании объекта команд. Например, код

    // Создали объект Command
    System.Data.SqlClient.SqlCommand cmd = 
        new System.Data.SqlClient.SqlCommand();
    
    // Настроили объект Command
    cmd.Connection = con;
    cmd.CommandType = System.Data.CommandType.Text;
    cmd.CommandText = "SELECT * FROM Employees";

    можно заменить кодом

    // Создали и настроили объект Command
    System.Data.SqlClient.SqlCommand cmd = 
        new System.Data.SqlClient.SqlCommand("SELECT * FROM Employees", con);

    В данном примере мы просто настроили, но не выполнили объект класса Command, поэтому никакой выборки данных из БД не получили. Объект Command имеет три метода выполнения команды, приведенные в таблице на примере класса SqlCommand

    Методы выполнения объекта Command
    Метод Описание
    System.Data.SqlClient.SqlCommand.ExecuteNonQuery() Выполняет команды, отличные от SELECT, такие как SQL-операторы вставки, удаления или обновления записей. Возвращает количество строк, обработанное командой
    System.Data.SqlClient.SqlCommand.ExecuteScalar() Выполняет запрос SELECT и возвращает первое поле первой строки результирующего набора, сгенерированного командой. Обычно применяется в запросах с функциями SUM() или COUNT() для вычисления единственного значения
    System.Data.SqlClient.SqlCommand.ExecuteReader() Выполняет запрос выборки данных SELECT и возвращает объект DataReader - результирующий набор данных, доступный только для чтения

    Метод SqlCommand.ExecuteReader()

    Объект класса DataReader представляет собой результирующий набор данных только для чтения и является самым простым и быстрым средством доступа к данным. Он является выборкой данных по запросу SQL-команды SELECT и состоит из строк и колонок, которые можно анализировать и отображать. Но в нем нет возможностей сортировки, как в более развитых объектах. Ниже приведены несколько методов DataReader на примере класса System.Data.SqlClient.SqlDataReader

    Методы класса DataReader
    Метод Описание
    SqlDataReader.Read() Перемещает курсор строки на следующую строку в потоке. Его нужно также вызывать первым после создания объекта DataReader, поскольку начальный курсор позиционируется перед первой строкой. Возвращает false при чтении последней строки набора данных
    SqlDataReader.GetValue(int) Читает значение поля текущей строки по указанному индексу (индекс начинается с нуля)
    SqlDataReader.GetValues(object[ ]) Сохраняет значение текущей строки в массиве. Количество сохраняемых полей зависит от размера массива, переданного этому методу. Чтобы сохранить все поля текущей строки, нужно создать массив размерности, определяемой свойством SqlDataReader.FieldCount
    SqlDataReader.GetInt32(int) Возвращает значение поля целого типа по индексу столбца
    SqlDataReader.GetDateTime(int) Возвращает значение поля типа даты по индексу столбца
    SqlDataReader.NextResult() Если команда, которая сгенерировала DataReader, возвратила более одного результирующего набора строк, то этот метод перемещает текущий курсор данных на первую строку следующего набора
    SqlDataReader.Close() Закрывает модуль чтения

    Обработка одного результирующего набора данных SqlDataReader

    Приведем пример, в котором создается и выполняется простая команда запроса для выборки всех записей из таблицы Employees учебной базы данных Northwind. Мы помним, что строка соединения была нами задана ранее в конфигурационном файле.

  • Создайте копию страницы TestConCommand.aspx с именем TestDataReader.aspx
  • Заполните страницу приведенным кодом
    (рис ) Код страницы TestDataReader.aspx с чтением из базы Northwind<%@ Page Language="C#" %>
        
    <script runat="server">
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Извлекли строку соединения из web.config
            string connectString = System.Web.Configuration.WebConfigurationManager.
                ConnectionStrings["Northwind"].ConnectionString;
        
            // Создали объект соединения
            System.Data.SqlClient.SqlConnection con =
                new System.Data.SqlClient.SqlConnection(connectString);
        
            // Создали объект Command
            System.Data.SqlClient.SqlCommand cmd =
                new System.Data.SqlClient.SqlCommand();
        
            // Настроили объект Command
            cmd.Connection = con;
            cmd.CommandType = System.Data.CommandType.Text;
            cmd.CommandText = "SELECT * FROM Employees";
        
            // Создаем ссылку на вспомогательную строку
            System.Text.StringBuilder htmlStr = new StringBuilder("");
            
            using (con)
            {
                // Открыли соединение
                con.Open();
        
                // Выполняем команду и получаем результирующий набор данных
                System.Data.SqlClient.SqlDataReader reader =
                    cmd.ExecuteReader();
                
                // Перебираем все записи результирующего набора
                // и строим HTML-строку
                while (reader.Read())
                {
                    htmlStr.Append("<li>Служащий ");
                    htmlStr.Append(reader["TitleOfCourtesy"]);
                    htmlStr.Append(" <b>");
                    htmlStr.Append(reader.GetString(1));
                    htmlStr.Append("</b>, ");
                    htmlStr.Append(reader.GetString(2));
                    htmlStr.Append(" - работает с ");
                    htmlStr.Append(reader.GetDateTime(6).ToString("d"));
                    htmlStr.Append("</li>");
                }
                
                // Закрываем набор данных
                reader.Close();
            }
        
            // Выдаем результаты запроса
            lblInfo.Text = htmlStr.ToString();
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        <div>
            <asp:Label ID="lblInfo" runat="server" Text="Label"></asp:Label></div>
        </form>
    </body>
    </html>
  • Исполните страницу TestDataReader.aspx, должен получиться такой клиентский результат
  • Служащий Ms. Davolio, Nancy - работает с 01.05.1992
  • Служащий Dr. Fuller, Andrew - работает с 14.08.1992
  • Служащий Ms. Leverling, Janet - работает с 01.04.1992
  • Служащий Mrs. Peacock, Margaret - работает с 03.05.1993
  • Служащий Mr. Buchanan, Steven - работает с 17.10.1993
  • Служащий Mr. Suyama, Michael - работает с 17.10.1993
  • Служащий Mr. King, Robert - работает с 02.01.1994
  • Служащий Ms. Callahan, Laura - работает с 05.03.1994
  • Служащий Ms. Dodsworth, Anne - работает с 15.11.1994
  • В этом примере объект класса StringBuilder существенно увеличивает производительность за счет выделения буфера памяти для символов. Если использовать объект класса String, то при каждой операции сложения строк будет создаваться новый объект с копированием результатов из старого объекта, что значительно медленнее.

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

    Обработка множественного результирующего набора данных SqlDataReader

    Команда SqlCommand.ExecuteReader() может возвращать множественный результирующий набор данных в двух случаях:

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

    string sql = "SELECT TOP 5 * FROM Employees;"
    		+ "SELECT TOP 5 * FROM Customers;"
    		+ "SELECT TOP 5 * FROM Suppliers";

    Приведенная строка содержит три запроса, каждый из которых возвращает в результирующий набор данных по пять первых записей из трех таблиц базы Northwind. Ниже приведен код страницы, в котором выполняется последовательная выборка данных из множественного результирующего набора. Переход на новый результирующий набор выполняется методом NextResult(), который возвратит false, когда больше не останется результирующих наборов.

  • Создайте копию страницы TestDataReader.aspx с именем TestMultiDataReader.aspx и определите ее стартовой
  • Заполните страницу приведенным кодом
    (рис ) Код страницы TestMultiDataReader.aspx для нескольких таблиц базы Northwind<%@ Page Language="C#" %>
        
    <script runat="server">
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Извлекли строку соединения из web.config
            string connectString = System.Web.Configuration.WebConfigurationManager.
                ConnectionStrings["Northwind"].ConnectionString;
        
            // Создали объект соединения
            System.Data.SqlClient.SqlConnection con =
                new System.Data.SqlClient.SqlConnection(connectString);
        
            // Создали объект Command
            System.Data.SqlClient.SqlCommand cmd =
                new System.Data.SqlClient.SqlCommand();
        
            // Настроили объект Command
            cmd.Connection = con;
            cmd.CommandType = System.Data.CommandType.Text;
            cmd.CommandText = "SELECT TOP 5 * FROM Employees;"
    		                + "SELECT TOP 5 * FROM Customers;"
    		                + "SELECT TOP 5 * FROM Suppliers";
        
            System.Text.StringBuilder htmlStr = new StringBuilder("");
            int i = 0;
            
            using (con)
            {
                // Открыли соединение
                con.Open();
        
                // Выполняем команду и получаем результирующий набор данных
                System.Data.SqlClient.SqlDataReader reader =
                    cmd.ExecuteReader();
                
                // Перебираем частные наборы в результирующем наборе данных
                do
                {
                    htmlStr.Append("<h2>Набор: ");// Заголовок h2 HTML
                    htmlStr.Append(i.ToString());
                    htmlStr.Append("</h2>");
                    htmlStr.Append("<ul>"); // Начало маркированного списка
                    
                    // Перебираем все записи текущего набора
                    // (их должно по команде быть 5 первых)
                    // в результирующем наборе и строим HTML-строку
                    while (reader.Read())
                    {
                        htmlStr.Append("<li>");// Строка списка HTML
                        // Перебираем все поля строки набора
                        for (int field = 0; field < reader.FieldCount; field++)
                        {
                            if (field >= 3) // Ограничимся тремя первыми полями 
                                continue;
                            htmlStr.Append("<b>");
                            htmlStr.Append(reader.GetName(field).ToString());// Имя поля
                            htmlStr.Append(": </b>");
                            htmlStr.Append(reader.GetValue(field).ToString());// Значение поля
                            htmlStr.Append("nbsp;nbsp;nbsp;nbsp;nbsp;");// Жесткие пробелы HTML
                        }
                        htmlStr.Append("</li>");
                    }
        
                    htmlStr.Append("</ul>"); // Конец маркированного списка
                    i++;
                    
                } while (reader.NextResult());
                
                // Закрываем набор данных
                reader.Close();
            }
        
            // Показываем пользователю результаты запроса
            lblInfo.Text = htmlStr.ToString();
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        <div>
            <asp:Label ID="lblInfo" runat="server" Text="Label"></asp:Label></div>
        </form>
    </body>
    </html>
  • Исполните страницу, должен получиться такой результат
  • Мы искусственно ограничили количество отображаемых полей тремя. Если этот оператор убрать, то на пользователя обрушится поток информации. Нам на данном этапе важен сам принцип извлечения информации из БД.

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

  • Создайте копию страницы TestMultiDataReader.aspx с именем GridViewDataReader.aspx и определите ее стартовой
  • Поместите на страницу элемент управления GridView из вкладки Data панели Toolbox
  • Заполните страницу приведенным кодом
    (рис ) Код страницы GridViewDataReader.aspx с неформатированным просмотром <%@ Page Language="C#" %>
        
    <script runat="server">
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Извлекли строку соединения из web.config
            string connectString = System.Web.Configuration.WebConfigurationManager.
                ConnectionStrings["Northwind"].ConnectionString;
        
            // Создали объект соединения
            System.Data.SqlClient.SqlConnection con =
                new System.Data.SqlClient.SqlConnection(connectString);
        
            // Создали объект Command
            System.Data.SqlClient.SqlCommand cmd =
                new System.Data.SqlClient.SqlCommand();
        
            // Настроили объект Command
            cmd.Connection = con;
            cmd.CommandType = System.Data.CommandType.Text;
            cmd.CommandText = "SELECT TOP 5 * FROM Suppliers";
        
            using (con)
            {
                // Открыли соединение
                con.Open();
        
                // Выполняем команду и получаем результирующий набор данных
                System.Data.SqlClient.SqlDataReader reader =
                    cmd.ExecuteReader();
        
                // Подключаем элемент управления GridView
                // к полученному результирующему набору
                GridView1.DataSource = reader;
        
                // Наполняем элемент управления GridView
                // всеми извлеченными записями DataReader
                GridView1.DataBind();
        
                // Закрываем набор данных
                reader.Close();
            }
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head id="Head1" runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
            <div>
                <asp:GridView ID="GridView1" runat="server">
                </asp:GridView>
            </div>
        </form>
    </body>
    </html>
  • Исполните страницу, должен получиться такой результат
  • SupplierID CompanyName ContactName ContactTitle Address City Region PostalCode Country Phone Fax HomePage
    1 Exotic Liquids Charlotte Cooper Purchasing Manager 49 Gilbert St. London EC1 4SD UK (171) 555-2222
    2 New Orleans Cajun Delights Shelley Burke Order Administrator P.O. Box 78934 New Orleans LA 70117 USA (100) 555-4822 #CAJUN.HTM#
    3 Grandma Kelly's Homestead Regina Murphy Sales Representative 707 Oxford Rd. Ann Arbor MI 48104 USA (313) 555-5735 (313) 555-3349
    4 Tokyo Traders Yoshi Nagase Marketing Manager 9-8 Sekimai Musashino-shi Tokyo 100 Japan (03) 3555-5011
    5 Cooperativa de Quesos 'Las Cabras' Antonio del Valle Saavedra Export Administrator Calle del Rosal 4 Oviedo Asturias 33007 Spain (98) 598 76 54

    Метод SqlCommand.ExecuteScalar()

    Этот метод объекта Command возвращает скалярную величину типа Object, которая является результатом работы агрегатных функций вроде COUNT() или SUM() SQL-запроса. Чтобы получить конкретное значение, к возвращенному объекту нужно применить явное преобразование типов.

  • Создайте копию страницы TestConCommand.aspx с именем TestExecuteScalar.aspx и сделайте ее стартовой
  • Скорректируйте страницу TestExecuteScalar.aspx так
    (рис ) Страница TestExecuteScalar.aspx<%@ Page Language="C#" %>
        
    <script runat="server">
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Извлекли строку соединения из web.config
            string connectString = System.Web.Configuration.WebConfigurationManager.
                ConnectionStrings["Northwind"].ConnectionString;
        
            // Создали объект соединения
            System.Data.SqlClient.SqlConnection con =
                new System.Data.SqlClient.SqlConnection(connectString);
        
            // Создали объект Command
            System.Data.SqlClient.SqlCommand cmd =
                new System.Data.SqlClient.SqlCommand();
        
            // Настроили объект Command
            cmd.Connection = con;
            cmd.CommandType = System.Data.CommandType.Text;
            cmd.CommandText = "SELECT COUNT(*) FROM Employees";
        
            using (con)
            {
                // Открыли соединение
                con.Open();
        
                // Выполнили команду подсчета записей таблицы Employees
                // с применением явного преобразования типа
                int recordCount = (int)cmd.ExecuteScalar();
        
                lblInfo.Text = "<b>Количество записей:</b> " + recordCount.ToString();
            }
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        <div>
            <asp:Label ID="lblInfo" runat="server" Text="Label"></asp:Label></div>
        </form>
    </body>
    </html>
  • Постройте страницу, результат будет таким
  • Метод SqlCommand.ExecuteNonQuery()

    Этот метод напоминает консольную утилиту, которая работает в пакетном режиме, выполняет какие-либо действия, но не возвращает результатов. Метод ExecuteNonQuery() выполняет такие SQL-команды, как INSERT, DELETE, UPDATE, а возвращает только число, равное количеству обработанных записей.

    В качестве примера приведем код страницы, удаляющий запись с определенным значением поля EmployeeID в таблице Employees БД Northwind. Поскольку эта база учебная, не будем ее портить и зададим заведомо несуществующее значение поля EmployeeID.

    <%@ Page Language="C#" %>
        
    <script runat="server">
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Формируем соединение
            string connectString = System.Web.Configuration.WebConfigurationManager.
                ConnectionStrings["Northwind"].ConnectionString;
            System.Data.SqlClient.SqlConnection con = 
                new System.Data.SqlClient.SqlConnection(connectString);
            
            // Формируем команду в конструкторе класса
            int empID = 111; // Заведомо несуществующее значение
            string sql = "DELETE FROM Employees WHERE EmployeeID = " + empID.ToString();
            System.Data.SqlClient.SqlCommand cmd = 
                new System.Data.SqlClient.SqlCommand(sql, con);
        
            try
            {
                con.Open();
                int recCount = cmd.ExecuteNonQuery();
                lblInfo.Text = string.Format("<b>Удалено записей:</b> {0}", recCount);
            }
            catch(System.Data.SqlClient.SqlException err)
            {
                lblInfo.Text = string.Format("<b>Ошибка удаления:</b> {0}", err.Message);
            }
            finally
            {
                con.Close();
            }
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        <div>
            <asp:Label ID="lblInfo" runat="server" Text="Label"></asp:Label></div>
        </form>
    </body>
    </html>

    Атаки на базу данных внедрением SQL

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

    Рассмотрим это на примере.

  • Сделайте копию с именем UserSqlGridData.aspx из файла GridViewDataReader.aspx и назначьте ее стартовой
  • Разместите выше элемента
  • Откорректируйте дескрипторный и встроенный коды страницы так
    (рис ) Код страницы UserSqlGridData.aspx<%@ Page Language="C#" %>
        
    <script runat="server">
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Извлекли строку соединения из web.config
            string connectString = System.Web.Configuration.WebConfigurationManager.
                ConnectionStrings["Northwind"].ConnectionString;
        
            // Создали объект соединения
            System.Data.SqlClient.SqlConnection con =
                new System.Data.SqlClient.SqlConnection(connectString);
            
            // Создали строку SQL-запроса
            string sql =
                "SELECT Orders.CustomerID, Orders.OrderID, COUNT(UnitPrice) AS Items, "
              + "SUM(UnitPrice * Quantity) AS Total FROM Orders "
              + "INNER JOIN[Order Details] "
              + "ON Orders.OrderID = [Order Details].OrderID "
              + "WHERE Orders.CustomerID = '" + TextBox1.Text + "' "
              + "GROUP BY Orders.OrderID, Orders.CustomerID";
        
            // Создали объект Command
            System.Data.SqlClient.SqlCommand cmd =
                new System.Data.SqlClient.SqlCommand(sql, con);
        
            using (con)
            {
                // Открыли соединение
                con.Open();
        
                // Выполняем команду и получаем результирующий набор данных
                System.Data.SqlClient.SqlDataReader reader =
                    cmd.ExecuteReader();
        
                // Подключаем элемент управления GridView
                // к полученному результирующему набору
                GridView1.DataSource = reader;
        
                // Наполняем элемент управления GridView
                // всеми извлеченными записями DataReader
                GridView1.DataBind();
        
                // Закрываем набор данных
                reader.Close();
            }
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head id="Head1" runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
            <div>
                Введите ID пользователя:<br />
                <asp:TextBox ID="TextBox1" runat="server" Width="248px" Font-Bold="True">ALFKI</asp:TextBox> 
                <asp:Button ID="Button1" runat="server" Text="Получить" /><br />
                <br />
                <asp:GridView ID="GridView1" runat="server">
                </asp:GridView>
            </div>
        </form>
    </body>
    </html>
  • Исполните страницу и должен получиться такой результат
  • CustomerID OrderID Items Total
    ALFKI 10643 3 1086,0000
    ALFKI 10692 1 878,0000
    ALFKI 10702 2 330,0000
    ALFKI 10835 2 851,0000
    ALFKI 10952 2 491,2000
    ALFKI 11011 2 960,0000

    При значении текстового поля ALFKI вычисленный SQL-оператор будет иметь такой вид:

    string sql =
        "SELECT Orders.CustomerID, Orders.OrderID, COUNT(UnitPrice) AS Items, "
      + "SUM(UnitPrice * Quantity) AS Total FROM Orders "
      + "INNER JOIN[Order Details] "
      + "ON Orders.OrderID = [Order Details].OrderID "
      + "WHERE Orders.CustomerID = 'ALFKI' "
      + "GROUP BY Orders.OrderID, Orders.CustomerID";

    Злоумышленник может попытаться подделать оператор SQL. Главной целью такой атаки является получение сообщения об ошибке. Если ошибка не обрабатывается правильно, то злоумышленник получит низкоуровневую информацию, которая может быть использована для разработки более изощренной атаки.

    Предположим, что злоумышленник введет в текстовое поле следующий текст

    ALFKI' OR '1' = '1

    В этом случае строка запроса будет такой

    string sql =
        "SELECT Orders.CustomerID, Orders.OrderID, COUNT(UnitPrice) AS Items, "
      + "SUM(UnitPrice * Quantity) AS Total FROM Orders "
      + "INNER JOIN[Order Details] "
      + "ON Orders.OrderID = [Order Details].OrderID "
      + "WHERE Orders.CustomerID = 'ALFKI' OR '1' = '1' "
      + "GROUP BY Orders.OrderID, Orders.CustomerID";

    Этот оператор вернет все записи о заказах, поскольку условие 1=1 истинно для всех строк

  • Исполните страницу UserSqlGridData.aspx и введите как злоумышленник в текстовое поле строку
    ALFKI' OR '1' = '1
  • Должен получиться результат, малая часть которого приведена ниже

    Начальная часть результата, возвращенного по искаженной строке запроса
    CustomerID OrderID Items Total
    VINET 10248 3 440,0000
    TOMSP 10249 2 1863,4000
    HANAR 10250 3 1813,0000
    VICTE 10251 3 670,8000
    SUPRD 10252 3 3730,0000
    HANAR 10253 3 1444,8000
    CHOPS 10254 3 625,2000
    RICSU 10255 4 2490,5000
    WELLI 10256 2 517,8000
    HILAA 10257 3 1119,9000
    ERNSH 10258 3 2018,6000
    CENTC 10259 2 100,8000

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

    Возможны и более сложные атаки. Например, злонамеренный пользователь может просто закомментировать остаток оператора SQL, добавив два тире (--). Эта атака специфична для SQL Server, но аналогичная атака возможна и в MySQL, если использовать символ #, и в Oracle, если задействовать точку с запятой (;).

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

    Вот что злоумышленник может ввести в текстовое поле, чтобы осуществить более изощренную атаку внедрением SQL, удалив все строки из таблицы Customers:

    ALFKI'; DELETE * FROM Customers --

    Не делайте этого с нашей учебной базой!!!

    Для того, чтобы противостоять атаке внедрением, следует помнить несколько правил:

  • Использовать свойство TextBox.MaxLength, чтобы предотвратить длинный ввод, когда в этом нет необходимости
  • Обрабатывать ошибки самому, чтобы ограничить информацию стандартного исключения Exception.Message
  • Выявлять и удалать из строки ввода специальные символы, например, заменять одиночную кавычку парой одиночных кавычек
    string ID = TextBox1.Text.Replace("'", "''");
  • Использовать параметризованные команды или хранимые процедуры, которые предусматривают собственную защиту от атак внедрением SQL
  • Ограничить права учетной записи, от имени которой выполняется доступ к базе данных. Этот способ может предовратить атаки, связанные с удалением таблиц, но не препятствует похищению информации, поскольку права по извлечению информации ограничить нельзя
  • Применение параметризованных команд

    Параметризованная команда, это обычная команда SQL, использующая спецификаторы, обозначающие место, в которое будут динамически подставлены объекту Command определенные параметры-заполнители SQL через коллекцию Parameters. Параметры-заполнители прописываются отдельно и автоматически кодируются.

    Синтаксис параметризованных команд слегка отличается у разных поставщиков данных. Так, поставщик SQL Server предусматривает именованные заполнители с уникальными именами. У поставщика OLE DB спецификаторы являются вопросами, а порядок следования заполнителей должен соответствовать контексту следования спецификаторов.

    Приведем пример, использующий параметризованную команду, в котором исключена возможность атаки внедрением SQL.

  • Сделайте из страницы UserSqlGridData.aspx копию с именем UserSqlParamCommand.aspx и назначьте ее стартовой
  • Откорректируйте страницу UserSqlParamCommand.aspx, чтобы она имела следующий код
    (рис ) Код страницы UserSqlParamCommand.aspx<%@ Page Language="C#" %>
        
    <script runat="server">
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Извлекли строку соединения из web.config
            string connectString = System.Web.Configuration.WebConfigurationManager.
                ConnectionStrings["Northwind"].ConnectionString;
        
            // Создали объект соединения
            System.Data.SqlClient.SqlConnection con =
                new System.Data.SqlClient.SqlConnection(connectString);
            
            // Создали строку SQL-запроса
            string sql =
                "SELECT Orders.CustomerID, Orders.OrderID, COUNT(UnitPrice) AS Items, "
              + "SUM(UnitPrice * Quantity) AS Total FROM Orders "
              + "INNER JOIN[Order Details] "
              + "ON Orders.OrderID = [Order Details].OrderID "
              + "WHERE Orders.CustomerID = @CustomID "
              + "GROUP BY Orders.OrderID, Orders.CustomerID";
        
            // Создали объект Command
            System.Data.SqlClient.SqlCommand cmd =
                new System.Data.SqlClient.SqlCommand(sql, con);
            
            // Дополнили коллекцию Parameters
            cmd.Parameters.Add("@CustomID", TextBox1.Text);
        
            using (con)
            {
                // Открыли соединение
                con.Open();
        
                // Выполняем команду и получаем результирующий набор данных
                System.Data.SqlClient.SqlDataReader reader =
                    cmd.ExecuteReader();
        
                // Подключаем элемент управления GridView
                // к полученному результирующему набору
                GridView1.DataSource = reader;
        
                // Наполняем элемент управления GridView
                // всеми извлеченными записями DataReader
                GridView1.DataBind();
        
                // Закрываем набор данных
                reader.Close();
            }
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head id="Head1" runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
            <div>
                Введите ID пользователя:<br />
                <asp:TextBox ID="TextBox1" runat="server" Width="248px" Font-Bold="True">ALFKI</asp:TextBox> 
                <asp:Button ID="Button1" runat="server" Text="Получить" /><br />
                <br />
                <asp:GridView ID="GridView1" runat="server">
                </asp:GridView>
            </div>
        </form>
    </body>
    </html>
  • Код страницы остался почти таким же, за исключением двух строк, вводящих команду с именованным параметром @CustomID.

  • Попробуйте исполнить атаку внедрением с содержимым поля ввода ALFKI' OR '1' = '1
  • Мы видим, что страница не возвращает никаких данных, поскольку ни одна запись таблицы Orders в поле CustomerID не имеет значение, равное введенной строке ALFKI' OR '1' = '1. Таким простым способом мы защитили данные от злоумышленного внедрения SQL.

    Вызов хранимых процедур

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

    Хранимые процедуры имеют множество достоинств:

  • Их легко сопровождать. Например, можно изменять команды в хранимой процедуре без перекомпиляции приложения, использующего эту процедуру
  • Они позволяют реализовать более безопасный доступ к базе данных. Например, можно позволить учетной записи Windows, запускающей наш код ASP.NET, использовать определенные хранимые процедуры, но ограничить доступ к лежащим в их основе таблицам
  • Они могут повысить производительность. Поскольку хранимые процедуры упаковывают вместе множество SQL-операторов, можно выполнить огромный объем работы за одно обращение к серверу базы данных, особенно, если база расположена на другом компьютере
  • Рассмотрим пример SQL-кода, необходимого при создании хранимой процедуры для вставки отдельной записи в таблицу Employees. Этой хранимой процедуры изначально нет в БД Northwind. Хранимая процедура должна реализовывать следующий код

    CREATE PROCEDURE InsertEmployee
    	@TitleOfCourtesy	varchar(25),
    	@LastName			varchar(20),
    	@FirstName			varchar(10),
    	@EmployeeID			int OUTPUT
    AS
    INSERT INTO Employees
    	(TitleOfCourtesy, LastName, FirstName, HireDate)
    	VALUES(@TitleOfCourtesy, @LastName, @FirstName, GETDATE());
    SET @EmployeeID = @@IDENTITY

    Эту хранимую процедуру можно добавить на этапе проектирования через панель Server Explorer оболочки, предварительно присоединившись к базе данных Nordhwind. Но я не смог это сделать, поскольку оболочка не видит эту базу из-за неправильно установленного пакета SQL Server 2005 (или потому, что у меня руки кривые). Чтобы как-то вывернуться, в приведенном ниже коде мы добавим ее программно.

  • Создайте новую страницу с именем TestStoredProcedure.aspx и назначьте ее стартовой
  • Поместите на страницу из вкладки Standard элементы управления согласно дескрипторному коду, приведенному ниже (устал писать, да и студент Зиборов ругается, но зато все работает!)
  • Настройте код страницы, чтобы в итоге он был таким
  • <%@ Page Language="C#" %>
        
    <script runat="server">
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Извлекли строку соединения из web.config
            string connectString = System.Web.Configuration.WebConfigurationManager.
                ConnectionStrings["Northwind"].ConnectionString;
        
            // Создали объект соединения
            System.Data.SqlClient.SqlConnection con =
                new System.Data.SqlClient.SqlConnection(connectString);
        
            try
            {
                string sql =
                    "CREATE PROCEDURE InsertEmployee "
                  + "@TitleOfCourtesy	varchar(25),"
                  + "@LastName			varchar(20),"
                  + "@FirstName			varchar(10),"
                  + "@EmployeeID		int OUTPUT "
                  + "AS "
                  + "INSERT INTO Employees "
                  + "(TitleOfCourtesy, LastName, FirstName, HireDate) "
                  + "VALUES(@TitleOfCourtesy, @LastName, @FirstName, GETDATE());"
                  + "SET @EmployeeID = @@IDENTITY";
                System.Data.SqlClient.SqlCommand cmd =
                    new System.Data.SqlClient.SqlCommand(sql, con);
                con.Open();
                cmd.ExecuteNonQuery();
            }
            catch (System.Data.SqlClient.SqlException error)
            {
            }
            finally
            {
                con.Close();
            }
        }
        
        protected void AddEmployee_Click(object sender, EventArgs e)
        {
            // Извлекли строку соединения из web.config
            string connectString = System.Web.Configuration.WebConfigurationManager.
                ConnectionStrings["Northwind"].ConnectionString;
        
            // Создали объект соединения
            System.Data.SqlClient.SqlConnection con =
                new System.Data.SqlClient.SqlConnection(connectString);
        
            // Создать и настроить объект Command 
            // для вызова хранимой процедуры InsertEmployee
            System.Data.SqlClient.SqlCommand cmd =
                new System.Data.SqlClient.SqlCommand("InsertEmployee", con);
            cmd.CommandType = System.Data.CommandType.StoredProcedure;
        
            // Добавить входные параметры хранимой процедуры
            // в коллекцию параметров Command.Parameters
            // Добавляем первый входной параметр
            cmd.Parameters.Add(new System.Data.SqlClient.SqlParameter(
                "@TitleOfCourtesy", System.Data.SqlDbType.NVarChar, 25));
            cmd.Parameters["@TitleOfCourtesy"].Value = title.Text;
            // Добавляем второй входной параметр
            cmd.Parameters.Add(new System.Data.SqlClient.SqlParameter(
                "@LastName", System.Data.SqlDbType.NVarChar, 20));
            cmd.Parameters["@LastName"].Value = lastName.Text;
            // Добавляем третий входной параметр
            cmd.Parameters.Add(new System.Data.SqlClient.SqlParameter(
                "@FirstName", System.Data.SqlDbType.NVarChar, 10));
            cmd.Parameters["@FirstName"].Value = firstName.Text;
        
            // Добавить выходной параметр хранимой процедуры
            // в коллекцию параметров Command.Parameters
            cmd.Parameters.Add(new System.Data.SqlClient.SqlParameter(
                "@EmployeeID", System.Data.SqlDbType.Int, 4));
            cmd.Parameters["@EmployeeID"].Direction = System.Data.ParameterDirection.Output;
        
            using (con)
            {
                // Открыли соединение
                con.Open();
        
                // Выполняем команду
                int recCount = cmd.ExecuteNonQuery();
                lblInfo.Text = string.Format("<b>Вставлено записей:</b> {0}<br />", recCount);
                // Получить вновь сгенерированный идентификатор
                int empID = (int)cmd.Parameters["@EmployeeID"].Value;
                lblInfo.Text += "Новый идентификатор: " + empID.ToString();
            }
            // Соединение закрылось автоматически
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head id="Head1" runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
            Введите титул:<asp:TextBox ID="title" runat="server">студент</asp:TextBox><br />
            Введите имя:<asp:TextBox ID="firstName" runat="server">Иван</asp:TextBox><br />
            Введите фамилию:<asp:TextBox ID="lastName" runat="server">Петров</asp:TextBox><br />
            <asp:Label ID="lblInfo" runat="server" />
            <br />
            <br />
            <asp:Button ID="AddEmployee" runat="server" Text="Добавить служащего" 
                OnClick="AddEmployee_Click" />
            <asp:LinkButton ID="btnResult" runat="server" 
                PostBackUrl="~/TestDataReader.aspx">Показать список</asp:LinkButton>
        </form>
    </body>
    </html>

    Интерфейс страницы будет таким

  • Добавьте служащего щелчком по кнопке, после чего выведите список. Результат будет примерно таким
  • Служащий Ms. Davolio, Nancy - работает с 01.05.1992
  • Служащий Dr. Fuller, Andrew - работает с 14.08.1992
  • Служащий Ms. Leverling, Janet - работает с 01.04.1992
  • Служащий Mrs. Peacock, Margaret - работает с 03.05.1993
  • Служащий Mr. Buchanan, Steven - работает с 17.10.1993
  • Служащий Mr. Suyama, Michael - работает с 17.10.1993
  • Служащий Mr. King, Robert - работает с 02.01.1994
  • Служащий Ms. Callahan, Laura - работает с 05.03.1994
  • Служащий Ms. Dodsworth, Anne - работает с 15.11.1994
  • Служащий студент Петров, Иван - работает с 25.04.2007
  • В обработчике Page_Load() мы намеренно применили пустой блок обработки исключений для того, чтобы подавить сообщение, которое выдается при попытке повторного создания хранимой процедуры с тем же именем.

    Транзакции

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

    Поставщики данных включают поддержку транзакций, которые начинаются при вызове метода Connection.BeginTransaction(). Принимается транзакция методом Transaction.Commit(), а отменяется, если возникло исключение, методом Transaction.Rollback().

    Приведем пример, в котором в таблицу Employees БД Nordhwind добавляется две записи через механизм контроля транзакций. Эти записи можно было бы добавить и обычным способом, но в критических случаях делать это нужно под контролем транзакций.

  • Сделайте из страницы TestStoredProcedure.aspx копию с именем TestTransaction.aspx и определите ее стартовой
  • Заполните страницу следующим кодом
    (рис ) Код страницы TestTransaction.aspx<%@ Page Language="C#" %>
        
    <script runat="server">
        
        // Объявляем переменные-ссылки как члены класса страницы
        string sql1 = "INSERT INTO Employees (LastName, FirstName, HireDate) "
            + "VALUES ('Зиборов', 'Алексей', GETDATE())";
        string sql2 = "INSERT INTO Employees (LastName, FirstName, HireDate) "
            + "VALUES ('Погодаева', 'Татьяна', GETDATE())";
        System.Data.SqlClient.SqlConnection con;
        System.Data.SqlClient.SqlCommand cmd1, cmd2;
        System.Data.SqlClient.SqlTransaction tran = null;
        
        /**** Для отладки ****/
        string sql3 = "DELETE FROM Employees WHERE EmployeeID > 9";
        System.Data.SqlClient.SqlCommand cmd3;
        /*********************/
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Извлечь строку соединения из web.config
            string connectString = System.Web.Configuration.WebConfigurationManager.
                ConnectionStrings["Northwind"].ConnectionString;
        
            // Создать объект соединения
            con = new System.Data.SqlClient.SqlConnection(connectString);
            
            // Создать объекты команд
            cmd1 = new System.Data.SqlClient.SqlCommand(sql1, con);
            cmd2 = new System.Data.SqlClient.SqlCommand(sql2, con);
            
            /**** Для отладки ****/
            cmd3 = new System.Data.SqlClient.SqlCommand(sql3, con);
            /*********************/
        }
        
        protected void AddEmployee_Click(object sender, EventArgs e)
        {
            try // Попытка
            {
                // Открыть соединение
                con.Open();
                
                /**** Для отладки ****/
                cmd3.ExecuteNonQuery(); // Удалить последние записи
                /*********************/
                
                // Начать транзакцию
                tran = con.BeginTransaction();
        
                // Пометить команды включенными в транзакцию
                cmd1.Transaction = tran;
                cmd2.Transaction = tran;
        
                // Выполнить обе команды
                // Должны выполняться только команды транзакции!!!
                cmd1.ExecuteNonQuery();
                cmd2.ExecuteNonQuery();
         
                // Искусственная генерация исключения
                // для тестирования отмены транзакции
                //throw new ApplicationException();
       
                // Подтвердить и закончить транзакцию
                tran.Commit();
                
                // Посчитать количество записей таблицы
                // Теперь можно выполнять другие команды
                cmd1.CommandText = "SELECT COUNT(*) FROM Employees";
                string recCount = cmd1.ExecuteScalar().ToString();
                
                // Выдать результат
                lblInfo.Text = "Добавлены две записи!<br />";
                lblInfo.Text += "Общее число записей " + recCount;
            }
            catch // Откат при любом исключении
            {
                lblInfo.Text = "Транзакция отменена!";
                tran.Rollback();
            }
            finally // Обязательное завершение
            {
                // Закрыть соединение по-любому
                con.Close();
            }
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head id="Head1" runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
            <asp:Label ID="lblInfo" runat="server" />
            <br />
            <br />
            <asp:Button ID="AddEmployee" runat="server" 
                Text="Выполнить транзакцию"
                OnClick="AddEmployee_Click" />
            <asp:LinkButton ID="btnResult" runat="server" 
                PostBackUrl="~/TestDataReader.aspx">
                Показать список</asp:LinkButton>
        </form>
    </body>
    </html>
  • Исполните страницу и получите следующий отклик при выполнении транзакции
  • Просмотрите таблицу Employees БД Nordhwind по гиперссылке, результат выполнения транзакции будет примерно таким
  • Служащий Ms. Davolio, Nancy - работает с 01.05.1992
  • Служащий Dr. Fuller, Andrew - работает с 14.08.1992
  • Служащий Ms. Leverling, Janet - работает с 01.04.1992
  • Служащий Mrs. Peacock, Margaret - работает с 03.05.1993
  • Служащий Mr. Buchanan, Steven - работает с 17.10.1993
  • Служащий Mr. Suyama, Michael - работает с 17.10.1993
  • Служащий Mr. King, Robert - работает с 02.01.1994
  • Служащий Ms. Callahan, Laura - работает с 05.03.1994
  • Служащий Ms. Dodsworth, Anne - работает с 15.11.1994
  • Служащий Зиборов, Алексей - работает с 25.04.2007
  • Служащий Погодаева, Татьяна - работает с 25.04.2007
  • Обратите внимание, что пока транзакция находится в процессе работы, должны выполняться только команды транзакции. Область работы транзакции определяется операторными скобками con.BeginTransaction(); ... tran.Commit() ;

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

    throw new ApplicationException();

    Точки сохранения для отката транзакции

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

    Немного изменим предыдущий пример, введя возможность частичного отката.

  • Скопируйте страницу TestTransaction.aspx в TestTransactionSave.aspx и назначьте ее стартовой
  • Измените код новой страницы, чтобы он был таким
    (рис ) Код страницы TestTransactionSave.aspx частичного отката транзакции<%@ Page Language="C#" %>
        
    <script runat="server">
        
        // Объявляем переменные-ссылки как члены класса страницы
        string sql1 = "INSERT INTO Employees (LastName, FirstName, HireDate) "
            + "VALUES ('Зиборов', 'Алексей', GETDATE())";
        string sql2 = "INSERT INTO Employees (LastName, FirstName, HireDate) "
            + "VALUES ('Погодаева', 'Татьяна', GETDATE())";
        System.Data.SqlClient.SqlConnection con;
        System.Data.SqlClient.SqlCommand cmd1, cmd2;
        System.Data.SqlClient.SqlTransaction tran = null;
        
        /**** Для отладки ****/
        string sql3 = "DELETE FROM Employees WHERE EmployeeID > 9";
        System.Data.SqlClient.SqlCommand cmd3;
        /*********************/
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Извлечь строку соединения из web.config
            string connectString = System.Web.Configuration.WebConfigurationManager.
                ConnectionStrings["Northwind"].ConnectionString;
        
            // Создать объект соединения
            con = new System.Data.SqlClient.SqlConnection(connectString);
            
            // Создать объекты команд
            cmd1 = new System.Data.SqlClient.SqlCommand(sql1, con);
            cmd2 = new System.Data.SqlClient.SqlCommand(sql2, con);
            
            /**** Для отладки ****/
            cmd3 = new System.Data.SqlClient.SqlCommand(sql3, con);
            /*********************/
        }
        
        protected void AddEmployee_Click(object sender, EventArgs e)
        {
            try // Попытка
            {
                // Открыть соединение
                con.Open();
                
                /**** Для отладки ****/
                cmd3.ExecuteNonQuery(); // Удалить последние записи
                /*********************/
                
                // Начать транзакцию
                tran = con.BeginTransaction();
        
                // Пометить команды включенными в транзакцию
                cmd1.Transaction = tran;
                cmd2.Transaction = tran;
        
                // Выполнить первую команду транзакции
                cmd1.ExecuteNonQuery();          
                // Пометить точку отката
                tran.Save("Ziborov");
        
                // Выполнить вторую команду транзакции
                cmd2.ExecuteNonQuery();   
                // Выбросить исключение
                throw new ApplicationException();
            }
            catch // Откат
            {
                // Откатить частично
                tran.Rollback("Ziborov");
                
                // Подтвердить и закончить транзакцию
                tran.Commit();
                
                lblInfo.Text = "Транзакция частично отменена!";
        
                // Посчитать количество записей таблицы
                cmd1.CommandText = "SELECT COUNT(*) FROM Employees";
                string recCount = cmd1.ExecuteScalar().ToString();
        
                // Выдать результат
                lblInfo.Text = "Добавлена одна запись!<br />";
                lblInfo.Text += "Общее число записей " + recCount;
            }
            finally // Обязательное завершение
            {
                // Закрыть соединение по-любому
                con.Close();
            }
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml">
    <head id="Head1" runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
            <asp:Label ID="lblInfo" runat="server" />
            <br />
            <br />
            <asp:Button ID="AddEmployee" runat="server" 
                Text="Выполнить транзакцию"
                OnClick="AddEmployee_Click" />
            <asp:LinkButton ID="btnResult" runat="server" 
                PostBackUrl="~/TestDataReader.aspx">
                Показать список</asp:LinkButton>
        </form>
    </body>
    </html>
  • Выполните страницу и убедитесь, что откат выполняется только до точки сохранения, а принимается только результат выполнения предыдущей команды (или блока команд) транзакции
  • Фабрики поставщиков

    Классы поставщиков оптимизированы под конкретный тип источника данных. Производители, разрабатывающие новую структуру источника данных, должны заботиться и о разработке соответствующего типа поставщика, чтобы этим источником данных могли пользоваться программисты (мы с вами) в своих приложениях. Но традиционный переход на новый тип поставщика требует существенной переделки кода приложения, что не всегда удобно для программистов. Поэтому в ASP.NET 2.0 предусмотрены специальные классы, которые позволяют "подкрутить" настройки только в одном месте, общедоступном для всех страниц приложения или группы страниц без необходимости изменения кода в каждой странице, использующей поставщик. Такой механизм называется фабриками поставщиков.

    Основная идея модели фабрик заключается в том, что можно использовать единственный объект-фабрику для создания всех прочих необходимых для конкретного поставщика объектов. Но это можно выполнить и созданием конкретных объектов поставщика. Самое главное удобство заключается в том, что конкретную фабрику поставщика можно создавать из обобщенного класса фабрик System.Data.Common.DbProviderFactories программным способом, используя информацию, заложенную в файлы web.config (в масштабах конкретного приложения) или machine.config (в масштабах конкретного компьютера-сервера)

    <?xml version="1.0"?>
    <configuration>
      <connectionStrings>
        <add name="Northwind" connectionString="Data Source=localhost; 
          Initial Catalog=Northwind; user id=sa; password=;"/>
       </connectionStrings>
       <system.data>
         <DbProviderFactories>
            <add name="SqlClient Data Provider" invariant="System.Data.SqlClient" 
        type="System.Data.SqlClient.SqlClientFactory" />
            <add name="Odbc Data Provider" invariant="System.Data.Odbc" 
        type="System.Data.Odbc.OdbcFactory" /> 
            <add name="OleDb Data Provider" invariant="System.Data.OleDb" 
        type="System.Data.OleDb.OleDbFactory" /> 
             <add name="OracleClient Data Provider" invariant="System.Data.OracleClient" 
        type="System.Data.OracleClient.OracleClientFactory" /> 
        </DbProviderFactories>
      </system.data>
      <system.web>
        <compilation debug="true"/>
      </system.web>
    </configuration>

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

    Обобщенный класс System.Data.Common.DbProviderFactories имеет статический метод GetFactory(string), который возвращает объект конкретной фабрики, указанной в строке аргумента. Например, для случая явного указания в коде конкретной фабрики это будет выглядеть так

    string factory = "System.Data.SqlClient";
    System.Data.Common.DbProviderFactory provider = 
        System.Data.Common.DbProviderFactories.GetFactory(factory);

    Получив конкретную фабрику из строки аргумента или конфигурационного файла далее с конкретным поставщиком можно работать через объект provider одноименной фабрики, устанавливая соединение с БД и посылая в нее SQL-запросы. Для создания объектов Connection и Command используются методы объекта (например, provider ) фабрики CreateXXX(), которые возвращают эти объекты

    Для создания независимого от поставщика универсального кода мы должны исходить из того, что не знаем заранее тип поставщика. Поэтому взаимодействовать с источником данных нужно только через методы объекта фабрики, возвращающие специфичные объекты Connection, Command, Parameter, DataAdapter базовых классов DbConnection, DbCommand, DbParameter, DbDataAdapter.

    Пример независимого от поставщика кода с использованием фабрик

    Чтобы лучше понять, как все вышесказанное работает, рассмотрим пример SQL-запроса, реализованного на странице TestDataReader.aspx. Первый шаг - настроить в файле web.config строку соединения, имя поставщика и текст SQL-запроса.

  • Откорректируйте файл web.config, чтобы он окончательно выглядел следующим образом
    (рис ) Дополнение файла web.config для применения обобщенного кода<?xml version="1.0"?>
    <configuration>
    	<connectionStrings>
    		<add name="Northwind" connectionString="Data Source=localhost; 
       Initial Catalog=Northwind; user id=sa; password=;"/>
    	</connectionStrings>
    	<appSettings>
    		<add key="factory" value="System.Data.SqlClient" />
    		<add key="employeeQuery" value="SELECT * FROM Employees" />
    	</appSettings>
    	<system.web>
    		<compilation debug="true"/>
    	</system.web>
    </configuration>
  • Создайте из копии TestDataReader.aspx новую страницу с именем TestFactory.aspx, назначьте ее стартовой и заполните следующим кодом
    (рис ) Страница TestFactory.aspx для реализации независимого от поставщика кода<%@ Page Language="C#" %>
        
    <script runat="server">
        
        protected void Page_Load(object sender, EventArgs e)
        {
            // Получить по ключу фабрику из файла web.config
            string factory = System.Web.Configuration.
                WebConfigurationManager.AppSettings["factory"];
            System.Data.Common.DbProviderFactory provider =
                System.Data.Common.DbProviderFactories.GetFactory(factory);
            
            // Использовать фабрику для получения объекта соединения
            // и извлечь строку соединения из файла web.config
            System.Data.Common.DbConnection con =
                provider.CreateConnection();
            con.ConnectionString = System.Web.Configuration.
                WebConfigurationManager.ConnectionStrings["Northwind"].ConnectionString;
        
            // Создать базовый объект DbCommand. Для его настройки 
            // и извлечь по ключу команду из файла web.config
            System.Data.Common.DbCommand cmd = provider.CreateCommand();
            cmd.Connection = con;
            cmd.CommandType = System.Data.CommandType.Text;
            cmd.CommandText = System.Web.Configuration.
                WebConfigurationManager.AppSettings["employeeQuery"];
        
            // Создаем ссылку на вспомогательную строку
            System.Text.StringBuilder htmlStr = new StringBuilder("");
            
            using (con)
            {
                // Открыли соединение
                con.Open();
        
                // Выполняем обобщенную команду и получаем результирующий набор данных
                System.Data.Common.DbDataReader reader =
                    cmd.ExecuteReader();
                
                // Перебираем все записи результирующего набора
                // и строим HTML-строку
                while (reader.Read())
                {
                    htmlStr.Append("<li>Служащий ");
                    htmlStr.Append(reader["TitleOfCourtesy"]);
                    htmlStr.Append(" <b>");
                    htmlStr.Append(reader.GetString(1));
                    htmlStr.Append("</b>, ");
                    htmlStr.Append(reader.GetString(2));
                    htmlStr.Append(" - работает с ");
                    htmlStr.Append(reader.GetDateTime(6).ToString("d"));
                    htmlStr.Append("</li>");
                }
                
                // Закрываем набор данных
                reader.Close();
            }
        
            // Выдаем результаты запроса
            lblInfo.Text = htmlStr.ToString();
        }
    </script>
        
    <html xmlns="http://www.w3.org/1999/xhtml" >
    <head runat="server">
        <title>Untitled Page</title>
    </head>
    <body>
        <form id="form1" runat="server">
        <div>
            <asp:Label ID="lblInfo" runat="server" Text="Label"></asp:Label></div>
        </form>
    </body>
    </html>
  • Выполните страницу TestFactory.aspx. Должен получиться такой же результат, что и на странице TestDataReader.aspx, а именно
  • Служащий Ms. Davolio, Nancy - работает с 01.05.1992
  • Служащий Dr. Fuller, Andrew - работает с 14.08.1992
  • Служащий Ms. Leverling, Janet - работает с 01.04.1992
  • Служащий Mrs. Peacock, Margaret - работает с 03.05.1993
  • Служащий Mr. Buchanan, Steven - работает с 17.10.1993
  • Служащий Mr. Suyama, Michael - работает с 17.10.1993
  • Служащий Mr. King, Robert - работает с 02.01.1994
  • Служащий Ms. Callahan, Laura - работает с 05.03.1994
  • Служащий Ms. Dodsworth, Anne - работает с 15.11.1994
  • Служащий Зиборов, Алексей - работает с 07.05.2007
  • Чтобы убедиться, что код получился действительно универсальным и настройка на конкретного поставщика осуществляется только в файле web.config, поменяем поставщика в этом файле без изменения кода самой страницы.

  • Откорректируйте файл web.config, чтобы он окончательно выглядел следующим образом
    (рис ) Изменение поставщика в файле web.config для проверки обобщенного кода<?xml version="1.0"?>
    <configuration>
    	<connectionStrings>
    		<add name="Northwind" connectionString="Provider=SQLOLEDB; Data Source=localhost; 
      Initial Catalog=Northwind; user id=sa; password=;"/>
    	</connectionStrings>
    	<appSettings>
    		<add key="factory" value="System.Data.OleDb" />
    		<add key="employeeQuery" value="SELECT * FROM Employees" />
    	</appSettings>
    	<system.web>
    		<compilation debug="true"/>
    	</system.web>
    </configuration>
  • Исполните страницу TestFactory.aspx для нового поставщика, указанного в файле web.config, и убедитесь, что прежний код страницы генерирует тот же самый результат, но уже с новым поставщиком
  • Механизм применения фабрик поставщиков не решает всех проблем, связанных с разработкой независимого от поставщика кода. Например, не существует обобщенных объектов исключений базы данных, а только специфичные для конкретных поставщиков. Поставщики могут различаться некоторыми специфическими параметрами или поддерживать специальные средства, недоступные для общих базовых классов. Тем не менее, продемонстрированных возможностей в большинстве практических случаев может быть достаточно для написания независимого кода страниц.

    Вернуться к учебному плану