Difference between revisions of "SQLdb Tutorial3"
Line 246: | Line 246: | ||
[[File:sqldbtutorial3mainform.png]] | [[File:sqldbtutorial3mainform.png]] | ||
− | == | + | == Getting data out of normal controls into the database == |
− | Previously, we have seen how to get data out of the database using queries | + | Previously, we have seen: |
− | You can also execute | + | * how to let SQLDB update the database with data-bound controls (earlier tutorials) |
+ | * how to get data out of the database using queries (this tutorial) | ||
+ | |||
+ | You can also execute SQL to get arbitrary data back into the database via code. 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: | |
+ | |||
+ | For ease of editing, set the grid's ''Options''/''goEditing'' to true; then assign the following procedure to the OnValidate event for the grid, which will be called every time a user has finished updating the grid: | ||
+ | |||
+ | <syntaxhighlight> | ||
+ | procedure TForm1.SalaryGridValidateEntry(sender: TObject; aCol, aRow: Integer; | ||
+ | const OldValue: string; var NewValue: String); | ||
+ | begin | ||
+ | if (aCol=3) and ((aRow=1) or (aRow=2)) then | ||
+ | begin | ||
+ | // Allow updates to min and max salary | ||
+ | if TryStrToInt(NewValue,-1)>0 then | ||
+ | begin | ||
− | + | LoadSalaryGrid; //reload standard deviation | |
− | + | end | |
− | + | else | |
+ | begin | ||
+ | showmessage('Invalid salary entered. Please enter integer numbers.'); | ||
+ | NewValue:=OldValue; | ||
+ | end; | ||
+ | end | ||
+ | else | ||
+ | begin | ||
+ | // Discard edits to any other cells | ||
+ | NewValue:=OldValue; | ||
+ | end; | ||
+ | end; | ||
+ | </syntaxhighlight> | ||
== Code == | == Code == |
Revision as of 14:42, 16 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.
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
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.
The resulting screen should show something like this:
Getting data out of normal controls into the database
Previously, we have seen:
- how to let SQLDB update the database with data-bound controls (earlier tutorials)
- how to get data out of the database using queries (this tutorial)
You can also execute SQL to get arbitrary data back into the database via code. 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:
For ease of editing, set the grid's Options/goEditing to true; then assign the following procedure to the OnValidate event for the grid, which will be called every time a user has finished updating the grid:
procedure TForm1.SalaryGridValidateEntry(sender: TObject; aCol, aRow: Integer;
const OldValue: string; var NewValue: String);
begin
if (aCol=3) and ((aRow=1) or (aRow=2)) then
begin
// Allow updates to min and max salary
if TryStrToInt(NewValue,-1)>0 then
begin
LoadSalaryGrid; //reload standard deviation
end
else
begin
showmessage('Invalid salary entered. Please enter integer numbers.');
NewValue:=OldValue;
end;
end
else
begin
// Discard edits to any other cells
NewValue:=OldValue;
end;
end;
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
- 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