SQLite во многих случаях является удобным, незаменимым инструментом. Я уже не могу себе представить - как мы все жили без него. Тем не менее, есть некоторые неудобства при его использовании, связанные с тем, что это легкая встраиваемая СУБД.

Самое большое неудобство для меня, как Delphi-разработчика - отсутствие хранимых процедур. Я очень не люблю смешивать Delphi-код и SQL-скрипты. Это делает код намного менее читабельным, и затрудняет его поддержку. Следовательно, нужно как-то разнести код Delphi и тексты SQL-скриптов.

Предлагаю свой вариант решения проблемы

  • Выносим весь SQL-код в отдельный тестовый файл ресурсов, подключенный к проекту.

  • Запросы в SQL-файле разделяем маркерами начала с идентификаторами и маркерами конца. В моём случае синтаксис маркера начала - //SQL ИмяПроцедуры. Маркер конца - GO.

  • Создаем класс - менеджер SQL-запросов. При загрузке приложения он читает SQL-файл из ресурсов и составляет из него список хранимых процедур с уникальными именами-идентификаторами.

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

Главная идея - простота и легкость использования, подобная вызову хранимых процедур и удобство при создании и модификации SQL-запросов

Код юнита менеджера запросов:

unit uSqlList;

interface

uses System.Classes, Winapi.Windows, System.SysUtils,
  System.Generics.Collections;

type
  TSqlList = class(TObjectDictionary<string, TStrings>)
  const
    SCRIPTS_RCNAME = 'SqlList';
  private
    function GetScripts(const AName: string): TStrings;
    procedure FillList;
    function GetItem(const AKey: string): string;
  public
    constructor Create;
  public
    property Sql[const Key: string]: string read GetItem; default;
  end;

var
  SqlList: TSqlList;

implementation

function GetStringResource(const AName: string): string;
var
  LResource: TResourceStream;
begin
  LResource := TResourceStream.Create(hInstance, AName, RT_RCDATA);

  with TStringList.Create do
    try
      LoadFromStream(LResource);
      Result := Text;
    finally
      Free;
      LResource.Free;
    end;
end;

{ TScriptList }

constructor TSqlList.Create;
begin
  inherited Create([doOwnsValues]);
  FillList;
end;

procedure TSqlList.FillList;
var
  LScripts: TStrings;
  I: Integer;
  S, LKey: string;
  LStarted: Boolean;
  LSql: TStrings;
begin
  LScripts := GetScripts(SCRIPTS_RCNAME);
  try
    LStarted := False;
    LSql := nil;
    for I := 0 to LScripts.Count - 1 do
    begin
      S := LScripts[I];
      if LStarted then
      begin
        if S = 'GO' then
        begin
          LStarted := False;
          Continue;
        end
        else if not S.StartsWith('//') then
          LSql.Add(S);
      end
      else
      begin
        LStarted := S.StartsWith('//SQL ');
        if LStarted then
        begin
          LKey := S.Substring(6);
          LSql := TStringList.Create;
          Add(LKey, LSql);
        end;
        Continue;
      end;
    end;
  finally
    LScripts.Free;
  end;
end;

function TSqlList.GetItem(const AKey: string): string;
begin
  Result := Items[AKey].Text;
end;

function TSqlList.GetScripts(const AName: string): TStrings;
begin
  Result := TStringList.Create;
  try
    Result.Text := GetStringResource(AName);
  except
    FreeAndNil(Result);
    raise;
  end;
end;

initialization

SqlList := TSqlList.Create;

finalization

FreeAndNil(SqlList);

end.

Пример содержимого файла SQL-скриптов:

//SQL GetOrder
SELECT * FROM Orders WHERE ID = :ID
GO

//SQL DeleteOpenedOrders
DELETE FROM Orders WHERE Closed = 0
GO

Подключение файла скриптов к проекту:

{$R 'SqlList.res' '..\Common\DataBase\SqlList.rc'}

Использование с компонентом TFDConnection:

  Connection.ExecSQL(SqlList['GetOrder'], ['123']);

Собственно, это всё. Использую данное решение уже в нескольких проектах и мне оно кажется очень удобным. Буду благодарен за советы и замечания. Рад, если мой посто кому-то будет полезен!