Языки программирования - Несколько слов об изменении запроса в run-time - PRCY⮭net
Постановка

Очень часто при работе с запросами приходится менять SQL этого запроса. Например, при изменении порядка сортировки или при необходимости изменения фильтра, прописанного в where. Сделать это стандартными средствами можно, но довольно муторно, т.к. весь запрос хранится в одном месте (для TQuery и её потомков это свойство Sql). При желании изменить, например, количество или порядок следования полей в order by, нужно программно найти этот order by, написать свой, вставить его вместо старого и т.д. Для меня, честно говоря, загадка, зачем борланд пошла по такому ущербному пути: стандарт ANSI SQL-92, с которым (и только с которым!) работает Bde, подразумевает достаточно жёсткий синтаксис запроса, вполне допускающий обработку на уровне отдельных секций.
Сегодня я хотел бы поделиться одним из вариантов реализации потомка TQuery, в котором задачи такого класса будут решаться на лету одной строчкой кода.
Смысл очень простой. Для того, чтобы уйти от ручной обработки текста sql-запроса, надо просто разбить его на стандартные секции. И менять их по отдельности. Ведь любой select-запрос имеет достаточно строгий синтаксис, состоя из определённого количества заранее известных секций (clauses), задаваемых в строго определённой последовательности. Рассмотрим этот синтаксис поподробнее на примере СУБД Interbase:
Код:
SELECT
[TRANSACTION transaction]
[DISTINCT | ALL]
{* | <val> [, <val> ]}
[INTO :var [, :var ]]
FROM [, ...]
[WHERE <search_condition>]
[GROUP BY col [COLLATE collation] [, col [COLLATE collation] ]
[HAVING <search_condition>]
[UNION <select_expr> [ALL]]
[PLAN ]
[ORDER BY <order_list>]
[FOR UPDATE [OF col [, col ]]];
Как видим, обязательными являются две секции: SELECT и FROM.
Ещё восемь секций опциональны. Наша задача сводится к тому, чтобы значение каждой секции устанавливать отдельно, при необходимости переоткрывая запрос. Можно было бы плясать от стандартного свойства Sql, выделять нужную секцию, менять и вставлять обратно. Но зачем это, если можно сам Sql формировать на основе заданных секций? Конечно, этот подход имеет тот минус, что накрывается прямая установка Sql одной строкой, что может быть неудобно при хранении запроса в реестре, базе и т.д., но и это, при желании, можно побороть.
В общем-то, ничего заумного, реализация до смешного проста, но при использовании в проектах позволяет сэкономить массу времени и значительно увеличить читабельность кода.

Реализация

Для начала определим все секции:

const
ciClauseCount = 10;
caClauses: array [1..ciClauseCount] of string = ('SELECT',
'INTO',
'FROM',
'WHERE',
'GROUP BY',
'HAVING',
'UNION',
'PLAN',
'ORDER BY',
'FOR UPDATE');

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

const
ciSelect = 1;
ciInto = 2;
ciFrom = 3;
ciWhere = 4;
ciGroupBy = 5;
ciHaving = 6;
ciUnion = 7;
ciPlan = 8;
ciOrderBy = 9;
ciForUdate = 10;

Определим тип нашего индексированного свойства и определим сам класс:
Код:
type
TClauseArray = array [1..ciClauseCount] of string;
TDynQuery = class (TQuery)
private fClauses: TClauseArray;
procedure UpdateSql;
procedure SetClause(Index: integer; Value: string);
function GetClause (Index: integer): string; public
property Clause [Index: integer]:
string read GetClause write SetClause;
end;
Свойство fClauses будет содержать все секции запроса, на основе которых и будет формироваться сам запрос. Занимается этим процедура UpdateSql. Ну а методы GetClause/SetClause стандартны, и служат для установки/чтения значений отдельных секций. Поглядим на сам код:
Код:
{ TDynQuery }
procedure TDynQuery.UpdateSql;
var
OldActive: boolean;
i: integer;
begin
DisableControls;
try
OldActive := Active;
Sql.Clear;
for i := 1 to ciClauseCount do begin
if fClauses [i] <> '' then
Sql.Add (caClauseNames [i] + ' ' + fClauses [i]);
end;
Active := OldActive;
finally
EnableControls;
end;
end;
function TDynQuery.GetClause (Index: integer): string;
begin
Result := fClauses [index];
end;
procedure TDynQuery.SetClause(Index: integer; Value: string);
begin
if fClauses [Index] <> Value then begin
fClauses [Index] := Value;
UpdateSql;
end;
end;

Всё достаточно прозрачно, отмечу лишь, что метод UpdateSql добавляет в текст Sql-запроса только те секции, для которых установлено начение, и переоткрывает квери, если она была открыта на момент изменения секции. Здесь есть мелкие недоработки, например, не проверяется выход индекса за пределы допустимых значений, я просто не хотел мусорить исходный код вещами, которые очевидны и принципиально не важны. Можно было бы привести код регистрации компонента в палире дельфи, но это также тривиально. Приведу лучше исходник тестового проекта, в котором используется этот квери. В этом проекте на форме находятся компоненты DbGrid1, подключенные к источнику данных DataSource1, динамически создаётся экземпляр TDynQuery, открывающий таблицу "biolife" из DbDemos, входящую в стандартную поставку Delphi. После этого изменяется по кликанью на заголовке (Title) грида меняется сортировка таблицы:
Код:
unit Unit1;


interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, DB, Grids, DBGrids, DBTables, StdCtrls, DynQuery;

type
TForm1 = class(TForm)
DataSource1: TDataSource;
DBGrid1: TDBGrid;
procedure FormCreate(Sender: TObject);
procedure DBGrid1TitleClick(Column: TColumn);
private
Query1: TDynQuery;
public
{ Public declarations }
end;

var
Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
begin
Query1 := TDynQuery.Create (Self);
DataSource1.DataSet := Query1;
Query1.DatabaseName := 'DbDemos';
Query1.Clause [ciSelect] := '*';
Query1.Clause [ciFrom] := '"biolife" b';
Query1.Clause [ciOrderBy] := 'b."category"';
Query1.Active := TRUE;
end;

procedure TForm1.DBGrid1TitleClick(Column: TColumn);
var
f: TField;
begin
f := DbGrid1.SelectedField;
// как и было обещано, порядок сортировки меняется одной строкой :)
Query1.Clause [ciOrderBy] := 'b."' + Column.FieldName + '"';
DbGrid1.SelectedField := f;
end;

end.
Как и обычно, все вопросы/замечания принимаются на e-mail, в ICQ (# 89576939) и форум. Удачи.

best regards,
x77.
Information
  • Posted on 31.01.2010 20:53
  • Просмотры: 2155