SQLdb Tutorial3
- 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, 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.
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.
<insert screenshot here>
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.
Possible additions/modifications:
- 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
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.
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
Code is presented below; 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
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.
dbconfiggui.pas
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.
mainform.pas
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.
See also
- SQLdb Tutorial1: First part of the DB tutorial series, showing how to set up a grid that shows database data
- SQLdb Tutorial2: Second part of the DB tutorial series, showing editing, inserting etc.
- Lazarus Database Overview: Information about the databases that Lazarus supports. Links to database-specific notes.
- SQLdb Package: information about the SQLdb package
- SQLdb Programming Reference: an overview of the interaction of the SQLdb database components
- SqlDBHowto: information about using the SQLdb package
- Working With TSQLQuery: information about TSQLQuery