Публикации Lazarus

ListView и разбиение данных на порции (ч.1)

20.07.2009
Leonardo M. Rame
Leonardo M. Rame
Кордоба, Аргентина 
Источник - Leonardo's blog
Перевод и переделка примера под FreePascal+Lazarus - Вадим Исаев

Предисловие от переводчика

Иногда возникает ситуация, когда необходимо просмотреть довольно большой объём данных собственными глазами. Например в 1С при несвоевременном обнаружении бухгалтерами косяков в начислении заработной платы. :-) Глаз замылиливается при просмотре и ошибку удаётся найти не всегда.

Леонардо предложил очень хороший способ - разбиение всего большого объёма данных на порции, который не зависит от среды разработки, а использует особенности построения запроса к БД.


Начало

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

Речь пойдёт о методе, который возвращает данные по порциям, чтобы затем представить их в ListView незаметно для пользователя.

Я знаю, что TDbGrid в Delphi позволяет делать нечто подобное, когда подключена к базе данных через ADO, но как найти общий метод постраничного просмотра данных, независимо от механизма подключения к СУБД?

Но не всё так плохо... :-)

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


Примечание переводчика: Здесь в качестве TDataSet используется компонент SQLQuery, который входит в стандартный комплект Lazarus и позволяет делать запрос к базам данных различных типов. В качестве базы данных используется БД типа SQLite3, дабы не привязывать пример к серверу и потому что SQLite3 (в отличие от Firebird последних версий) не требует большого количества файлов для обслуживания БД. :-)

В БД у нас есть таблицы с названием Sol и Rus, с более чем полутора тысячами записей. У таблиц есть следующие поля:

Таблица SolТаблица RusТип поля
IdIdInteger
 Sol_idInteger
NameNameVarchar

Текст основного запроса:

SELECT rus.id, sol.name, rus.name
FROM rus INNER JOIN sol
ON rus.sol_id=sol.id
ORDER BY sol.name, rus.name

ListView в режиме "Отчёт"

Положите TListView на форму, затем установите следующие свойства:

  ViewStyle := vsReport
  Columns := (3 колонки:  №№, Исполнитель, Песня)

Итак, начнём...

В событии создания формы напишем следующий код:

procedure TForm1.FormCreate(Sender: TObject);
begin
  //Определяем общее количество записей
  SQLQuery1.SQL.Clear;
  SQLQuery1.SQL.Text:='SELECT COUNT(*) FROM rus';
  SQLQuery1.Open;
  FAllRecords:=SQLQuery1.Fields[0].AsInteger;
  SQLQuery1.Close;

  //Основной запрос
  SQLQuery1.SQL.Clear;
  SQLQuery1.SQL.Add('SELECT rus.id, sol.name, rus.name');
  SQLQuery1.SQL.Add('FROM rus INNER JOIN sol');
  SQLQuery1.SQL.Add('ON rus.sol_id=sol.id');
  SQLQuery1.SQL.Add('LIMIT '+cPageSize+' OFFSET :n');
  SQLQuery1.Params[0].AsInteger:=0;
  SQLQuery1.Open;
  GetCurrentPage(dqCurr);
end;

Разбиение данных на порции

То, что я хочу сделать, делит данные по 100 записей на порцию, затем показывает по одной порции за раз. Запрос только по 100 записей позволяет практически мгновенно показывать первую порцию данных, далее пользователь просматривает следующие 100 записей, повторно сделав запрос к базе данных для записей 101 - 200 и так далее.

Почти у каждого механизма СУБД есть метод, который выполняет запрос данных частями - у MySql есть "LIMIT nn TO mm", у Firebird есть "ROWS nn TO mm", у MsSql есть "TOP", у SQLite3 есть "LIMIT nn OFFSET mm" и т.д.

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

procedure TForm1.GetCurrentPage(Dir: TDirQuery);
Var
  i: Integer;
Begin
  //В зависимости от параметра процедуры определяем с какого номера записи проводить выборку
  Case Dir of
    dqPrev: If SQLQuery1.Params[0].AsInteger>=cPageSize Then
              SQLQuery1.Params[0].AsInteger:=SQLQuery1.Params[0].AsInteger-cPageSize;
    dqNext: If SQLQuery1.Params[0].AsInteger<=FAllRecords-cPageSize Then
              SQLQuery1.Params[0].AsInteger:=SQLQuery1.Params[0].AsInteger+cPageSize;
  End;

  SQLQuery1.Close;
  SQLQuery1.Open;
  ListView1.Items.Clear;

  //Заполняем полученой порцией данных компонент отображения
  For i:=0 To SQLQuery1.RecordCount-1 Do
  Begin
    ListView1.Items.Add;
    ListView1.Items[i].Caption:=SQLQuery1.Fields[0].AsString;
    ListView1.Items[i].SubItems.Add(SQLQuery1.Fields[1].AsString);
    ListView1.Items[i].SubItems.Add(SQLQuery1.Fields[2].AsString);
    SQLQuery1.Next;
  End;
End;

Добавьте константу "const cPageSize = 100;" в секцию модуля "implementation". Также добавьте скрытый атрибут "FAllRecords: integer;" в private секции формы.

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

Я надеюсь, что вы обрадованы этим так же как и я, когда закончил писать. :-) В следующей части статьи я переделаю эту версию, используя TCollections вместо TDataSets так же, как обычно делаю в своих проектах.

Пример 1: listview1.s3.7z

Актуальные версии
FPC3.2.2release
Lazarus3.2release
MSE5.10.0release
fpGUI1.4.1release
links