unit Un_TpsCachedUpdates;

interface
uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs,
  Db, DBTables, RxQuery,Math,Consts;

type

   TcuOnUpdateRecord = procedure (DataSet:TDataSet) of object;
   TcuOnBeforeApplyCancel = procedure (Sender:TObject;var DoAction:Boolean) of object;
   TcuOnAfterApplyCancel = procedure (Sender:TObject) of object;

   TpsCachedUpdates = class;

   TDataSetMember = class (TPersistent)
   private
       FpsCachedUpdates:TpsCachedUpdates;
       FDataSet:TDBDataSet;
       FCheckUpdate:Boolean;
       FCheckDelete:Boolean;
       procedure SetDataSet(const Value: TDBDataSet);
   published
       property DataSet:TDBDataSet read FDataSet write SetDataSet;
       property CheckUpdate:Boolean read FCheckUpdate write FCheckUpdate;
       property CheckDelete:Boolean read FCheckDelete write FCheckDelete;
   end;

   TpsCachedUpdates = class (TComponent)
   private
      FDataSets:array[1..10] of TDataSetMember;
      FRxQuery:TRxQuery;
      FSQLScript:TSQLScript;
      FOnDelete: TcuOnUpdateRecord;
      FOnInsert: TcuOnUpdateRecord;
      FOnUpdate: TcuOnUpdateRecord;
      FOnAfterApply: TcuOnAfterApplyCancel;
      FOnBeforApply: TcuOnBeforeApplyCancel;
      FOnBeforCancel: TcuOnBeforeApplyCancel;
      FOnAfterCancel: TcuOnAfterApplyCancel;
      FCanModifyOnly,FCanDeleteOnly: Integer;
      function GetTransaction: Boolean;
      procedure SetTransaction(const Value: Boolean);
      function GetDataset(Index:Integer):TDataSetMember;
      procedure SetDataSet(Index:Integer;Value:TDataSetMember);
      procedure FillSQL(DataSet: TDataSet;UpdateKind: TUpdateKind; var UpdateAction: TUpdateAction);
      function GetDatabaseName: string;
      procedure SetDatabaseName(const Value: string);
      procedure SetOnDelete(const Value: TcuOnUpdateRecord);
      procedure SetOnInsert(const Value: TcuOnUpdateRecord);
      procedure SetOnUpdate(const Value: TcuOnUpdateRecord);
      procedure SetOnAfterApply(const Value: TcuOnAfterApplyCancel);
      procedure SetOnAfterCancel(const Value: TcuOnAfterApplyCancel);
      procedure SetOnBeforeApply(const Value: TcuOnBeforeApplyCancel);
      procedure SetOnBeforeCancel(const Value: TcuOnBeforeApplyCancel);
   protected
      procedure Notification(AComponent: TComponent; Operation: TOperation);override;
   public
      constructor Create(AOwner:TComponent);override;
      destructor Destroy;override;
      procedure ApplyUpdates(FirstSQLs,LastSQLs:array of TStrings;ToScript:TSQLScript=nil);
      procedure CancelUpdates;
      function UpdatesPending(Index:Integer=-1):Boolean;
      procedure GetLastSQL(var SQL:TStrings);
   published
      property DataSet01:TDataSetMember index 01 read GetDataSet write SetDataSet;
      property DataSet02:TDataSetMember index 02 read GetDataSet write SetDataSet;
      property DataSet03:TDataSetMember index 03 read GetDataSet write SetDataSet;
      property DataSet04:TDataSetMember index 04 read GetDataSet write SetDataSet;
      property DataSet05:TDataSetMember index 05 read GetDataSet write SetDataSet;
      property DataSet06:TDataSetMember index 06 read GetDataSet write SetDataSet;
      property DataSet07:TDataSetMember index 07 read GetDataSet write SetDataSet;
      property DataSet08:TDataSetMember index 08 read GetDataSet write SetDataSet;
      property DataSet09:TDataSetMember index 09 read GetDataSet write SetDataSet;
      property DataSet10:TDataSetMember index 10 read GetDataSet write SetDataSet;
      property Transaction:Boolean read GetTransaction write SetTransaction;
      property DatabaseName:string read GetDatabaseName write SetDatabaseName;
      property OnDelete:TcuOnUpdateRecord read FOnDelete write SetOnDelete;
      property OnInsert:TcuOnUpdateRecord read FOnInsert write SetOnInsert;
      property OnUpdate:TcuOnUpdateRecord read FOnUpdate write SetOnUpdate;
      property OnBeforApply:TcuOnBeforeApplyCancel read FOnBeforApply write SetOnBeforeApply;
      property OnAfterApply:TcuOnAfterApplyCancel read FOnAfterApply write SetOnAfterApply;
      property OnBeforCancel:TcuOnBeforeApplyCancel read FOnBeforCancel write SetOnBeforeCancel;
      property OnAfterCancel:TcuOnAfterApplyCancel read FOnAfterCancel write SetOnAfterCancel;
      property CanModifyOnly:Integer read FCanModifyOnly write FCanModifyOnly default 1;
      property CanDeleteOnly:Integer read FCanDeleteOnly write FCanDeleteOnly default 1;
   end;

function Get16(Str:string):string;
function RealSQL(Q:TRxQuery):string;

implementation

uses RxStrUtils,Variants;

{$R TpsCachedUpdates.RES}
//Перевод строки в шестнадцатеричный вид
function Get16(Str:string):string;
var
  I:Integer;
begin
  Result:='0x';
  for I:=1 to Length(Str) do
    Result:=Result+Dec2Hex(Ord(Str[I]),2);
end;


//Замена макросов в RxQuery работает неправильно.
function RealSQL(Q:TRxQuery):string;
const
   ValidChars:set of Char=['A'..'Z','a'..'z','_'];
type
   TValue = record
               Start,Length:Integer;
               Text:string;
            end;
   PValue = ^TValue;
var
   I,K:Integer;
   OutStr:string;
   Pref:Char;
   pMacro:TParam;
   lBuf:TList;
   Value:PValue;
begin
   lBuf:=TList.Create;
   try
      Pref:=Q.MacroChar;
      OutStr:=Q.SQL.Text;
      I:=Pos(Pref,OutStr);
      while I>0 do begin
        K:=I+1;
        while (OutStr[K] in ValidChars) do Inc(K);
        pMacro:=Q.Macros.FindParam(Copy(OutStr,I+1,K-I-1));
        if (pMacro<>nil) then begin
          New(Value);
          with Value^ do begin
            Start:=I;
            Length:=K-I;
            if pMacro.IsNull or (pMacro.AsString='0=0') then
              Text:='NULL'
            else Text:=pMacro.AsString;
          end;
          lBuf.Add(Value);
        end;
        I:=Pos(Pref,Copy(OutStr,K,Length(OutStr)-K+1));
        if (I>0) then I:=I+K-1;
      end;
      K:=0;
      for I:=0 to lBuf.Count-1 do begin
        Value:=PValue(lBuf[I]);
        Delete(OutStr,Value^.Start+K,Value^.Length);
        Insert(Value^.Text,OutStr,Value^.Start+K);
        K:=K+Length(Value^.Text)-Value^.Length;
      end;
      Result:=OutStr;
   finally
      for I:=0 to lBuf.Count-1 do
        Dispose(PValue(lBuf[I]));
      lBuf.Free;
   end;
end;



{ TCachedUpdates }

procedure TpsCachedUpdates.ApplyUpdates(FirstSQLs,LastSQLs:array of TStrings;ToScript:TSQLScript=nil);
var
   I:Integer;
   Buf:TUpdateRecordEvent;
   DoApply:Boolean;
begin
   try
      DoApply:=UpdatesPending;
      if DoApply and Assigned(FOnBeforApply) then
         FOnBeforApply(Self,DoApply);
      if DoApply then
       begin
          FSQLScript.SQL.Clear;
          for I:=Low(FirstSQLs) to High(FirstSQLs) do
           begin
              FSQLScript.SQL.AddStrings(FirstSQLs[I]);
              FSQLScript.SQL.Add (FSQLScript.Term);
           end;
          for I:=Low(FDataSets) to High(FDataSets) do
           if (FDataSets[I].DataSet<>nil) and (FDataSets[I].DataSet.Active) and (FDataSets[I].DataSet.UpdatesPending) then
            begin
               Buf:=FDataSets[I].DataSet.OnUpdateRecord;
               FDataSets[I].DataSet.OnUpdateRecord:=FillSQL;
               try
                  FDataSets[I].DataSet.ApplyUpdates;
                  FDataSets[I].DataSet.OnUpdateRecord:=Buf;
               except
                  FDataSets[I].DataSet.OnUpdateRecord:=Buf;
                  Raise;
               end;
            end;
          for I:=Low(LastSQLs) to High(LastSQLs) do
           begin
              FSQLScript.SQL.AddStrings(LastSQLs[I]);
              FSQLScript.SQL.Add (FSQLScript.Term);
           end;
          if Trim(FSQLScript.SQL.Text)<>'' then
           begin
              if ToScript=nil then
                 FSQLScript.ExecSQL
              else ToScript.SQL.AddStrings(FSQLScript.SQL);
              FSQLScript.SQL.Add(Name);
              FSQLScript.SQL.Add(FormatDateTime('dd_mm_yyyy__hh_nn_ss',Now));
              FSQLScript.SQL.SaveToFile (ExtractFilePath(Application.ExeName)+'LastSQL.txt');
           end;
          for I:=Low(FDataSets) to High(FDataSets) do
           if (FDataSets[I].DataSet<>nil) and (FDataSets[I].DataSet.Active) then
              FDataSets[I].DataSet.CommitUpdates;
          if Assigned(FOnAfterApply) then
             FOnAfterApply(Self);
       end;
   except
    on E:Exception do
     begin
        FSQLScript.SQL.Add(Name);
        FSQLScript.SQL.Add('Error:'+E.Message);
        FSQLScript.SQL.SaveToFile (ExtractFilePath(Application.ExeName)+Name+'_'+FormatDateTime('dd_mm_yyyy__hh_nn_ss',Now)+'.txt');
        Raise;
     end;
   end;
end;

procedure TpsCachedUpdates.CancelUpdates;
var
   I:Integer;
   DoCancel:Boolean;
begin
   DoCancel:=True;
   if DoCancel and Assigned(FOnBeforCancel) then
       FOnBeforCancel(Self,DoCancel);
   if DoCancel then
    begin
       for I:=Low(FDataSets) to High(FDataSets) do
        if (FDataSets[I].DataSet<>nil) and (FDataSets[I].DataSet.Active) then
           FDataSets[I].DataSet.CancelUpdates;
       if Assigned(FOnAfterCancel) then
           FOnAfterCancel(Self);
    end;
end;

constructor TpsCachedUpdates.Create(AOwner: TComponent);
var
   I:Integer;
begin
  FCanModifyOnly:=1;
  FCanDeleteOnly:=1;
  for I:=Low(FDataSets) to High(FDataSets) do
   begin
      FDataSets[I]:=TDataSetMember.Create;
      FDataSets[I].FpsCachedUpdates:=Self;
   end;
  inherited;
  FRxQuery:=TRxQuery.Create(Self);
  with FRxQuery do
   begin
      MacroChar:=':';
   end;
  FSQLScript:=TSQLScript.Create(Self);
  with FSQLScript do
   begin
      SemicolonTerm:=False;
   end;
  Transaction:=True;
end;

procedure TpsCachedUpdates.FillSQL(DataSet: TDataSet;UpdateKind: TUpdateKind; var UpdateAction: TUpdateAction);
var
   Data:TDBDataSet;
   UpdData:TUpdateSQL;
   I:Integer;
   CurParam:TParam;
   CurField:string;
   CurValue:Variant;
   CheckUpdate,CheckDelete:Boolean;
begin
   case UpdateKind of
      ukModify:if Assigned(FOnUpdate) then FOnUpdate(DataSet);
      ukInsert:if Assigned(FOnInsert) then FOnInsert(DataSet);
      ukDelete:if Assigned(FOnDelete) then FOnDelete(DataSet);
   end;
   Data:=TDBDataSet(DataSet);
   UpdData:=TUpdateSQL(Data.UpdateObject);
   FRxQuery.SQL.Assign (UpdData.SQL[UpdateKind]);
   for I:=0 to FRxQuery.MacroCount-1 do
    begin
       CurParam:=FRxQuery.Macros[I];
       if UpperCase(Copy(CurParam.Name,1,Min(4,Length(CurParam.Name))))<>'OLD_' then
        begin
           CurField:=CurParam.Name;
           CurValue:=Data.FieldByName(CurField).NewValue;
        end
       else
        begin
           CurField:=Copy(CurParam.Name,5,Length(CurParam.Name)-4);
           CurValue:=Data.FieldByName(CurField).OldValue;
        end;
       if (Data.FieldByName(CurField) is TBlobField)and(TBlobField(Data.FieldByName(CurField)).BlobType<>ftMemo)or(Data.FieldByName(CurField) is TStringField) then
        begin
           CurParam.DataType:=ftString;
           if (CurValue=null) then
              CurParam.AsString:='NULL'
           else CurParam.AsString:=Get16(String(CurValue));
        end
       else if (Data.FieldByName(CurField) is TBlobField)and(TBlobField(Data.FieldByName(CurField)).BlobType=ftMemo) then
        begin
           CurParam.DataType:=ftString;
           if (CurValue=null) then
              CurParam.AsString:='NULL'
           else CurParam.AsString:='convert(varchar(8000),'+Get16(String(CurValue))+')';
        end
       else
        case VarType(CurValue) of
          varNull   :begin
                        CurParam.DataType:=ftString;
                        CurParam.AsString:='NULL';
                     end;
          varString :begin
                        CurParam.DataType:=ftString;
                        CurParam.AsString:=''''+String(CurValue)+'''';
                     end;
          varDate   :begin
                        CurParam.DataType:=ftString;
                        CurParam.AsString:=''''+FormatDateTime('dd-mm-yyyy hh:nn:ss',TDateTime(CurValue))+'''';
                     end;
          varInteger :begin
                         CurParam.DataType:=ftInteger;
                         CurParam.AsInteger:=Integer(CurValue);
                      end;
          varDouble,
          varCurrency:begin
                         CurParam.DataType:=ftCurrency;
                         CurParam.AsFloat:=Double(CurValue);
                      end;
        else
           CurParam.DataType:=Data.FieldByName(CurField).DataType;
           CurParam.Value:=CurValue;
        end;
    end;
    //Заполнение параметров значениями
    // RxQuery неправильно заполняет Macro,если например :id1,:id.
    FRxQuery.SQL.Text:=RealSQL(FRxQuery);
    
    CheckUpdate:=False;
    CheckDelete:=False;
    if (UpdateKind in [ukModify,ukDelete]) and (FCanModifyOnly>=0) then
     for I:=Low(FDataSets) to High(FDataSets) do
      if (FDataSets[I].DataSet<>nil)and(FDataSets[I].DataSet=DataSet) then
       begin
          CheckUpdate:=FDataSets[I].CheckUpdate and (UpdateKind=ukModify);
          CheckDelete:=FDataSets[I].CheckDelete and (UpdateKind=ukDelete);
          Break;
       end;

    with FSQLScript.SQL do
     begin
        if CheckUpdate or CheckDelete then
         begin
            Add('--DataSet.Name='+DataSet.Owner.Name+'.'+DataSet.Name);
            Add('SET NoCount ON            ');
            Add('DECLARE @Count AS INT     ');
         end;
        AddStrings(FRxQuery.RealSQL);
        if CheckUpdate then
         begin
            Add('SELECT @Count=@@ROWCOUNT  ');
            Add('IF @COUNT=0               ');
            Add('   RAISERROR (''Нет записей для обновления.'',16,1)   ');
            Add('ELSE IF @COUNT<>'+IntToStr(FCanModifyOnly));
            Add(' BEGIN                    ');
            Add('    DECLARE @Msg AS VARCHAR(50)   ');
            Add('    SELECT @Msg='''+DataSet.Owner.Name+'.'+DataSet.Name+':Попытка обновить ''+CONVERT(VARCHAR(10),@Count)+'' записи.''  ');
            Add('    RAISERROR (@Msg,16,1) ');
            Add(' END                      ');
            Add('SET NOCOUNT OFF           ');
         end
        else if CheckDelete then
         begin
            Add('SELECT @Count=@@ROWCOUNT  ');
            Add('IF @COUNT=0               ');
            Add('   RAISERROR (''Нет записей для удаления.'',16,1)   ');
            Add('ELSE IF @COUNT<>'+IntToStr(FCanDeleteOnly));
            Add(' BEGIN                    ');
            Add('    DECLARE @Msg AS VARCHAR(50)   ');
            Add('    SELECT @Msg='''+DataSet.Owner.Name+'.'+DataSet.Name+':Попытка удалить ''+CONVERT(VARCHAR(10),@Count)+'' записи.''  ');
            Add('    RAISERROR (@Msg,16,1) ');
            Add(' END                      ');
            Add('SET NOCOUNT OFF           ');
         end;
        Add(FSQLScript.Term);
     end;
    UpdateAction:=uaApplied;
end;

function TpsCachedUpdates.GetDatabaseName: string;
begin
   Result:=FSQLScript.DatabaseName;
end;

function TpsCachedUpdates.GetDataset(Index: Integer): TDataSetMember;
begin
   Result:=FDataSets[Index];
end;

procedure TpsCachedUpdates.GetLastSQL(var SQL: TStrings);
begin
   SQL.Assign (FSQLScript.SQL);
end;

function TpsCachedUpdates.GetTransaction: Boolean;
begin
   Result:=FSQLScript.Transaction;
end;

function TpsCachedUpdates.UpdatesPending(Index:Integer=-1): Boolean;
var
   I:Integer;
   CurData:TDBDataSet;
begin
   if Index=-1 then
    begin
       Result:=False;
       for I:=Low(FDataSets) to High(FDataSets) do
        begin
           CurData:=FDataSets[I].DataSet;
           if (CurData<>nil)and(CurData.Active) then
            begin
               if (CurData.State in [dsEdit,dsInsert]) then
                  CurData.Post;
               Result:=Result or (CurData.UpdatesPending);
            end;
        end;
    end
   else begin
      CurData:=FDataSets[Low(FDataSets)+Index].DataSet;
      Result:=(CurData<>nil)and(CurData.Active)and(CurData.UpdatesPending);
   end;
end;

procedure TpsCachedUpdates.SetDatabaseName(const Value: string);
begin
   FSQLScript.DatabaseName:=Value;
end;

procedure TpsCachedUpdates.SetDataSet(Index: Integer;Value:TDataSetMember);
var
   I:Integer;
begin
   if (csLoading in ComponentState) then
       FDataSets[Index]:=Value
   else begin
      if FDataSets[Index].DataSet=Value.DataSet then Exit;
      if Value.DataSet=nil then
       begin
          FDataSets[Index]:=nil;
          Exit;
       end;
      //Проверка
      for I:=Low(FDataSets) to High(FDataSets) do
         if (FDataSets[I].DataSet=Value.DataSet) then
            Raise Exception.Create('Этот компонент уже есть.');
      if not Value.DataSet.CachedUpdates then
         Raise Exception.Create('У компонента св-о CachedUpdates должно быть True')
      else if Value.DataSet.UpdateObject=nil then
         Raise Exception.Create('У компонента должен быть UpdateObject');
      //Запись
      FDataSets[Index]:=Value;
   end;
end;

procedure TpsCachedUpdates.SetOnAfterApply(const Value: TcuOnAfterApplyCancel);
begin
  FOnAfterApply := Value;
end;

procedure TpsCachedUpdates.SetOnAfterCancel(const Value: TcuOnAfterApplyCancel);
begin
  FOnAfterCancel := Value;
end;

procedure TpsCachedUpdates.SetOnBeforeApply(const Value: TcuOnBeforeApplyCancel);
begin
  FOnBeforApply := Value;
end;

procedure TpsCachedUpdates.SetOnBeforeCancel(const Value: TcuOnBeforeApplyCancel);
begin
  FOnBeforCancel := Value;
end;

procedure TpsCachedUpdates.SetOnDelete(const Value: TcuOnUpdateRecord);
begin
  FOnDelete := Value;
end;

procedure TpsCachedUpdates.SetOnInsert(const Value: TcuOnUpdateRecord);
begin
  FOnInsert := Value;
end;

procedure TpsCachedUpdates.SetOnUpdate(const Value: TcuOnUpdateRecord);
begin
  FOnUpdate := Value;
end;

procedure TpsCachedUpdates.SetTransaction(const Value: Boolean);
begin
   FSQLScript.Transaction:=Value;
end;

{ TDataSetMember }

procedure TDataSetMember.SetDataSet(const Value: TDBDataSet);
var
   I:Integer;
begin
   if (csLoading in FpsCachedUpdates.ComponentState) then
       FDataSet:=Value
   else begin
      if FDataSet=Value then Exit;
      if Value=nil then
       begin
          FDataSet:=nil;
          FCheckUpdate:=False;
          FCheckDelete:=False;
          Exit;
       end;
      //Проверка
      for I:=Low(FpsCachedUpdates.FDataSets) to High(FpsCachedUpdates.FDataSets) do
         if (FpsCachedUpdates.FDataSets[I].DataSet=Value) then
            Raise Exception.Create('Этот компонент уже есть.');
      if not Value.CachedUpdates then
         Raise Exception.Create('У компонента св-о CachedUpdates должно быть True')
      else if Value.UpdateObject=nil then
         Raise Exception.Create('У компонента должен быть UpdateObject');
      //Запись
      FDataSet:=Value;
      FCheckUpdate:=True;
      FCheckDelete:=True;
   end;
end;

destructor TpsCachedUpdates.Destroy;
var
   I:Integer;
begin
  inherited;
  for I:=Low(FDataSets) to High(FDataSets) do
     FDataSets[I].Free;
end;

procedure TpsCachedUpdates.Notification(AComponent: TComponent;Operation: TOperation);
var
   I:Integer;
begin
  inherited;
  if (Operation=opRemove)and(AComponent is TDBDataSet) then
   for I:=Low(FDataSets) to High(FDataSets) do
    if (FDataSets[I].DataSet=AComponent) then
     begin
        FDataSets[I].DataSet:=nil;
        Break;
     end;
end;

end.


Сайт управляется системой uCoz