Delphi programming blog
Источник: http://teran.karelia.pro/articles/item_6334.html
 

for-in для DB-запросов

Опубликовано 23.09.2014 г. 13:25

Наконец появилась мысль для написания статьи. В общем-то заметка будет скорее относиться к разряду прикладных "tricks&tips". Достаточно часто в работе возникает необходимость весьма не хитрым способом обрабатывать результаты работы SQL запросов, и обычно эта процедура выглядит так:

q := TAdoQuery.Create(nil);
q.SQL.text := '..';
try
    q.Open();
    while not q.eof do begin
        writeln(q.fieldByName('id').AsInteger);
        q.Next();
    end;
finalyy
    q.Free();
end;

Сейчас в связи с производственной необходимостью читаю книги по C#, .NET & MVC, и они там так ловко обращаются с результатами запросов, что появилась мысль, почему бы и нам несколько не упростить эту конструкцию.  И под фразой "упростить" подразумевается замена конструкции "while not eof do ... next" на "for row in query", что будет по сути делать то же самое, но избавлять нас от частых фраз вида "епт, опять next забыл" с уходом в бесконечный цикл.

Собственно, как вы понимаете процедура перехода к такому циклу весьма проста. И нам всего лишь необходимо расширить класс реализующий запрос методом GetEnumerator, либо (что было бы более эффективно в нашем случае), расширить интерфейсом IEnumerable. Но для примера выберем простой вариант.

Класс мы расширим без использования наследования - за счет классов помощников, поскольку в данном случае нам не потребуется иных модификаций исходного кода. Итак, на примере TADOQuery нам понадобится следующее класс помощник, класс перечислителя, и класс для текущей записи. Напишем класс помощник:

interface
uses ADODB, DB;

type
    TRow = class;
    TRowEnumerator = class;

    TADOQueryHelper = class helper for TADOQuery
        function getEnumerator():TRowEnumerator;
    end;


.....


function TADOQueryHelper.getEnumerator(): TRowEnumerator;
begin
    result := TRowEnumerator.Create(self);
end;

Естественно, чтобы перечислителю было с чем работать, то передаем ему ссылку на сам запрос с которым работать в качестве параметра конструктора. Далее сам перечислитель, где нам нужно реализовать всего два метода - MoveNext, возвращающий булевый результат, и свойство Current.

    TRowEnumerator = class(TObject)
      strict private
        FQuery : TADOQuery;
        FFirstTime : boolean;
        FRow : TRow;
      public
        constructor Create(q : TADOQuery);
        destructor Destroy(); override;

        function MoveNext():boolean;
        property Current : TRow read FRow;
    end;

Текущий элемент будет представлен классом TRow, реализация которого позволяет много фантазировать, но для примера, мне нужен только функционал получения доступа к полям текущей строки запроса:

    TRow = class(TObject)
      private
        FQuery : TADOQuery;
        constructor Create(q : TADOQuery);
      public
        function FieldByName(aField : string) : TField;
    end;

.....

constructor TRow.Create(q: TADOQuery);
begin
    inherited Create();
    FQuery := q;
end;

function TRow.FieldByName(aField: string): TField;
begin
    result := FQuery.FieldByName(aField);
end;

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

constructor TRowEnumerator.Create(q: TADOQuery);
begin
    inherited Create();
    FQuery := q;
    FRow := TRow.Create(FQuery);

    FFirstTime := true;
end;

destructor TRowEnumerator.Destroy();
begin
    FRow.Free();
    inherited;
end;

function TRowEnumerator.MoveNext(): boolean;
begin
    if not FFirstTime then
        FQuery.Next()
    else FFirstTime := false;

    result := not FQuery.Eof;
end;

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

        q.SQL.Text := 'SELECT TOP 10 id, name_eng FROM subjects ORDER BY id';

        q.Open();
        try
            for row in q do begin
                writeln(row.FieldByName('id').AsInteger, ' - ',
                        row.FieldByName('name_eng').AsString);
            end;
        finally
            q.Close();
        end;
Метки:  class helper  |  IEnumerable  |  ADO 

Комментарии

Роман
23.09.2014 в 14:11
А что если пойти дальше?

Как-то так:

for row in query('SELECT TOP 10 id, name_eng FROM subjects ORDER BY id') do
begin
writeln(row.FieldByName('id').AsInteger, ' - ',
row.FieldByName('name_eng').AsString);
end;

Функция query в этом коде должна возвращать интерфейс, в котором реализован getEnumerator и внутри есть TADOQuery.

В голове вроде компилируется, проверить пока негде.
ter
23.09.2014 в 15:43
в целом мысль интересная, но большой запрос в оператор цикла вставлять как то не эстетично :)
а внутреннему экземпляру запроса еще коннекшен понадобится где то получить, что, конечно, тоже решаемо.
Роман
23.09.2014 в 15:00
Дело привычки. В каком-нибудь PL/SQL такие вот штуки - обыденность:

for rec in (select col_1, col_2 from table_a) loop
/*Statements, use rec.col_1 and rec.col_2 */
end loop;

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

P.S. Но это так, теоретизирование. Я в последнее время очень мало работаю с БД.
Николай Зверев
23.09.2014 в 21:14
Что-то я на совсем понял трюк с FRow: TRow. Курсор-то всё равно находится на уровне исходного датасета...


Мне кажется, что в данном случае хелпера будет более чем достаточно. Ну т.е. можно привести к такому синтаксису:

q.Open;
while q.MoveNext do
q.FieldByName...
q.Close;

где MoveNext описать в хелпере так:
if Bof then
First
else
Next;
Result := not Eof;

И всё, и не надо никаких издержек на перечислитель и доп переменную для for-in
ter
23.09.2014 в 22:31
ну, суть была именно в синтаксисе for-in, а уж возможностей реализации и других подходов придумать можно много.
Николай Зверев
23.09.2014 в 21:25
и, кстати, Open и Close тоже можно в MoveNext добавить.
т.е. если ещё not Active - Open + First. Если уже Eof - то Close.
Николай Зверев
23.09.2014 в 22:56
хм, а вообще, Close вызывается в деструкторе, так что он тут особо и не нужен
Юрий
30.09.2014 в 21:23
Адски нужная вещь. Автору респект!
ter
02.10.2014 в 23:17
как выяснилось подобный код в EDN был опубликован еще в 2006 что ли году :)
ter
02.10.2014 в 23:40
если вдруг решите использовать, и дочитаете до конца, то снабдите функцию FieldByName класса TRow директивой inline :)
Валдис
14.12.2014 в 21:54
'FieldByName' внутри циклов выглядит смешно (повторяющийся суббцикл). В некоторых конторах на этом этапе завершается собеседование.
teran
21.12.2014 в 18:32
когда мы будем на собеседовании, либо писать статьи по оптимизации, либо будем заниматься оптимизацией кода в системах, где это критично, тогда и поговорим о подобных замечаниях и посмеемся.
в данном же случае нужна была простота и наглядность исходного кода, демонстрирующего итераторы для наборов данных.
- Имя
- e-mail*
- Сайт
вы можете использовать теги [i],[b],[code],[quote]
Дополнительно