Sign in Join
Wiki copytemporary

How to write in-memory database applications in Lazarus/FPC/ja: what changed

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

OldNewLine
11---
22[[Portal:Databases/ja|**データベースのポータル**]]
33参照:
44
55 * [[Databases/ja|Databases 概要]]
66 * [[Database libraries/ja|ライブラリ]]
77 * [[Database field type/ja|フィールドの型]]
88 * [[Data Controls tab/ja|コントロール]]
99 * [[Lazarus DB Faq/ja|FAQ]]
1010 * [[SqlDBHowto/ja]]
1111 * [[Working With TSQLQuery/ja|TSQLQuery の利用]]
1212 * メモリ上で動作するデータベースの作り方
1313
1414チュートリアル/練習となる記事:
1515
1616 * [[Lazarus Database Overview/ja|概要]]
1717 * [[SQLdb Tutorial0/ja|0 - データベースのセットアップ]]
1818 * [[SQLdb Tutorial1/ja|1 - 開始]]
1919 * [[SQLdb Tutorial2/ja|2 - 編集]]
2020 * [[SQLdb Tutorial3/ja|3 - クエリ]]
2121 * [[SQLdb Tutorial4/ja|4 - データモジュール]]
2222 * [[SQLdb Programming Reference/ja]]
2323
2424各種データベース
2525
2626 [[Advantage Database Server/ja|Advantage]] \- [[MySQLDatabases/ja|MySQL]] \- [MSSQL](</index.php?title=mssqlconn/ja&action=edit&redlink=1> "mssqlconn/ja \(page does not exist\)") \- [[postgres/ja|Postgres]] \- [[Firebird/ja|Interbase]] \- [[Firebird/ja|Firebird]] \- [[Oracle/ja|Oracle]] \- [[ODBCConn/ja|ODBC]] \- [Paradox](</index.php?title=TParadox/ja&action=edit&redlink=1> "TParadox/ja \(page does not exist\)") \- [[SQLite/ja|SQLite]] \- [dBASE](</index.php?title=Lazarus_Tdbf_Tutorial/ja&action=edit&redlink=1> "Lazarus Tdbf Tutorial/ja \(page does not exist\)") \- [[MS Access/ja|MS Access]] \- [Zeos](</index.php?title=Zeos_tutorial/ja&action=edit&redlink=1> "Zeos tutorial/ja \(page does not exist\)")
2727
2828## Contents
2929
3030 * 1 イントロダクション
3131 * 2 Saving MemDatasets to persistent files
3232 * 3 Autoincrement Primary Keys
3333 * 4 Enforcing Referential Integrity
3434 * 5 Known problems
3535 * 6 TBufDataSet
3636 * 7 Sorting DBGrid on TitleClick event for TBufDataSet
3737 * 8 Sorting multiple columns in grid
3838 * 9 ZMSQL
3939 * 10 Contributors
4040
4141## イントロダクション
4242
4343There are certain circumstances when in-memory datasets make sense. If you need a fast, single-user, non mission-critical, non SQL database, without need for transactions, [[TMemDataset]] could suit your needs.
4444
4545Some benefits are:
4646
4747 * Fast execution. Since all processing is done in memory, no data is saved on hard disk until explicitly asked. Memory is surely faster than hard disk.
4848 * No need for external libraries (no .so or .dll files), no need for server installation
4949 * Code is multiplatform and can be compiled on any OS instantly
5050 * Since all programming is done in Lazarus/FPC, such applications are easier for maintenance. Instead of constantly switching from back-end programming to front-end programming, by using MemDatasets you can concentrate on your Pascal code.
5151
5252![Light bulb](https://upload.wikimedia.org/wikipedia/commons/d/d8/Nuvola_apps_ktip.png) **Note:** later on in this article, BufDataset is introduced. [[TBufDataset]] often is a better choice than [[TMemDataset]]
5353
5454I will illustrate how to program relational non-SQL memory databases, focusing on enforcing relation integrity and filtering, simulating auto-increment primary fields and similar.
5555
5656This page shares with you what I have learned experimenting with TMemDatasets. There might be some other, more efficient way to do this. If so, please, feel free to contribute to this document for the benefit of the Lazarus/FPC community.
5757
5858The memds unit provides TMemDataset, so you will need to add that to your uses clause.
5959
6060## Saving MemDatasets to persistent files
6161
6262In the [[Interface|interface]] part of your code, declare an array type for storing information about all the TMemDataSets that you want to make persistent at the end of a session and restore at the beginning of the next session. You have to declare a variable of type TSaveTables, too.
6363
6464I also use a global variable vSuppressEvents of type boolean, for suppressing Dataset events used for referential integrity enforcement, during data restore.
6565
6666You get this:
6767```
6868type
6969 TSaveTables=array[1..15] of TMemDataset;
7070 var
7171 //Global variable that holds tables for saving/restoring session
7272 vSaveTables:TSaveTables;
7373 //Suppress events flag variables. Used during data loading from files.
7474 vSuppressEvents:Boolean;
7575```
7676
7777Instead of using global variables like I did, you could make them a property of the main form, also. TMemDataset has a way to natively store data to persistent file: the SaveToFile method. But, you could rather choose to save data to [[CSV]] files for easier external post processing. Therefore, I will combine both ways into same procedures. I define a constant cSaveRestore in the Interface part, by which I can define whether data will be stored and loaded as native MemDataset files or CSV files.
7878```
7979const
8080 //Constant cSaveRestore determines the way for saving and restoring of MemDatasets to persistent files
8181 cSaveRestore=0; //0=MemDataset native way, 1=saving and restoring from CSV
8282```
8383
8484Now, you can save MemDatasets on FormClose event and load them on FormCreate event. Instantiate elements of the array of MemDatasets on the FormCreate event, too.
8585```
8686procedure TMainForm.FormCreate(Sender: TObject);
8787 begin
8888 //List of tables to be saved/restored for a session
8989 vSaveTables[1]:=Products;
9090 vSaveTables[2]:=Boms;
9191 vSaveTables[3]:=Stocks;
9292 vSaveTables[4]:=Orders;
9393 vSaveTables[5]:=BomCalculationProducts;
9494 vSaveTables[6]:=BomCalculationComponents;
9595 vSaveTables[7]:=BomCalculationFooter;
9696 vSaveTables[8]:=BomCalculationProductsMultiple;
9797 vSaveTables[9]:=BomCalculationComponentsMultiple;
9898 vSaveTables[10]:=BomCalculationFooterMultiple;
9999 vSaveTables[11]:=ImportVariants;
100100 vSaveTables[12]:=ImportToTables;
101101 vSaveTables[13]:=ImportToFields;
102102 vSaveTables[14]:=ImportFromTables;
103103 vSaveTables[15]:=ImportFromFields;
104104 //Restore session
105105 RestoreSession;
106106 GetAutoincrementPrimaryFields;
107107 end;
108108```
109109```
110110procedure TMainForm.FormClose(Sender: TObject; var CloseAction: TCloseAction);
111111 begin
112112 //Save memdatasets to files (to save current session)
113113 SaveSession;
114114 end;
115115```
116116```
117117procedure RestoreSession;
118118 var
119119 I:Integer;
120120 begin
121121 try
122122 MemoMessages.Append(TimeToStr(Now())+' Starting restoration of previously saved session.');
123123 vSuppressEvents:=True; //Supress events used for referential integrity enforcing
124124 //Disable controls and refresh all datasets
125125 for I:=Low(vSaveTables) to High(vSaveTables) do begin
126126 vSaveTables[I].DisableControls;
127127 vSaveTables[I].Refresh; //Important if dataset was filtered
128128 end;
129129 //Load memdatasets from files (to restore previous session)
130130 for I:=Low(vSaveTables) to High(vSaveTables) do begin
131131 vSaveTables[I].First;
132132 MemoMessages.Append(TimeToStr(Now())+' Starting restoration of table: '+vSaveTables[I].Name);
133133 try
134134 //If data is loaded from a csv file, then table must be deleted first.
135135 if cSaveRestore=1 then begin
136136 MemoMessages.Append(TimeToStr(Now())+' Starting delete of all records in table: '+vSaveTables[I].Name);
137137 //This way of deleting all records is incredibly slow.
138138 {while not vSaveTables[I].EOF do begin
139139 vSaveTables[I].Delete;
140140 end;}
141141 //This method for deleting of all records is much faster
142142 EmptyMemDataSet(vSaveTables[I]);
143143 MemoMessages.Append(TimeToStr(Now())+' All records from table: '+vSaveTables[I].Name+' deleted.');
144144 end;
145145 except
146146 on E:Exception do begin
147147 MemoMessages.Append(TimeToStr(Now())+' Error while deleteing records from table: '+vSaveTables[I].Name +'. '+E.Message);
148148 end;
149149 end;
150150 try
151151 try
152152 MemoMessages.Append(TimeToStr(Now())+' Restoring table: '+vSaveTables[I].Name);
153153 //Check constant for way of saving/restoring data and load saved session
154154 case cSaveRestore of
155155 0:vSaveTables[I].LoadFromFile(vSaveTables[I].Name);
156156 1:LoadFromCsv(vSaveTables[I]);
157157 end;
158158 except
159159 on E:Exception do begin
160160 MemoMessages.Append(TimeToStr(Now())+' Error while restoring table: '+vSaveTables[I].Name +'. '+E.Message);
161161 end;
162162 end;
163163 finally
164164 vSaveTables[I].Active:=True;//Needed because of LoadFromFile method....
165165 end;
166166 MemoMessages.Append(TimeToStr(Now())+' Table: '+vSaveTables[I].Name+' restored.');
167167 end;
168168 finally
169169 vSuppressEvents:=False;
170170 //Refresh all datasets and enable controls
171171 for I:=Low(vSaveTables) to High(vSaveTables) do begin
172172 vSaveTables[I].Refresh; //Needed for tables that are filtered.
173173 vSaveTables[I].EnableControls;
174174 end;
175175 MemoMessages.Append(TimeToStr(Now())+' All tables restored from saved files.');
176176 end;
177177 end;
178178```
179179```
180180procedure SaveSession;
181181 var
182182 I:Integer;
183183 begin
184184 try
185185 MemoMessages.Append(TimeToStr(Now())+' Starting saving session to persistent files.');
186186 vSuppressEvents:=True;
187187 //Disable controls and refresh all datasets
188188 for I:=Low(vSaveTables) to High(vSaveTables) do begin
189189 vSaveTables[I].DisableControls;
190190 vSaveTables[I].Refresh; //Important if dataset was filtered
191191 end;
192192 //Save session to file
193193 for I:=Low(vSaveTables) to High(vSaveTables) do begin
194194 vSaveTables[I].First;
195195 MemoMessages.Append(TimeToStr(Now())+' Saving table: '+vSaveTables[I].Name);
196196 try
197197 //Check constant for way of saving/restoring data and save session
198198 case cSaveRestore of
199199 0:vSaveTables[I].SaveToFile(vSaveTables[I].Name);
200200 1:SaveToCsv(vSaveTables[I]);
201201 end;
202202 except
203203 on E:Exception do begin
204204 MemoMessages.Append(TimeToStr(Now())+' Error while saving table: '+vSaveTables[I].Name +'. '+E.Message);
205205 end;
206206 end;
207207 MemoMessages.Append(TimeToStr(Now())+' Table: '+vSaveTables[I].Name+' saved.');
208208 end;
209209 finally
210210 vSuppressEvents:=False;
211211 //Refresh all datasets and enable controls
212212 for I:=Low(vSaveTables) to High(vSaveTables) do begin
213213 vSaveTables[I].Refresh; //Needed for tables that are filtered
214214 vSaveTables[I].EnableControls;
215215 end;
216216 MemoMessages.Append(TimeToStr(Now())+' All tables saved to files.');
217217 end;
218218 end;
219219```
220220```
221221procedure EmptyMemDataSet(DataSet:TMemDataSet);
222222 var
223223 vTemporaryMemDataSet:TMemDataSet;
224224 vFieldDef:TFieldDef;
225225 I:Integer;
226226 begin
227227 try
228228 //Create temporary MemDataSet
229229 vTemporaryMemDataSet:=TMemDataSet.Create(nil);
230230 //Store FieldDefs to Temporary MemDataSet
231231 for I:=0 to DataSet.FieldDefs.Count-1 do begin
232232 vFieldDef:=vTemporaryMemDataSet.FieldDefs.AddFieldDef;
233233 with DataSet.FieldDefs[I] do begin
234234 vFieldDef.Name:=Name;
235235 vFieldDef.DataType:=DataType;
236236 vFieldDef.Size:=Size;
237237 vFieldDef.Required:=Required;
238238 end;
239239 end;
240240 //Clear existing fielddefs
241241 DataSet.Clear;
242242 //Restore fielddefs
243243 DataSet.FieldDefs:=vTemporaryMemDataSet.FieldDefs;
244244 DataSet.Active:=True;
245245 finally
246246 vTemporaryMemDataSet.Clear;
247247 vTemporaryMemDataSet.Free;
248248 end;
249249 end;
250250```
251251```
252252procedure LoadFromCsv(DataSet:TDataSet);
253253 var
254254 vFieldCount:Integer;
255255 I:Integer;
256256 begin
257257 try
258258 //Assign SdfDataSetTemporary
259259 with SdfDataSetTemporary do begin
260260 Active:=False;
261261 ClearFields;
262262 FileName:=DataSet.Name+'.txt';
263263 FirstLineAsSchema:=True;
264264 Active:=True;
265265 //Determine number of fields
266266 vFieldCount:=FieldDefs.Count;
267267 end;
268268 //Iterate through SdfDataSetTemporary and insert records into MemDataSet
269269 SdfDataSetTemporary.First;
270270 while not SdfDataSetTemporary.EOF do begin
271271 DataSet.Append;
272272 //Iterate through FieldDefs
273273 for I:=0 to vFieldCount-1 do begin
274274 try
275275 DataSet.Fields[I].Value:=SdfDataSetTemporary.Fields[I].Value;
276276 except
277277 on E:Exception do begin
278278 MemoMessages.Append(TimeToStr(Now())+' Error while setting value for field: '
279279 +DataSet.Name+'.'+DataSet.Fields[I].Name +'. '+E.Message);
280280 end;
281281 end;
282282 end;
283283 try
284284 DataSet.Post;
285285 except
286286 on E:Exception do begin
287287 MemoMessages.Append(TimeToStr(Now())+' Error while posting record to table: '
288288 +DataSet.Name+'.'+E.Message);
289289 end;
290290 end;
291291 SdfDataSetTemporary.Next;
292292 end;
293293 finally
294294 SdfDataSetTemporary.Active:=False;
295295 SdfDataSetTemporary.ClearFields;
296296 end;
297297 end;
298298```
299299```
300300procedure SaveToCsv(DataSet:TDataSet);
301301 var
302302 myFileName:string;
303303 myTextFile: TextFile;
304304 i: integer;
305305 s: string;
306306 begin
307307 myFileName:=DataSet.Name+'.txt';
308308 //create a new file
309309 AssignFile(myTextFile, myFileName);
310310 Rewrite(myTextFile);
311311 s := ''; //initialize empty string
312312 try
313313 //write field names (as column headers)
314314 for i := 0 to DataSet.Fields.Count - 1 do
315315 begin
316316 s := s + Format('%s,', [DataSet.Fields[i].FieldName]);
317317 end;
318318 Writeln(myTextFile, s);
319319 DataSet.First;
320320 //write field values
321321 while not DataSet.Eof do
322322 begin
323323 s := '';
324324 for i := 0 to DataSet.FieldCount - 1 do
325325 begin
326326 //Numerical fields without quotes, string fields with quotes
327327 if ((DataSet.FieldDefs[i].DataType=ftInteger)
328328 or (DataSet.FieldDefs[i].DataType=ftFloat)) then
329329 s := s + Format('%s,', [DataSet.Fields[i].AsString])
330330 else
331331 s := s + Format('"%s",', [DataSet.Fields[i].AsString]);
332332 end;
333333 Writeln(myTextfile, s);
334334 DataSet.Next;
335335 end;
336336 finally
337337 CloseFile(myTextFile);
338338 end;
339339 end;
340340```
341341
342342## Autoincrement Primary Keys
343343
344344Autoincrement field type is not supported by MemDataset. Nevertheless, you can imitate it by using Integer field type and providing a calculator for autoincrement fields. We need global variables or public properties for storing current autoincrement field value. I prefer global variables, declared in Interface part.
345345```
346346var
347347 //Global variables used for calculation of autoincrement primary key fields of MemDatasets
348348 vCurrentId:Integer=0;
349349 vProductsId:Integer=0;
350350 vBomsId:Integer=0;
351351 vBomCalculationProductsId:Integer=0;
352352 vBomCalculationComponentsId:Integer=0;
353353 vBomCalculationFooterId:Integer=0;
354354 vBomCalculationProductsMultipleId:Integer=0;
355355 vBomCalculationComponentsMultipleId:Integer=0;
356356 vBomCalculationFooterMultipleId:Integer=0;
357357 vStocksId:Integer=0;
358358 vOrdersId:Integer=0;
359359 vImportVariantsId:Integer=0;
360360 vImportToTablesId:Integer=0;
361361 vImportToFieldsId:Integer=0;
362362 vImportFromTablesId:Integer=0;
363363 vImportFromFieldsId:Integer=0;
364364```
365365
366366Then we have a procedure for autoincrement field values calculation:
367367```
368368procedure GetAutoincrementPrimaryFields;
369369 var
370370 I:Integer;
371371 vId:^Integer;
372372 begin
373373 try
374374 MemoMessages.Lines.Append(TimeToStr(Now())+' Getting information about autoincrement fields');
375375 vSuppressEvents:=True;
376376 //Disable controls and refresh all datasets
377377 for I:=Low(vSaveTables) to High(vSaveTables) do begin
378378 vSaveTables[I].DisableControls;
379379 vSaveTables[I].Refresh; //Important if dataset was filtered
380380 end;
381381 for I:=Low(vSaveTables) to High(vSaveTables) do begin
382382 with vSaveTables[I] do begin
383383 //Use appropriate global variable
384384 case StringToCaseSelect(Name,
385385 ['Products','Boms','Stocks','Orders',
386386 'BomCalculationProducts','BomCalculationComponents','BomCalculationFooter',
387387 'BomCalculationProductsMultiple','BomCalculationComponentsMultiple','BomCalculationFooterMultiple',
388388 'ImportVariants','ImportToTables','ImportToFields','ImportFromTables','ImportFromFields']) of
389389 0:vId:=@vProductsId;
390390 1:vId:=@vBomsId;
391391 2:vId:=@vStocksId;
392392 3:vId:=@vOrdersId;
393393 4:vId:=@vBomCalculationProductsId;
394394 5:vId:=@vBomCalculationComponentsId;
395395 6:vId:=@vBomCalculationFooterId;
396396 7:vId:=@vBomCalculationProductsMultipleId;
397397 8:vId:=@vBomCalculationComponentsMultipleId;
398398 9:vId:=@vBomCalculationFooterMultipleId;
399399 10:vId:=@vImportVariantsId;
400400 11:vId:=@vImportToTablesId;
401401 12:vId:=@vImportToFieldsId;
402402 13:vId:=@vImportFromTablesId;
403403 14:vId:=@vImportFromFieldsId;
404404 end;
405405 try
406406 //Find last value of Id and save it to global variable
407407 Last;
408408 vCurrentId:=FieldByName(Name+'Id').AsInteger;
409409 if (vCurrentId>vId^) then vId^:=vCurrentId;
410410 finally
411411 //Remove reference;
412412 vId:=nil;
413413 end;
414414 end;
415415 end;
416416 finally
417417 vSuppressEvents:=False;
418418 //Refresh all datasets and enable controls
419419 for I:=Low(vSaveTables) to High(vSaveTables) do begin
420420 vSaveTables[I].Refresh;
421421 vSaveTables[I].EnableControls;
422422 end;
423423 MemoMessages.Lines.Append(TimeToStr(Now())+' Autoincrement fields - done.');
424424 end;
425425 end;
426426```
427427```
428428function StringToCaseSelect(Selector:string;CaseList:array of string):Integer;
429429 var
430430 cnt: integer;
431431 begin
432432 Result:=-1;
433433 for cnt:=0 to Length(CaseList)-1 do
434434 begin
435435 if CompareText(Selector, CaseList[cnt]) = 0 then
436436 begin
437437 Result:=cnt;
438438 Break;
439439 end;
440440 end;
441441 end;
442442```
443443
444444The GetAutoincrementPrimaryFields procedure is called every time after you restore (load) data from persistent files, in order to load last autoincrement values into global variables (or properties, as you prefer). Autoincrementing is done in OnNewRecord event of every MemDataset. For example, for MemDataset Orders:
445445```
446446procedure TMainForm.OrdersNewRecord(DataSet: TDataSet);
447447 begin
448448 if vSuppressEvents=True then Exit;
449449 //Set new autoincrement value
450450 vOrdersId:=vOrdersId+1;
451451 DataSet.FieldByName('OrdersId').AsInteger:=vOrdersId;
452452 end;
453453```
454454
455455As already explained, I use vSuppressEvents global variable as flag for the case of restoring data from persistent files.
456456
457457## Enforcing Referential Integrity
458458
459459There is no enforced referential integrity implemented in MemDataset component, so you have to do it on your own.
460460
461461Let's assume we have two tables: MasterTable and DetailTable.
462462
463463There are various places where referential integrity code needs to be used:
464464
465465 * Insert/Update code is located in the `BeforePost` event of the DetailTable: before a new/updated detail record is posted/saved, it needs to be checked for meeting referential integrity requirements
466466 * Delete code is located in the `BeforeDelete` event of the MasterTable: before a master record is deleted, it needs to make sure any child records meet referential integrity requirements
467467
468468```
469469procedure TMainForm.MasterTableBeforeDelete(DataSet: TDataSet);
470470 begin
471471 if vSuppressEvents=True then Exit;
472472 try
473473 DetailTable.DisableControls;
474474 // Enforce referential delete ("cascade delete") for table "MasterTable"
475475 while not DetailTable.EOF do begin
476476 DetailTable.Delete;
477477 end;
478478 DetailTable.Refresh;
479479 finally
480480 DetailTable.EnableControls;
481481 end;
482482 end;
483483```
484484```
485485procedure TMainForm.DetailTableBeforePost(DataSet: TDataSet);
486486 begin
487487 if vSuppressEvents=True then Exit;
488488 // Enforce referential insert/update for table "DetailTable" with
489489 // foreign key "MasterTableID" linking to
490490 // the MasterTable ID primary key field
491491 DataSet.FieldByName('MasterTableId').AsInteger:=
492492 MasterTable.FieldByName('ID').AsInteger;
493493 end;
494494```
495495
496496After you provided referential Insert/Update/Delete, all you must do is provide code for master/detail filtering of data. You do it in the `AfterScroll` event of the MasterTable and in the `OnFilter` event of the DetailTable.
497497
498498Don't forget to set the `Filtered` property of DetailTable to True.
499499```
500500procedure TMainForm.MasterTableAfterScroll(DataSet: TDataSet);
501501 begin
502502 if vSuppressEvents=True then Exit;
503503 DetailTable.Refresh;
504504 end;
505505```
506506```
507507procedure TMainForm.DetailTableFilterRecord(DataSet: TDataSet;
508508 var Accept: Boolean);
509509 begin
510510 if vSuppressEvents=True then Exit;
511511 // Show only child fields whose foreign key points to current
512512 // master table record
513513 Accept:=DataSet.FieldByName('MasterTableId').AsInteger=
514514 MasterTable.FieldByName('ID').AsInteger;
515515 end;
516516```
517517
518518## Known problems
519519
520520There are several limitations when using MemDatasets.
521521
522522 * Locate method does not work
523523 * Filtering by using Filter and Filtered property does not work. You must use hardcoding in the OnFilter event.
524524 * Looping deletion of records seems to be incredibly slow. Therefore I use my EmptyMemDataset procedure instead of while not EOF do Delete;
525525 * In FPC 2.6.x and earlier, CopyFromDataSet method copies data only from the current cursor position to the end of the source dataset. So, you have to write MemDataset1.First; before MemDataSet2.CopyFromDataSet(MemDataset1);. Fixed in FPC trunk revision 26233.
526526 * Note that older versions of FPC has no CopyFromDataset in Bufdataset, at the time an advantage for MemDs.
527527 * See bug report <http://bugs.freepascal.org/view.php?id=25426>.
528528
529529## TBufDataSet
530530
531531As previously mentioned, MemDataSet lacks custom filters, autoincrement data type and the Locate method, so it is better to use TBufDataSet instead. TBufDataset is provided by the BufDataset unit.
532532
533533Since there is no component for design-time editing of TBufDataSet (but you can set up field definitions at design time), you could create a custom wrapper component or use it through code, in the same way as ClientDataSet in Delphi. Look at the Delphi documentation relating to client datasets for details.
534534
535535You can use the same methods for enforcing referential integrity and primary autoincrement fields as explained for MemDataSet.
536536
537537There are only small differences between MemDataSet and BufDataset:
538538
539539MemDataSet | BufDataset
540540---|---
541541DataSet.ClearFields | DataSet.Fields.Clear
542542DataSet.CreateTable | DataSet.CreateDataSet
543543
544544## Sorting DBGrid on TitleClick event for TBufDataSet
545545
546546If you wish to enable consecutive ascending and descending sorting of a DBGrid showing some data from TBufDataSet, you could use the following method:
547547```
548548Uses
549549 BufDataset, typinfo;
550550
551551 function SortBufDataSet(DataSet: TBufDataSet;const FieldName: String): Boolean;
552552 var
553553 i: Integer;
554554 IndexDefs: TIndexDefs;
555555 IndexName: String;
556556 IndexOptions: TIndexOptions;
557557 Field: TField;
558558 begin
559559 Result := False;
560560 Field := DataSet.Fields.FindField(FieldName);
561561 //If invalid field name, exit.
562562 if Field = nil then Exit;
563563 //if invalid field type, exit.
564564 if {(Field is TObjectField) or} (Field is TBlobField) or
565565 {(Field is TAggregateField) or} (Field is TVariantField)
566566 or (Field is TBinaryField) then Exit;
567567 //Get IndexDefs and IndexName using RTTI
568568 if IsPublishedProp(DataSet, 'IndexDefs') then
569569 IndexDefs := GetObjectProp(DataSet, 'IndexDefs') as TIndexDefs
570570 else
571571 Exit;
572572 if IsPublishedProp(DataSet, 'IndexName') then
573573 IndexName := GetStrProp(DataSet, 'IndexName')
574574 else
575575 Exit;
576576 //Ensure IndexDefs is up-to-date
577577 IndexDefs.Updated:=false; {<<<<---This line is critical as IndexDefs.Update will do nothing on the next sort if it's already true}
578578 IndexDefs.Update;
579579 //If an ascending index is already in use,
580580 //switch to a descending index
581581 if IndexName = FieldName + '__IdxA'
582582 then
583583 begin
584584 IndexName := FieldName + '__IdxD';
585585 IndexOptions := [ixDescending];
586586 end
587587 else
588588 begin
589589 IndexName := FieldName + '__IdxA';
590590 IndexOptions := [];
591591 end;
592592 //Look for existing index
593593 for i := 0 to Pred(IndexDefs.Count) do
594594 begin
595595 if IndexDefs[i].Name = IndexName then
596596 begin
597597 Result := True;
598598 Break
599599 end; //if
600600 end; // for
601601 //If existing index not found, create one
602602 if not Result then
603603 begin
604604 if IndexName=FieldName + '__IdxD' then
605605 DataSet.AddIndex(IndexName, FieldName, IndexOptions, FieldName)
606606 else
607607 DataSet.AddIndex(IndexName, FieldName, IndexOptions);
608608 Result := True;
609609 end; // if not
610610 //Set the index
611611 SetStrProp(DataSet, 'IndexName', IndexName);
612612 end;
613613```
614614
615615So, you can call this function from a DBGrid in this way:
616616```
617617procedure TFormMain.DBGridProductsTitleClick(Column: TColumn);
618618 begin
619619 SortBufDataSet(Products, Column.FieldName);
620620 end;
621621```
622622
623623
624624
625625## Sorting multiple columns in grid
626626
627627I have written TDBGridHelper for sorting grid by multiple columns while holding shift key. Note MaxIndexesCount must be set quite large for TBufDataSet because there can be quite large combinations of possible sorting options. But I think people would not use more than 10 so setting it 100 should be teoretically Ok.
628628```
629629{ TDBGridHelper }
630630
631631 TDBGridHelper = class helper for TDBGrid
632632 public const
633633 cMaxColCOunt = 3;
634634 private
635635 procedure Interbal_MakeNames(Fields: TStrings; out FieldsList, DescFields: String);
636636 procedure Internal_SetColumnsIcons(Fields: TStrings; AscIdx, DescIdx: Integer);
637637 function Internal_IndexNameExists(IndexDefs: TIndexDefs; IndexName: String): Boolean;
638638 public
639639 procedure Sort(const FieldName: String; AscIdx: Integer = -1; DescIdx: Integer = -1);
640640 procedure ClearSort;
641641 end;
642642
643643 { TDBGridHelper }
644644
645645 procedure TDBGridHelper.Interbal_MakeNames(Fields: TStrings; out FieldsList, DescFields: String);
646646 var
647647 FldList: TStringList;
648648 DscList: TStringList;
649649 FldDesc, FldName: String;
650650 i: Integer;
651651 begin
652652 if Fields.Count = 0 then
653653 begin
654654 FieldsList := '';
655655 DescFields := '';
656656 Exit;
657657 end;
658658
659659 FldList := TStringList.Create;
660660 DscList := TStringList.Create;
661661 try
662662 FldList.Delimiter := ';';
663663 DscList.Delimiter := ';';
664664
665665 for i := 0 to Fields.Count - 1 do
666666 begin
667667 Fields.GetNameValue(i, FldName, FldDesc);
668668 FldList.Add(FldName);
669669
670670 if FldDesc = 'D' then
671671 DscList.Add(FldName);
672672 end;
673673
674674 FieldsList := FldList.DelimitedText;
675675 DescFields := DscList.DelimitedText;
676676 finally
677677 FldList.Free;
678678 DscList.Free;
679679 end;
680680 end;
681681
682682 procedure TDBGridHelper.Internal_SetColumnsIcons(Fields: TStrings; AscIdx, DescIdx: Integer);
683683 var
684684 i: Integer;
685685 FldDesc: String;
686686 begin
687687 for i := 0 to Self.Columns.Count - 1 do
688688 begin
689689 FldDesc := Fields.Values[Self.Columns[i].Field.FieldName];
690690
691691 if FldDesc = 'A' then
692692 Self.Columns[i].Title.ImageIndex := AscIdx
693693 else
694694 if FldDesc = 'D' then
695695 Self.Columns[i].Title.ImageIndex := DescIdx
696696 else
697697 Self.Columns[i].Title.ImageIndex := -1
698698 end;
699699 end;
700700
701701 function TDBGridHelper.Internal_IndexNameExists(IndexDefs: TIndexDefs; IndexName: String): Boolean;
702702 var
703703 i: Integer;
704704 begin
705705 for i := 0 to IndexDefs.Count - 1 do
706706 begin
707707 if IndexDefs[i].Name = IndexName then
708708 Exit(True)
709709 end;
710710
711711 Result := False
712712 end;
713713
714714 procedure TDBGridHelper.Sort(const FieldName: String; AscIdx: Integer;
715715 DescIdx: Integer);
716716 var
717717 Field: TField;
718718 DataSet: TBufDataset;
719719 IndexDefs: TIndexDefs;
720720 IndexName, Dir, DescFields, FieldsList: String;
721721 Fields: TStringList;
722722 begin
723723 if not Assigned(DataSource.DataSet) or
724724 not DataSource.DataSet.Active or
725725 not (DataSource.DataSet is TBufDataset) then
726726 Exit;
727727 DataSet := DataSource.DataSet as TBufDataset;
728728
729729 Field := DataSet.FieldByName(FieldName);
730730 if (Field is TBlobField) or (Field is TVariantField) or (Field is TBinaryField) then
731731 Exit;
732732
733733 IndexDefs := DataSet.IndexDefs;
734734 IndexName := DataSet.IndexName;
735735
736736 if not IndexDefs.Updated then
737737 IndexDefs.Update;
738738
739739 Fields := TStringList.Create;
740740 try
741741 Fields.DelimitedText := IndexName;
742742 Dir := Fields.Values[FieldName];
743743
744744 if Dir = 'A' then
745745 Dir := 'D'
746746 else
747747 if Dir = 'D' then
748748 Dir := 'A'
749749 else
750750 Dir := 'A';
751751
752752 //If shift is presed then add field to field list
753753 if ssShift in GetKeyShiftState then
754754 begin
755755 Fields.Values[FieldName] := Dir;
756756 //We do not add to sor any more field if total field count exids cMaxColCOunt
757757 if Fields.Count > cMaxColCOunt then
758758 Exit;
759759 end
760760 else
761761 begin
762762 Fields.Clear;
763763 Fields.Values[FieldName] := Dir;
764764 end;
765765
766766 IndexName := Fields.DelimitedText;
767767 if not Internal_IndexNameExists(IndexDefs, IndexName) then
768768 begin
769769 Interbal_MakeNames(Fields, FieldsList, DescFields);
770770 TBufDataset(DataSet).AddIndex(IndexName, FieldsList, [], DescFields, '');
771771 end;
772772
773773 DataSet.IndexName := IndexName;
774774 Internal_SetColumnsIcons(Fields, AscIdx, DescIdx)
775775 finally
776776 Fields.Free;
777777 end;
778778 end;
779779
780780 procedure TDBGridHelper.ClearSort;
781781 var
782782 DataSet: TBufDataset;
783783 Fields: TStringList;
784784 begin
785785 if not Assigned(DataSource.DataSet) or
786786 not DataSource.DataSet.Active or
787787 not (DataSource.DataSet is TBufDataset) then
788788 Exit;
789789 DataSet := DataSource.DataSet as TBufDataset;
790790
791791 DataSet.IndexName := '';
792792
793793 Fields := TStringList.Create;
794794 try
795795 Internal_SetColumnsIcons(Fields, -1, -1)
796796 finally
797797 Fields.Free
798798 end
799799 end;
800800```
801801
802802To use sorting you need to call helper methods in OnCellClick and onTitleClick. OnTitleClick - If you hold shift ads new column to sot list ore changes direction to selected column or just sorts one column OnCellClick - If you double click on cell[0, 0] grid clears its sorting
803803```
804804procedure TForm1.grdCountriesCellClick(Column: TColumn);
805805 begin
806806 if not Assigned(Column) then
807807 grdCountries.ClearSort
808808 end;
809809
810810 procedure TForm1.grdCountriesTitleClick(Column: TColumn);
811811 begin
812812 grdCountries.Sort(Column.Field.FieldName, 0, 1);
813813 end;
814814```
815815
816816If you have assigned TitleImageList then you can specify which image use for ascending and which for descending operations.
817817
818818## ZMSQL
819819
820820Another, often better way to write in-memory databases is to use the [[ZMSQL|ZMSQL package]]:
821821
822822 * <http://sourceforge.net/projects/lazarus-ccr/files/zmsql/>
823823 * <http://www.lazarus.freepascal.org/index.php/topic,13821.30.html>
824824 * <http://lazarus.freepascal.org/index.php/topic,13821>
825825
826826## Contributors
827827
828828Original text written by: Zlatko Matić (matalab@gmail.com)
829829
830830Other contributions by contributors as shown in the page History.
831831
832832---
833-*From the Free Pascal wiki, page "How to write in-memory database applications in Lazarus/FPC/ja", as the Internet Archive kept it on 2025-01-17. 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).*
833+*From the Free Pascal wiki, page "How to write in-memory database applications in Lazarus/FPC/ja", as the Internet Archive kept it on 2025-01-17, last changed on 2024-03-27. 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