Difference between revisions of "SQLdb Tutorial3"

From Lazarus wiki
Jump to navigationJump to search
(Code solution for stddev; shows loop in query)
Line 145: Line 145:
 
     try
 
     try
 
       FConn.Open;
 
       FConn.Open;
      {
+
       if FConn.Connected=false then
      // We could run both queries at once, but this
 
      // would be a nasty query if there are lots of rows in employee
 
      // because the subqueries in the where condition are run for each employee
 
      // in the table.
 
      // We use order by to make sure the lowest salary is presented first, then
 
      // the highest.
 
      SalaryQuery.SQL.Text:='select ' +
 
      '    first_name, ' +
 
      '    last_name, ' +
 
      '    salary ' +
 
      '  from employee  ' +
 
      '  where  ' +
 
      '    salary=(select min(salary) from employee) or  ' +
 
      '    salary=(select max(salary) from employee) ' +
 
      '  order by salary ' ;
 
      }
 
       if FConn.Connected=true then
 
 
       begin
 
       begin
         // Lowest salary
+
         ShowMessage('Error connecting to the database. Aborting data loading.');
        // If your db does not support rows 1, leave it out;
+
        exit;
        // we'll deal with db dependent SQL later
+
      end;
        FQuery.SQL.Text:='select ' +
+
 
          '    e.first_name, ' +
+
 
          '    e.last_name, ' +
+
      // Lowest salary
          '    e.salary ' +
+
      // If your db does not support rows 1, leave it out;
          'from employee e ' +
+
      // we'll deal with db dependent SQL later
          'order by e.salary asc ' +
+
      FQuery.SQL.Text:='select ' +
          'rows 1 ';
+
        '    e.first_name, ' +
        FTran.StartTransaction;
+
        '    e.last_name, ' +
        FQuery.Open;
+
        '    e.salary ' +
        SalaryGrid.Cells[1,1]:=FQuery.Fields[0].AsString;
+
        'from employee e ' +
        SalaryGrid.Cells[2,1]:=FQuery.Fields[1].AsString;
+
        'order by e.salary asc ' +
        SalaryGrid.Cells[3,1]:=FQuery.Fields[2].AsString;
+
        'rows 1 ';
        FQuery.Close;
+
      FTran.StartTransaction;
        // Always commit(retain) an opened transaction, even if only reading
+
      FQuery.Open;
        FTran.Commit;
+
      SalaryGrid.Cells[1,1]:=FQuery.Fields[0].AsString;
 +
      SalaryGrid.Cells[2,1]:=FQuery.Fields[1].AsString;
 +
      SalaryGrid.Cells[3,1]:=FQuery.Fields[2].AsString;
 +
      FQuery.Close;
 +
      // Always commit(retain) an opened transaction, even if only reading
 +
      FTran.Commit;
 
...
 
...
 
       end;
 
       end;
Line 195: Line 183:
 
Things to note: we catch database errors using try..except. You'll notice we forgot to roll back the transaction in case of errors - which is left as an exercise to the reader.
 
Things to note: we catch database errors using try..except. You'll notice we forgot to roll back the transaction in case of errors - which is left as an exercise to the reader.
  
We ''Open'' the query object, thereby asking ''FQuery'' to query the databse via its ''SQL'' statement. Once this is done, we're on the first row of data. We simply assume there is data now; it would be tider to check for ''FQuery.EOF'' being true (or ''FQuery.RecordCount'' being >0).
+
We ''Open'' the query object, thereby asking ''FQuery'' to query the databse via its ''SQL'' statement. Once this is done, we're on the first row of data. We simply assume there is data now; this is actually a programming error: it would be tidier to check for ''FQuery.EOF'' being true (or ''FQuery.RecordCount'' being >0).
  
Next, we retrieve the data from the first row of results. If we wanted to move to the next row, we'd use ''FQuery.NExt'', but that is not necessary here. We put the results in the stringgrid, giving the lowest salary in the list. A similar approach can be taken for the highest salary.
+
Next, we retrieve the data from the first row of results. If we wanted to move to the next row, we'd use ''FQuery.Next'', but that is not necessary here. We put the results in the stringgrid, giving the lowest salary in the list. A similar approach can be taken for the highest salary.
  
 
== Adapting SQL for various databases ==
 
== Adapting SQL for various databases ==
Line 233: Line 221:
 
... now let's implement a code-based solution for other databases that do not support standard deviation:
 
... now let's implement a code-based solution for other databases that do not support standard deviation:
 
<syntaxhighlight>
 
<syntaxhighlight>
...to do
+
  // For other databases, use the code approach:
 +
  // 1. Get average of values
 +
  FQuery.SQL.Text:='select avg(salary) from employee ';
 +
  FQuery.Open;
 +
  Average:=FQuery.Fields[0].AsFloat;
 +
  FQuery.Close;
 +
  // 2. For each value, calculate the square of (value-average), and add it up
 +
  FQuery.SQL.Text:='select salary from employee where salary is not null ';
 +
  FQuery.Open;
 +
  while not(FQuery.EOF) do
 +
  begin
 +
    DifferencesSquared:=DifferencesSquared+Sqr(FQuery.Fields[0].AsFloat-Average);
 +
    Count:=Count+1;
 +
    FQuery.Next;
 +
  end;
 +
  FQuery.Close;
 +
  // 3. Now calculate the average "squared difference" and take the square root
 +
  SalaryGrid.Cells[3,3]:=FloatToStr(Sqrt(DifferencesSquared/Count));
 
</syntaxhighlight>
 
</syntaxhighlight>
 
+
Note that here in the loop we retrieve a value, use ''FQuery.Next'' to move to the next one and properly check if the query dataset has hit the last record, and stop retrieving data.
 
 
use standard deviation for salary select ... from employees...
 
Hopefully postgresql has a built in stddev function
 
Indicate this may not work on all dbs
 
Show result in labels or tedits
 
  
 
== Executing queries & getting data out of normal controls into the database ==
 
== Executing queries & getting data out of normal controls into the database ==
Previously, we have seen how to get data out of the databse using queries, and how to let SQLDB update the databse with db controls.
+
Previously, we have seen how to get data out of the databse using queries, and how to let SQLDB update the database with db controls.
 
You can also execute any SQL you want on the database via code, and we're going to use that to show how to use normal non db controls to get data into the database.
 
You can also execute any SQL you want on the database via code, and we're going to use that to show how to use normal non db controls to get data into the database.
  
 
This allows you to use controls that have no db aware equivalent such as sliders or custom controls to enter data into the database, at the expense of a bit more coding.
 
This allows you to use controls that have no db aware equivalent such as sliders or custom controls to enter data into the database, at the expense of a bit more coding.
INSERT INTO ... EMPLOYEE...
+
 
or
+
Here we are going to implement changes in the lowest and highest salary in the stringgrid:
 
UPDATE EMPLOYEE...
 
UPDATE EMPLOYEE...
or
+
* todo: finish
MERGE...
 
Think about this one...
 
  
 
== Code ==
 
== Code ==
Code is presented below; it would be a better idea if this code could be included in the Lazarus examples directory...
 
 
Current version can be downloaded from [https://bitbucket.org/reiniero/fpc_laz_patch_playground/src], directory SQLTutorial3.
 
Current version can be downloaded from [https://bitbucket.org/reiniero/fpc_laz_patch_playground/src], directory SQLTutorial3.
  
 
+
It would be a better idea if this code could be included in the Lazarus examples directory...
* to do: update code, specify where the zip with code can be found (or place in Laz examples)
 
 
 
=== dbconfig.pas ===
 
<syntaxhighlight>
 
unit dbconfig;
 
 
 
{ Small unit that retrieves connection settings for your database
 
 
 
  Copyright (c) 2012 Reinier Olislagers
 
 
 
  Permission is hereby granted, free of charge, to any person obtaining a copy
 
  of this software and associated documentation files (the "Software"), to
 
  deal in the Software without restriction, including without limitation the
 
  rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 
  sell copies of the Software, and to permit persons to whom the Software is
 
  furnished to do so, subject to the following conditions:
 
 
 
  The above copyright notice and this permission notice shall be included in
 
  all copies or substantial portions of the Software.
 
 
 
  THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 
  IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 
  FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 
  AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 
  FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
 
  IN THE SOFTWARE.
 
}
 
 
 
//todo: add command line support (--dbtype=, --db=, --dbhost=, dbuser=, dbpass=, dbcharset=
 
{$mode objfpc}{$H+}
 
 
 
interface
 
 
 
uses
 
  Classes, SysUtils, IniFiles;
 
 
 
type
 
  { TDBConnectionConfig }
 
  TDBConnectionConfig = class(TObject)
 
  private
 
    FDBCharset: string;
 
    FDBType: string;
 
    FDBHost: string;
 
    FDBPath: string;
 
    FDBUser: string;
 
    FDBPassword: string;
 
    FSettingsFileIsRead: boolean; //indicates if we read in settings file
 
    FSettingsFile: string;
 
    function GetDBCharset: string;
 
    function GetDBHost: string;
 
    function GetDBPassword: string;
 
    function GetDBPath: string;
 
    function GetDBType: string;
 
    function GetDBUser: string;
 
    function GetDefaultSettingsFile:string;
 
    procedure ReadINIFile;
 
    procedure SetDBType(AValue: string);
 
    procedure SetSettingsFile(AValue: string);
 
  public
 
    property DBCharset: string read GetDBCharset write FDBCharset; //Character set used for database (e.g. UTF8)
 
    property DBHost: string read GetDBHost write FDBHost; //Database host/server (name or IP address). Leave empty for embedded
 
    property DBPath: string read GetDBPath write FDBPath; //Path/database name
 
    property DBUser: string read GetDBUser write FDBUser; //User name needed for database (e.g. sa, SYSDBA)
 
    property DBPassword: string read GetDBPassword write FDBPassword; //Password needed for user name
 
    property DBType: string read GetDBType write SetDBType; //Type of database connection, e.g. Firebird, Oracle
 
    property SettingsFile: string read FSettingsFile write SetSettingsFile; //ini file to read settings from. If empty defaults to <programname>.ini
 
    constructor Create;
 
    constructor Create(DefaultType:string; DefaultHost:string=''; DefaultPath:string='data.fdb';
 
      DefaultUser:string='SYSDBA'; DefaultPassword:string='masterkey'; DefaultCharSet:string='UTF8');
 
    destructor Destroy; override;
 
  end;
 
 
 
implementation
 
 
 
{ TDBConnectionConfig }
 
 
 
function TDBConnectionConfig.GetDBHost: string;
 
begin
 
  if not(FSettingsFileIsRead) then ReadINIFile;
 
  result:=FDBHost;
 
end;
 
 
 
function TDBConnectionConfig.GetDBCharset: string;
 
begin
 
  if not(FSettingsFileIsRead) then ReadINIFile;
 
  result:=FDBCharset;
 
end;
 
 
 
function TDBConnectionConfig.GetDBPassword: string;
 
begin
 
  if not(FSettingsFileIsRead) then ReadINIFile;
 
  result:=FDBPassword;
 
end;
 
 
 
function TDBConnectionConfig.GetDBPath: string;
 
begin
 
  if not(FSettingsFileIsRead) then ReadINIFile;
 
  result:=FDBPath;
 
end;
 
 
 
function TDBConnectionConfig.GetDBType: string;
 
begin
 
  if not(FSettingsFileIsRead) then ReadINIFile;
 
  result:=FDBType;
 
end;
 
 
 
function TDBConnectionConfig.GetDBUser: string;
 
begin
 
  if not(FSettingsFileIsRead) then ReadINIFile;
 
  result:=FDBUser;
 
end;
 
 
 
function TDBConnectionConfig.GetDefaultSettingsFile:string;
 
begin
 
  result:=ChangeFileExt(ParamStr(0), '.ini');
 
end;
 
 
 
procedure TDBConnectionConfig.SetSettingsFile(AValue: string);
 
begin
 
  // If empty value given, use the program name
 
  if AValue='' then AValue:=GetDefaultSettingsFile;
 
  if FSettingsFile=AValue then Exit;
 
  FSettingsFile:=AValue;
 
  // Read from file if present
 
  ReadINIFile;
 
end;
 
 
 
 
 
procedure TDBConnectionConfig.SetDBType(AValue: string);
 
begin
 
  if FDBType=AValue then Exit;
 
  case UpperCase(AValue) of
 
    'FIREBIRD': FDBType:='Firebird';
 
    'POSTGRES', 'POSTGRESQL': FDBType:='PostgreSQL';
 
    'SQLITE','SQLITE3': FDBType:='SQLite';
 
  else FDBType:=AValue;
 
  end;
 
end;
 
 
 
procedure TDBConnectionConfig.ReadINIFile;
 
var
 
  INI: TIniFile;
 
begin
 
  if FileExists(FSettingsFile) then
 
  begin
 
    INI := TINIFile.Create(FSettingsFile);
 
    try
 
      FDBType := INI.ReadString('Database', 'DatabaseType', FDBType); //Default to Firebird
 
      FDBHost := INI.ReadString('Database', 'Host', FDBHost);
 
      FDBPath := INI.ReadString('Database', 'Database', FDBPath);
 
      FDBUser := INI.ReadString('Database', 'User', 'SYSDBA');
 
      FDBPassword := INI.ReadString('Database', 'Password', 'masterkey');
 
      FSettingsFileIsRead:=true;
 
    finally
 
      INI.Free;
 
    end;
 
  end;
 
end;
 
 
 
constructor TDBConnectionConfig.Create;
 
begin
 
  inherited Create;
 
  // Defaults
 
  FSettingsFile:=GetDefaultSettingsFile;
 
  FSettingsFileIsRead:=false;
 
  FDBType := 'Firebird';
 
  FDBHost := ''; //embedded
 
  FDBPath := 'data.fdb';
 
  FDBUser := 'SYSDBA';
 
  FDBPassword := 'masterkey';
 
end;
 
 
 
constructor TDBConnectionConfig.Create(DefaultType:string; DefaultHost:string=''; DefaultPath:string='data.fdb';
 
  DefaultUser:string='SYSDBA'; DefaultPassword:string='masterkey'; DefaultCharSet:string='UTF8');
 
begin
 
  create;
 
  FDBCharset:=DefaultCharset;
 
  FDBHost:=DefaultHost;
 
  FDBPassword:=DefaultPassword;
 
  FDBPath:=DefaultPath;
 
  FDBType:=DefaultType;
 
  FDBUser:=DefaultUser;
 
end;
 
 
 
destructor TDBConnectionConfig.Destroy;
 
begin
 
  inherited Destroy;
 
end;
 
 
 
end.
 
</syntaxhighlight>
 
 
 
=== dbconfiggui.pas ===
 
<syntaxhighlight>
 
unit dbconfiggui;
 
 
 
{$mode objfpc}{$H+}
 
 
 
interface
 
 
 
uses
 
  Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs, StdCtrls, dbconfig;
 
 
 
type
 
  TConnectionTestFunction = function(ChosenConfig: TDBConnectionConfig): boolean of object;
 
  { TDBConfigForm }
 
 
 
  TDBConfigForm = class(TForm)
 
    OKButton: TButton;
 
    CancelButton: TButton;
 
    TestButton: TButton;
 
    ConnectorType: TComboBox;
 
    Host: TEdit;
 
    Database: TEdit;
 
    Label1: TLabel;
 
    Label2: TLabel;
 
    Label3: TLabel;
 
    Label4: TLabel;
 
    Label5: TLabel;
 
    Password: TEdit;
 
    User: TEdit;
 
    procedure ConnectorTypeEditingDone(Sender: TObject);
 
    procedure DatabaseEditingDone(Sender: TObject);
 
    procedure FormCreate(Sender: TObject);
 
    procedure FormDestroy(Sender: TObject);
 
    procedure FormShow(Sender: TObject);
 
    procedure HostEditingDone(Sender: TObject);
 
    procedure PasswordEditingDone(Sender: TObject);
 
    procedure TestButtonClick(Sender: TObject);
 
    procedure UserEditingDone(Sender: TObject);
 
  private
 
    FConnectionConfig: TDBConnectionConfig;
 
    FConnectionTestFunction: TConnectionTestFunction;
 
    FSetupComplete: boolean;
 
    { private declarations }
 
  public
 
    property Config: TDBConnectionConfig read FConnectionConfig;
 
    property ConnectionTestCallback: TConnectionTestFunction write FConnectionTestFunction;
 
    { public declarations }
 
  end;
 
 
 
var
 
  DBConfigForm: TDBConfigForm;
 
 
 
implementation
 
 
 
{$R *.lfm}
 
 
 
{ TDBConfigForm }
 
 
 
procedure TDBConfigForm.TestButtonClick(Sender: TObject);
 
begin
 
  // Call callback with settings, let it figure out if connection succeeded and
 
  // get test result back
 
  if assigned(FConnectionTestFunction) and assigned(FConnectionConfig) then
 
    if FConnectionTestFunction(FConnectionConfig) then
 
      showmessage('Connection test succeeded.')
 
      else
 
        showmessage('Connection test failed.')
 
  else
 
    showmessage('Error: connection test code has not been implemented.');
 
end;
 
 
 
procedure TDBConfigForm.UserEditingDone(Sender: TObject);
 
begin
 
  FConnectionConfig.DBUser:=User.Text;
 
end;
 
 
 
procedure TDBConfigForm.FormCreate(Sender: TObject);
 
begin
 
  FConnectionConfig:=TDBConnectionConfig.Create;
 
  FSetupComplete:=false;
 
end;
 
 
 
procedure TDBConfigForm.ConnectorTypeEditingDone(Sender: TObject);
 
begin
 
  FConnectionConfig.DBType:=ConnectorType.Text;
 
end;
 
 
 
procedure TDBConfigForm.DatabaseEditingDone(Sender: TObject);
 
begin
 
  FConnectionConfig.DBPath:=Database.Text;
 
end;
 
 
 
procedure TDBConfigForm.FormDestroy(Sender: TObject);
 
begin
 
  FConnectionConfig.Free;
 
end;
 
 
 
procedure TDBConfigForm.FormShow(Sender: TObject);
 
begin
 
  if not FSetupComplete then
 
  begin
 
    // Only do this once in form's lifetime
 
    FSetupComplete:=true;
 
    ConnectorType.Text:=FConnectionConfig.DBType;
 
    Host.Text:=FConnectionConfig.DBHost;
 
    Database.Text:=FConnectionConfig.DBPath;
 
    User.Text:=FConnectionConfig.DBUser;
 
    Password.Text:=FConnectionConfig.DBPassword;
 
  end;
 
end;
 
 
 
procedure TDBConfigForm.HostEditingDone(Sender: TObject);
 
begin
 
  FConnectionConfig.DBHost:=Host.Text;
 
end;
 
 
 
procedure TDBConfigForm.PasswordEditingDone(Sender: TObject);
 
begin
 
  FConnectionConfig.DBPassword:=Password.Text;
 
end;
 
 
 
end.
 
</syntaxhighlight>
 
 
 
=== mainform.pas ===
 
<syntaxhighlight>
 
unit mainform;
 
 
 
{$mode objfpc}{$H+}
 
 
 
interface
 
 
 
uses
 
  Classes, SysUtils, FileUtil, Forms, Controls, Graphics, Dialogs,
 
  dbconfiggui,dbconfig,
 
  {General db unit}sqldb,
 
  {Now we add all databases we want to support, otherwise their drivers won't be loaded}
 
  IBConnection,pqconnection,sqlite3conn;
 
 
 
type
 
 
 
  { TForm1 }
 
 
 
  TForm1 = class(TForm)
 
    procedure FormCreate(Sender: TObject);
 
  private
 
    { private declarations }
 
    function ConnectionTest(ChosenConfig: TDBConnectionConfig): boolean;
 
  public
 
    { public declarations }
 
  end;
 
 
 
var
 
  Form1: TForm1;
 
 
 
implementation
 
 
 
{$R *.lfm}
 
 
 
{ TForm1 }
 
 
 
procedure TForm1.FormCreate(Sender: TObject);
 
var
 
  LoginForm: TDBConfigForm;
 
begin
 
  LoginForm:=TDBConfigForm.Create(self);
 
  try
 
    // The test button on dbconfiggui will link to this procedure:
 
    LoginForm.ConnectionTestCallback:=@ConnectionTest;
 
    LoginForm.ConnectorType.Clear; //remove any default connectors
 
    // Now add the dbs that you support - use the name of the *ConnectionDef.TypeName property
 
    LoginForm.ConnectorType.AddItem('Firebird', nil);
 
    LoginForm.ConnectorType.AddItem('PostGreSQL', nil);
 
    LoginForm.ConnectorType.AddItem('SQLite3', nil); //No connectiondef object yet in FPC2.6.0
 
    case LoginForm.ShowModal of
 
    mrOK:
 
      begin
 
        //user wants to connect, so copy over db info
 
      end;
 
    mrCancel:
 
      begin
 
        ShowMessage('You canceled the database login. Application will terminate.');
 
        Close;
 
      end;
 
    end;
 
  finally
 
    LoginForm.Free;
 
  end;
 
end;
 
 
 
function TForm1.ConnectionTest(ChosenConfig: TDBConnectionConfig): boolean;
 
// Callback function that uses the info in dbconfiggui to test a connection
 
// and return the result of the test to dbconfiggui
 
var
 
  // Generic database connector...
 
  Conn: TSQLConnector;
 
begin
 
  result:=false;
 
  Conn:=TSQLConnector.Create(nil);
 
  Screen.Cursor:=crHourglass;
 
  try
 
    // ...actual connector type is determined by this property.
 
    // Make sure the ChosenConfig.DBType string matches
 
    // the connectortype.
 
    Conn.ConnectorType:=ChosenConfig.DBType;
 
    Conn.HostName:=ChosenConfig.DBHost;
 
    Conn.DatabaseName:=ChosenConfig.DBPath;
 
    Conn.UserName:=ChosenConfig.DBUser;
 
    Conn.Password:=ChosenConfig.DBPassword;
 
    try
 
      Conn.Open;
 
      if Conn.Connected=true then
 
        result:=true;
 
    except
 
      // Result is already false
 
    end;
 
    Conn.Close;
 
  finally
 
    Screen.Cursor:=crDefault;
 
    Conn.Free;
 
  end;
 
end;
 
 
 
end.
 
</syntaxhighlight>
 
  
 
== See also ==
 
== See also ==

Revision as of 21:22, 15 November 2012

Overview

In this tutorial, you will learn to make your application suitable for multiple databases. You will see how to get database data in normal controls (instead of database controls), and how to execute parametrized queries.

Multi database support

Using any database and a login form, you can support multiple different database servers/embedded libraries that SQLDB supports.

Advantages:

  • the user/programmer can dynamically use any sqldb t*connection, so he can choose between dbs

Disadvantages:

  • More complicated SQL will likely need to still be adapted. Each database has its own dialect; it is of course possible to call db-specific SQL, this can grow into a maintenance problem.
  • You can't use T*connection specific properties (like TIBConnection.Dialect to set Firebird dialect).

To use multi-database support, instead of your specific T*Connection such as TIBConnection, use TSQLConnector (not TSQLConnection), which dynamically (when the program is running) chooses the specific T*Connection to use based on its ConnectorType property:

uses
...
var
  Conn: TSQLConnector;
begin
  Conn:=TSQLConnector.Create(nil);
  try
    // ...actual connector type is determined by this property.
    // Make sure the ChosenConfig.DBType string matches
    // the connectortype (e.g. see the string in the 
    // T*ConnectionDef.TypeName for that connector .
    Conn.ConnectorType:=ChosenConfig.DBType;
    // the rest is as usual:
    Conn.HostName:='DBSERVER';
    Conn.DatabaseName:='bigdatabase.fdb';
    Conn.UserName:='SYSDBA';
    Conn.Password:='masterkey';
    try
      Conn.Open;

Login form

As mentioned in SQLdb Tutorial1, a user should login to the database using a form (or perhaps a configuration file that is securely stored), not via hardcoded credentials in the application. Besides security considerations, having to recompile the application whenever the database server information changes is not a very good idea.

In dbconfiggui.pas, we will set up a login form that pulls in default values from an ini file, if it exists. This allows you to set up a default connection with some details (database server, database name) filled out for e.g. enterprise deployments. The user can add/edit his username/password, and test the connection before going further.

dbloginform.png

We use a separate dbconfig.pas unit with a TDBConnectionConfig class to store our chosen connection details. This class has support for reading default settings from an ini file.

This allows use without login forms (e.g. when running unattended/batch operations), and allows reuse in e.g. web applications.

This TDBConnectionConfig class is surfaced in the login form as the Config property, so that the main program can show the config form modally, detect an OK click by the user and retrieve the selected configuration before closing the config form.

Connection test callback function

To keep the login form flexible (it may be used with other database layers like Zeos), we implement the test section as a callback function and let the main program deal with it.

The definition in dbconfiggui:

type
  TConnectionTestFunction = function(ChosenConfig: TDBConnectionConfig): boolean of object;

The main form must implement a function that matches this definition to handle the test request from the config form.

The callback function takes the config passed on by the config form, and uses that to construct a connection with the chosen database type. It then simply tries to connect with the server; if it is succesful, the result is positive, otherwise the result stays false.

Because database connection attempts to non-existing servers can have a long timeout, we indicate to the user that he must wait by setting the cursor to the hourglass icon.

uses
...
dbconfig, dbconfiggui
...
function TForm1.ConnectionTest(ChosenConfig: TDBConnectionConfig): boolean;
// Callback function that uses the info in dbconfiggui to test a connection
// and return the result of the test to dbconfiggui
var
  // Generic database connector...
  Conn: TSQLConnector;
begin
  result:=false;
  Conn:=TSQLConnector.Create(nil);
  Screen.Cursor:=crHourglass;
  try
    // ...actual connector type is determined by this property.
    // Make sure the ChosenConfig.DBType string matches
    // the connectortype (e.g. see the string in the
    // T*ConnectionDef.TypeName for that connector .
    Conn.ConnectorType:=ChosenConfig.DBType;
    Conn.HostName:=ChosenConfig.DBHost;
    Conn.DatabaseName:=ChosenConfig.DBPath;
    Conn.UserName:=ChosenConfig.DBUser;
    Conn.Password:=ChosenConfig.DBPassword;
    try
      Conn.Open;
      if Conn.Connected=true then
        result:=true;
    except
      // Result is already false
    end;
    Conn.Close;
  finally
    Screen.Cursor:=crDefault;
    Conn.Free;
  end;
end;

Finally, the code in dbconfiggui.pas that actually calls the callback is linked to the Test button. It tests if the callback function is assigned (to avoid crashes), for completeness also checks if there is a valid configuration object and then simply calls the callback function:

procedure TDBConfigForm.TestButtonClick(Sender: TObject);
begin
  // Call callback with settings, let it figure out if connection succeeded and
  // get test result back
  if assigned(FConnectionTestFunction) and assigned(FConnectionConfig) then
    if FConnectionTestFunction(FConnectionConfig) then
       showmessage('Connection test succeeded.')
      else
        showmessage('Connection test failed.')
  else
    showmessage('Error: connection test code has not been implemented.');
end;

Additions/modifications

Possible additions/modifications for the login form:

  • Add command line arguments handling for dbconfig to preload suitable defaults, so the program can be used in batch scripts, shortcuts etc
  • Add a "Select profile" combobox in the login form; use multiple profiles in the ini file that specify database type and connection details.
  • Hide database type combobox when only one database type supported.
  • Hide username/password when you're sure an embedded database is selected.
  • Add support for specifying port number, or instance name with MS SQL Server connector
  • Add support for trusted authentication for dbs that support it (Firebird, MS SQL): disable ussername/password controls
  • Create database after a confirmation request if an embedded database is selected but the file does ot exist
  • Create a command-line/TUI version of the login form (e.g. using the curses library) for command-line applictions


Getting database data into normal controls

Light bulb  Note: Before starting this section, please make sure you have set up the sample employee database as specified in SQLDB Tutorial1#Requirements

In previous tutorials, data-bound controls were covered: special controls such as the dbgrid that can bind its contents to a datasource, get updates from that source and send user edits back.

It is also possible to programmatically retrieve database content and fill any kind of control with that content. As an example, we will look at filling a stringgrid with some salary details for the sample employee database table.

On the main form, let's add a stringgrid and retrieve the data (e.g. via a procedure LoadSalaryGrid called in the OnCreate event):

    // Load from DB
    try
      FConn.Open;
      if FConn.Connected=false then
      begin
        ShowMessage('Error connecting to the database. Aborting data loading.');
        exit;
      end;


      // Lowest salary
      // If your db does not support rows 1, leave it out;
      // we'll deal with db dependent SQL later
      FQuery.SQL.Text:='select ' +
        '    e.first_name, ' +
        '    e.last_name, ' +
        '    e.salary ' +
        'from employee e ' +
        'order by e.salary asc ' +
        'rows 1 ';
      FTran.StartTransaction;
      FQuery.Open;
      SalaryGrid.Cells[1,1]:=FQuery.Fields[0].AsString;
      SalaryGrid.Cells[2,1]:=FQuery.Fields[1].AsString;
      SalaryGrid.Cells[3,1]:=FQuery.Fields[2].AsString;
      FQuery.Close;
      // Always commit(retain) an opened transaction, even if only reading
      FTran.Commit;
...
      end;
    except
      on D: EDatabaseError do
      begin
        MessageDlg('Error', 'A database error has occurred. Technical error message: ' +
          D.Message, mtError, [mbOK], 0);
      end;
    end;

Things to note: we catch database errors using try..except. You'll notice we forgot to roll back the transaction in case of errors - which is left as an exercise to the reader.

We Open the query object, thereby asking FQuery to query the databse via its SQL statement. Once this is done, we're on the first row of data. We simply assume there is data now; this is actually a programming error: it would be tidier to check for FQuery.EOF being true (or FQuery.RecordCount being >0).

Next, we retrieve the data from the first row of results. If we wanted to move to the next row, we'd use FQuery.Next, but that is not necessary here. We put the results in the stringgrid, giving the lowest salary in the list. A similar approach can be taken for the highest salary.

Adapting SQL for various databases

Often, various databases support various versions of SQL (either in addition to or in contradiction to the official ISO SQL standards). Fortunately, you can customize your application based on which DB it ends up using, which will be demonstrated by getting the standard deviation of the employees' salaries - built into e.g. PostgreSQL SQL but not available by default in Firebird.

In our LoadSalaryGrid procedure, we'll use the SQL for PostgreSQL and build a code solution for other databases. First detect which database is loaded, below the other lines:

  SalaryGrid.Cells[3,2]:=FQuery.Fields[2].AsString;
  FQuery.Close;
  // Always commit(retain) an opened transaction, even if only reading
  FTran.Commit;
//end of existing code

  if FConn.ConnectorType:='PostGreSQL' then
  begin
    // For PostgreSQL, use a native SQL solution:
    FQuery.SQL.Text:='select stddev_pop(salary) from employee ';
    FTran.StartTransaction;
    FQuery.Open;
    SalaryGrid.Cells[4,1]:=FQuery.Fields[1].AsString;
    FQuery.Close;
    // Always commit(retain) an opened transaction, even if only reading
    FTran.Commit;
  end
  else
  begin
    // For other database, use the code approach:
    // .... see below in this tutorial...
  end;

Notice the use of ConnectorType; the string used must match exactly.

... now let's implement a code-based solution for other databases that do not support standard deviation:

  // For other databases, use the code approach:
  // 1. Get average of values
  FQuery.SQL.Text:='select avg(salary) from employee ';
  FQuery.Open;
  Average:=FQuery.Fields[0].AsFloat;
  FQuery.Close;
  // 2. For each value, calculate the square of (value-average), and add it up
  FQuery.SQL.Text:='select salary from employee where salary is not null ';
  FQuery.Open;
  while not(FQuery.EOF) do
  begin
    DifferencesSquared:=DifferencesSquared+Sqr(FQuery.Fields[0].AsFloat-Average);
    Count:=Count+1;
    FQuery.Next;
  end;
  FQuery.Close;
  // 3. Now calculate the average "squared difference" and take the square root
  SalaryGrid.Cells[3,3]:=FloatToStr(Sqrt(DifferencesSquared/Count));

Note that here in the loop we retrieve a value, use FQuery.Next to move to the next one and properly check if the query dataset has hit the last record, and stop retrieving data.

Executing queries & getting data out of normal controls into the database

Previously, we have seen how to get data out of the databse using queries, and how to let SQLDB update the database with db controls. You can also execute any SQL you want on the database via code, and we're going to use that to show how to use normal non db controls to get data into the database.

This allows you to use controls that have no db aware equivalent such as sliders or custom controls to enter data into the database, at the expense of a bit more coding.

Here we are going to implement changes in the lowest and highest salary in the stringgrid: UPDATE EMPLOYEE...

  • todo: finish

Code

Current version can be downloaded from [1], directory SQLTutorial3.

It would be a better idea if this code could be included in the Lazarus examples directory...

See also