Sign in Join
Wiki copytemporary

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

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

OldNewLine
11---
22[[Portal:Databases/ja|**データベースのポータル**]]
3-参照:
4-
5- * [[Databases/ja|Databases 概要]]
6- * [[Database libraries/ja|ライブラリ]]
7- * [[Database field type/ja|フィールドの型]]
8- * [[Data Controls tab/ja|コントロール]]
9- * [[Lazarus DB Faq/ja|FAQ]]
10- * [[SqlDBHowto/ja]]
11- * [[Working With TSQLQuery/ja|TSQLQuery の利用]]
12- * メモリ上で動作するデータベースの作り方
13-
14-チュートリアル/練習となる記事:
15-
16- * [[Lazarus Database Overview/ja|概要]]
17- * [[SQLdb Tutorial0/ja|0 - データベースのセットアップ]]
18- * [[SQLdb Tutorial1/ja|1 - 開始]]
19- * [[SQLdb Tutorial2/ja|2 - 編集]]
20- * [[SQLdb Tutorial3/ja|3 - クエリ]]
21- * [[SQLdb Tutorial4/ja|4 - データモジュール]]
22- * [[SQLdb Programming Reference/ja]]
23-
24-各種データベース
25-
26- [[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\)")
3+参照: · [[Databases/ja|Databases 概要]] · [[Database libraries/ja|ライブラリ]] · [[Database field type/ja|フィールドの型]] · [[Data Controls tab/ja|コントロール]] · [[Lazarus DB Faq/ja|FAQ]] · [[SqlDBHowto/ja]] · [[Working With TSQLQuery/ja|TSQLQuery の利用]] · メモリ上で動作するデータベースの作り方 · チュートリアル/練習となる記事: · [[Lazarus Database Overview/ja|概要]] · [[SQLdb Tutorial0/ja|0 - データベースのセットアップ]] · [[SQLdb Tutorial1/ja|1 - 開始]] · [[SQLdb Tutorial2/ja|2 - 編集]] · [[SQLdb Tutorial3/ja|3 - クエリ]] · [[SQLdb Tutorial4/ja|4 - データモジュール]] · [[SQLdb Programming Reference/ja]] · 各種データベース · [[Advantage Database Server/ja|Advantage]] \- [[MySQLDatabases/ja|MySQL]] \- [[mssqlconn/ja|MSSQL]] \- [[postgres/ja|Postgres]] \- [[Firebird/ja|Interbase]] \- [[Firebird/ja|Firebird]] \- [[Oracle/ja|Oracle]] \- [[ODBCConn/ja|ODBC]] \- [[TParadox/ja|Paradox]] \- [[SQLite/ja|SQLite]] \- [[Lazarus Tdbf Tutorial/ja|dBASE]] \- [[MS Access/ja|MS Access]] \- [[Zeos tutorial/ja|Zeos]]
274
28-## Contents
29-
30- * 1 イントロダクション
31- * 2 Saving MemDatasets to persistent files
32- * 3 Autoincrement Primary Keys
33- * 4 Enforcing Referential Integrity
34- * 5 Known problems
35- * 6 TBufDataSet
36- * 7 Sorting DBGrid on TitleClick event for TBufDataSet
37- * 8 Sorting multiple columns in grid
38- * 9 ZMSQL
39- * 10 Contributors
405
416## イントロダクション
427
438There 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.
449
4510Some benefits are:
4611
4712 * 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.
4813 * No need for external libraries (no .so or .dll files), no need for server installation
4914 * Code is multiplatform and can be compiled on any OS instantly
5015 * 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.
5116
5217![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]]
5318
5419I will illustrate how to program relational non-SQL memory databases, focusing on enforcing relation integrity and filtering, simulating auto-increment primary fields and similar.
5520
5621This 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.
5722
5823The memds unit provides TMemDataset, so you will need to add that to your uses clause.
5924
6025## Saving MemDatasets to persistent files
6126
6227In 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.
6328
6429I also use a global variable vSuppressEvents of type boolean, for suppressing Dataset events used for referential integrity enforcement, during data restore.
6530
6631You get this:
6732```
6833type
6934 TSaveTables=array[1..15] of TMemDataset;
7035 var
7136 //Global variable that holds tables for saving/restoring session
7237 vSaveTables:TSaveTables;
7338 //Suppress events flag variables. Used during data loading from files.
7439 vSuppressEvents:Boolean;
7540```
7641
7742Instead 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.
7843```
7944const
8045 //Constant cSaveRestore determines the way for saving and restoring of MemDatasets to persistent files
8146 cSaveRestore=0; //0=MemDataset native way, 1=saving and restoring from CSV
8247```
8348
8449Now, 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.
8550```
8651procedure TMainForm.FormCreate(Sender: TObject);
8752 begin
8853 //List of tables to be saved/restored for a session
8954 vSaveTables[1]:=Products;
9055 vSaveTables[2]:=Boms;
9156 vSaveTables[3]:=Stocks;
9257 vSaveTables[4]:=Orders;
9358 vSaveTables[5]:=BomCalculationProducts;
9459 vSaveTables[6]:=BomCalculationComponents;
9560 vSaveTables[7]:=BomCalculationFooter;
9661 vSaveTables[8]:=BomCalculationProductsMultiple;
9762 vSaveTables[9]:=BomCalculationComponentsMultiple;
9863 vSaveTables[10]:=BomCalculationFooterMultiple;
9964 vSaveTables[11]:=ImportVariants;
10065 vSaveTables[12]:=ImportToTables;
10166 vSaveTables[13]:=ImportToFields;
10267 vSaveTables[14]:=ImportFromTables;
10368 vSaveTables[15]:=ImportFromFields;
10469 //Restore session
10570 RestoreSession;
10671 GetAutoincrementPrimaryFields;
10772 end;
10873```
10974```
11075procedure TMainForm.FormClose(Sender: TObject; var CloseAction: TCloseAction);
11176 begin
11277 //Save memdatasets to files (to save current session)
11378 SaveSession;
11479 end;
11580```
11681```
11782procedure RestoreSession;
11883 var
11984 I:Integer;
12085 begin
12186 try
12287 MemoMessages.Append(TimeToStr(Now())+' Starting restoration of previously saved session.');
12388 vSuppressEvents:=True; //Supress events used for referential integrity enforcing
12489 //Disable controls and refresh all datasets
12590 for I:=Low(vSaveTables) to High(vSaveTables) do begin
12691 vSaveTables[I].DisableControls;
12792 vSaveTables[I].Refresh; //Important if dataset was filtered
12893 end;
12994 //Load memdatasets from files (to restore previous session)
13095 for I:=Low(vSaveTables) to High(vSaveTables) do begin
13196 vSaveTables[I].First;
13297 MemoMessages.Append(TimeToStr(Now())+' Starting restoration of table: '+vSaveTables[I].Name);
13398 try
13499 //If data is loaded from a csv file, then table must be deleted first.
135100 if cSaveRestore=1 then begin
136101 MemoMessages.Append(TimeToStr(Now())+' Starting delete of all records in table: '+vSaveTables[I].Name);
137102 //This way of deleting all records is incredibly slow.
138103 {while not vSaveTables[I].EOF do begin
139104 vSaveTables[I].Delete;
140105 end;}
141106 //This method for deleting of all records is much faster
142107 EmptyMemDataSet(vSaveTables[I]);
143108 MemoMessages.Append(TimeToStr(Now())+' All records from table: '+vSaveTables[I].Name+' deleted.');
144109 end;
145110 except
146111 on E:Exception do begin
147112 MemoMessages.Append(TimeToStr(Now())+' Error while deleteing records from table: '+vSaveTables[I].Name +'. '+E.Message);
148113 end;
149114 end;
150115 try
151116 try
152117 MemoMessages.Append(TimeToStr(Now())+' Restoring table: '+vSaveTables[I].Name);
153118 //Check constant for way of saving/restoring data and load saved session
154119 case cSaveRestore of
155120 0:vSaveTables[I].LoadFromFile(vSaveTables[I].Name);
156121 1:LoadFromCsv(vSaveTables[I]);
157122 end;
158123 except
159124 on E:Exception do begin
160125 MemoMessages.Append(TimeToStr(Now())+' Error while restoring table: '+vSaveTables[I].Name +'. '+E.Message);
161126 end;
162127 end;
163128 finally
164129 vSaveTables[I].Active:=True;//Needed because of LoadFromFile method....
165130 end;
166131 MemoMessages.Append(TimeToStr(Now())+' Table: '+vSaveTables[I].Name+' restored.');
167132 end;
168133 finally
169134 vSuppressEvents:=False;
170135 //Refresh all datasets and enable controls
171136 for I:=Low(vSaveTables) to High(vSaveTables) do begin
172137 vSaveTables[I].Refresh; //Needed for tables that are filtered.
173138 vSaveTables[I].EnableControls;
174139 end;
175140 MemoMessages.Append(TimeToStr(Now())+' All tables restored from saved files.');
176141 end;
177142 end;
178143```
179144```
180145procedure SaveSession;
181146 var
182147 I:Integer;
183148 begin
184149 try
185150 MemoMessages.Append(TimeToStr(Now())+' Starting saving session to persistent files.');
186151 vSuppressEvents:=True;
187152 //Disable controls and refresh all datasets
188153 for I:=Low(vSaveTables) to High(vSaveTables) do begin
189154 vSaveTables[I].DisableControls;
190155 vSaveTables[I].Refresh; //Important if dataset was filtered
191156 end;
192157 //Save session to file
193158 for I:=Low(vSaveTables) to High(vSaveTables) do begin
194159 vSaveTables[I].First;
195160 MemoMessages.Append(TimeToStr(Now())+' Saving table: '+vSaveTables[I].Name);
196161 try
197162 //Check constant for way of saving/restoring data and save session
198163 case cSaveRestore of
199164 0:vSaveTables[I].SaveToFile(vSaveTables[I].Name);
200165 1:SaveToCsv(vSaveTables[I]);
201166 end;
202167 except
203168 on E:Exception do begin
204169 MemoMessages.Append(TimeToStr(Now())+' Error while saving table: '+vSaveTables[I].Name +'. '+E.Message);
205170 end;
206171 end;
207172 MemoMessages.Append(TimeToStr(Now())+' Table: '+vSaveTables[I].Name+' saved.');
208173 end;
209174 finally
210175 vSuppressEvents:=False;
211176 //Refresh all datasets and enable controls
212177 for I:=Low(vSaveTables) to High(vSaveTables) do begin
213178 vSaveTables[I].Refresh; //Needed for tables that are filtered
214179 vSaveTables[I].EnableControls;
215180 end;
216181 MemoMessages.Append(TimeToStr(Now())+' All tables saved to files.');
217182 end;
218183 end;
219184```
220185```
221186procedure EmptyMemDataSet(DataSet:TMemDataSet);
222187 var
223188 vTemporaryMemDataSet:TMemDataSet;
224189 vFieldDef:TFieldDef;
225190 I:Integer;
226191 begin
227192 try
228193 //Create temporary MemDataSet
229194 vTemporaryMemDataSet:=TMemDataSet.Create(nil);
230195 //Store FieldDefs to Temporary MemDataSet
231196 for I:=0 to DataSet.FieldDefs.Count-1 do begin
232197 vFieldDef:=vTemporaryMemDataSet.FieldDefs.AddFieldDef;
233198 with DataSet.FieldDefs[I] do begin
234199 vFieldDef.Name:=Name;
235200 vFieldDef.DataType:=DataType;
236201 vFieldDef.Size:=Size;
237202 vFieldDef.Required:=Required;
238203 end;
239204 end;
240205 //Clear existing fielddefs
241206 DataSet.Clear;
242207 //Restore fielddefs
243208 DataSet.FieldDefs:=vTemporaryMemDataSet.FieldDefs;
244209 DataSet.Active:=True;
245210 finally
246211 vTemporaryMemDataSet.Clear;
247212 vTemporaryMemDataSet.Free;
248213 end;
249214 end;
250215```
251216```
252217procedure LoadFromCsv(DataSet:TDataSet);
253218 var
254219 vFieldCount:Integer;
255220 I:Integer;
256221 begin
257222 try
258223 //Assign SdfDataSetTemporary
259224 with SdfDataSetTemporary do begin
260225 Active:=False;
261226 ClearFields;
262227 FileName:=DataSet.Name+'.txt';
263228 FirstLineAsSchema:=True;
264229 Active:=True;
265230 //Determine number of fields
266231 vFieldCount:=FieldDefs.Count;
267232 end;
268233 //Iterate through SdfDataSetTemporary and insert records into MemDataSet
269234 SdfDataSetTemporary.First;
270235 while not SdfDataSetTemporary.EOF do begin
271236 DataSet.Append;
272237 //Iterate through FieldDefs
273238 for I:=0 to vFieldCount-1 do begin
274239 try
275240 DataSet.Fields[I].Value:=SdfDataSetTemporary.Fields[I].Value;
276241 except
277242 on E:Exception do begin
278243 MemoMessages.Append(TimeToStr(Now())+' Error while setting value for field: '
279244 +DataSet.Name+'.'+DataSet.Fields[I].Name +'. '+E.Message);
280245 end;
281246 end;
282247 end;
283248 try
284249 DataSet.Post;
285250 except
286251 on E:Exception do begin
287252 MemoMessages.Append(TimeToStr(Now())+' Error while posting record to table: '
288253 +DataSet.Name+'.'+E.Message);
289254 end;
290255 end;
291256 SdfDataSetTemporary.Next;
292257 end;
293258 finally
294259 SdfDataSetTemporary.Active:=False;
295260 SdfDataSetTemporary.ClearFields;
296261 end;
297262 end;
298263```
299264```
300265procedure SaveToCsv(DataSet:TDataSet);
301266 var
302267 myFileName:string;
303268 myTextFile: TextFile;
304269 i: integer;
305270 s: string;
306271 begin
307272 myFileName:=DataSet.Name+'.txt';
308273 //create a new file
309274 AssignFile(myTextFile, myFileName);
310275 Rewrite(myTextFile);
311276 s := ''; //initialize empty string
312277 try
313278 //write field names (as column headers)
314279 for i := 0 to DataSet.Fields.Count - 1 do
315280 begin
316281 s := s + Format('%s,', [DataSet.Fields[i].FieldName]);
317282 end;
318283 Writeln(myTextFile, s);
319284 DataSet.First;
320285 //write field values
321286 while not DataSet.Eof do
322287 begin
323288 s := '';
324289 for i := 0 to DataSet.FieldCount - 1 do
325290 begin
326291 //Numerical fields without quotes, string fields with quotes
327292 if ((DataSet.FieldDefs[i].DataType=ftInteger)
328293 or (DataSet.FieldDefs[i].DataType=ftFloat)) then
329294 s := s + Format('%s,', [DataSet.Fields[i].AsString])
330295 else
331296 s := s + Format('"%s",', [DataSet.Fields[i].AsString]);
332297 end;
333298 Writeln(myTextfile, s);
334299 DataSet.Next;
335300 end;
336301 finally
337302 CloseFile(myTextFile);
338303 end;
339304 end;
340305```
341306
342307## Autoincrement Primary Keys
343308
344309Autoincrement 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.
345310```
346311var
347312 //Global variables used for calculation of autoincrement primary key fields of MemDatasets
348313 vCurrentId:Integer=0;
349314 vProductsId:Integer=0;
350315 vBomsId:Integer=0;
351316 vBomCalculationProductsId:Integer=0;
352317 vBomCalculationComponentsId:Integer=0;
353318 vBomCalculationFooterId:Integer=0;
354319 vBomCalculationProductsMultipleId:Integer=0;
355320 vBomCalculationComponentsMultipleId:Integer=0;
356321 vBomCalculationFooterMultipleId:Integer=0;
357322 vStocksId:Integer=0;
358323 vOrdersId:Integer=0;
359324 vImportVariantsId:Integer=0;
360325 vImportToTablesId:Integer=0;
361326 vImportToFieldsId:Integer=0;
362327 vImportFromTablesId:Integer=0;
363328 vImportFromFieldsId:Integer=0;
364329```
365330
366331Then we have a procedure for autoincrement field values calculation:
367332```
368333procedure GetAutoincrementPrimaryFields;
369334 var
370335 I:Integer;
371336 vId:^Integer;
372337 begin
373338 try
374339 MemoMessages.Lines.Append(TimeToStr(Now())+' Getting information about autoincrement fields');
375340 vSuppressEvents:=True;
376341 //Disable controls and refresh all datasets
377342 for I:=Low(vSaveTables) to High(vSaveTables) do begin
378343 vSaveTables[I].DisableControls;
379344 vSaveTables[I].Refresh; //Important if dataset was filtered
380345 end;
381346 for I:=Low(vSaveTables) to High(vSaveTables) do begin
382347 with vSaveTables[I] do begin
383348 //Use appropriate global variable
384349 case StringToCaseSelect(Name,
385350 ['Products','Boms','Stocks','Orders',
386351 'BomCalculationProducts','BomCalculationComponents','BomCalculationFooter',
387352 'BomCalculationProductsMultiple','BomCalculationComponentsMultiple','BomCalculationFooterMultiple',
388353 'ImportVariants','ImportToTables','ImportToFields','ImportFromTables','ImportFromFields']) of
389354 0:vId:=@vProductsId;
390355 1:vId:=@vBomsId;
391356 2:vId:=@vStocksId;
392357 3:vId:=@vOrdersId;
393358 4:vId:=@vBomCalculationProductsId;
394359 5:vId:=@vBomCalculationComponentsId;
395360 6:vId:=@vBomCalculationFooterId;
396361 7:vId:=@vBomCalculationProductsMultipleId;
397362 8:vId:=@vBomCalculationComponentsMultipleId;
398363 9:vId:=@vBomCalculationFooterMultipleId;
399364 10:vId:=@vImportVariantsId;
400365 11:vId:=@vImportToTablesId;
401366 12:vId:=@vImportToFieldsId;
402367 13:vId:=@vImportFromTablesId;
403368 14:vId:=@vImportFromFieldsId;
404369 end;
405370 try
406371 //Find last value of Id and save it to global variable
407372 Last;
408373 vCurrentId:=FieldByName(Name+'Id').AsInteger;
409374 if (vCurrentId>vId^) then vId^:=vCurrentId;
410375 finally
411376 //Remove reference;
412377 vId:=nil;
413378 end;
414379 end;
415380 end;
416381 finally
417382 vSuppressEvents:=False;
418383 //Refresh all datasets and enable controls
419384 for I:=Low(vSaveTables) to High(vSaveTables) do begin
420385 vSaveTables[I].Refresh;
421386 vSaveTables[I].EnableControls;
422387 end;
423388 MemoMessages.Lines.Append(TimeToStr(Now())+' Autoincrement fields - done.');
424389 end;
425390 end;
426391```
427392```
428393function StringToCaseSelect(Selector:string;CaseList:array of string):Integer;
429394 var
430395 cnt: integer;
431396 begin
432397 Result:=-1;
433398 for cnt:=0 to Length(CaseList)-1 do
434399 begin
435400 if CompareText(Selector, CaseList[cnt]) = 0 then
436401 begin
437402 Result:=cnt;
438403 Break;
439404 end;
440405 end;
441406 end;
442407```
443408
444409The 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:
445410```
446411procedure TMainForm.OrdersNewRecord(DataSet: TDataSet);
447412 begin
448413 if vSuppressEvents=True then Exit;
449414 //Set new autoincrement value
450415 vOrdersId:=vOrdersId+1;
451416 DataSet.FieldByName('OrdersId').AsInteger:=vOrdersId;
452417 end;
453418```
454419
455420As already explained, I use vSuppressEvents global variable as flag for the case of restoring data from persistent files.
456421
457422## Enforcing Referential Integrity
458423
459424There is no enforced referential integrity implemented in MemDataset component, so you have to do it on your own.
460425
461426Let's assume we have two tables: MasterTable and DetailTable.
462427
463428There are various places where referential integrity code needs to be used:
464429
465430 * 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
466431 * 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
467432
468433```
469434procedure TMainForm.MasterTableBeforeDelete(DataSet: TDataSet);
470435 begin
471436 if vSuppressEvents=True then Exit;
472437 try
473438 DetailTable.DisableControls;
474439 // Enforce referential delete ("cascade delete") for table "MasterTable"
475440 while not DetailTable.EOF do begin
476441 DetailTable.Delete;
477442 end;
478443 DetailTable.Refresh;
479444 finally
480445 DetailTable.EnableControls;
481446 end;
482447 end;
483448```
484449```
485450procedure TMainForm.DetailTableBeforePost(DataSet: TDataSet);
486451 begin
487452 if vSuppressEvents=True then Exit;
488453 // Enforce referential insert/update for table "DetailTable" with
489454 // foreign key "MasterTableID" linking to
490455 // the MasterTable ID primary key field
491456 DataSet.FieldByName('MasterTableId').AsInteger:=
492457 MasterTable.FieldByName('ID').AsInteger;
493458 end;
494459```
495460
496461After 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.
497462
498463Don't forget to set the `Filtered` property of DetailTable to True.
499464```
500465procedure TMainForm.MasterTableAfterScroll(DataSet: TDataSet);
501466 begin
502467 if vSuppressEvents=True then Exit;
503468 DetailTable.Refresh;
504469 end;
505470```
506471```
507472procedure TMainForm.DetailTableFilterRecord(DataSet: TDataSet;
508473 var Accept: Boolean);
509474 begin
510475 if vSuppressEvents=True then Exit;
511476 // Show only child fields whose foreign key points to current
512477 // master table record
513478 Accept:=DataSet.FieldByName('MasterTableId').AsInteger=
514479 MasterTable.FieldByName('ID').AsInteger;
515480 end;
516481```
517482
518483## Known problems
519484
520485There are several limitations when using MemDatasets.
521486
522487 * Locate method does not work
523488 * Filtering by using Filter and Filtered property does not work. You must use hardcoding in the OnFilter event.
524489 * Looping deletion of records seems to be incredibly slow. Therefore I use my EmptyMemDataset procedure instead of while not EOF do Delete;
525490 * 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.
526491 * Note that older versions of FPC has no CopyFromDataset in Bufdataset, at the time an advantage for MemDs.
527492 * See bug report <http://bugs.freepascal.org/view.php?id=25426>.
528493
529494## TBufDataSet
530495
531496As 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.
532497
533498Since 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.
534499
535500You can use the same methods for enforcing referential integrity and primary autoincrement fields as explained for MemDataSet.
536501
537502There are only small differences between MemDataSet and BufDataset:
538503
539504MemDataSet | BufDataset
540505---|---
541506DataSet.ClearFields | DataSet.Fields.Clear
542507DataSet.CreateTable | DataSet.CreateDataSet
543508
544509## Sorting DBGrid on TitleClick event for TBufDataSet
545510
546511If you wish to enable consecutive ascending and descending sorting of a DBGrid showing some data from TBufDataSet, you could use the following method:
547512```
548513Uses
549514 BufDataset, typinfo;
550515
551516 function SortBufDataSet(DataSet: TBufDataSet;const FieldName: String): Boolean;
552517 var
553518 i: Integer;
554519 IndexDefs: TIndexDefs;
555520 IndexName: String;
556521 IndexOptions: TIndexOptions;
557522 Field: TField;
558523 begin
559524 Result := False;
560525 Field := DataSet.Fields.FindField(FieldName);
561526 //If invalid field name, exit.
562527 if Field = nil then Exit;
563528 //if invalid field type, exit.
564529 if {(Field is TObjectField) or} (Field is TBlobField) or
565530 {(Field is TAggregateField) or} (Field is TVariantField)
566531 or (Field is TBinaryField) then Exit;
567532 //Get IndexDefs and IndexName using RTTI
568533 if IsPublishedProp(DataSet, 'IndexDefs') then
569534 IndexDefs := GetObjectProp(DataSet, 'IndexDefs') as TIndexDefs
570535 else
571536 Exit;
572537 if IsPublishedProp(DataSet, 'IndexName') then
573538 IndexName := GetStrProp(DataSet, 'IndexName')
574539 else
575540 Exit;
576541 //Ensure IndexDefs is up-to-date
577542 IndexDefs.Updated:=false; {<<<<---This line is critical as IndexDefs.Update will do nothing on the next sort if it's already true}
578543 IndexDefs.Update;
579544 //If an ascending index is already in use,
580545 //switch to a descending index
581546 if IndexName = FieldName + '__IdxA'
582547 then
583548 begin
584549 IndexName := FieldName + '__IdxD';
585550 IndexOptions := [ixDescending];
586551 end
587552 else
588553 begin
589554 IndexName := FieldName + '__IdxA';
590555 IndexOptions := [];
591556 end;
592557 //Look for existing index
593558 for i := 0 to Pred(IndexDefs.Count) do
594559 begin
595560 if IndexDefs[i].Name = IndexName then
596561 begin
597562 Result := True;
598563 Break
599564 end; //if
600565 end; // for
601566 //If existing index not found, create one
602567 if not Result then
603568 begin
604569 if IndexName=FieldName + '__IdxD' then
605570 DataSet.AddIndex(IndexName, FieldName, IndexOptions, FieldName)
606571 else
607572 DataSet.AddIndex(IndexName, FieldName, IndexOptions);
608573 Result := True;
609574 end; // if not
610575 //Set the index
611576 SetStrProp(DataSet, 'IndexName', IndexName);
612577 end;
613578```
614579
615580So, you can call this function from a DBGrid in this way:
616581```
617582procedure TFormMain.DBGridProductsTitleClick(Column: TColumn);
618583 begin
619584 SortBufDataSet(Products, Column.FieldName);
620585 end;
621586```
622587
623588
624589
625590## Sorting multiple columns in grid
626591
627592I 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.
628593```
629594{ TDBGridHelper }
630595
631596 TDBGridHelper = class helper for TDBGrid
632597 public const
633598 cMaxColCOunt = 3;
634599 private
635600 procedure Interbal_MakeNames(Fields: TStrings; out FieldsList, DescFields: String);
636601 procedure Internal_SetColumnsIcons(Fields: TStrings; AscIdx, DescIdx: Integer);
637602 function Internal_IndexNameExists(IndexDefs: TIndexDefs; IndexName: String): Boolean;
638603 public
639604 procedure Sort(const FieldName: String; AscIdx: Integer = -1; DescIdx: Integer = -1);
640605 procedure ClearSort;
641606 end;
642607
643608 { TDBGridHelper }
644609
645610 procedure TDBGridHelper.Interbal_MakeNames(Fields: TStrings; out FieldsList, DescFields: String);
646611 var
647612 FldList: TStringList;
648613 DscList: TStringList;
649614 FldDesc, FldName: String;
650615 i: Integer;
651616 begin
652617 if Fields.Count = 0 then
653618 begin
654619 FieldsList := '';
655620 DescFields := '';
656621 Exit;
657622 end;
658623
659624 FldList := TStringList.Create;
660625 DscList := TStringList.Create;
661626 try
662627 FldList.Delimiter := ';';
663628 DscList.Delimiter := ';';
664629
665630 for i := 0 to Fields.Count - 1 do
666631 begin
667632 Fields.GetNameValue(i, FldName, FldDesc);
668633 FldList.Add(FldName);
669634
670635 if FldDesc = 'D' then
671636 DscList.Add(FldName);
672637 end;
673638
674639 FieldsList := FldList.DelimitedText;
675640 DescFields := DscList.DelimitedText;
676641 finally
677642 FldList.Free;
678643 DscList.Free;
679644 end;
680645 end;
681646
682647 procedure TDBGridHelper.Internal_SetColumnsIcons(Fields: TStrings; AscIdx, DescIdx: Integer);
683648 var
684649 i: Integer;
685650 FldDesc: String;
686651 begin
687652 for i := 0 to Self.Columns.Count - 1 do
688653 begin
689654 FldDesc := Fields.Values[Self.Columns[i].Field.FieldName];
690655
691656 if FldDesc = 'A' then
692657 Self.Columns[i].Title.ImageIndex := AscIdx
693658 else
694659 if FldDesc = 'D' then
695660 Self.Columns[i].Title.ImageIndex := DescIdx
696661 else
697662 Self.Columns[i].Title.ImageIndex := -1
698663 end;
699664 end;
700665
701666 function TDBGridHelper.Internal_IndexNameExists(IndexDefs: TIndexDefs; IndexName: String): Boolean;
702667 var
703668 i: Integer;
704669 begin
705670 for i := 0 to IndexDefs.Count - 1 do
706671 begin
707672 if IndexDefs[i].Name = IndexName then
708673 Exit(True)
709674 end;
710675
711676 Result := False
712677 end;
713678
714679 procedure TDBGridHelper.Sort(const FieldName: String; AscIdx: Integer;
715680 DescIdx: Integer);
716681 var
717682 Field: TField;
718683 DataSet: TBufDataset;
719684 IndexDefs: TIndexDefs;
720685 IndexName, Dir, DescFields, FieldsList: String;
721686 Fields: TStringList;
722687 begin
723688 if not Assigned(DataSource.DataSet) or
724689 not DataSource.DataSet.Active or
725690 not (DataSource.DataSet is TBufDataset) then
726691 Exit;
727692 DataSet := DataSource.DataSet as TBufDataset;
728693
729694 Field := DataSet.FieldByName(FieldName);
730695 if (Field is TBlobField) or (Field is TVariantField) or (Field is TBinaryField) then
731696 Exit;
732697
733698 IndexDefs := DataSet.IndexDefs;
734699 IndexName := DataSet.IndexName;
735700
736701 if not IndexDefs.Updated then
737702 IndexDefs.Update;
738703
739704 Fields := TStringList.Create;
740705 try
741706 Fields.DelimitedText := IndexName;
742707 Dir := Fields.Values[FieldName];
743708
744709 if Dir = 'A' then
745710 Dir := 'D'
746711 else
747712 if Dir = 'D' then
748713 Dir := 'A'
749714 else
750715 Dir := 'A';
751716
752717 //If shift is presed then add field to field list
753718 if ssShift in GetKeyShiftState then
754719 begin
755720 Fields.Values[FieldName] := Dir;
756721 //We do not add to sor any more field if total field count exids cMaxColCOunt
757722 if Fields.Count > cMaxColCOunt then
758723 Exit;
759724 end
760725 else
761726 begin
762727 Fields.Clear;
763728 Fields.Values[FieldName] := Dir;
764729 end;
765730
766731 IndexName := Fields.DelimitedText;
767732 if not Internal_IndexNameExists(IndexDefs, IndexName) then
768733 begin
769734 Interbal_MakeNames(Fields, FieldsList, DescFields);
770735 TBufDataset(DataSet).AddIndex(IndexName, FieldsList, [], DescFields, '');
771736 end;
772737
773738 DataSet.IndexName := IndexName;
774739 Internal_SetColumnsIcons(Fields, AscIdx, DescIdx)
775740 finally
776741 Fields.Free;
777742 end;
778743 end;
779744
780745 procedure TDBGridHelper.ClearSort;
781746 var
782747 DataSet: TBufDataset;
783748 Fields: TStringList;
784749 begin
785750 if not Assigned(DataSource.DataSet) or
786751 not DataSource.DataSet.Active or
787752 not (DataSource.DataSet is TBufDataset) then
788753 Exit;
789754 DataSet := DataSource.DataSet as TBufDataset;
790755
791756 DataSet.IndexName := '';
792757
793758 Fields := TStringList.Create;
794759 try
795760 Internal_SetColumnsIcons(Fields, -1, -1)
796761 finally
797762 Fields.Free
798763 end
799764 end;
800765```
801766
802767To 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
803768```
804769procedure TForm1.grdCountriesCellClick(Column: TColumn);
805770 begin
806771 if not Assigned(Column) then
807772 grdCountries.ClearSort
808773 end;
809774
810775 procedure TForm1.grdCountriesTitleClick(Column: TColumn);
811776 begin
812777 grdCountries.Sort(Column.Field.FieldName, 0, 1);
813778 end;
814779```
815780
816781If you have assigned TitleImageList then you can specify which image use for ascending and which for descending operations.
817782
818783## ZMSQL
819784
820785Another, often better way to write in-memory databases is to use the [[ZMSQL|ZMSQL package]]:
821786
822787 * <http://sourceforge.net/projects/lazarus-ccr/files/zmsql/>
823788 * <http://www.lazarus.freepascal.org/index.php/topic,13821.30.html>
824789 * <http://lazarus.freepascal.org/index.php/topic,13821>
825790
826791## Contributors
827792
828793Original text written by: Zlatko Matić (matalab@gmail.com)
829794
830795Other contributions by contributors as shown in the page History.
831796
832797---
833798*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