Как выполнить сценарий SQL с помощью dbExpress?

Я переношу старое приложение Delphi (используя ZeosDB) на Delphi XE2. Я хочу использовать dbExpress в качестве замены ZeosDB для доступа к базе данных Firebird 2.5 или MS-SQL. Мне нужно запустить множество sql-скриптов для создания таблиц, представлений и хранимых процедур. Команды сценария Firebird разделяются символом ^, команды сценария MS-SQL - "GO".

Как я могу запустить эти сценарии в базе данных, используя соединение dbexpress? ZeosDB предоставляет TZSqlProcessor, но я не могу найти эквивалентного компонента для dbExpress.


person cytrinox    schedule 01.03.2012    source источник
comment
избавьте себя от множества головных болей и используйте сторонний комплект коммерческих компонентов, всего два цента ...   -  person    schedule 06.03.2012
comment
Следует отметить иронию: в Delphi XE2 (и некоторых других более новых версиях) вкладка, на которой находятся все компоненты ADO, помечена как dbGo, но ничего там (о чем я знаю) не поддерживает использование оператора GO .. .   -  person Jerry Dodge    schedule 11.03.2012


Ответы (5)


Я не использую DBExpress, но, насколько мне известно, вы можете выполнять (с помощью Execute или ExecuteDirect) только одну команду SQL за раз. Другими словами, вы не можете поместить весь скрипт в метод Execute.

Это не связано с другим синтаксисом команд, используемым FireBird и MS SQL (^ vs. GO). Вы должны понимать, что знак «^» или команда «GO» не является «командой TSQL»! Оба являются определенными разделителями команд, используемыми соответствующим приложением для выполнения команд с механизмами SQL. Вместо этого это разница между «Firebird Manager» (или как он называется) и «SQL Query Profiler» (или «SQL Server Management Studio»).

Решение состоит в том, чтобы использовать какой-то синтаксический анализатор, разделить скрипт на список отдельных команд и выполнить TSQLConnection. Выполните эти команды одну за другой.

Что-то вроде этого псевдокода:

var
  DelimiterPos: Integer;
  S: String;
  Command: String;
begin
  S:= ScriptFile; // ScriptFile: String - your whole script
  While True Do
  begin
    DelimiterPos:= Pos('^', ScriptFile);
    if DelimiterPos = 0 then DelimiterPos:= Length(S);
    Command:= Copy(S, 1, DelimiterPos - 1);
    SQLConnection.Execute(Command);
    Delete(S, 1, DelimiterPos);
    if Lengh(S) = 0 Then Exit;
  end;
end;

Обратите внимание, что приведенный выше пример будет работать правильно только в тех случаях, когда знак '^' не используется нигде в скрипте, а используется в качестве разделителя команд.

В качестве примечания: я уверен, что есть некоторые уже созданные компоненты, которые сделают это за вас (например, TZSQLProcessor). Я не знаю никого, на кого можно было бы указать.

Примечание 2: я почти уверен, что вам придется изменить свои скрипты, чтобы они были полностью совместимы с MS SQL. Несмотря на то, что Firebird и MS SQL являются серверами SQL, всегда есть разница в синтаксисе DML / DDL.

Изменить:

  1. Если вы можете «переписать» сценарий SQL в код, вы можете использовать компонент Jedi VCL jvStringHolder. Поместите каждую отдельную команду как один элемент (типа TStrings) в jvStringHolder.

  2. Создание парсера довольно сложно, но отменить его невозможно. Вдохновленный SynEdit, я сделал эти классы именно тем, что вам нужно: загрузите скрипт с помощью TSQLScript.ParseScript, затем выполните итерацию по свойству Command [index: integer]. SQLLexer не является полноценным лексером SQL, но реализует разделение ключевых слов с учетом комментариев, скобок, сворачивания кода и т. Д. Я также добавил специальный синтаксис в комментарии (знак $ в блоке комментариев), который помогает мне вставлять заголовки в скрипт. Это полный копипаст из одного из моих проектов. Я не буду давать никаких дополнительных объяснений, но надеюсь, что вы сможете понять эту идею и воплотить ее в своем проекте.

unit SQLParser;

interface

type

  TTokenKind = (tkUknown, tkEOF, tkComment, tkKeyword, tkIdentifier,
                tkCommentParam, tkCommentParamValue, tkCommandEnd, tkCRLF);

  TBlockKind = (bkNone, bkLineComment, bkBlockComment);

  TSQLLexer = class
  private
    FBlockKind: TBlockKind;
    FParseString: String;
    FPosition: PChar;
    FTokenKind: TTokenKind;
    FTokenPosition: PChar;
    function GetToken: String;
    procedure Reset;
    procedure SetParseString(Value: String);
  protected
    procedure ReadComment;
    procedure ReadCommentParam;
    procedure ReadCommentParamValue;
    procedure ReadCRLF;
    procedure ReadIdentifier;
    procedure ReadSpace;
  public
    constructor Create(ParseString: String);
    function NextToken: TTokenKind;

    property Position: PChar read FPosition;
    property SQLText: String read FParseString write SetParseString;
    property Token: String read GetToken;
    property TokenKind: TTokenKind read FTokenKind;
    property TokenPosition: PChar read FTokenPosition;
  end;



implementation

uses SysUtils;

{ TSQLLexer }

constructor TSQLLexer.Create(ParseString: string);
begin
  inherited Create;
  FParseString:= ParseString;
  Reset;
end;

function TSQLLexer.GetToken;
begin
  SetString(Result, FTokenPosition, FPosition - FTokenPosition);
end;

function TSQLLexer.NextToken: TTokenKind;
begin
  case FBlockKind of
    bkLineComment, bkBlockComment: ReadComment;
    else
      case FPosition^ of
      #0: FTokenKind:= tkEOF;
      #1..#9, #11, #12, #14..#32:
        begin
          ReadSpace;
          NextToken;
        end;
      #10, #13: ReadCRLF;
      '-':
        if PChar(FPosition +1)^ = '-' then
          ReadComment
        else
          Inc(FPosition);
      '/':
        if PChar(FPosition +1)^ = '*' then
          ReadComment
        else
          Inc(FPosition);
      'a'..'z', 'A'..'Z': ReadIdentifier;
      ';':
        begin
          FTokenPosition:= FPosition;
          Inc(FPosition);
          FTokenKind:= tkCommandEnd;
        end
      else
        Inc(FPosition);
      end;
  end;
  Result:= FTokenKind;
end;


procedure TSQLLexer.ReadComment;
begin
  FTokenPosition:= FPosition;
  if not (FBlockKind in [bkLineComment, bkBlockComment])  then
  begin
    if FPosition^ = '/' then
      FBlockKind:= bkBlockComment
    else
      FBlockKind:= bkLineComment;
    Inc(FPosition, 2);
  end;
  case FPosition^ of
    '$': ReadCommentParam;
    ':': ReadCommentParamValue;
  else
    while not CharInSet(FPosition^, [#0, '$']) do
    begin
      if FBlockKind = bkBlockComment then
      begin
        if (FPosition^ = '*') And (PChar(FPosition + 1)^ = '/') then
        begin
          Inc(FPosition, 2);
          FBlockKind:= bkNone;
          Break;
        end;
      end
      else
      begin
        if CharInSet(Fposition^, [#10, #13]) then
        begin
          ReadCRLF;
          FBlockKind:= bkNone;
          Break;
        end;
      end;
      Inc(FPosition);
    end;
    FTokenKind:= tkComment;
  end;
end;

procedure TSQLLexer.ReadCommentParam;
begin
  Inc(FPosition);
  ReadIdentifier;
  FTokenKind:= tkCommentParam;
end;

procedure TSQLLexer.ReadCommentParamValue;
begin
  Inc(FPosition);
  ReadSpace;
  FTokenPosition:= FPosition;
  while not CharInSet(FPosition^, [#0, #10, #13]) do
    Inc(FPosition);
  FTokenKind:= tkCommentParamValue;
end;

procedure TSQLLexer.ReadCRLF;
begin
  while CharInSet(FPosition^, [#10, #13]) do
    Inc(FPosition);
  FTokenKind:= tkCRLF;
end;

procedure TSQLLexer.ReadIdentifier;
begin
  FTokenPosition:= FPosition;
  while CharInSet(FPosition^, ['a'..'z', 'A'..'Z', '_']) do
    Inc(FPosition);

  FTokenKind:= tkIdentifier;

  if Token = 'GO' then
    FTokenKind:= tkKeyword;
end;

procedure TSQLLexer.ReadSpace;
begin
  while CharInSet(FPosition^, [#1..#9, #11, #12, #14..#32]) do
  Inc(FPosition);
end;

procedure TSQLLexer.Reset;
begin
  FTokenPosition:= PChar(FParseString);
  FPosition:= FTokenPosition;
  FTokenKind:= tkUknown;
  FBlockKind:= bkNone;
end;

procedure TSQLLexer.SetParseString(Value: String);
begin
  FParseString:= Value;
  Reset;
end;

end.

Парсер:

type
  TScriptCommand = class
  private
    FCommandText: String;
  public
    constructor Create(ACommand: String);
    property CommandText: String read FCommandText write FCommandText;
  end;

  TSQLScript = class
  private
    FCommands: TStringList;
    function GetCount: Integer;
    function GetCommandList: TStrings;
    function GetCommand(index: Integer): TScriptCommand;
  protected
    procedure AddCommand(AName: String; ACommand: String);
  public
    Constructor Create;
    Destructor Destroy; override;
    procedure ParseScript(Script: TStrings);

    property Count: Integer read GetCount;
    property CommandList: TStrings read GetCommandList;
    property Command[index: integer]: TScriptCommand read GetCommand;
  end;

{ TSQLScriptCommand }

constructor TScriptCommand.Create(ACommand: string);
begin
  inherited Create;
  FCommandText:= ACommand;
end;

{ TSQLSCript }

constructor TSQLScript.Create;
begin
  inherited;
  FCommands:= TStringList.Create(True);
  FCommands.Duplicates:= dupIgnore;
  FCommands.Sorted:= False;
end;

destructor TSQLScript.Destroy;
begin
  FCommands.Free;
  inherited;
end;

procedure TSQLScript.AddCommand(AName, ACommand: String);
var
  ScriptCommand: TScriptCommand;
  S: String;
begin
  if AName = '' then
    S:= SUnnamedCommand
  else
    S:= AName;
  ScriptCommand:= TScriptCommand.Create(ACommand);
  FCommands.AddObject(S, ScriptCommand);
end;

function TSQLScript.GetCommand(index: Integer): TScriptCommand;
begin
  Result:= TScriptCommand(FCommands.Objects[index]);
end;

function TSQLScript.GetCommandList: TStrings;
begin
  Result:= FCommands;
end;

function TSQLScript.GetCount: Integer;
begin
  Result:= FCommands.Count;
end;

procedure TSQLScript.ParseScript(Script: TStrings);
var
  Title: String;
  Command: String;
  LastParam: String;
  LineParser: TSQLLexer;
  IsNewLine: Boolean;
  LastPos: PChar;

  procedure AppendCommand;
  var
    S: String;
  begin
    SetString(S, LastPos, LineParser.Position - LastPos);
    Command:= Command + S;
    LastPos:= LineParser.Position;
  end;

  procedure FinishCommand;
  begin
    if Command <> '' then
      AddCommand(Title, Command);
    Title:= '';
    Command:= '';
    LastPos:= LineParser.Position;
    if LastPos^ = ';' then Inc(LastPos);
  end;

begin
  LineParser:= TSQLLexer.Create(Script.Text);
  try
    LastPos:= LineParser.Position;
    IsNewLine:= True;
    repeat
      LineParser.NextToken;
      case LineParser.TokenKind of
        tkComment: LastPos:= LineParser.Position;
        tkCommentParam:
          begin
            LastParam:= UpperCase(LineParser.Token);
            LastPos:= LineParser.Position;
          end;
        tkCommentParamValue:
          if LastParam = 'TITLE' then
          begin
            Title:= LineParser.Token;
            LastParam:= '';
            LastPos:= LineParser.Position;
          end;
        tkKeyword:
            if (LineParser.Token = 'GO') and IsNewLine then FinishCommand
            else
              AppendCommand;
        tkEOF:
          FinishCommand;
        else
          AppendCommand;
      end;
      IsNewLine:= LineParser.TokenKind in [tkCRLF, tkCommandEnd];
    until LineParser.TokenKind = tkEOF;
  finally
    LineParser.Free;
  end;
end;
person Tom Hagen    schedule 05.03.2012
comment
Может ли кто-нибудь указать мне на хороший процессор сценариев sql? Я не могу понять, почему Embarcadero забыл реализовать такую ​​важную функцию. - person cytrinox; 06.03.2012
comment
Второй комментарий: написать парсер намного сложнее. Для ключевого слова GO вы должны написать токенизатор и проверить некоторые правила синтаксиса, чтобы предотвратить несоответствия, такие как CREATE .. - MY GO COMMENT или INSERT ... 'GO FOO BAR'. Вы должны проверить литералы, комментарии и т. Д. Сценарии должны быть совместимы с соответствующими утилитами баз данных, поэтому простая замена GO на ^ или что-то еще не является решением. - person cytrinox; 06.03.2012
comment
Я тоже написал парсер SQL, как уже писали все остальные. Я использую «GO» в единственной строке, чтобы сигнализировать парсеру, что нужно выполнить все, начиная с «последнего GO». Вы можете легко проверить «GO» как одну строку, например: if SameText(Trim(OneLineOfSQL), 'GO') then ExecuteSQLStatement;. - person James L.; 08.03.2012
comment
+1 Я потратил около 4 дней на создание чего-то подобного, чтобы иметь возможность разделить скрипт MSSQL с помощью GO. Хотя тогда я еще учился ... - person Jerry Dodge; 08.03.2012
comment
В одном из моих проектов я фактически создал библиотеку на Delphi, которая автоматически проверяет актуальность всей структуры базы данных, а сценарии SQL встроены в приложение. Он даже распознает версию и соответствующим образом создает базу данных. Библиотека используется в приложении для обновления нашего программного обеспечения. Эта библиотека также стала полезной для других задач в гораздо более крупном проекте, для которого она была разработана. К сожалению, этот проект требует совершенно нового подхода, и я не уверен, что вы готовы к нему перейти. Однако это тоже может оказаться большим преимуществом. - person Jerry Dodge; 08.03.2012

Вам нужно использовать TSQLConnection. У этого компонента есть два метода: Execute и ExecuteDirect. Первый не принимает параметры, а второй - принимает.

По первому способу:

procedure TForm1.Button1Click(Sender: TObject);
var
   MeuSQL: String;
begin
   MeuSQL := 'INSERT INTO YOUR_TABLE ('FIELD1', 'FIELD2') VALUES ('VALUE1', 'VALUE2')';
   SQLConnection.ExecuteDirect(MeuSQL);
end;

При желании можно использовать транзакцию.

Второй способ:

procedure TForm1.Button1Click(Sender: TObject);
var
  MySQL: string;
  MyParams: TParams;
begin
  MySQL := 'INSERT INTO TABLE ("FIELD1", "FIELD2") VALUE (:PARAM1, :PARAM2)';
  MyParams.Create;
  MyParams.CreateParam(ftString, 'PARAM1', ptInput).Value := 'Seu valor1';
  MyParams.CreateParam(ftString, 'PARAM2', ptInput).Value := 'Seu valor2';

  SQLConnection1.Execute(MySQL,MyParams, Nil);
end;
person adrianosantospro    schedule 01.03.2012
comment
[...] для создания таблиц, представлений и хранимых процедур [...] из файла сценария SQL. Ваш ответ не имеет отношения к вопросу! - person cytrinox; 01.03.2012
comment
Конечно, есть. Мой коллега хотел знать, какой компонент использовать для запуска скриптов и сколько денег было потрачено. TSQLConnection - лучший компонент для этого. Однако вы также можете использовать TStoredProc для подключения или запуска хранимых процедур скриптов. - person adrianosantospro; 03.03.2012
comment
Как построить оператор SQL для второго сценария, если я хочу использовать предложение order by? - person Dev; 24.05.2012

Я примерно на 90% уверен, что вы не можете этого сделать, по крайней мере, без разбора отдельных команд между GO и последующего последовательного выполнения каждой из них, что, как вы уже указали, проблематично.

(Я был бы счастлив, если меня опровергли вышесказанное, и мне было бы интересно увидеть решение ...)

Если вы просто используете сценарий в качестве логики инициализации (например, для создания таблиц и т. Д.), Другим решением, которое вы могли бы рассмотреть, было бы запускать сценарии в пакетном файле и выполнять их через 'Sqlcmd', который может быть выполнен через ваш delphi app (с помощью ShellExecute), который затем ожидает его завершения, прежде чем продолжить.

Не так элегантно, как использование компонента, но если это просто для логики инициализации, это может быть быстрый и приемлемый компромисс. Я бы определенно не стал рассматривать вышеизложенное для какой-либо обработки после инициализации.

person Peter    schedule 06.03.2012
comment
Наша компания сочла чрезвычайно полезным использовать OSQL из командных файлов. Это довольно просто, одна команда может выглядеть как OSQL -Usa -P MyPass -S MyServer -d MyDatabase -i MySQLScript.sql -o MyOutputFile.txt (в базе данных MSSQL) - person Jerry Dodge; 11.03.2012
comment
Привет, Джерри, да, это именно то, о чем я имел в виду, за исключением, конечно, того, что SqlCmd заменил osql. - person Peter; 12.03.2012

Это не ограничение dbExpress, а ограничение языка SQL. Я не уверен насчет T-SQL, но похоже, что GO похож на анонимный блок в Oracle PL / SQL. Вы можете поместить следующий код PL / SQL в TSqlDataSet.CommandText и вызвать ExecSQL для создания нескольких таблиц. Возможно, в T-SQL есть аналогичный способ сделать это:

begin
execute immediate 'CREATE TABLE suppliers 
( supplier_id number(10) not null, 
  supplier_name varchar2(50) not null, 
  contact_name varchar2(50)  
)'; 
execute immediate 'CREATE TABLE customers 
( customer_id number(10) not null, 
  customer_name varchar2(50) not null, 
  address varchar2(50),  
  city varchar2(50),  
  state varchar2(25),  
  zip_code varchar2(10),  
  CONSTRAINT customers_pk PRIMARY KEY (customer_id) 
)';
end;
person Stewart    schedule 06.03.2012

Я не знаю, как часто вам нужно создавать эти таблицы, но как насчет помещения всех отдельных сценариев создания SQL в таблицу с последовательной нумерацией / нумерацией версий? Затем вы можете просмотреть эту таблицу и выполнить команду один за другим. Вам нужно будет один раз разделить свои скрипты, но после этого будет намного удобнее поддерживать.

person Jan Doggen    schedule 06.03.2012