Опубликован: 04.05.2010 | Уровень: для всех | Доступ: платный
Практическая работа 3:

Обеспечение взаимодействия Интернет-магазина с базой данных

< Лекция 8 || Практическая работа 3: 123 || Лекция 9 >
Аннотация: Данное практическое занятие освещает вопросы обеспечения взаимодействия Интернет-магазина с базой данных на примере SQL Server 2008.

Цель практического занятия: На предыдущем занятии Интернет-магазин был переведен на технологию ASP.NET, однако для наполнения контента сайта использовались заранее "вбитые" в код данные. Целью данного занятия является рассмотрение возможности подключения Веб-приложения на технологии ASP.NET к базе данных, а также вопросы манипуляции этими данными.

Файлы к практическому занятию Вы можете скачать здесь.

11.1. Динамическая генерация меню

Как уже отмечалось на прошлом занятии, пункты меню могут генерироваться динамически. Это удобно, например, для того, чтобы предоставить пользователям сайта возможность просматривать продукты, сгруппированные по категориям. Для этого, добавим в раздел меню "Продукты" подразделы, соответствующие записям в таблице ProductCategory, а в них добавим подпункты, соответствующие записям в таблице ProductSubcategory. Для того чтобы это сделать, добавим в проект в папку App_Code файл DataSetMenu.xsd. Это можно сделать, выбрав пункт контекстного меню Add New Item, а в открывшемся диалоговом окне элемент DataSet (рис. 11.1).

Окно добавления нового типизированного DataSet в проект

увеличить изображение
Рис. 11.1. Окно добавления нового типизированного DataSet в проект

В результате в проект будет добавлен новый типизированный источник данных DataSetMenu. Чтобы добавить в него таблицы, необходимо открыть в Microsoft Visual Studio окно Server Explorer и установить соединение с базой данных AdventureWorks (рис. 11.2).

Окно Server Explorer с открытым списком таблиц базы данных AdventureWorks

Рис. 11.2. Окно Server Explorer с открытым списком таблиц базы данных AdventureWorks

После того, как соединение будет установлено, необходимо выбрать таблицы ProductCategory и ProductSubcategory и перетащить их мышкой в окно дизайнера источника данных (рис. 11.3).

Дизайнер источника данных DataSetMenu с добавленными таблицами ProductCategory и ProductSubcategory

увеличить изображение
Рис. 11.3. Дизайнер источника данных DataSetMenu с добавленными таблицами ProductCategory и ProductSubcategory

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

  • ProductCategory и ProductSubcategory: эти два класса наследуют от DataTable и представляют собой отсоединенные контейнеры данных для записей из таблиц БД. При этом каждая запись в этой таблице наследует от класса DataRow, предоставляя доступ к значениям в ячейках не только через итератор, например ProductCategoryRow["Name"], но и свойства, например ProductCategoryRow.Name.
  • ProductCategoryTableAdapter и ProductSubcategorTableAdapter: эти классы инкапсулируют класс SqlTableAdapter, позволяя получать данные с сервера и передавать изменения, сделанные на клиенте обратно в БД.

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

Перейдем в файл, содержащий серверный код мастера страниц ( Master.master.cs ), и доопределим метод Page_Load.

Рассмотрим три разных способа расширения меню разрабатываемого Интернет-магазина. В первом варианте воспользуемся только что созданным типизированным DataSetMenu:

#region Первый вариант (типизированный DataSet)

DataSetMenu dsm = new DataSetMenu();

ProductCategoryTableAdapter pcta = new ProductCategoryTableAdapter();
pcta.Fill(dsm.ProductCategory);

ProductSubcategoryTableAdapter psta = new ProductSubcategoryTableAdapter();
psta.Fill(dsm.ProductSubcategory);

for (int i = 0; i < dsm.ProductCategory.Count; i++)
{
    var nm = new MenuItem(dsm.ProductCategory[i].Name) 
    { NavigateUrl = "~/products/default.aspx?category=" + dsm.ProductCategory[i].ProductCategoryID };
    MainMenu.Items[1].ChildItems.Add(nm);
    var productSubCategoryRows = dsm.ProductCategory[i].GetProductSubcategoryRows();
    for (int j = 0; j < productSubCategoryRows.Count(); j++)
    {
        MainMenu.Items[1].ChildItems[i].ChildItems.Add(new MenuItem(productSubCategoryRows[j].Name) 
        { NavigateUrl = "~/products/default.aspx?category=" + dsm.ProductCategory[i].ProductCategoryID + 
                        "&subCategory=" + productSubCategoryRows[j].ProductSubcategoryID });
    }
}

#endregion

Разберем приведенный код. На первом шаге мы создаем экземпляр объекта DataSetMenu. Следующие две строчки создают объект ProductCategoryTableAdapter и вызывают его метод Fill, тем самым скачивая целиком таблицу категорий и помещая записи в одну из таблиц объекта dsm. Далее аналогичные действия выполняются для извлечения данных из таблицы подкатегорий. После этого мы пробегаемся циклом for по таблице dsm.ProductCategory и для каждой записи создаем новый объект MenuItem, свойство текст которого означивается именем текущей категории, а свойство NavigateUrl определяется конкатенацией строк "~/products/default.aspx?category=" и идентификатором текущей категории. Затем созданный пункт меню динамически добавляется в коллекцию подразделов второго пункта меню (пункт "Продукты"). Дальше, при помощи вызова метода GetProductSubcategoryRows() для текущей категории извлекаются все связанные подкатегории. После, в цикле создаются новые разделы меню, которые добавляются в коллекцию дочерних элементов пункта меню, соотвествующего текущей категории.

В качестве альтернативы рассмотрим, как аналогичное меню может быть сделано без использования типизированных источников данных:

#region Второй вариант (не типизированный DataSet)

DataSet ds = new DataSet();
SqlDataAdapter sda =
    new SqlDataAdapter(new SqlCommand("usp_ProductCategoryList",
                                      new SqlConnection(
                                          "Data Source=localhost;Initial Catalog=AdventureWorks;Integrated Security=True"))
                           {CommandType = CommandType.StoredProcedure});
sda.Fill(ds);

DataRelation dr = new DataRelation("ProductCategoryProductSubCategory",
                                   ds.Tables[0].Columns["ProductCategoryID"],
                                   ds.Tables[1].Columns["ProductCategoryID"]);
ds.Relations.Add(dr);

for (int i = 0; i < ds.Tables[0].Rows.Count; i++)
{
    var nm = new MenuItem(ds.Tables[0].Rows[i]["Name"].ToString())
                 {
                     NavigateUrl =
                         "~/products/default.aspx?category=" + ds.Tables[0].Rows[i]["ProductCategoryID"]
                 };
    MainMenu.Items[1].ChildItems.Add(nm);
    var productSubCategoryRows = ds.Tables[0].Rows[i].GetChildRows(dr);
    for (int j = 0; j < productSubCategoryRows.Length; j++)
    {
        MainMenu.Items[1].ChildItems[i].ChildItems.Add(
            new MenuItem(productSubCategoryRows[j]["Name"].ToString())
                {
                    NavigateUrl =
                        "~/products/default.aspx?category=" + ds.Tables[0].Rows[i]["ProductCategoryID"] +
                        "&subCategory=" + productSubCategoryRows[j]["ProductSubcategoryID"]
                });
    }
}

#endregion

Видно, что этот код идентичен тому, который описан в первом примере. Вместо DataSetMenu используется обычный DataSet , а вместо ProductCategoryTableAdapter и SubproductCategoryTableAdapter используется обычный SqlDataAdapter. В этом примере для извлечения категорий и подкатегорий используется хранимая процедура usp_ProductCategoryList, которая возвращет сразу обе таблицы, но при необходимости можно было бы указать нужный SQL-код для извлечения данных в SqlDataAdapter. Также стоит отметить, что в коллекцию отношений объекта ds добавляется отношение, связывающее обе таблицы, иначе метод ds.Tables[0].Rows[i].GetChildRows(dr) не сможет извлечь подкатегории текущей категории.

Оба приведенных варианта, впрочем, недостаточно хороши. Дело в том, что данный код выполняется относительно долго, так как содержит запрос к базе данных и в то же время будет вызываться каждый раз, когда пользователь будет запрашивать страницу. Чтобы этого избежать, слегка модифицируем код первого вырианта, использовав шаблон проектирования "Одиночка" (Singleton). Для этого в начале класса Master добавим следующий код:

private static object _forLock = new object();

    private static DataSetMenu _menuItems;
    public static DataSetMenu MenuItems
    {
        get
        {
            if (_menuItems == null)
            {
                lock (_forLock)
                {
                    if (_menuItems == null)
                    {
                        _menuItems = new DataSetMenu();

                        ProductCategoryTableAdapter pcta = new ProductCategoryTableAdapter();
                        pcta.Fill(_menuItems.ProductCategory);

                        ProductSubcategoryTableAdapter psta = new ProductSubcategoryTableAdapter();
                        psta.Fill(_menuItems.ProductSubcategory);
                    }
                }
            }
            return _menuItems;
        }
    }

Этот код, реализует весь шаблон проектирования "Одиночка". Создается статическая переменная _menuItems типа DataSetMenu, которая будет заполнена данными лишь один раз, при первом обращении к любой странице нашего сайта. Причем код lock() обеспечит то, что данный объект не будет случайно перезаписан несколько раз, если сразу несколько пользователей одновременно запросят страницу. Так как переменная _menuItems является статической, то она будет сохранять свое значение до тех пор, пока приложение не будет остановлено.

Примечание: Если есть вероятность, что разделы меню могут быть изменены, то необходимо предусмотреть возможность изменения содержимого объекта _menuItems, иначе пользователи сайта не увидят нового меню. Более того, здесь могут возникнуть проблемы, связанные с тем, что пользователь при первом обращении к странице получит одну структуру меню, а при выборе какого-то пункта, может оказаться что он удален, или указывает на другую страницу. Проблемы правильной организации кэширования данных выходят за рамки данного курса.
Примечание: Стоит обратить внимание на то, что проверка _menuItems на null выполняется несколько раз. Это сделано для того, чтобы еще сэкономить время выполнения кода – операция lock хоть и выполняется достаточно быстро (порядка 40 наносекунд на современных компьютерах) тем не менее требует определенного времени на выполнение. Конечно, в случае с Интренет-магазином это не актуально, но если бы наш сайт занимался обработкой видео-потоков в реальном времени, например поступающих с камер наблюдения, принебрегать этим временем было бы неправильно. Внутри команды lock делается еще одна проверка на null – это сделано для того, чтобы сразу два потока, случайно попавшие во внешний if в момент, когда _menuItems еще не инициализирован, не помешали друг другу.

Теперь остается повторить участок кода, создающего разделы меню из первого примера:

#region Третий вариант (быстрый)
DataSetMenu dsm = MenuItems;
for (int i = 0; i < dsm.ProductCategory.Count; i++)
{
    var nm = new MenuItem(dsm.ProductCategory[i].Name) 
             { NavigateUrl = "~/products/default.aspx?category=" + 
                             dsm.ProductCategory[i].ProductCategoryID };
    MainMenu.Items[1].ChildItems.Add(nm);
    var productSubCategoryRows = dsm.ProductCategory[i].GetProductSubcategoryRows();
    for (int j = 0; j < productSubCategoryRows.Count(); j++)
    {
        MainMenu.Items[1].ChildItems[i].ChildItems.Add(new MenuItem(productSubCategoryRows[j].Name) 
            { NavigateUrl = "~/products/default.aspx?category=" + dsm.ProductCategory[i].ProductCategoryID + 
                            "&subCategory=" + productSubCategoryRows[j].ProductSubcategoryID });
    }
}

#endregion

Какой бы вариант не был бы выбран, в результате меню будет иметь вид, представленный на рис. 11.4.

Вид меню, после добавления категорий и подкатегорий

Рис. 11.4. Вид меню, после добавления категорий и подкатегорий
< Лекция 8 || Практическая работа 3: 123 || Лекция 9 >
Зарина Каримова
Зарина Каримова
Казахстан, Алматы, Гимназия им. Ахмета Байтурсынова №139, 2008
Akiyev Begench
Akiyev Begench
Беларусь, Полоцк, полоцкий государственный университет