Эффективная работа с большими объемами данных (VB)

Скотт Митчелл

Скачивание PDF

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

Введение

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

  • По умолчанию можно реализовать разбиение по умолчанию, просто проверив параметр "Включить разбиение по страницам" в смарт-теге веб-элемента управления данными; однако при просмотре страницы данных объект ObjectDataSource извлекает все записи, даже если на странице отображаются только подмножество из них.
  • Настраиваемое разбиение на страницы повышает производительность разбиения по умолчанию, извлекая только те записи из базы данных, которые должны отображаться для конкретной страницы данных, запрошенных пользователем; однако настраиваемое разбиение по страницам включает в себя немного больше усилий по реализации, чем по умолчанию

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

Задача пользовательского разбиения на страницы заключается в том, чтобы написать запрос, который возвращает точный набор записей, необходимых для определенной страницы данных. К счастью, Microsoft SQL Server 2005 предоставляет новое ключевое слово для ранжирования результатов, что позволяет нам создавать запрос, который может эффективно извлекать соответствующие подмножества записей. В этом руководстве мы посмотрим, как использовать это новое ключевое слово SQL Server 2005 для реализации пользовательского разбиения по страницам в элементе управления GridView. Хотя пользовательский интерфейс настраиваемого разбиения на страницы идентичен интерфейсу разбиения страниц по умолчанию, шаг от одной страницы к следующей, используя настраиваемое разбиение на страницы, может быть в несколько раз быстрее, чем разбиение по умолчанию.

Примечание.

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

Шаг 1. Общие сведения о пользовательском процессе разбиения по страницам

При разбиении по страницам точные записи, отображаемые на странице, зависят от страницы запрашиваемых данных и количества записей, отображаемых на каждой странице. Например, представьте, что мы хотели просмотреть 81 продукт, отображая по 10 продуктов на каждой странице. При просмотре первой страницы мы хотим, чтобы продукты от 1 до 10; при просмотре второй страницы мы будем заинтересованы в продуктах от 11 до 20 и т. д.

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

  • Индекс начальной строки, это индекс первой строки на странице данных для отображения; этот индекс можно вычислить, умножив индекс страницы на число записей, отображаемых на странице, и прибавив один. Например, при просмотре записей по 10 за раз, на первой странице (индекс страницы 0) начальный индекс строки равен 0 * 10 + 1, или 1; для второй страницы (индекс страницы 1) начальный индекс строки равен 1 * 10 + 1, или 11.
  • Максимальное число строк — максимальное количество записей, отображаемых на каждой странице. Эта переменная называется максимальной строкой, так как для последней страницы может быть меньше записей, возвращаемых, чем размер страницы. Например, при разбивке 81 продукта на страницы по 10 записей на каждой, девятая, последняя страница будет содержать только одну запись. Однако ни одна страница не будет отображать больше записей, чем указано в значении «Максимальные строки».
  • Общее количество записей, страницируемых. Хотя эта переменная не требуется для определения записей, извлекаемой для данной страницы, она диктует интерфейс разбиения на страницы. Например, если есть 81 продукт, то интерфейс пагинации настроен отобразить девять номеров страниц.

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

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

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

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

Шаг 2: Возврат общего количества записей, которые просматриваются

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

SELECT COUNT(*)
FROM Products

Давайте добавим метод в наш DAL, который возвращает эти сведения. В частности, мы создадим метод DAL TotalNumberOfProducts(), который выполняет указанную выше инструкцию SELECT.

Начните с открытия Northwind.xsd файла Typed DataSet в папке App_Code/DAL . Затем щелкните правой кнопкой мыши на элементе ProductsTableAdapter в конструкторе и выберите команду "Добавить запрос". Как мы видели в предыдущих руководствах, это позволит нам добавить новый метод в DAL, который при вызове будет выполнять определенную инструкцию SQL или хранимую процедуру. Как и в наших методах TableAdapter в предыдущих руководствах, в этом случае стоит использовать AD-HOC SQL-запрос.

Использование инструкции AD-Hoc SQL

Рис. 1. Использование инструкции AD-Hoc SQL

На следующем экране можно указать тип создаваемого запроса. Так как этот запрос вернет одно скалярное значение — общее количество записей в таблице Products, выберите параметр SELECT, который возвращает единичное значение.

Настройка запроса для использования инструкции SELECT, возвращающей одно значение

Рис. 2. Настройка запроса для использования инструкции SELECT, возвращающей одно значение

После указания типа используемого запроса необходимо далее указать запрос.

Использование запроса SELECT COUNT(*) FROM Products

Рис. 3. Использование запроса SELECT COUNT(*) FROM Products

Наконец, укажите имя метода. Как уже упоминалось, давайте будем использовать TotalNumberOfProducts.

Имя метода DAL TotalNumberOfProducts

Рис. 4. Имя метода DAL TotalNumberOfProducts

После нажатия кнопки «Готово» мастер добавит метод TotalNumberOfProducts в DAL. Методы в DAL, возвращающие скалярные значения, возвращают nullable-тип в случае, если результат SQL-запроса является NULL. Однако наш COUNT запрос всегда возвращает значение, отличное от NULL; тем не менее, метод DAL возвращает целое число, допускающее NULL.

Помимо метода DAL, нам также нужен метод в BLL. Откройте файл класса ProductsBLL и добавьте метод TotalNumberOfProducts, который просто вызывает метод TotalNumberOfProducts DAL.

Public Function TotalNumberOfProducts() As Integer
    Return Adapter.TotalNumberOfProducts().GetValueOrDefault()
End Function

Метод DAL TotalNumberOfProducts возвращает целое число с допускающим значением NULL. Однако мы создали метод класса ProductsBLLTotalNumberOfProducts, чтобы он возвращал стандартное целое число. Поэтому необходимо, ProductsBLL чтобы метод класса TotalNumberOfProducts возвращал часть значения целого числа, допускающего значение NULL, возвращаемого методом DAL TotalNumberOfProducts . Вызов GetValueOrDefault() возвращает значение целого числа с поддержкой NULL, если оно существует; если же это значение равно null, возвращается значение целого числа по умолчанию, 0.

Шаг 3. Возврат точного подмножества записей

Наша следующая задача — создать методы в DAL и BLL, которые принимают переменные индекс начальной строки и максимальное количество строк, как обсуждалось ранее, и возвращают соответствующие записи. Прежде чем это сделать, давайте сначала рассмотрим необходимый скрипт SQL. Перед нами стоит задача эффективно назначить индекс каждой строке в результатах постраничной обработки, чтобы мы могли возвращать только те записи, начиная с начального индекса строк (и до максимального количества записей).

Это не проблема, если в таблице базы данных уже есть столбец, который служит индексом строк. На первый взгляд мы можем подумать, что поле ProductsProductID будет достаточно, так как у первого продукта ProductID равно 1, у второго - 2 и так далее. Однако удаление продукта оставляет пробел в последовательности, что делает этот подход неэффективным.

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

  • Использование ключевого слова ROW_NUMBER() в SQL Server 2005, которое является новым для SQL Server 2005, ROW_NUMBER() ключевое слово связывает рейтинг с каждой возвращаемой записью на основе определенного порядка. Этот рейтинг можно использовать в качестве индекса строк для каждой строки.

  • Использование табличной переменной и SET ROWCOUNT Инструкцию SQL Server SET ROWCOUNT можно использовать для указания количества общих записей, которые должны обрабатываться перед завершением запроса; переменные таблицы — это локальные переменные T-SQL, которые могут содержать табличные данные, а также временные таблицы. Этот подход работает одинаково хорошо с Microsoft SQL Server 2005 и SQL Server 2000 (в то время как ROW_NUMBER() подход работает только с SQL Server 2005).

    Здесь необходимо создать переменную таблицы, которая содержит IDENTITY столбец и столбцы для первичных ключей таблицы, данные которых выстраиваются. Затем содержимое таблицы, данные которой просматриваются по страницам, перемещаются в переменную таблицы, и тем самым задавая последовательный индекс строк (через столбец IDENTITY) для каждой записи в таблице. После заполнения SELECT переменной таблицы можно выполнить инструкцию в переменной таблицы, присоединенной к базовой таблице, чтобы извлечь определенные записи. Инструкция SET ROWCOUNT используется для интеллектуального ограничения количества записей, которые необходимо дампать в переменную таблицы.

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

В этом руководстве реализовано настраиваемое разбиение по страницам с помощью ключевого слова ROW_NUMBER(). Дополнительные сведения об использовании табличной переменной и методики SET ROWCOUNT см. в статье «Более эффективный метод постраничного просмотра больших наборов данных».

Ключевое ROW_NUMBER() слово, которое связывает рейтинг с каждой записью, возвращаемой в соответствии с определённым порядком, с помощью следующего синтаксиса:

SELECT columnList,
       ROW_NUMBER() OVER(orderByClause)
FROM TableName

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

SELECT ProductName, UnitPrice,
       ROW_NUMBER() OVER(ORDER BY UnitPrice DESC) AS PriceRank
FROM Products

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

Ранг цены включен для каждой возвращаемой записи.

Рис. 5. Ценовой ранг включён для каждой возвращаемой записи.

Примечание.

ROW_NUMBER() — это только один из многих новых функций ранжирования, доступных в SQL Server 2005. Для более подробного обсуждения ROW_NUMBER(), а также других функций ранжирования, см. в документации ROW_NUMBER.

При ранжировании результатов по указанному ORDER BY столбцу в OVER предложении (UnitPriceв приведенном выше примере) SQL Server должен сортировать результаты. Это быстрая операция при наличии кластеризованного индекса по столбцам, по которым результаты упорядочены или если имеется индекс покрытия, но может оказаться более дорогостоящим в противном случае. Чтобы повысить производительность достаточно больших запросов, рекомендуется добавить некластикционный индекс для столбца, по которому результаты упорядочивается. Дополнительные сведения о рекомендациях по производительности см. в статье "Ранжирование функций и производительности" в SQL Server 2005 .

Данные ранжирования, которые возвращаются ROW_NUMBER(), нельзя напрямую использовать в предложении WHERE. Однако производная таблица может использоваться для возврата ROW_NUMBER() результата, который затем может появиться в предложении WHERE . Например, следующий запрос использует производную таблицу для возврата столбцов ProductName и UnitPrice, а также результата ROW_NUMBER(), затем использует условие WHERE для возврата только тех продуктов, ценовой рейтинг которых находится в диапазоне от 11 до 20.

SELECT PriceRank, ProductName, UnitPrice
FROM
   (SELECT ProductName, UnitPrice,
       ROW_NUMBER() OVER(ORDER BY UnitPrice DESC) AS PriceRank
    FROM Products
   ) AS ProductsWithRowNumber
WHERE PriceRank BETWEEN 11 AND 20

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

SELECT PriceRank, ProductName, UnitPrice
FROM
   (SELECT ProductName, UnitPrice,
       ROW_NUMBER() OVER(ORDER BY UnitPrice DESC) AS PriceRank
    FROM Products
   ) AS ProductsWithRowNumber
WHERE PriceRank > <i>StartRowIndex</i> AND
    PriceRank <= (<i>StartRowIndex</i> + <i>MaximumRows</i>)

Примечание.

Как мы увидим далее в этом руководстве, StartRowIndex, предоставленный ObjectDataSource, индексируется с нуля, тогда как значение ROW_NUMBER(), возвращаемое SQL Server 2005, индексируется с 1. Поэтому предложение WHERE возвращает эти записи, где PriceRankStartRowIndex строго больше и меньше или равноStartRowIndex + MaximumRows.

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

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

В предыдущем разделе мы создали метод DAL в качестве нерегламентированной инструкции SQL. К сожалению, средство синтаксического анализа T-SQL в Visual Studio, используемое мастером TableAdapter, не поддерживает синтаксис OVER, используемый функцией ROW_NUMBER(). Поэтому необходимо создать этот метод DAL как хранимую процедуру. Выберите обозреватель серверов в меню "Вид" (или нажмите клавиши CTRL+ALT+S) и разверните NORTHWND.MDF узел. Чтобы добавить новую хранимую процедуру, щелкните правой кнопкой мыши узел хранимых процедур и выберите команду "Добавить новую хранимую процедуру" (см. рис. 6).

Добавьте новую хранимую процедуру для постраничного просмотра продуктов

Рис. 6. Добавление новой хранимой процедуры для пагинации продуктов

Эта хранимая процедура должна принимать два целочисленных входных параметра — @startRowIndex и @maximumRows и использовать функцию ROW_NUMBER(), упорядоченную по полю ProductName, возвращая только те строки, которые больше чем указанный @startRowIndex и меньше или равны @startRowIndex + @maximumRow s. Введите следующий сценарий в новую хранимую процедуру и щелкните значок "Сохранить", чтобы добавить хранимую процедуру в базу данных.

CREATE PROCEDURE dbo.GetProductsPaged
(
    @startRowIndex int,
    @maximumRows int
)
AS
    SELECT     ProductID, ProductName, SupplierID, CategoryID, QuantityPerUnit,
               UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel, Discontinued,
               CategoryName, SupplierName
FROM
   (
       SELECT ProductID, ProductName, SupplierID, CategoryID, QuantityPerUnit,
              UnitPrice, UnitsInStock, UnitsOnOrder, ReorderLevel, Discontinued,
              (SELECT CategoryName
               FROM Categories
               WHERE Categories.CategoryID = Products.CategoryID) AS CategoryName,
              (SELECT CompanyName
               FROM Suppliers
               WHERE Suppliers.SupplierID = Products.SupplierID) AS SupplierName,
              ROW_NUMBER() OVER (ORDER BY ProductName) AS RowRank
        FROM Products
    ) AS ProductsWithRowNumbers
WHERE RowRank > @startRowIndex AND RowRank <= (@startRowIndex + @maximumRows)

После создания хранимой процедуры, уделите время, чтобы протестировать её. Щелкните правой кнопкой мыши по имени хранимой процедуры GetProductsPaged в обозревателе серверов и выберите параметр "Выполнить". Затем Visual Studio предложит вам входные параметры @startRowIndex и @maximumRow s (см. рис. 7). Попробуйте различные значения и изучите результаты.

Введите значение для класса <span= @startRowIndex и @maximumRows параметров" />

Рис. 7. Введите значение для @startRowIndex и @maximumRows параметров

После выбора этих значений входных параметров окно вывода отобразит результаты. На рисунке 8 показаны результаты при передаче 10 для обоих параметров @startRowIndex и @maximumRows.

Возвращаются записи, которые будут отображаться на второй странице данных

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

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

Создание метода DAL с помощью существующей хранимой процедуры

Рис. 9. Создание метода DAL с помощью существующей хранимой процедуры

Затем нам будет предложено выбрать хранимую процедуру для вызова. Выберите хранимую процедуру GetProductsPaged из раскрывающегося списка.

Выберите хранимую процедуру GetProductsPaged из списка Drop-Down

Рис. 10. Выберите хранимую процедуру GetProductsPaged из списка Drop-Down

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

Указывает, что хранимая процедура возвращает табличные данные

Рис. 11. Указывает, что хранимая процедура возвращает табличные данные

Наконец, укажите имена созданных методов. Как и в предыдущих руководствах, не стесняйтесь создавать методы, используя как Fill a DataTable, так и Return a DataTable. Назовите первый метод FillPaged и второй GetProductsPaged.

Назовите методы FillPaged и GetProductsPaged

Рис. 12. Имя методов FillPaged и GetProductsPaged

Помимо создания метода DAL для возврата определенной страницы продуктов, необходимо также предоставить такие функции в BLL. Как и в методе DAL, метод GetProductsPaged BLL должен принимать два целочисленных входных данных для указания начального индекса строк и максимальных строк, и должен возвращать только те записи, которые попадают в указанный диапазон. Создайте такой метод BLL в классе ProductsBLL, который просто вызывает метод GetProductsPaged DAL, как показано ниже.

<System.ComponentModel.DataObjectMethodAttribute( _
    System.ComponentModel.DataObjectMethodType.Select, False)> _
Public Function GetProductsPaged(startRowIndex As Integer, maximumRows As Integer) _
    As Northwind.ProductsDataTable
    Return Adapter.GetProductsPaged(startRowIndex, maximumRows)
End Function

Вы можете использовать любое имя входных параметров метода BLL, но, как мы увидим в ближайшее время, выбор использования startRowIndex и maximumRows избавляет нас от лишней работы при настройке ObjectDataSource для использования этого метода.

Шаг 4. Настройка ObjectDataSource для использования пользовательского разбиения по страницам

После завершения разработки методов BLL и DAL для доступа к определенному подмножеству записей, мы готовы создать элемент управления GridView, который перелистывает свои базовые записи, используя пользовательское разбиение на страницы. Начните с открытия EfficientPaging.aspx страницы в PagingAndSorting папке, добавьте GridView на страницу и настройте ее для использования нового элемента управления ObjectDataSource. В наших прошлых руководствах мы часто имели объект ObjectDataSource, настроенный ProductsBLL для использования метода класса GetProducts . Однако на этот раз мы хотим использовать метод GetProductsPaged, потому что метод GetProducts возвращает все продукты в базе данных, а GetProductsPaged возвращает только часть записей.

Настройка ObjectDataSource для использования метода GetProductsPaged класса ProductsBLL

Рис. 13. Настройка ObjectDataSource для использования метода GetProductsPaged класса ProductsBLL

Так как мы создаем GridView только для чтения, установите раскрывающийся список методов во вкладках INSERT, UPDATE и DELETE на «Нет».

Затем мастер ObjectDataSource запрашивает у нас источники значений GetProductsPaged параметров метода startRowIndex и maximumRows входных параметров. Эти входные параметры на самом деле задаются в GridView автоматически, поэтому просто оставьте исходный параметр "Нет" и нажмите кнопку "Готово".

Оставьте источники входных параметров в положении

Рис. 14. Оставьте входные источники параметров как нет

Завершив работу мастера ObjectDataSource, GridView будет содержать BoundField или CheckBoxField для каждого поля данных продукта. Вы можете настроить внешний вид GridView по вашему усмотрению. Я решил отобразить только поля ProductName, CategoryName, SupplierName, QuantityPerUnit и UnitPrice BoundFields. Кроме того, настройте GridView для поддержки разбиения по страницам, установив флажок "Включить разбиение по страницам" в смарт-теге. После этих изменений декларативная разметка GridView и ObjectDataSource должна выглядеть следующим образом:

<asp:GridView ID="GridView1" runat="server" AutoGenerateColumns="False"
    DataKeyNames="ProductID" DataSourceID="ObjectDataSource1" AllowPaging="True">
    <Columns>
        <asp:BoundField DataField="ProductName" HeaderText="Product"
            SortExpression="ProductName" />
        <asp:BoundField DataField="CategoryName" HeaderText="Category"
            ReadOnly="True" SortExpression="CategoryName" />
        <asp:BoundField DataField="SupplierName" HeaderText="Supplier"
            SortExpression="SupplierName" />
        <asp:BoundField DataField="QuantityPerUnit" HeaderText="Qty/Unit"
            SortExpression="QuantityPerUnit" />
        <asp:BoundField DataField="UnitPrice" DataFormatString="{0:c}"
            HeaderText="Price" HtmlEncode="False" SortExpression="UnitPrice" />
    </Columns>
</asp:GridView>
<asp:ObjectDataSource ID="ObjectDataSource1" runat="server"
    OldValuesParameterFormatString="original_{0}" SelectMethod="GetProductsPaged"
    TypeName="ProductsBLL">
    <SelectParameters>
        <asp:Parameter Name="startRowIndex" Type="Int32" />
        <asp:Parameter Name="maximumRows" Type="Int32" />
    </SelectParameters>
</asp:ObjectDataSource>

Если вы посещаете страницу через браузер, однако GridView не виден.

GridView не отображается

Рис. 15. GridView не отображается

GridView отсутствует, потому что ObjectDataSource в данный момент использует значение 0 для всех входных параметров GetProductsPaged, startRowIndex и maximumRows. Поэтому результирующий SQL-запрос не возвращает записей, поэтому GridView не отображается.

Чтобы устранить эту проблему, необходимо настроить ObjectDataSource для использования настраиваемого разбиения по страницам. Это можно сделать в следующих шагах:

  1. EnablePaging
  2. Настройте свойства StartRowIndexParameterName и MaximumRowsParameterName объекта ObjectDataSource соответственно,StartRowIndexParameterName и MaximumRowsParameterName свойства указывают имена входных параметров, переданных в SelectMethod для целей пользовательской пагинации. По умолчанию эти имена параметров - startIndexRow и maximumRows, поэтому при создании GetProductsPaged метода в BLL я использовал эти значения для входных параметров. Если вы решили использовать разные имена параметров для метода BLL, такие как GetProductsPaged и startIndex, например, необходимо соответствующим образом задать свойства ObjectDataSource, такие как StartRowIndexParameterName и MaximumRowsParameterName (например, startIndex для StartRowIndexParameterName и maxRows для MaximumRowsParameterName).
  3. Присвойте свойству ObjectDataSource SelectCountMethod имя метода, который возвращает общее количество записей для страничного представления (TotalNumberOfProducts). Напомним, что метод класса ProductsBLLTotalNumberOfProducts возвращает это общее количество с использованием метода DAL, выполняющего SELECT COUNT(*) FROM Products запрос. Эти сведения необходимы объекту ObjectDataSource для правильной отрисовки интерфейса разбиения по страницам.
  4. startRowIndex Удалите элементы maximumRows<asp:Parameter> из декларативной разметки ObjectDataSource при настройке ObjectDataSource с помощью мастера, Visual Studio автоматически добавил два <asp:Parameter> элемента для GetProductsPaged входных параметров метода. При установке EnablePaging на true, эти параметры будут переданы автоматически; если они также появляются в декларативном синтаксисе, ObjectDataSource попытается передать четыре параметра методу GetProductsPaged и два параметра методу TotalNumberOfProducts. Если вы забыли удалить эти <asp:Parameter> элементы, при посещении страницы через браузер вы получите сообщение об ошибке, например : ObjectDataSource "ObjectDataSource1" не удалось найти не универсальный метод TotalNumberOfProducts, имеющий параметры: startRowIndex, maximumRows.

После внесения этих изменений декларативный синтаксис ObjectDataSource должен выглядеть следующим образом:

<asp:ObjectDataSource ID="ObjectDataSource1" runat="server"
    OldValuesParameterFormatString="original_{0}" SelectMethod="GetProductsPaged"
    TypeName="ProductsBLL" EnablePaging="True" SelectCountMethod="
    TotalNumberOfProducts">
</asp:ObjectDataSource>

Обратите внимание, что свойства EnablePaging и SelectCountMethod заданы, а элементы <asp:Parameter> были удалены. На рисунке 16 показан снимок экрана окно свойств после внесения этих изменений.

Чтобы использовать настраиваемое разбиение по страницам, настройте элемент управления ObjectDataSource

Рис. 16. Чтобы использовать настраиваемое разбиение на страницах, настройте элемент управления ObjectDataSource

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

Данные, упорядоченные по имени продукта, отображаются на странице с помощью настраиваемого разбиения на страницы

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

Примечание.

При настройке разбиения страниц значение счетчика страниц, возвращаемое объектом ObjectDataSource SelectCountMethod , хранится в состоянии представления GridView. Другие переменные PageIndex, EditIndex, SelectedIndex, DataKeys коллекции и так далее хранятся в состоянии управления, которое сохраняется независимо от значения свойства EnableViewState GridView. Поскольку значение сохраняется между постбэками с помощью состояния представления, при использовании интерфейса пагинации, который включает ссылку на последнюю страницу, необходимо включить состояние представления для GridView. (Если интерфейс разбиения страниц не содержит прямую ссылку на последнюю страницу, вы можете отключить состояние просмотра.)

Щелчок по последней ссылке страницы вызывает повторную отправку запроса и указывает GridView обновить его свойство PageIndex. Если щелкнуть последнюю ссылку на страницу, GridView назначает его PageIndex свойству значение, меньшее его PageCount свойству. При отключенном состоянии представления PageCount, значение теряется при перезагрузках страницы, а PageIndex присваивается максимальное значение. Затем GridView пытается определить начальный индекс строки путем умножения PageSize и PageCount свойств. Это приводит к тому, что OverflowException результат превышает максимально допустимое значение целого числа.

Внедрение настраиваемого разбиения на страницы и сортировки

Текущая пользовательская реализация разбиения на страницы требует, чтобы при создании хранимой процедуры GetProductsPaged статически указывался порядок, по которому данные будут разбиваться на страницы. Однако, возможно, вы отметили, что смарт-тег GridView содержит флажок "Включить сортировку" в дополнение к параметру "Включить разбиение по страницам". К сожалению, добавление поддержки сортировки в GridView с текущей пользовательской реализацией разбиения страниц будет отсортировать только записи на текущей странице данных. Например, если вы настроите GridView для поддержки разбивки на страницы, а затем, просматривая первую страницу данных, отсортируете продукты по имени в порядке убывания, это обратит порядок продуктов на странице 1. Как показано на рисунке 18, Carnarvon Tigers представлен как первый товар при сортировке в обратном алфавитном порядке, которая игнорирует 71 другой товар, следующие за Carnarvon Tigers в алфавитном порядке; в сортировке учитываются только записи на первой странице.

Только данные, отображаемые на текущей странице, отсортированы

Рис. 18. Отсортированы только данные, отображаемые на текущей странице (щелкните, чтобы просмотреть изображение полного размера)

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

Реализация пользовательского разбиения по страницам и удаление

Если вы включаете функцию удаления в GridView, данные которой выстраиваются с помощью пользовательских методов разбиения на страницы, вы увидите, что при удалении последней записи с последней страницы GridView PageIndexисчезает, а не соответствующим образом уменьшается. Чтобы воспроизвести эту ошибку, включите удаление только что созданного учебника. Перейдите на последнюю страницу (страницу 9), где вы должны увидеть один продукт, поскольку всего 81 продукт, и мы разбиваем их по страницам по 10 продуктов на каждой. Удалите этот продукт.

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

  1. Удаление записи
  2. Получение соответствующих записей для отображения указанных PageIndex и PageSize
  3. Убедитесь, что количество страниц данных в источнике данных не превышает PageIndex; если превышает, автоматически уменьшается свойство PageIndex в GridView.
  4. Привязка соответствующей страницы данных к GridView с помощью записей, полученных на шаге 2.

Проблема связана с тем, что на шаге 2 PageIndex используемый при захвате записей для отображения по-прежнему PageIndex является последней страницей, единственная запись которой была только что удалена. Таким образом, на шаге 2 не возвращается ни одной записи, так как последняя страница данных больше не содержит записей. Затем на шаге 3 GridView понимает, что его PageIndex свойство больше общего количества страниц в источнике данных (так как мы удалили последнюю запись на последней странице) и, следовательно, уменьшает его PageIndex свойство. На шаге 4 GridView пытается привязать себя к данным, извлеченным на шаге 2; однако, на шаге 2 записи не были получены, и, следовательно, GridView остается пустым. При разбиении по умолчанию эта проблема не возникает, так как на шаге 2 все записи извлекаются из источника данных.

Чтобы устранить эту проблему, у нас есть два варианта. Сначала необходимо создать обработчик событий для обработчика событий GridView RowDeleted , который определяет количество записей, отображаемых на странице, которая была только что удалена. Если была только одна запись, то только что удаленная запись должна была быть последней, и нам нужно уменьшить GridView s PageIndex. Конечно, мы хотим обновить PageIndex только в том случае, если операция удаления была действительно успешной, что может быть определено, проверив, что свойство e.Exception равно null.

Этот подход работает, потому что обновляет PageIndex после шага 1, но до шага 2. Поэтому в шаге 2 возвращается соответствующий набор записей. Для этого используйте следующий код:

Protected Sub GridView1_RowDeleted(sender As Object, e As GridViewDeletedEventArgs) _
    Handles GridView1.RowDeleted
    ' If we just deleted the last row in the GridView, decrement the PageIndex
    If e.Exception Is Nothing AndAlso GridView1.Rows.Count = 1 Then
        ' we just deleted the last row
        GridView1.PageIndex = Math.Max(0, GridView1.PageIndex - 1)
    End If
End Sub

Альтернативным решением является создание обработчика событий для события ObjectDataSource RowDeleted и установка AffectedRows свойства значением 1. После удаления записи на шаге 1 (но перед извлечением данных на шаге 2) GridView обновляет его PageIndex свойство, если одна или несколько строк пострадали от операции. AffectedRows Однако свойство не задано ObjectDataSource, поэтому этот шаг не указан. Один из способов выполнения этого шага заключается в том, чтобы вручную задать AffectedRows свойство, если операция удаления успешно завершится. Это можно сделать с помощью следующего кода:

Protected Sub ObjectDataSource1_Deleted( _
    sender As Object, e As ObjectDataSourceStatusEventArgs) _
    Handles ObjectDataSource1.Deleted
    ' If we get back a Boolean value from the DeleteProduct method and it's true, then
    ' we successfully deleted the product. Set AffectedRows to 1
    If TypeOf e.ReturnValue Is Boolean AndAlso CType(e.ReturnValue, Boolean) = True Then
        e.AffectedRows = 1
    End If
End Sub

Код для обоих этих обработчиков событий можно найти в классе CodeBehind примера EfficientPaging.aspx.

Сравнение производительности по умолчанию и настраиваемого разбиения на страницы

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

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

Моя статья "Настраиваемое разбиение по страницам в ASP.NET 2.0 с SQL Server 2005" содержит некоторые тесты производительности, которые я провёл, чтобы продемонстрировать различия в производительности между этими двумя способами разбиения по страницам в таблице базы данных с 50 000 записей. В этих тестах я изучил время выполнения запроса на уровне SQL Server (с помощью SQL Profiler) и на странице ASP.NET с помощью функций трассировки ASP.NET. Помните, что эти тесты были запущены в поле разработки с одним активным пользователем, и поэтому не являются ненаучными и не имитируют типичные шаблоны загрузки веб-сайта. Независимо от этого, результаты показывают относительные различия во времени выполнения по умолчанию и настраиваемом разбиении по страницам при работе с достаточно большими объемами данных.

Среднее значение. Длительность (с) Читает
Профилировщик SQL с разбиением на страницы по умолчанию 1.411 383
Профилировщик SQL с настраиваемой постраничной навигацией 0,002 29
Трассировка ASP.NET по умолчанию 2.379 N/A
Настраиваемая трассировка на страницах ASP.NET 0,029 N/A

Как видно, получение определенной страницы данных требовало на 354 операции чтения меньше в среднем и завершилось быстрее во много раз. На странице ASP.NET пользовательская страница могла отрисовываться почти за 1/100 того времени, которое потребовалось бы при использовании стандартной разбивки на страницы.

Итоги

Реализация разбиения на страницы по умолчанию очень проста — достаточно установить флажок "Включить разбиение по страницам" в смарт-теге веб-элемента управления данными, но такая простота ухудшает производительность. При разбиении по умолчанию, когда пользователь запрашивает любую страницу данных, возвращаются все записи, даже если может отображаться только небольшая часть из них. Для борьбы с этой производственной нагрузкой объект ObjectDataSource предлагает альтернативный вариант разбиения по страницам — пользовательское разбиение на страницы.

Хотя настраиваемое разбиение на страницы улучшает проблемы производительности разбиения по умолчанию, извлекая только те записи, которые необходимо отображать, реализация настраиваемого разбиения на страницы требует больше усилий. Во-первых, запрос должен быть записан, чтобы правильно (и эффективно) получать доступ к определенному подмножеству запрошенных записей. Это можно сделать несколькими способами; То, что мы рассмотрели в этом руководстве, — использовать новую ROW_NUMBER() функцию SQL Server 2005 для ранжирования результатов, а затем возвращать только те результаты, ранжирование которых попадает в указанный диапазон. Кроме того, необходимо добавить средства для определения общего количества просматриваемых записей. После создания этих методов DAL и BLL также необходимо настроить ObjectDataSource, чтобы определить, сколько всего записей просматриваются, и правильно передавать значения начального индекса строк и максимального количества строк в BLL.

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

Счастливое программирование!

Об авторе

Скотт Митчелл, автор семи книг ASP/ASP.NET и основатель 4GuysFromRolla.com, работает с технологиями Microsoft Web с 1998 года. Скотт работает независимым консультантом, тренером и писателем. Его последняя книга Сэмс Учит себя ASP.NET 2,0 в 24 часах. Его можно достичь по mitchell@4GuysFromRolla.com.