Рейтинг@Mail.ru

Совершенствуем перебор записей в Delphi 10.2 Tokyo с классом-помощником TDataSetHelper

Автор: Alex. Опубликовано в Программирование . просмотров: 9323

Рейтинг:  5 / 5

Звезда активнаЗвезда активнаЗвезда активнаЗвезда активнаЗвезда активна
 

Работая с данными в Delphi с помощью классов унаследованных от TDataSet нам часто нужно делать перебор записей. При этом почти всегда сначала требуется отключить элементы управления, сохранить закладку, выключить фильтры, отписаться от событий, а после перебора восстановить всё в обратном порядке. И благодаря возможности в последних версиях Delphi создавать классы-помощники и использовать анонимные функции, вся эта рутина сокращается до нескольких строк кода. Давайте рассмотрим, мой класс-помощник TDataSetHelper и научимся его использовать.

Совершенствуем перебор записей в Delphi 10.2 Tokyo с классом-помощником TDataSetHelper

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

Рутина перебора записей

Здесь я покажу, насколько много строк кода приходится писать каждый раз, когда мы просто хотим перебрать все записи в наборе данных в Delphi.

Давайте создадим проект для экспериментов. Итак, создаём оконное приложение для Windows (пункт меню File -> New -> VCL Forms Application), затем на форму кладём таблицу TDBGrid и одну кнопку TButton. Кнопка будет запускать наш первый эксперимент. Зададим кнопке текст «Эксперимент 1». Также кладём на форму компоненты TDataSource и TClientDataSet. Теперь связываем таблицу TDBGrid с источником данных TDataSource (у нас это DataSource1), а источник данных TDataSource связываем с набором данных TClientDataSet (у нас это ClientDataSet1).

Форма для экспериментов с классом-помощником TDataSetHelper

Теперь по событию формы OnCreate создаём поля в наборе данных TClientDataSet и заполняем его случайными значениями.

procedure TForm1.FormCreate(Sender: TObject);
var
    i: integer;
    guid: TGUID;
begin
    ClientDataSet1.FieldDefs.Add('NumberField', ftInteger);
    ClientDataSet1.FieldDefs.Add('StringField', ftString, 38);
    ClientDataSet1.FieldDefs.Add('DateField', ftDate);
    ClientDataSet1.CreateDataSet;
    for i := 0 to 10000 do
    begin
        ClientDataSet1.Append;
        ClientDataSet1.FieldByName('NumberField').AsInteger := Random(1000);
        CreateGUID(guid);
        ClientDataSet1.FieldByName('StringField').AsString := GUIDToString(guid);
        ClientDataSet1.FieldByName('DateField').AsDateTime := IncDay(Now, -Random(1000));
        ClientDataSet1.Post;
    end;
    ClientDataSet1.First;
end;

Вот как будет выглядеть форма после запуска нашего приложения:

Приложение для тестирования класса-помощника TDataSetHelper

Теперь напишем код для проведения первого эксперимента. Допустим нам нужно пройтись по всем записям и подсчитать у скольки записей значение поля NumberField больше 100. Делать мы это будем по нажатию на кнопку «Эксперимент 1». После подсчёта результат будет отображать функцией ShowMessage. Вот как будет выглядеть код:

procedure TForm1.Button1Click(Sender: TObject);
var
    n: integer;
begin
    n := 0;
    ClientDataSet1.First;
    while not ClientDataSet1.Eof do
    begin
        if ClientDataSet1.FieldByName('NumberField').AsInteger > 100 then
            Inc(n);
        ClientDataSet1.Next;
    end;
    ShowMessage(IntToStr(n));
end;

Если вы теперь запустите приложение и нажмёте на кнопку «Эксперимент 1», то увидите, что перебор идёт медленно сверху вниз. При этом таблица всё время перерисовывается. Зачем нам перерисовывать таблицу? Этого нам не нужно, поэтому на время перебора записей нам нужно отключить отображение данных в таблице, а затем включить обратно. Это делается с помощью функций DisableControls и EnableControls.

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

После изменений код подрос и стал таким:

procedure TForm1.Button1Click(Sender: TObject);
var
    n: integer;
    bookmark: TBookmark;
begin
    n := 0;
    //Отключаем отображение данных в связанных элементах управления.
    ClientDataSet1.DisableControls;
    try
        //Сохраняем закладку для текущей записи.
        bookmark := ClientDataSet1.Bookmark;
        try
            ClientDataSet1.First;
            while not ClientDataSet1.Eof do
            begin
                if ClientDataSet1.FieldByName('NumberField').AsInteger > 100 then
                    Inc(n);
                ClientDataSet1.Next;
            end;
        finally
            //Восстанавливаем текущую запись, используя закладку.
            ClientDataSet1.Bookmark := bookmark;
        end;
    finally
        //Включаем отображение данных в связанных элементах управления.
        ClientDataSet1.EnableControls;
    end;
    ShowMessage(IntToStr(n));
end;

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

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

ClientDataSet1.Filter := Format('DateField = ''%s''', [FormatDateTime('yyyy-mm-dd', Now)]);
ClientDataSet1.Filtered := true;

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

procedure TForm1.Button1Click(Sender: TObject);
var
    n: integer;
    bookmark: TBookmark;
    filtered: boolean;
begin
    n := 0;
    //Отключаем отображение данных в связанных элементах управления.
    ClientDataSet1.DisableControls;
    try
        //Сохраняем закладку для текущей записи.
        bookmark := ClientDataSet1.Bookmark;
        try
            //Отключаем фильтр, если он включён.
            filtered := ClientDataSet1.Filtered;
            if filtered then
                ClientDataSet1.Filtered := false;
            try
                ClientDataSet1.First;
                while not ClientDataSet1.Eof do
                begin
                    if ClientDataSet1.FieldByName('NumberField').AsInteger > 100 then
                        Inc(n);
                    ClientDataSet1.Next;
                end;
            finally
                //Обратно включаем фильтр, если он был включён.
                if filtered then
                    ClientDataSet1.Filtered := true;
            end;
        finally
            //Восстанавливаем текущую запись, используя закладку.
            ClientDataSet1.Bookmark := bookmark;
        end;
    finally
        //Включаем отображение данных в связанных элементах управления.
        ClientDataSet1.EnableControls;
    end;
    ShowMessage(IntToStr(n));
end;
 

А ещё вспомним о том, что бывают ситуации, когда мы подписаны на события AfterScroll и BeforeScroll, чтобы отслеживать изменение текущей записи. Но ведь для перебора нам это не нужно. Значит, на время перебора нам нужно временно отключить эти события.

Заодно ещё сразу подумаем, что набор данных может быть неактивным. В этом случае при попытке перебора мы получим ошибку, что не хотелось бы.

В итоге получим следующий код перебора записей:

procedure TForm1.Button1Click(Sender: TObject);
var
    n: integer;
    bookmark: TBookmark;
    filtered: boolean;
    beforeScroll, afterScroll: TDataSetNotifyEvent;
begin
    n := 0;
    //Если набор данных не активен, то ничего не делаем.
    if ClientDataSet1.Active then
    begin
        //Отключаем события BeforeScroll и AfterScroll.
        beforeScroll := ClientDataSet1.BeforeScroll;
        try
            ClientDataSet1.BeforeScroll := nil;
            afterScroll := ClientDataSet1.AfterScroll;
            try
                ClientDataSet1.AfterScroll := nil;
                //Отключаем отображение данных в связанных элементах управления.
                ClientDataSet1.DisableControls;
                try
                    //Сохраняем закладку для текущей записи.
                    bookmark := ClientDataSet1.Bookmark;
                    try
                        //Отключаем фильтр, если он включён.
                        filtered := ClientDataSet1.Filtered;
                        if filtered then
                            ClientDataSet1.Filtered := false;
                        try
                            ClientDataSet1.First;
                            while not ClientDataSet1.Eof do
                            begin
                                if ClientDataSet1.FieldByName('NumberField').AsInteger > 100 then
                                    Inc(n);
                                ClientDataSet1.Next;
                            end;
                        finally
                            //Обратно включаем фильтр, если он был включён.
                            if filtered then
                                ClientDataSet1.Filtered := true;
                        end;
                    finally
                        //Восстанавливаем текущую запись, используя закладку.
                        ClientDataSet1.Bookmark := bookmark;
                    end;
                finally
                    //Включаем отображение данных в связанных элементах управления.
                    ClientDataSet1.EnableControls;
                end;
            finally
                //Восстанавливаем событие AfterScroll.
                ClientDataSet1.AfterScroll := afterScroll;
            end;
        finally
            //Восстанавливаем событие BeforeScroll.
            ClientDataSet1.BeforeScroll := beforeScroll;
        end;
    end;
    ShowMessage(IntToStr(n));
end;
 

Использование функции ForEach класса-помощника TDataSetHelper для перебора записей

Чтобы каждый раз при переборе записей не делать ту рутину, которую вы видите выше, я сделал класс-помощник TDataSetHelper и опубликовал его на GitHub-е здесь. Для уменьшения кода нам поможет функция ForEach. Чтобы воспользоваться функцией, давайте добавим ещё одну кнопку и назовём её «Эксперимент 2». Затем подключим юнит DataSetHelper. По событию от второй кнопки сделаем тот же подсчёт, что и по событию от первой кнопки, но с помощью функции ForEach. Смотрите, как уменьшится код:

procedure TForm1.Button2Click(Sender: TObject);
var
    n: integer;
begin
    n := 0;
    ClientDataSet1.ForEach(procedure
        begin
            if ClientDataSet1.FieldByName('NumberField').AsInteger > 100 then
                Inc(n);
        end
    );
    ShowMessage(IntToStr(n));
end;

Этот код намного меньше и понятнее. Давайте посмотрим, что делает функция ForEach:

    1. Проверяет, активен ли набор данных и, если активен, то делает перебор;
    2. Отключает события AfterScroll и BeforeScroll;
    3. Отключает перерисовку элементов управления, если она включена;
    4. Сохраняет закладку для текущей записи;
    5. Отключает фильтр, если он включён;
    6. Проходит по всем записям и для каждой вызывает анонимную функцию;
    7. Включает фильтр обратно, если он был включён;
    8. Включает перерисовку элементов управления, если она была включена;
    9. Восстанавливает события AfterScroll и BeforeScroll.

Здесь приведён самый частый случай использования функции. Но что делать, если вам, например, не нужно отключать фильтр или не восстанавливать текущую запись? А может быть вам нужно сделать перебор в обратном порядке или начать с текущей записи? Для настройки перебора предусмотрены специальные флажки:

      • dsloNoRestoreBookmark - запрещает восстановление закладок.
      • dsloNoDisableControls - запрещает отключение элементов управления.
      • dsloNoDisableFilter - запрещает отключение фильтров.
      • dsloNoNullifyBeforeScrollEvent - запрещает обнуление события BeforeScroll.
      • dsloNoNullifyAfterScrollEvent - запрещает обнуление события AfterScroll.
      • dsloFromCurrent - начинать с текущей записи.
      • dsloReverseOrder - идти в обратном порядке.
      • dsloOnlyCurrentRecord - только текущая запись.
      • dsloOnlyModifiedRecords - только измененные записи.

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

procedure TForm1.Button3Click(Sender: TObject);
var
    text: string;
begin
    text := '';
    ClientDataSet1.ForEach([dsloNoDisableFilter, dsloReverseOrder],
        procedure
        begin
            if ClientDataSet1.FieldByName('NumberField').AsInteger > 100 then
                text := text + ' ' + ClientDataSet1.FieldByName('NumberField').AsString;
        end
    );
    ShowMessage(text);
end;

Как видите, первым параметром идёт набор флажков, а вторым параметром – анонимная функция.

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

procedure TForm1.Button4Click(Sender: TObject);
var
    n, s: integer;
begin
    n := 0;
    s := 0;
    ClientDataSet1.ForEach(procedure(state: TDataSetLoopState)
        begin
            s := s + ClientDataSet1.FieldByName('NumberField').AsInteger;
            Inc(n);
            if n = 10 then
                state.Break;
        end
    );
    ShowMessage(IntToStr(s));
end;

Здесь в нашу анонимную функцию всё время передаётся указатель на экземпляр класса TDataSetLoopState, у которого есть функция Break для прерывания цикла. Кстати, у функции Break есть параметр restoreBookmark, с помощью которого вы можете запретить восстановление текущей записи. Это может быть полезно, например, когда вы выполняете поиск по записям и если не нашли то что искали, то закладка восстанавливается, а если нашли, то текущей делается запись с найденным результатом. Вот пример поиска определённого числа:

procedure TForm1.Button5Click(Sender: TObject);
var
    found: boolean;
begin
    found := false;
    ClientDataSet1.ForEach(procedure(state: TDataSetLoopState)
        begin
            if ClientDataSet1.FieldByName('NumberField').AsInteger = 100 then
            begin
                found := true;
                state.Break(false);
            end;
        end
    );
    if found then
        ShowMessage('Число найдено!')
    else
        ShowMessage('Число не найдено!');
end;

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

Поиск определённого значения в поле с помощью класса-помощника TDataSetHelper

Запись данных в XML с помощью класса-помощника TDataSetHelper

Кроме собственно самого перебора можно выделить и ещё несколько частых действий с данными. Одно из них – это запись данных в XML-документ, например, с целью дальнейшей передачи этого XML в процедуру базы данных или для сохранения в файл. Для этих целей у класса-помощника TDataSetHelper есть функции ToXML. Вот пример кода для создания XML-документа по набору данных и записи его в файл:

procedure TForm1.Button6Click(Sender: TObject);
begin
    ClientDataSet1.ToXML.SaveToFile('test1.xml');
end;

А вот фрагмент получившегося файла test.xml:

<data>
    <row NumberField="348" StringField="{35E2BD68-1EF7-4844-AD68-8986F872F2E7}" DateField="2016-03-29"/>
    <row NumberField="569" StringField="{EA424DE3-9469-4FF9-9B38-9998E535607A}" DateField="2015-10-06"/>
    <row NumberField="806" StringField="{B6FCB5B5-37EE-4A0E-B15E-618E12F741C5}" DateField="2016-10-25"/>
    <row NumberField="673" StringField="{23F29F66-3EB0-4BAA-AB4A-441DFB6C9A5C}" DateField="2017-02-14"/>
    <row NumberField="436" StringField="{52898338-85CE-42AE-8CAD-D1C712F2A288}" DateField="2017-10-18"/>
    <row NumberField="7" StringField="{A01DCDC4-52D7-4133-B705-B661FD414A87}" DateField="2015-06-22"/>
    <row NumberField="213" StringField="{A01DCDC3-52D5-4132-B404-B681FD414A87}" DateField="2015-06-11"/>
</data>

Как видите, для каждой записи создаётся элемент, а для каждого поля – атрибут. Имя атрибута соответствует имени поля.

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

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

ClientDataSet1.ToXML(['NumberField', 'DateField']).SaveToFile('test1.xml');

Результат будет следующим:

<data>
    <row NumberField="700" DateField="2016-02-15"/>
    <row NumberField="225" DateField="2016-11-10"/>
    <row NumberField="996" DateField="2017-04-03"/>
    <row NumberField="956" DateField="2016-11-14"/>
    <row NumberField="478" DateField="2017-04-25"/>
    <row NumberField="988" DateField="2016-01-06"/>
    <row NumberField="5" DateField="2016-01-07"/>
</data>

Также вы можете менять имена элементов, например, если нам нужен корневой элемент с именем «Collection», а для каждой записи элементы с именем «Item», то в функцию ToXML нужно указать путь к элементу для записи следующим образом:

ClientDataSet1.ToXML('/Collection/Item', ['NumberField', 'DateField']).SaveToFile('test1.xml');

Результат будет следующим:

<Collection>
    <Item NumberField="457" DateField="2015-06-15"/>
    <Item NumberField="829" DateField="2015-09-18"/>
    <Item NumberField="530" DateField="2016-06-23"/>
    <Item NumberField="317" DateField="2016-10-25"/>
    <Item NumberField="141" DateField="2015-07-22"/>
    <Item NumberField="195" DateField="2016-02-20"/>
    <Item NumberField="61" DateField="2015-06-02"/>
</Collection>

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

Проверка наличия записей в TDataSet

При работе с TDataSet частенько приходится узнавать, активен ли набор данных и есть ли в нём записи. Обычно этот код выглядит так:

if ClientDataSet1.Active and (ClientDataSet1.RecordCount > 0) then

С классом-помощником TDataSetHelper он сокращается до следующего:

if ClientDataSet1.HasRecords then

Подсчёт суммы, среднего арифметического, поиск максимума и минимума

Кроме всего перечисленного не лишним будут и функции подсчёта суммы и среднего арифметического для какого-либо числового поля. Или поиск минимума и максимума. Для таких целей я сделал класс-помощник TFieldHelper с функциями Sum, Avg, Min и Max. Вот пример использования:

ShowMessage(ClientDataSet1.FieldByName('NumberField').Sum);
ShowMessage(ClientDataSet1.FieldByName('NumberField').Avg);
ShowMessage(ClientDataSet1.FieldByName('NumberField').Min);
ShowMessage(ClientDataSet1.FieldByName('NumberField').Max);

Функции возвращают тип variant, причём, если записей нет или все поля не имеют значений (т.е. содержат null), то функции вернут null. При переборе записей поля со значениями null пропускаются.

Итог

Вот собственно и всё, что я хотел написать про рутину перебора набора данных и про класс-помощник TDataSetHelper. Если у вас есть вопросы и замечания, оставляйте их в комментариях ниже.

Tags: TDataSetHelper Учебники по программированию Delphi

Комментарии   

Cyber-Robot
0 #1 Cyber-Robot 03.03.2018 23:35
Если нужно быстро работать, то нужно использовать TQuery

Select count(*) from Table where num > 100

ну прикольная возможность, эти хелперы, нов данном случае это просто прикольная фишка и все

Компоненты Table я уже лет 18 не использую, и без них нормально обхожусь.

Быстрее чем прямые SQL запросы не сделать
Цитировать
Alex
0 #2 Alex 04.03.2018 14:37
Цитирую Cyber-Robot:
Компоненты Table я уже лет 18 не использую, и без них нормально обхожусь.

В статье нет ни слова про компоненты Table.
Цитировать
Cyber-Robot
0 #3 Cyber-Robot 08.03.2018 03:02
Цитирую Alex:
В статье нет ни слова про компоненты Table.

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

методика работы та же что и с TTable (и не важно что это у вас в статье абстактный Data Set)

это на первый взгляд, кажется что быстрее и удобнее использовать компоненты типа TDBTable на самом деле это куча проблем всегда будет, и жуткие тормоза, ну а нормальную многопользовательскую работу, вообще не организовать
Цитировать
Cyber-Robot
0 #4 Cyber-Robot 08.03.2018 17:23
Цитирую Alex:

В статье нет ни слова про компоненты Table.


А методы Append и Post это к запросу TQuery применяете? :lol:
Цитировать
Alex
0 #5 Alex 08.03.2018 18:11
Цитирую Cyber-Robot:
А методы Append и Post это к запросу TQuery применяете? :lol:
К TClientDataSet.
Цитировать
Alex
0 #6 Alex 08.03.2018 18:14
Цитирую Cyber-Robot:
Компоненты Table я уже лет 18 не использую, и без них нормально обхожусь.
А что используете?
Цитировать
Cyber-Robot
0 #7 Cyber-Robot 23.03.2018 23:52
Цитирую Alex:
А что используете?

SQL запросы, только их.

Цитата:
Допустим нам нужно пройтись по всем записям и подсчитать у скольки записей значение поля NumberField больше 100.
решение такое
Select Count(*) FROM TABLE WHERE NumberField > 100

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


для отображения данных, опять же SELECT и сохраняем данные самостоятельно, без всех этих TDBTable и прочегоф
такие компоненты только кажется, что они упрощают, на практике - это тормоза и геморой
Цитировать
Alex
0 #8 Alex 24.03.2018 11:38
Цитирую Cyber-Robot:
SQL запросы, только их.

Но в статье речь не об SQL-запросах, а о переборе записей на клиенте. Таких задач очень много и TDataSetHelper как раз в этом помогает.
Цитировать
ВОЛОДИМИР
0 #9 ВОЛОДИМИР 12.05.2019 09:44
Большое спсаибо. Для простых разработчиков, не проффесионалов, очень полезная статья. Думаю, вообще, нужно бы с каждой версией Delphi нужно бы обращать внимание что полезного для упрощения разработки появилось.
Цитировать

Добавить комментарий