Delphi-PRAXiS

Delphi-PRAXiS (https://www.delphipraxis.net/forum.php)
-   Object-Pascal / Delphi-Language (https://www.delphipraxis.net/32-object-pascal-delphi-language/)
-   -   Query in Thread abarbeiten (UniDac mit MsSQL) (https://www.delphipraxis.net/191515-query-thread-abarbeiten-unidac-mit-mssql.html)

norwegen60 23. Jan 2017 13:46

Delphi-Version: 10 Seattle

Query in Thread abarbeiten (UniDac mit MsSQL)
 
Hallo,

seit Monaten will ich mich an dieses Thema wagen. Ich habe versucht unterschiedlichste Informationen zu berücksichtigen und habe es auch geschafft dass das Ganze läuft.

Bei dem gezeigten Beispiel geht es zunächst nur um Grundsätzliches. Natürlich wird der Thread später in eine eigene Unit verschoben und auch die Masterdaten entsprechend gehandelt.

Was soll das Programm machen:
  • Sobald man in den Masterdaten blättert, sollen die Detaildaten (ca. 20000) ausgelesen und in einem Chart und einer Virtualstringtree dargestellt werden.
  • Blättert man schnell durch die Daten soll der laufende Thread beendet werden bzw. für die Überblätterten Daten gar nicht erst beginnen
  • Natürlich sollen später auch die Masterdaten weg von der Oberfläche aber zum Testen tut es so.
  • Und auch die Frage, ob man 20000 Werte in einer Tabelle darstellen will, überlasse ich später dem Anwender.
Das funktioniert mit dem gezeigten Code auch ohne Probleme. Das Anzeigen von 20000 Daten in TChart und VST benötigt je 0.1s. Das Ecxecute der Query dauert ca. 1.2s. Eigentlich schon schnell genug um alles zu belassen, aber wie gesagt geht es mir generell um das Thema Threads und DB.

Delphi-Quellcode:
unit fo_ShowData;
{ ******************************************************************************
fo_ShowData
Programm um folgende Dinge zu testen
  * Daten per Thread auslesen und anzeigen
--------------------------------------------------------------------------------
Historie
23.01.17, V1.0.1, GPA, Delphi XE10
  * Verwaltung der Daten in TList unter Verwendung von TMonitor.Enter/Exit
23.01.17, V1.0.0, GPA, Delphi XE10
  * Verwaltung der Daten in TThreadList unter Verwendung von LockList/UnlockList
****************************************************************************** }

interface

uses
  Winapi.Windows, Winapi.Messages, System.SysUtils, System.Variants,
  System.Classes, Vcl.Graphics, ActiveX,
  Generics.Collections,
  Generics.Defaults,
  Vcl.Controls, Vcl.Forms, Vcl.Dialogs, Data.DB, VclTee.TeeGDIPlus,
  VclTee.Chart, VclTee.TeEngine, VclTee.Series, Vcl.StdCtrls, VclTee.TeeProcs,
  VclTee.DBChart, Vcl.Grids, Vcl.DBGrids, VirtualTrees, Vcl.ExtCtrls,
  Vcl.ComCtrls, Nav, MemDS, DBAccess, Uni, UniProvider, SQLServerUniProvider,
  VclTee.TeeXML, VclTee.TeeSeriesTextEd, VclTee.TeeURL, VclTee.TeeExcelSource;

type
  TForm2 = class(TForm)
    paTop: TPanel;
    paData: TPanel;
    StatusBar1: TStatusBar;
    paManuell: TPanel;
    vstData: TVirtualStringTree;
    chForce: TChart;
    Series3: TLineSeries;
    Series4: TLineSeries;
    UniConnection1: TUniConnection;
    SQLServerUniProvider1: TSQLServerUniProvider;
    UniQuery1: TUniQuery;
    DataSource1: TDataSource;
    DBNavigatorSpec1: TDBNavigatorSpec;
    laReport: TLabel;
    procedure FormCreate(Sender: TObject);
    procedure DataSource1DataChange(Sender: TObject; Field: TField);
    procedure vstDataGetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
      Column: TColumnIndex; TextType: TVSTTextType; var CellText: string);
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure vstDataGetNodeDataSize(Sender: TBaseVirtualTree;
      var NodeDataSize: Integer);
    procedure vstDataInitNode(Sender: TBaseVirtualTree;
      ParentNode, Node: PVirtualNode;
      var InitialStates: TVirtualNodeInitStates);
  private
    { Private-Deklarationen }
    procedure CreateThrReadDb(iID: Integer);
  public
    { Public-Deklarationen }
    procedure SyncReady(bValue: Boolean);
  end;

  TThreadBoolean = procedure(bValue: Boolean) of Object;

  PValues = ^TValues;

  TValues = class
    dtTime: TDateTime;
    rForce,
    rFtarget,
    rTemperatur,
    rHumidity: Real;
  End;

  TListData = class(TList<TValues>)
  end;

  TThrReadDb = class(TThread)
  protected
    procedure Execute; override;
  private
    _Ready:      Boolean;
    _ReadyEvent: TThreadBoolean;
    dbcon:       TUniConnection;
    dbProvider:  TSQLServerUniProvider;
    dbQuery:     TUniQuery;
    procedure syncReadyEvent;
  public
    property ReadyEvent: TThreadBoolean read _ReadyEvent write _ReadyEvent;
  end;

var
  Form2:      TForm2;
  lstData:    TListData;
  thrReadDb:  TThrReadDb;
  bThrReadDbIsRunning: Boolean;

implementation

{$R *.dfm}

procedure TForm2.DataSource1DataChange(Sender: TObject; Field: TField);
// Bei jedem Blättervorgang in den Masterdaten Thread zum Auslesen der
// Detaildaten erzeugen
begin
  laReport.Caption := format('%s [%d]', [UniQuery1.FieldByName('Nr').AsString,
    UniQuery1.FieldByName('ID').AsInteger]);

  CreateThrReadDb(UniQuery1.FieldByName('ID').AsInteger)
end;

procedure TForm2.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  lstData.Free;
end;

procedure TForm2.FormCreate(Sender: TObject);
begin
  lstData := TListData.Create;
  UniQuery1.Open;
end;

procedure TForm2.CreateThrReadDb(iID: Integer);
// Thread zum Auslesen der Daten aus DB erzeugen
begin
  // Falls noch ein Thread zum Laden von Daten läuft diesen abbrechen
  if (bThrReadDbIsRunning) then
  begin
   // thrReadDb.dbQuery.BreakExec;    // Führt zu Fehlermeldung "operation was canceled"
    thrReadDb.Terminate;
    bThrReadDbIsRunning := false
  end;

  lstData.Clear;

  bThrReadDbIsRunning := TRUE;
  thrReadDb := TThrReadDb.Create(TRUE);
  thrReadDb.ReadyEvent := Form2.SyncReady;

  { Create suspended--secondProcess does not run yet. }
  thrReadDb.FreeOnTerminate := TRUE;
  { You do not need to clean up after termination. }
  thrReadDb.Priority := tpLower; // Set the priority to lower than normal.

  if not assigned(thrReadDb.dbQuery) then
  begin
    thrReadDb.dbcon := TUniConnection.Create(NIL);
    thrReadDb.dbcon.ProviderName := TSQLServerUniProvider.Create(NIL).Name;
    thrReadDb.dbcon.ConnectString :=
      'Provider Name=SQL Server;Data Source=PsServer;Initial Catalog=ADatabase;Port=0;User ID=sa;Password=THREAD';

    thrReadDb.dbQuery := TUniQuery.Create(NIL);
    thrReadDb.dbQuery.Connection := thrReadDb.dbcon;
  end;

  if thrReadDb.dbQuery.Active then
    thrReadDb.dbQuery.Close;
  thrReadDb.dbQuery.SQL.Text :=
    format('select * from Cal_force where CalReportID = %d order by ID', [iID]);

  thrReadDb.Resume; { thread starten }
end;

procedure TForm2.SyncReady(bValue: Boolean);
// Daten der kompletten Liste VST und TChart zuweisen
var
  i:     Integer;
  Values: TValues;

begin
  vstData.NodeDataSize := SizeOf(TValues);
  vstData.BeginUpdate;
  vstData.Clear;
  System.TMonitor.Enter(lstData);
  try
    vstData.RootNodeCount := lstData.Count;
  finally
    System.TMonitor.Exit(lstData);
  end;
  vstData.EndUpdate;

  Series3.Clear;
  Series4.Clear;
  System.TMonitor.Enter(lstData);
  try
    Values := TValues.Create;
    for i := 0 to lstData.Count - 1 do
    begin
      Values := lstData.Items[i];
      Series3.AddXY(Values.dtTime, Values.rForce);
      Series4.AddXY(Values.dtTime, Values.rFtarget);
    end;
  finally
    System.TMonitor.Exit(lstData);
  end;
end;

procedure TForm2.vstDataGetNodeDataSize(Sender: TBaseVirtualTree;
  var NodeDataSize: Integer);
begin
  NodeDataSize := SizeOf(TValues);
end;

procedure TForm2.vstDataGetText(Sender: TBaseVirtualTree; Node: PVirtualNode;
  Column: TColumnIndex; TextType: TVSTTextType; var CellText: string);
// Daten in Zellen schreiben
var
  Data: PValues;

begin
  Data := Sender.GetNodeData(Node);
  case Column of
    0: CellText := FormatDateTime('yyyy-mm-dd hh.mm.ss.zzz', Data^.dtTime);
    1: CellText := format('%7.2f', [Data^.rForce]);
    2: CellText := format('%7.2f', [Data^.rFtarget]);
  end;
end;

procedure TForm2.vstDataInitNode(Sender: TBaseVirtualTree;
  ParentNode, Node: PVirtualNode; var InitialStates: TVirtualNodeInitStates);
// Den Knoten in VST die Daten von lstData zuweisen
var
  Data: PValues;
begin
  Data := Sender.GetNodeData(Node);

  begin
    System.TMonitor.Enter(lstData);
    try
      Data^ := lstData[Node^.Index];
    finally
      System.TMonitor.Exit(lstData);
    end;
  end;
end;

{ ======================== TThrReadDB ======================================== }

procedure TThrReadDb.Execute;
// Zuerst Query öffnen und dann alle Daten in lstData ablegen
var
  Values: TValues;
  iT, iT1 : LargeInt;

begin
  inherited;

  bThrReadDbIsRunning := true;
  CoInitialize(nil);
  try
    thrReadDb.dbQuery.Open;

    while not thrReadDb.Terminated and not thrReadDb.dbQuery.eof do
    begin
      Values := TValues.Create;
      Values.dtTime := thrReadDb.dbQuery.FieldByName('ErstDat').Value;
      Values.rForce := thrReadDb.dbQuery.FieldByName('Fact').Value;
      Values.rFtarget := thrReadDb.dbQuery.FieldByName('Ftarget').Value;

      System.TMonitor.Enter(lstData);   // Liste gegen Zweitzugriff sperren
      try
        lstData.Add(Values);
      finally
        System.TMonitor.Exit(lstData);  // Sperrung der Liste wieder aufheben
      end;

      thrReadDb.dbQuery.Next;
    end;

    if not thrReadDb.Terminated then
    begin
      _Ready := true;
      Synchronize(syncReadyEvent);
    end;
  finally
    CoUnInitialize;
    bThrReadDbIsRunning := false;
  end;
end;

procedure TThrReadDb.syncReadyEvent;
// Formular benachrichtigen, dass Daten komplett in lstData vorliegen
begin
  if assigned(_ReadyEvent) then
    _ReadyEvent(_Ready);
end;

end.
Jetzt meine Fragen:
  1. Wo erzeuge ich Speicherlecks
  2. Wo ist der Thread nicht sauber gekapselt
  3. Ist beim Abrufuf von
    Delphi-Quellcode:
    vstData.RootNodeCount := lstData.Count;
    ein locken der Liste notwendig?
  4. Sieht jemand eine Möglichkeit, auch eine laufende Queryabfrage abzubrechen. Der von UniDac ermöglichte Aufruf von BreakExec führt beim nachfolgenden neu erzeugten Thread zur Fehlermeldung "operation was canceled"
  5. Wie kann ich z.B. eine 150ms Verzögerung einbauen, damit beim schnellen Blättern in den Daten der Thread (vor allem Query.Execute) nicht gestartet wird
Wie gesagt, geht es mir ums Verständnis und ich wäre deshalb froh, wenn jemand die Fehler aufzeigen könnte und nicht alles über den Haufen schmeisst. Ich hoffe doch, dass der Ansatz nicht ganz so verkehrt ist.

Danke
Gerd

haentschman 23. Jan 2017 17:17

AW: Query in Thread abarbeiten (UniDac mit MsSQL)
 
Hallöle... :P

Ich lehne mich mal weit aus dem Fenster ohne alles nachgelesen zu haben...

[Nur meine Meinung...über vieles läßt sich streiten 8-)]
1. Hat das einen Sinn das verschiede Variablen den_ Unterstrich tragen statt wie "üblich" F?
2. Warum hat das Event für den Thread den Namen "TThreadBoolean" statt einem sprechenden Namen wie "TFinishLoadList" ... am Namen sollte man schon die Funktionsweise "erahnen" :P
3. Die Connection gehört in den Thread... :thumb:
4. Das gehört nicht in den Thread
Delphi-Quellcode:
thrReadDb.ReadyEvent := Form2.SyncReady;
Den Eventhander in der Form deklarieren und verbinden.
5.
Delphi-Quellcode:
TListData = class(TList<TValues>)
... erzeugt dir Speicherlecks mit
Delphi-Quellcode:
Values := TValues.Create;
besser http://docwiki.embarcadero.com/Libra...ectList.Create
6. Keine Passworte hardcodiert im Connectionstring
7.
Delphi-Quellcode:
thrReadDb.Resume;
... nicht mehr verwenden sondern START
8.
Delphi-Quellcode:
thrReadDb.FreeOnTerminate := TRUE;
habe ich fast überlesen zwischen den Kommentaren.
9.
Delphi-Quellcode:
thrReadDb.dbcon.ProviderName := TSQLServerUniProvider.Create(NIL).Name;
... einfach zuordnen statt von neuer Instanz. (imho legt die UniDAC automatisch intern eine Instanz an) besser:
Delphi-Quellcode:
FConnection.ProviderName := 'Interbase';
:thumb:
10. Aufteilung in separate Units. Die Logik (Thread) hat nichts in der Form Unit zu suchen.
11. WICHTG: Der Code ist nicht konsistent. Mal "true" mal "TRUE". Benutze lieber einen Codeformatter...

:P

norwegen60 23. Jan 2017 21:42

AW: Query in Thread abarbeiten (UniDac mit MsSQL)
 
Hallo,

vielen Danke fürs Feedback. Ich weiß dass es mühsam ist, den ganzen Code anzuschauen. Wenn ich aber 10 Threads mit unterschiedlichen Fragen öffne wirds wohl auch nicht einfacher. Deshalb habe ich mal alles in einen Topf geworfen (und auch in eine Unit :wink:)

Zitat:

Zitat von haentschman (Beitrag 1359756)
1. Hat das eine Sinn das verschiede Variablen den_ Unterstrich tragen statt wie "üblich" F?

Habe ich geändert. Kam aus einem der ersten Beispiele und ich dachte das macht man bei Threads vieleicht so
Zitat:

Zitat von haentschman (Beitrag 1359756)
2. Warum hat das Event für den Thread den Namen "TThreadBoolean" statt einem sprechenden Namen wie "TFinishLoadList" ... am Namen sollte man schon die Funktionsweise "erahnen"

Weil ich den für verschiedene Aufgaben verwende wo ich ein Boolean übergebe
Zitat:

Zitat von haentschman (Beitrag 1359756)
4. Das gehört nicht in den Thread
Delphi-Quellcode:
thrReadDb.ReadyEvent := Form2.SyncReady;
Den Eventhander in der Form deklarieren und verbinden.

Habe ich geändert als ich den Thread in eine eigen Unit verschoben habe und eine Rückverlinkung zum Form benötigt hätte
Zitat:

Zitat von haentschman (Beitrag 1359756)
5.
Delphi-Quellcode:
TListData = class(TList<TValues>)
... erzeugt dir Speicherlecks mit
Delphi-Quellcode:
Values := TValues.Create;
besser http://docwiki.embarcadero.com/Libra...ectList.Create

Habe ich mir angeschaut. Hintergrung für
Delphi-Quellcode:
TListData = class(TList<TValues>)
ist, dass ich die Liste dem Master-Record anhängen will. In etwas so:
Delphi-Quellcode:
  TCalReport = class
  private
    FNr : String;
    ...
    FValues: TListData
  public
    ...
  end;
Zitat:

Zitat von haentschman (Beitrag 1359756)
6. Keine Passworte hardcodiert im Connectionstring

War nur für die Schnelle
Zitat:

Zitat von haentschman (Beitrag 1359756)
7.
Delphi-Quellcode:
thrReadDb.Resume;
... nicht mehr verwenden sondern START

Werde ich ändern
Zitat:

Zitat von haentschman (Beitrag 1359756)
8.
Delphi-Quellcode:
thrReadDb.FreeOnTerminate := TRUE;
habe ich fast überlesen zwischen den Kommentaren.

Was meinst du damit?
Zitat:

Zitat von haentschman (Beitrag 1359756)
9.
Delphi-Quellcode:
thrReadDb.dbcon.ProviderName := TSQLServerUniProvider.Create(NIL).Name;
... einfach zuordnen statt von neuer Instanz. (imho legt die UniDAC automatisch intern eine Instanz an) besser:
Delphi-Quellcode:
FConnection.ProviderName := 'Interbase';

Habe die DB-Komponenten bisher immer in einem Datamodul angelegt. Da muss ich wohl noch üben.
Zitat:

Zitat von haentschman (Beitrag 1359756)
10. Aufteilung in separate Units. Die Logik (Thread) hat nichts in der Form Unit zu suchen.

Da war ich die letzten Stunden dran, denn das ist ja eines der Ziele der Übung. Trennung GUI - Logic - Daten
Zitat:

Zitat von haentschman (Beitrag 1359756)
11. WICHTG: Der Code ist nicht konsistent. Mal "true" mal "TRUE". Benutze lieber einen Codeformatter...

Bin ich mir am angewöhnen.

Es ist nicht ganz einfach Quellen zu finden, auf die man sich verlassen kann. Auch die Delphi-Beispiele verwenden z.B. Resume http://docwiki.embarcadero.com/CodeE...dList_(Delphi)
Und im Moment habe ich noch meine liebe Mühe mit dem Thema "Thread"

Grüße
Gerd

haentschman 24. Jan 2017 07:07

AW: Query in Thread abarbeiten (UniDac mit MsSQL)
 
Hallöle... 8-)
Zitat:

8. thrReadDb.FreeOnTerminate := TRUE; habe ich fast überlesen zwischen den Kommentaren.
Was meinst du damit?
:P Das ist ja in Ordnung so. Ich hätte nur das
Delphi-Quellcode:
thrReadDb.FreeOnTerminate := TRUE;
direkt nach dem Erzeugen plaziert und die Kommentare weggelassen. Am Anfang bin ich davon ausgegangen das der Tread nicht fregegeben wird.
Zitat:

Habe ich mir angeschaut. Hintergrung für TListData = class(TList<TValues>) ist, dass ich die Liste dem Master-Record anhängen will. In etwas so:
Auch wenn der Master Record freigeben wird, gilt das nicht für den Listeninhalt. Du legst eine Instanz rein
Delphi-Quellcode:
Values := TValues.Create;
und die sollte freigegeben werden. Der Master Record kümmert sich nur um die Listeninstanz. Den tangiert in keinster Weise der Inhalt der Liste...:zwinker:
Zitat:

2. Warum hat das Event für den Thread den Namen "TThreadBoolean" statt einem sprechenden Namen wie "TFinishLoadList" ... am Namen sollte man schon die Funktionsweise "erahnen"
Über Namen kann man streiten. Ist ja in Ordnung so. :wink: Was ich meinte... Lieber ein Event mehr deklariert als ein "Master" Event. Bei dem Beispiel mit der Liste kann man im besser genahmsten Event die Liste im Event mit übergeben. Da gibt es zig Varianten der Ausführung... :zwinker:


Alle Zeitangaben in WEZ +1. Es ist jetzt 04:47 Uhr.

Powered by vBulletin® Copyright ©2000 - 2024, Jelsoft Enterprises Ltd.
LinkBacks Enabled by vBSEO © 2011, Crawlability, Inc.
Delphi-PRAXiS (c) 2002 - 2023 by Daniel R. Wolf, 2024 by Thomas Breitkreuz