SQLdb Tutorial3

From Free Pascal wiki
Revision as of 16:41, 13 November 2012 by BigChimp (talk | contribs) (Added preliminary code)
Jump to navigationJump to search
      • work in progress; to do: finish me ***

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.

advantages: - 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 - you can't use T*connection specific properties (like TIBConnection.Dialect to set Firebird dialect). - complicates code some

to do: Expand this code to also show a form if needed for user input and do arguments handling from command line. Also perhaps create a non-gui version to do: get ludob's version from the forum, IIRC that was a form based thingy.

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.

add section on creating dbs here, handy for embedded (sqlite, firebird); also do firebird c/s to contrast

Getting database data into normal controls

Refer to the SQLdb Tutorial Database setup page (to be created) with instructions on how to set up employee database with the employees tables.

Aggregate query: max and min salary and corresponding employee (select ... from salary...) Get results in stringgrid.

Adapting SQL for various databases

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

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. 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. INSERT INTO ... EMPLOYEE... or UPDATE EMPLOYEE... or MERGE... Think about this one...

Code

dbconfig.pas

<syntaxhiglight> 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

<syntaxhiglight> 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

<syntaxhiglight> 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>

[Category:Databases] [Category:Tutorials] [Category:Lazarus]