Sign in Join
Wiki copytemporary

SQLdb Tutorial3: what changed

Revision 1 to 2. Removed lines have a minus sign; added lines have a plus sign.

OldNewLine
11## Overview
22
33In this tutorial, you will learn how to
44
55- make your application suitable for multiple databases, including using a login form
66- get database data into normal controls (instead of database controls)
77- get data out of the controls and back into the database
88- execute parametrized queries.
99
1010## Multi database support
1111
1212Using any database and a login form, you can support multiple different database servers/embedded libraries that SQLDB supports.
1313
1414Advantages:
1515
1616- the user/programmer can dynamically use any sqldb t*connection, so he can choose between dbs
1717
1818Disadvantages:
1919
2020- 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.
2121- You can't use T*connection specific properties (like *TIBConnection.Dialect* to set Firebird dialect).
2222
2323To 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:
2424
2525``` pascal
2626uses
2727...
2828var
2929 Conn: TSQLConnector;
3030begin
3131 Conn:=TSQLConnector.Create(nil);
3232 try
3333 // ...actual connector type is determined by this property.
3434 // Make sure the ChosenConfig.DBType string matches
3535 // the connectortype (e.g. see the string in the
3636 // T*ConnectionDef.TypeName for that connector .
3737 Conn.ConnectorType:=ChosenConfig.DBType;
3838 // the rest is as usual:
3939 Conn.HostName:='DBSERVER';
4040 Conn.DatabaseName:='bigdatabase.fdb';
4141 Conn.UserName:='SYSDBA';
4242 Conn.Password:='masterkey';
4343 try
4444 Conn.Open;
4545```
4646
4747## Login form
4848
4949As 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.
5050
5151In *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. In the form, the user can add/edit his username/password, and test the connection before going further.
5252
53-<figure>
54-<img src="dbloginform.png" title="dbloginform.png" />
55-<figcaption>dbloginform.png</figcaption>
56-</figure>
53+![](/wiki-images/b/b2/Dbloginform.png)
5754
5855We 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.
5956
6057This allows use without GUI login forms (e.g. when running unattended/batch operations), and allows reuse in e.g. web applications.
6158
6259This *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.
6360
6461### Connection test callback function
6562
6663To 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.
6764
6865The definition in the login form in *dbconfiggui.pas*:
6966
7067``` pascal
7168type
7269 TConnectionTestFunction = function(ChosenConfig: TDBConnectionConfig): boolean of object;
7370```
7471
7572The main form must implement a function that matches this definition to handle the test request from the config form.
7673
7774The callback function takes the config object 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, it sets the function result to true, otherwise the result stays false.
7875
7976Because 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.
8077
8178``` pascal
8279uses
8380...
8481dbconfig, dbconfiggui
8582...
8683procedure TForm1.FormCreate(Sender: TObject);
8784 LoginForm:=TDBConfigForm.Create(self);
8885 try
8986 // The test button on dbconfiggui will link to this procedure:
9087 ... this links the callback in ''dbconfiggui.pas'' to the ConnectionTest function here...
9188 LoginForm.ConnectionTestCallback:=@ConnectionTest;
9289...
9390function TForm1.ConnectionTest(ChosenConfig: TDBConnectionConfig): boolean;
9491// Callback function that uses the info in dbconfiggui to test a connection
9592// and return the result of the test to dbconfiggui
9693var
9794 // Generic database connector...
9895 Conn: TSQLConnector;
9996begin
10097 result:=false;
10198 Conn:=TSQLConnector.Create(nil);
10299 Screen.Cursor:=crHourglass;
103100 try
104101 // ...actual connector type is determined by this property.
105102 // Make sure the ChosenConfig.DBType string matches
106103 // the connectortype (e.g. see the string in the
107104 // T*ConnectionDef.TypeName for that connector .
108105 Conn.ConnectorType:=ChosenConfig.DBType;
109106 Conn.HostName:=ChosenConfig.DBHost;
110107 Conn.DatabaseName:=ChosenConfig.DBPath;
111108 Conn.UserName:=ChosenConfig.DBUser;
112109 Conn.Password:=ChosenConfig.DBPassword;
113110 try
114111 Conn.Open;
115112 result:=Conn.Connected;
116113 except
117114 // Result is already false
118115 end;
119116 Conn.Close;
120117 finally
121118 Screen.Cursor:=crDefault;
122119 Conn.Free;
123120 end;
124121end;
125122```
126123
127124Finally, 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:
128125
129126``` pascal
130127...
131128TDBConfigForm = class(TForm)
132129...
133130 private
134131 FConnectionTestFunction: TConnectionTestFunction;
135132 public
136133 property ConnectionTestCallback: TConnectionTestFunction write FConnectionTestFunction;
137134...
138135procedure TDBConfigForm.TestButtonClick(Sender: TObject);
139136begin
140137 // Call callback with settings, let it figure out if connection succeeded and
141138 // get test result back
142139 if assigned(FConnectionTestFunction) and assigned(FConnectionConfig) then
143140 if FConnectionTestFunction(FConnectionConfig) then
144141 showmessage('Connection test succeeded.')
145142 else
146143 showmessage('Connection test failed.')
147144 else
148145 showmessage('Error: connection test code has not been implemented.');
149146end;
150147```
151148
152149### Additions/modifications
153150
154151Possible additions/modifications for the login form:
155152
156153- Add command line arguments handling for dbconfig to preload suitable defaults, so the program can be used in batch scripts, shortcuts etc
157154- Add a "Select profile" combobox in the login form; use multiple profiles in the ini file that specify database type and connection details.
158155- Hide database type combobox when only one database type supported.
159156- Hide username/password when you're sure an embedded database is selected.
160157- Add support for specifying port number, or instance name with MS SQL Server connector
161158- Add support for trusted authentication for dbs that support it (Firebird, MS SQL): disable ussername/password controls
162159- If an embedded database is selected but the file does not exist: show a confirmation request and create the database
163160- Create a command-line/TUI version of the login form (e.g. using the curses library) for command-line applictions
164161
165162Updates to this article/the code warmly welcomed.
166163
167164## Getting database data into normal controls
168165
169166> **Note:** Before starting this section, please make sure you have set up the sample employee database as specified in [[SQLdb Tutorial0|SQLdb Tutorial0#Requirements]]
170167
171168In previous tutorials, data-bound controls were covered: special controls such as the [[TDBGrid]] that can bind its contents to a [[TDataSource]], get updates from that source and send user edits back.
172169
173170It is also possible to programmatically retrieve database content and fill any kind of control (or variable) with that content. As an example, we will look at filling a stringgrid with some salary details for the sample employee database table.
174171
175172On the main form, let's add a [[TStringGrid]] and retrieve the data (e.g. via a procedure *LoadSalaryGrid* called in the *OnCreate* event):
176173
177174``` pascal
178175 // Load from DB
179176 try
180177 if not FConn.Connected then
181178 FConn.Open;
182179 if not FConn.Connected then
183180 begin
184181 ShowMessage('Error connecting to the database. Aborting data loading.');
185182 exit;
186183 end;
187184
188185 // Lowest salary
189186 // Note: we would like to only retrieve 1 row, but unfortunately the SQL
190187 // used differs for various dbs. As we'll deal with db dependent SQL later
191188 // in the tutorial, we leave this for now.
192189 // MS SQL: 'select top 1 '...
193190 FQuery.SQL.Text:='select ' +
194191 ' e.first_name, ' +
195192 ' e.last_name, ' +
196193 ' e.salary ' +
197194 'from employee e ' +
198195 'order by e.salary asc ';
199196 // ISO SQL+Firebird SQL: add
200197 //'rows 1 '; here and below... won't work on e.g. PostgreSQL though
201198 FTran.StartTransaction;
202199 FQuery.Open;
203200 SalaryGrid.Cells[1,1]:=FQuery.Fields[0].AsString; // i.e. Cells[Col,Row]
204201 SalaryGrid.Cells[2,1]:=FQuery.Fields[1].AsString;
205202 SalaryGrid.Cells[3,1]:=FQuery.Fields[2].AsString;
206203 FQuery.Close;
207204 // Always commit(retain) an opened transaction, even if only reading
208205 // this will allow updates by others to be seen when reading again
209206 FTran.Commit;
210207...
211208 end;
212209 except
213210 on D: EDatabaseError do
214211 begin
215212 MessageDlg('Error', 'A database error has occurred. Technical error message: ' +
216213 D.Message, mtError, [mbOK], 0);
217214 end;
218215 end;
219216```
220217
221218Things 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.
222219
223220We *Open* the query object, thereby asking *FQuery* to query the database 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).
224221
225222Next, 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.
226223
227224## Adapting SQL for various databases
228225
229226As we noticed above, 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 e.g. Firebird.
230227
231228In our LoadSalaryGrid procedure, we'll use the SQL for PostgreSQL and build a code solution for all other databases. First detect which database is loaded, below the other lines add:
232229
233230``` pascal
234231 ...
235232 SalaryGrid.Cells[3,2]:=FQuery.Fields[2].AsString;
236233 FQuery.Close;
237234 // Always commit(retain) an opened transaction, even if only reading
238235 FTran.Commit;
239236//end of existing code
240237
241238 if FConn.ConnectorType = 'PostGreSQL' then
242239 begin
243240 // For PostgreSQL, use a native SQL solution:
244241 FQuery.SQL.Text:='select stddev_pop(salary) from employee ';
245242 FTran.StartTransaction;
246243 FQuery.Open;
247244 if not FQuery.EOF then
248245 SalaryGrid.Cells[3,3]:=FQuery.Fields[0].AsString;
249246 FQuery.Close;
250247 // Always commit(retain) an opened transaction, even if only reading
251248 FTran.Commit;
252249 end
253250 else
254251 begin
255252 // For other database, use the code approach:
256253 ....see below...
257254 end;
258255```
259256
260257Notice the use of *ConnectorType*; the string used must match exactly. We also properly check for empty results from the query (which might happen if the employee table is empty).
261258
262259... now let's implement a code-based solution for other databases that do not support standard deviation:
263260
264261``` pascal
265262 // For other databases, use the code approach:
266263 // 1. Get average of values
267264 FQuery.SQL.Text:='select avg(salary) from employee ';
268265 FQuery.Open;
269266 if FQuery.EOF then
270267 SalaryGrid.Cells[3,3]:='No data'
271268 else
272269 begin
273270 Average:=FQuery.Fields[0].AsFloat;
274271 FQuery.Close;
275272 // 2. For each value, calculate the square of (value-average), and add it up
276273 FQuery.SQL.Text:='select salary from employee where salary is not null ';
277274 FQuery.Open;
278275 while not FQuery.EOF do
279276 begin
280277 DifferencesSquared:=DifferencesSquared+Sqr(FQuery.Fields[0].AsFloat-Average);
281278 Count:=Count+1;
282279 FQuery.Next;
283280 end;
284281 // 3. Now calculate the average "squared difference" and take the square root
285282 if Count>0 then //avoid division by 0
286283 SalaryGrid.Cells[3,3]:=FloatToStr(Sqrt(DifferencesSquared/Count))
287284 else
288285 SalaryGrid.Cells[3,3]:='No data';
289286 end;
290287 FQuery.Close;
291288```
292289
293290Note that we use *FQuery.EOF* to check for empty data (and avoid division by zero errors etc). The loop shows how to:
294291
295292- retrieve a database value into a variable
296293- use *FQuery.Next* to move to the next record
297294- properly check if the query dataset has hit the last record, then stop retrieving data.
298295
299296The resulting screen should show something like this - note the use of a decimal comma - while your computer may show a decimal point depending on your locale:
300297
301-<figure>
302-<img src="sqldbtutorial3mainform.png" title="sqldbtutorial3mainform.png" />
303-<figcaption>sqldbtutorial3mainform.png</figcaption>
304-</figure>
298+![](/wiki-images/6/6a/Sqldbtutorial3mainform.png)
305299
306300### Revisiting our lowest/highest salary
307301
308302*This section gives some more useful details on SQL but is not required to work through for the rest of the tutorial*
309303
310304Now we know how to deal with detecting various database connections, we can adjust the SQL that gets the lowest and highest salary as well to make use of db specific functionality.
311305
312306An example: this would work for MS SQL Server by limiting the number of returned rows to just the first:
313307
314308``` SQL
315309select top 1
316310e.first_name, e.last_name, e.salary
317311from employee e
318312order by e.salary asc
319313```
320314
321315to get the lowest salary.
322316
323317This efficiently returns one record. Other databases use other syntax, such as the ISO ROWS 1. The diligent SQL student will soon learn not to miss out that important part and request entire large recordsets just for one required record!
324318
325319Let's briefly examine other ways to achieve the same thing, that are worth knowing.
326320
327321Another way to retrieve the record(s) with the minimum salary would be :''
328322
329323``` SQL
330324SELECT e.first_name, e.last_name, e.salary FROM employee e WHERE e.salary=(SELECT min(salary) FROM employee)
331325```
332326
333327SQL students would greatly benefit from researching Common Table Expressions.
334328
335329A CTE allows a virtual temporary table to be used in a following expression, allowing you to clearly code some very complex queries that otherwise may not be possible. Knowing about CTEs will catapult you ahead of colleagues who have never heard of them! For example the above may be rewritten (example in Microsoft SQL Server syntax) as : ''
336330
337331``` SQL
338332WITH TheMinimum as
339333(
340334 SELECT min(salary) as MinimumPay FROM Employee
341335)
342336SELECT e.first_name, e.last_name, e.salary FROM Employee e WHERE e.salary=(SELECT MinimumPay FROM TheMinimum)
343337```
344338
345339Several such temporary tables may be chained together, each using the results from the previous tables. You can treat these virtual tables as though they were real tables in the database, using JOINs to link recordsets together. And it can be very useful for quick tests using hardcoded data - this can be run without any database connection :
346340
347341``` SQL
348342WITH TestEmployee as
349343(
350344 SELECT 'Fred' as first_name, 'Bloggs' as last_name, 10500 as salary
351345 UNION
352346 SELECT 'Joe' as first_name, 'Public' as last_name, 10000 as salary
353347 UNION
354348 SELECT 'Mike' as first_name, 'Mouse' as last_name, 11000 as salary
355349),
356350 TheMinimum as
357351(
358352 SELECT min(salary) as MinimumPay FROM TestEmployee
359353)
360354SELECT e.first_name, e.last_name, e.salary FROM TestEmployee e WHERE e.salary=(SELECT MinimumPay FROM TheMinimum)
361355```
362356
363357You can end up with quite long strings for the code of such SQL queries, but it is only *one* query and may be called from anywhere where you are limited to a simple single expression - it can be useful to answer complex queries without resorting to functions or stored procedures.
364358
365359## Getting data out of normal controls into the database
366360
367361Previously, we have seen:
368362
369363- how to let SQLDB update the database with data-bound controls (earlier tutorials)
370364- how to get data out of the database using queries (the section above)
371365
372366You can also execute SQL to get arbitrary data back into the database via code. This allows you to use variables or 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.
373367
374368As an example, we are going to allow the user to change the lowest and highest salary in the stringgrid.
375369
376370For ease of editing, set the grid's *Options*/*goEditing* to true; then assign the procedure below to the OnValidate event for the grid, which will be called every time a user has finished updating the grid.
377371
378372### Parameterized queries
379373
380374The following code also demonstrates how to use parameterized queries to avoid SQL injection, fiddling with quoting for string values, date formatting, etc.
381375
382376As you can see in the code, you can name your parameters whatever you wish and prefix them with : in the SQL. In code, you can set/get their values by *\<somequery\>.Params.ParamByName('\<thename\>').As'\<variabletype\>'; the code demonstrates*.AsFloat*and*.AsString''.
383377
384378Parameterized queries are especially useful (and can be much faster) if you run the same query, only with different parameters, in a loop (think e.g. bulk loading of data).
385379
386380Continuing with our example: after having set up the query SQL and parameters, the transaction is started (and later on committed) as usual, then the query is run by calling *ExecSQL* (which does not return a result set; if the SQL statement were e.g. a SELECT or INSERT...RETURNING that does return data, you would use *Open* as in the examples above):
387381
388382``` pascal
389383procedure TForm1.SalaryGridValidateEntry(sender: TObject; aCol, aRow: Integer;
390384 const OldValue: string; var NewValue: String);
391385begin
392386 // Only these cells have min and max salary:
393387 if (aCol=3) and ((aRow=1) or (aRow=2)) then
394388 begin
395389 // Allow updates to min and max salary if positive numerical data is entered
396390 if StrToFloatDef(NewValue,-1)>0 then
397391 begin
398392 // Storing the primary key in e.g. a hidden cell in the grid and using that in our
399393 // update query would be cleaner, but we can do it the hard way as well:
400394 FQuery.SQL.Text:='update employee set salary=:newsalary '+
401395 ' where first_name=:firstname and last_name=:lastname and salary=:salary ';
402396 FQuery.Params.ParamByName('newsalary').AsFloat:=StrToFloatDef(NewValue,0);
403397 FQuery.Params.ParamByName('firstname').AsString:=SalaryGrid.Cells[1,aRow];
404398 FQuery.Params.ParamByName('lastname').AsString:=SalaryGrid.Cells[2,aRow];
405399 FQuery.Params.ParamByName('salary').AsFloat:=StrToFloatDef(OldValue,0);
406400 FTran.StartTransaction;
407401 FQuery.ExecSQL;
408402 FTran.Commit;
409403 LoadSalaryGrid; //reload standard deviation
410404 end
411405 else
412406 begin
413407 // Notify user that his input was wrong... he'll be wondering otherwise:
414408 Showmessage('Invalid salary entered.');
415409 NewValue:=OldValue;
416410 end;
417411 end
418412 else
419413 begin
420414 // Silently discard edits to any other cells
421415 NewValue:=OldValue;
422416 end;
423417end;
424418```
425419
426420Note how we forgot to add a try..except block to this code to nicely catch database errors and display a sensible error message. If you are running the Firebird sample EMPLOYEE database for this tutorial, try to change the salary to a very low value (say 1) and see what happens.
427421
428422Finally, while this example showed an UPDATE SQL query, you could just as well run INSERT queries to insert new data programmatically. Also, you can use parameters in any kind of SQL query (SELECT, UPDATE, etc) as long as you use them for fields, not for table/view/procedure names.
429423
430424## Summary
431425
432426This tutorial explained:
433427
434428- how to code for multiple database types
435429- how to use a login form to decouple db access configuration from your program
436430- how to retrieve and update data programmatically
437431
438432## Code
439433
440434Since November 2012, the code can be found in \$(lazarusdir)examples/database/sqldbtutorial3
441435
442436If you have an older version (e.g. Lazarus 1.0.2), you can also download the code via [the Lazarus SVN website](http://svn.freepascal.org/svn/lazarus/trunk/examples/database/sqldbtutorial3/)
443437
444438## See also
445439
446440- [[SQLdb Tutorial0]]: Instructions for setting up sample tables/sample data for the tutorial series.
447441- [[SQLdb Tutorial1]]: First part of the DB tutorial series, showing how to set up a grid that shows database data
448442- [[SQLdb Tutorial2]]: Second part of the DB tutorial series, showing editing, inserting etc.
449443- [[SQLdb Tutorial4]]: Fourth part of the DB tutorial series, showing how to use data modules
450444- [[Lazarus Database Overview]]: Information about the databases that Lazarus supports. Links to database-specific notes.
451445- [[SQLdb Package]]: information about the SQLdb package
452446- [[SQLdb Programming Reference]]: an overview of the interaction of the SQLdb database components
453447- [using parameters](http://www.freepascal.org/docs-html/fcl/sqldb/usingparams.html)
454448- [[SqlDBHowto]]: information about using the SQLdb package
455449- [[Working With TSQLQuery]]: information about TSQLQuery
456450
457451---
458452*From the Free Pascal wiki, page "SQLdb Tutorial3", as it stood on 2024-03-28 after a change by Ariben, out of the wiki's dump of 2024-05-26. Copyright stays with the author who wrote it, according to the old wiki's [[Licensing]] page. If you have concerns about this material, [message the admin](/wiki-contact).*

Back to history