Difference between revisions of "Lazarus Tdbf Tutorial"

From Lazarus wiki
Jump to navigationJump to search
(Reorg filtering)
(Undo revision 73530 by BigChimp (talk))
Line 190: Line 190:
 
// we have a field named MODIFIED here:
 
// we have a field named MODIFIED here:
 
MyDBF.Filter:='DTOS(MODIFIED) > "20051231"';
 
MyDBF.Filter:='DTOS(MODIFIED) > "20051231"';
 +
//adding some spaces also works:
 +
//MyDBF.Filter:='DTOS(MODIFIED)>"20051231"';
  
 
// if you want to compare datetime, that can be done:
 
// if you want to compare datetime, that can be done:
Line 206: Line 208:
 
|+||Concatenate two strings, or add two numbers
 
|+||Concatenate two strings, or add two numbers
 
|-
 
|-
|-||Subtract two numbers, or (FPC 2.7.1+) take the negative of a number/constant
+
|-||Subtract two numbers
 
|-
 
|-
 
|*||Multiply two numbers
 
|*||Multiply two numbers
Line 246: Line 248:
 
|}
 
|}
  
== Code example: creating a table/DBF file ==
+
== Code example: creating a table/dbf file ==
 
The following sample creates a new table "customers" in code. This of course only needs done once, and after that you just OPEN the table, don't create it. ;-)
 
The following sample creates a new table "customers" in code. This of course only needs done once, and after that you just OPEN the table, don't create it. ;-)
  
Line 253: Line 255:
 
else this program will fail.}}
 
else this program will fail.}}
  
<syntaxhighlight>
+
<syntaxhighlight>($MODE OBJFPC}
($MODE OBJFPC}
 
 
Program DatabaseTest;
 
Program DatabaseTest;
{ We need the following units be in the USES clause: }
+
{ We will require the following units be in the USES clause: }
 
uses Dbf, db, Dbf_Common, SysUtils;   
 
uses Dbf, db, Dbf_Common, SysUtils;   
 
                                  
 
                                  
Line 290: Line 291:
 
     MyDbf.Free;
 
     MyDbf.Free;
 
   end;
 
   end;
end;
+
end;</syntaxhighlight>
</syntaxhighlight>
 
  
 
== External Index Files ==
 
== External Index Files ==
Line 351: Line 351:
 
On the invoices TDbf component set the following:
 
On the invoices TDbf component set the following:
  
<syntaxhighlight>
+
<syntaxhighlight>InvDbf.IndexName := 'idxcustid'; // our field that will match the customers table ID
InvDbf.IndexName := 'idxcustid'; // our field that will match the customers table ID
 
 
InvDbf.MasterSource := dsCustomers; // datasource that is linked to the customers TDbf
 
InvDbf.MasterSource := dsCustomers; // datasource that is linked to the customers TDbf
InvDbf.MasterFields := 'Id'; // field on the customers table we are matching against our index
+
InvDbf.MasterFields := 'Id'; // field on the customers table we are matching against our index</syntaxhighlight>
</syntaxhighlight>
 
  
 
===Searching and Displaying a data set by using filters===
 
===Searching and Displaying a data set by using filters===
Line 379: Line 377:
 
   dbf1.next;                                                // use .next here NOT .findnext!
 
   dbf1.next;                                                // use .next here NOT .findnext!
 
end;
 
end;
Dbf1.Close;
+
Dbf1.Close;</syntaxhighlight>   
</syntaxhighlight>   
 
 
 
Note that you can use Ddf1.FindFirst to get the first record in the filtered set, then use
 
Dbf1.Next to move though the data. I found that using Dbf1.FindNext just causes the program to hang. 
 
  
== Filters ==
+
Note that you can use Ddf1.findfirst to get the first record in the filtered set, then use  
There are two ways to use filters:
+
Dbf1.next to move though the data. I found that using Dbf1.Findnext just causes the program to hang.
* use filter expressions (see examples above)
 
* write an event handler for OnFilterRecord; if you want to show the record, set Accept to true.  
 
  
In both cases, set the Filtered property to true to enable filtering.
 
 
Example: imagine you have a database that identifies the products by a code, for example, B019, and you need to filter only those products that are of this code so that a report is generated with LazReport.
 
Let's assume that the field with product codes is called "product_no"
 
 
=== Expression filters ===
 
Implement filtering using a filter expression like this:
 
<syntaxhighlight>
 
TDBF1.Filter := 'product_no' = "B019"';
 
TDBF1.Filtered := true;
 
</syntaxhighlight>
 
 
{{Warning|Apparently, expression filtering does not work with LazReport; use the OnFilterRecord method instead. Please verify this and correct this warning.}}
 
 
=== OnFilterRecords ===
 
To solve the problem with LazReport you can use the OnFilterRecord event handler and use the following code:
 
 
Accept: = Copy (Trim (dadosNot.FieldByName ('product_no'). AsString), 1,4) = 'B019';
 
 
It is necessary to colcoar dados.filtered to true, out of that event.
 
 
As said before, when the value is false Accept the registers are disregarded and this way you can show off all products with that code in the report LazReport.
 
  
 +
== How to Filter Date in Blanks ==
 
You can show only date fields that have no contents (are blank) e.g. in Lazreport.
 
You can show only date fields that have no contents (are blank) e.g. in Lazreport.
  
 
The easiest way is using the OnFilterRecord event handler with code like the following:
 
The easiest way is using the OnFilterRecord event handler with code like the following:
 
<syntaxhighlight>
 
<syntaxhighlight>
Accept:=YourDBFComponentName.FieldByName('YOURDATEFIELD').IsNull;
+
if not YourDBFComponentName.FieldByName('YOURDATEFIELD').IsNull then Accept:=False;
 
</syntaxhighlight>
 
</syntaxhighlight>
 
+
{{Note| the following code would seem to be similar, but shorter:}}
All records that give a false value for Accept are filtered out i.e. are not displayed in the report.
 
 
 
== Packing and rebuilding the tables ==
 
When a record is deleted, it's not truly removed from the physical table. Periodically you must "pack" a table to recover that lost space. This should be done with exclusive mode set. See above for remarks on packing external index files.
 
 
 
<syntaxhighlight>MyDbf.Exclusive := True;
 
MyDbf.Open;
 
MyDbf.PackTable;
 
// let's also rebuild all the indexes
 
MyDbf.RegenerateIndexes;
 
MyDbf.Close;
 
MyDbf.Exclusive := False;</syntaxhighlight>
 
 
 
 
 
== Encryption ==
 
Apparently at least DBase IV and 5 support some kind of encryption (there is an encryption flag in the file structure), but it may well be that the password/encryption key is in the DBF itself (see [http://www.antionline.com/archive/index.php/t-236215.html]), making it insecure.
 
 
 
There are other options if you want to encrypt your data. See here [http://www.lazarus.freepascal.org/index.php/topic,15422.msg83016.htm|forum discussion on DBase encryption]
 
 
 
=== Operating system level encryption ===
 
You can use an encrypted filesystem such as TrueCrypt (containers) to encrypt the directory where the database files get stored.
 
 
 
=== Encrypt the DBF file ===
 
You can use streams (e.g. the Blowfish encyprtion stream code supplied with Lazarus/FPC) to encrypt the data (all tables etc) by storing the unencrypted file e.g. in memory, then writing that to disk:
 
# Open your file in a TFileStream.
 
# Decrypt TFileStream to a new stream, YourDecryptedStream
 
# Tdbf.UserStream:= YourDecryptedStream; //Note: this will not encrypt .dbt/memo files! Use '''UserMemoStream''' for this (in FPC development/trunk only)
 
# Tdbf.Storage:=stoMemory;
 
# Tdbf.Open;
 
 
 
When finished
 
#  Tdbf.Close
 
#  Encrypt YourDecryptedStream to TFileStream
 
#  Free stream(s)
 
Detailed code can be found in the forum thread mentioned above. People implementing this are welcome to adapt this article with working code.
 
 
 
=== Encrypt field data ===
 
You could also choose to encrypt field data by implementing the OnGetText and OnSetText events to encrypt/decrypt the field contents.
 
 
 
== Third party software ==
 
* [http://mydbfstudio.altervista.org/ MyDbf Studio] is an open source admin tool to manage your dbase table entirely written under lazarus IDE and based on TDbf.
 
* [http://vlad-karpov.narod.ru/Components.html#VK%20DBF] are Delphi components for DBF files.
 
 
 
== See also ==
 
* [[fpdbfexport]] Exports any kind of dataset to DBF files
 
* See the dbftool example included in FPC 2.7.1+: creating, using DBF files and exporting data using db export (also downloadable via http://svn.freepascal.org/svn/fpc/trunk/packages/fcl-db/examples/)
 
 
 
[[Category:Databases]]
 
[[Category:Tutorials]]
 
[[Category:Lazarus]]
 
[[Category:FPC]]
 
{{Lazarus Tdbf Tutorial}}
 
 
 
== Overview ==
 
 
 
This tutorial is about basic database development using the [http://tdbf.sf.net TDbf] component (by Micha Nelissen) with Lazarus.
 
 
 
In Lazarus, the DbfLaz component makes use of the FPC TDbf code.
 
 
 
=== Documentation ===
 
For official TDbf documentation PDF go to the [https://sourceforge.net/project/showfiles.php?group_id=34085&package_id=26371  SourceForge] site. It may be useful to keep that pdf alongside this document while reading.
 
 
 
This wiki article serves as additional documentation for new features since the original documentation.
 
 
 
 
 
=== Alternatives and known problems ===
 
Alternatives for embedded database use include Firebird embedded (advantage: easily scalable to client/server), SQLite (advantage: easy deployment), or ZMSQL (using CSV files; advantage: only Pascal code required).
 
 
 
Known problems:
 
The TDBF code lacked a maintainer for a long while. Currently (March 2013), both the Sourceforge upstream project and FPC database developers are looking into fixing TDBF bugs again.
 
 
 
Current issues: please see the bugs in the bug tracker, especially
 
http://bugs.freepascal.org/view.php?id=22177
 
 
 
==== Limitations ====
 
* TDBF sets database encoding based on OS language settings. Moving a DB to another computer that does not support that encoding may lead to a read-only database. ''Note: investigate whether this applies to Tablelevel 7 (Visual DBase) and 25/30 (Foxpro/Visual Foxpro)''
 
* Indexing a DBase file with [ixCaseInsensitive] currently does not work.
 
* Currently there is no support for referential integrity/master-detail between multiple tables.
 
* No support for internally encrypted .dbf files: the DbaseIV encryption mechanism is quite weak anyway. Please use one of the encryption methods mentioned below.
 
 
 
FPC 2.6.2 and lower:
 
* On ARM processors, the TDBF code is known not to work due to bus alignment issues. This is being addressed in the FPC development/trunk version.
 
* Choosing the FoxPro tablelevel (25) will produce files that cannot be read by Visual Foxpro drivers. This has been fixed in the FPC development/trunk version.
 
 
 
=== No need for installation ===
 
The DbfLaz package is installed by default in both FPC and Lazarus. The DBFLaz package uses the TDbf and associated units in the FreePascal Free Component Library. In other words, no need to install anything if you have a fairly recent version of Lazarus.
 
 
 
=== What the TDbf package provides ===
 
The TDbf package provides access to dBase and FoxPro database tables for Lazarus (and others). It allows for reading, writing and creation of dBase III+, dBase IV, Visual dBase VII and FoxPro tables. It does all of this without the need for additional libraries or database engines. Simply drop the TDbf on your form and you have instant access to a cross-platform database environment. The TDbf works in both Windows and Linux using Lazarus.
 
 
 
=== What to put in your uses section ===
 
If you use the visual DBF components in Lazarus, Lazarus will put Dbf in your uses clauses. If you use FPC (console mode) only, you can put it there yourself.
 
Handy other units are db for DataSet support and Dbf_Common for things such as field type definitions.
 
 
 
So you'd get:
 
 
<syntaxhighlight>
 
<syntaxhighlight>
uses Dbf, db, Dbf_Common;
+
Accept:=YourDBFComponentName.FieldByName('YOURDATEFIELD').IsNull;
 
</syntaxhighlight>
 
</syntaxhighlight>
  
== How to create a new database table ==
+
When we put the value of the Accept False all records that do not meet that particular condition are disregarded, i.e, are not displayed in the report.
There is a "Database Desktop" application for Lazarus (open the project in your Lazarus directory/tools/LazDataDesktop). You could create a new data dictionary, then create code to set up DBF files based on the dictionary. As of now (July 2011), this process is still a bit convoluted.
 
  
Another option is to use OpenOffice or LibreOffice Calc and save your spreadsheet as a .dbf file.
+
It is also necessary to put the Filtered property database to true, but outside of that event.
  
You may also (on windows - works with wine too) try this application: http://www.dirfile.com/cdbf_explorer.htm
+
== How to Filter a Value with TDBF ==
  
In any event, we shall illustrate creating a new database in code below.
+
Imagine you have a database that identifies the products by a code, for example, B019, and you need to filter only those products that are of this code (B019, in this example so that a report is generated with LazReport. Component is TDbf called the data here. field that stores the value called "product_no"
  
=== Setting the path ===
+
We have two ways to filter. The first would be to use the following code:
It's a good idea to give your application's database its own directory. This simplifies making backups of the data.
 
  
There are two ways to set the path:
+
dados.filter: = 'product_no' = "B019" '
* you can set the full path using the FilePathFull property
 
* you can set a path relative to the current application path with FilePath.
 
  
For instance, setting "FilePath" at runtime to "data/" would use a data subdirectory just below the executable file. Setting the "FilePathFull" property to "/var/data/" would place all dbfs, index files etc in that exact folder, whatever the application location is.
+
It turns out that this works very well with several components, but does not work with LazReport.
  
An example:
+
To solve the problem with LazReport you can use the OnFilterRecord event handler and use the following code:
<syntaxhighlight>MyDbf.FilePathFull := '/location/to/my/data'; //Directory where all .dbf files will be stored
 
MyDbf.TableName := 'customers.dbf'; // note: is the .dbf really required?
 
</syntaxhighlight>
 
  
=== Choosing a TableLevel ===
+
Accept: = Copy (Trim (dadosNot.FieldByName ('product_no'). AsString), 1,4) = 'B019';
By default, the TDbf components will create dBase IV tables. While this is very compatible, there are features you may wish to use that are not supported. E.g. to support auto-incrementing fields, you must use something newer. The table types are:
 
{| {{table}}
 
| '''Table level'''
 
| '''*Base product compatibility'''
 
| '''Auto-increment field support'''
 
| '''Field length limit'''
 
| '''Remarks'''
 
|-
 
| Level 3||dBase III+||no||10 characters [http://www.clicketyclick.dk/databases/xbase/format/dbf.html#DBF_STRUCT]||Quite obsolete
 
|-
 
| Level 4||dBase IV||no||10 characters [http://www.clicketyclick.dk/databases/xbase/format/dbf.html#DBF_STRUCT]||Used to be very popular. Basis for GIS shape files (.shp)
 
|-
 
| Level 7||Visual dBase VII||yes||32 characters [http://www.dbase.com/KnowledgeBase/int/db7_file_fmt.htm]||
 
|-
 
| Level 25||FoxPro||yes||10 characters [http://www.dynamic.com/v7/what-are-limitations-foxpro-full-isam-file]
 
[http://www.foxite.com/archives/foxpro-field-names-0000066805.htm]
 
|FoxPro; compatible with FoxPro for DOS and Visual FoxPro. Note: implementation in FPC 2.6.2 produces files that cannot be read by Visual FoxPro drivers!
 
|-
 
| Level 30||Visual FoxPro||yes||Normally 10 characters [http://www.dynamic.com/v7/what-are-limitations-foxpro-full-isam-file]
 
[http://www.foxite.com/archives/foxpro-field-names-0000066805.htm]
 
Not supported in FPC: when using a DBC (Database container) file: 27 characters
 
||Not yet in FPC 2.6.2; only in current development FPC version
 
|-
 
|
 
|}
 
See bug [http://bugs.freepascal.org/view.php?id=20134] for more details on field length limitations.
 
  
You choose a table type by setting the TableLevel property appropriately:
+
It is necessary to colcoar dados.filtered to true, out of that event.
<syntaxhighlight>
 
MyDbf.TableLevel := 7; //Visual dBase 7
 
</syntaxhighlight>
 
  
=== Adding fields ===
+
As said before, when the value is false Accept the registers are disregarded and this way you can show off all products with that code in the report LazReport.
Creating fields for your new table at runtime pretty much follows the old Delphi standard. An example:
 
 
 
<syntaxhighlight>
 
With MyDbf.FieldDefs do begin
 
  Add('Id', ftAutoInc, 0, True); //Autoincrement field called Id
 
  Add('Name', ftString, 80, True); //80 character string field called Name
 
End;</syntaxhighlight>
 
 
 
List of field types - bold types are currently supported by the DBF code. See [[Database_field_type]] for a explanation of field types:
 
*    ftUnknown
 
*    '''ftString'''
 
*    '''ftSmallInt'''
 
*    '''ftInteger'''
 
*    '''ftWord'''
 
*    '''ftBoolean'''
 
*    '''ftFloat'''
 
*    '''ftCurrency''' (only supported in TableLevel 25)
 
*    '''ftBCD''' (only supported in TableLevel 25)
 
*    '''ftDate'''
 
*    ftTime
 
*    '''ftDateTime''': probably the recommended one for storing ftDate or ftDateTime values. At least on FoxPro, seems to store milliseconds as well.
 
*    '''ftBytes''' (only supported in TableLevel 25)
 
*    ftVarBytes
 
*    '''ftAutoInc''' (only supported in TableLevel 7 or 25)
 
*    '''ftBlob'''
 
*    '''ftMemo'''
 
*    ftGraphic
 
*    ftFmtMemo
 
*    ftParadoxOle
 
*    '''ftDBaseOle'''
 
*    ftTypedBinary
 
*    ftCursor
 
*    '''ftFixedChar'''
 
*    '''ftWideString'''
 
*    '''ftLargeInt'''
 
*    ftADT
 
*    ftArray
 
*    ftReference
 
*    ftDataSet
 
*    ftOraBlob
 
*    ftOraClob
 
*    ftVariant
 
*    ftInterface
 
*    ftIDispatch
 
*    ftGuid
 
*    ftTimeStamp
 
*    ftFMTBcd
 
 
 
=== Creating the table ===
 
Once you have defined the fields you wish to use in your new table, you can go ahead and create it with:
 
 
 
<syntaxhighlight>MyDbf.CreateTable;</syntaxhighlight>
 
 
 
== How to add indexes to a table ==
 
If your database is larger than a few records, chances are you will want to have indexes defined to make searching faster. To change the index structure of a table, we need to have exclusive access to the table - which we would have while creating it anyway.
 
 
 
<syntaxhighlight>MyDbf.Exclusive := True;
 
MyDbf.Open;</syntaxhighlight>
 
 
 
Now, we just have to add the index - or two, as in this example:
 
 
 
<syntaxhighlight>
 
MyDbf.AddIndex('custid', 'Id', [ixPrimary, ixUnique]);
 
MyDbf.AddIndex('custname','Name', [ixCaseInsensitive]);
 
MyDbf.Close;
 
</syntaxhighlight>
 
 
 
== Expressions ==
 
Indexes and filters can use expressions according to the [http://sourceforge.net/projects/tdbf/files/TDbf%20documentation/TDbf%20documentation%201.1/ TDbf documentation].
 
 
 
=== Index 100 character limit ===
 
Index expressions that return a string may be maximum 100 characters long; use SUBSTR to cut off extra characters, e.g.:
 
<syntaxhighlight>
 
MyDbf.AddIndex('SOMEFANCYINDEX', 'DTOS(DATEFIELD)+SUBSTR(LONGFIELD ,1,10)+SUBSTR(LONGFIELD2 ,1,20)', []);
 
</syntaxhighlight>
 
 
 
=== Index/filter on TDateTimes ===
 
To set an index on a date/time field, use an expression index and the DTOS function:
 
<syntaxhighlight>
 
MyDbf.AddIndex('PUBLISHDATEIDX', 'DTOS(PUBLISHDATE)', []);
 
</syntaxhighlight>
 
 
 
Filtering is similar - note: apparently you need to surround the string value you are comparing against with double quotes. See below for details on the DTOS function; basically you need to format the date as YYYYMMDD:
 
<syntaxhighlight>
 
// we have a field named MODIFIED here:
 
MyDBF.Filter:='DTOS(MODIFIED) > "20051231"';
 
 
 
// if you want to compare datetime, that can be done:
 
// everything later than 2 January 2006, 4:34PM
 
MyDBF.Filter:='DTOS(MODIFIED) > "20060102 163400"';
 
 
 
MyDBF.Filtered:=true;
 
</syntaxhighlight>
 
 
 
=== Index/filter operators/functions ===
 
Supported operators (from the documentation mentioned above):
 
{| {{table}}
 
| '''Operator/function'''
 
| '''Description'''
 
|-
 
|+||Concatenate two strings, or add two numbers
 
|-
 
|-||Subtract two numbers, or (FPC 2.7.1+) take the negative of a number/constant
 
|-
 
|*||Multiply two numbers
 
|-
 
|/||Divide two numbers
 
|-
 
|=||Compare two strings, numbers or booleans for equality
 
|-
 
|<>||Compare two strings, numbers or booleans for inequality
 
|-
 
|<||True if the first argument is smaller than second argument
 
|-
 
|<=||True if the first argument is smaller than or equal to the second argument
 
|-
 
|>||True if the first argument is greater than the second argument
 
|-
 
|>=||True if the first argument is greater than or equal to the second argument
 
|-
 
|NOT||Negate the boolean argument to the right
 
|-
 
|AND||Returns true if both the first and second boolean argument are true
 
|-
 
|OR||Returs true if the first or second boolean argument is true
 
|-
 
|STR(num[,size,precision])||Converts a number num to a string with optional size and precision
 
num: number to convert to string
 
size: the total number of characters to output
 
precision: the number of digits to the right of the decimal point
 
|-
 
|DTOS(date)||Converts date to a string with representation YYYYMMDD
 
|-
 
|SUBSTR(str,index,count)||Extracts a substring from string str from position index and length count
 
|-
 
|UPPER(str)||Returns the uppercase equivalent of string str
 
|-
 
|LOWER(str)||Returns the lowercase equivalent of string str
 
|-
 
|
 
|}
 
 
 
== Code example: creating a table/DBF file ==
 
The following sample creates a new table "customers" in code. This of course only needs done once, and after that you just OPEN the table, don't create it. ;-)
 
 
 
{{Note| When using Windows (at least it was so with XP Professional)
 
you must create the relative data directory before trying to run this example, or
 
else this program will fail.}}
 
 
 
<syntaxhighlight>
 
($MODE OBJFPC}
 
Program DatabaseTest;
 
{ We need the following units be in the USES clause: }
 
uses Dbf, db, Dbf_Common, SysUtils; 
 
                               
 
{ The Dbf is put there e.g. when you drop a TDbf component on a form...  }
 
{ but you will need db for the DataSet object and Dbf_Common              }
 
{ for things such as the field type definitions                          }
 
{ Finally, use SysUtils for ForceDirectories.                            }
 
var
 
  MyDbf: TDbf;
 
begin
 
  MyDbf := TDbf.Create(nil);
 
  try
 
    { make sure the data directory exists: }
 
    ForceDirectories('data');
 
    { use relative path to "data" directory }
 
    MyDbf.FilePath := 'data' + DirectorySeparator;
 
    { we want to use Visual dBase VII compatible tables }
 
    // Note: November 2011: tablelevel 25 does not seem to work...
 
    MyDbf.TableLevel := 7;
 
    MyDbf.Exclusive := True;
 
    MyDbf.TableName := 'customers.dbf';
 
    With MyDbf.FieldDefs do begin
 
      Add('Id', ftAutoInc, 0, True);
 
      Add('Name', ftString, 80, True);
 
    End;
 
    MyDbf.CreateTable;
 
    MyDbf.Open;
 
    MyDbf.AddIndex('custid', 'Id', [ixPrimary, ixUnique]);
 
    { add a secondary index }
 
    MyDbf.AddIndex('custname','Name', [ixCaseInsensitive]);
 
    MyDbf.Close;
 
  finally
 
    MyDbf.Free;
 
  end;
 
end;
 
</syntaxhighlight>
 
 
 
== External Index Files ==
 
The TDbf component also supports storing secondary indexes in a separate file. This might be helpful if the database is expected to be very large. Secondary index files are created almost identically to normal indexes, but with the addition of the '.ndx' file extension:
 
 
 
<syntaxhighlight>MyDbf.AddIndex('custname.ndx','Name', [ixCaseInsensitive]);</syntaxhighlight>
 
 
 
 
 
Each time the TDbf is opened, the index file must be loaded:
 
 
 
<syntaxhighlight>MyDbf.OpenIndexFile('custname.ndx');</syntaxhighlight>
 
 
 
 
 
And indexes must be referenced including the extension:
 
 
 
<syntaxhighlight>MyDbf.IndexName := 'custname.ndx';</syntaxhighlight>
 
 
 
 
 
Index files can be packed (i.e. entries pointing to deleted data rows are removed) separately using:
 
 
 
<syntaxhighlight>MyDbf.CompactIndexFile('custname.ndx');</syntaxhighlight>
 
 
 
== How to link the TDbf component to data-aware components ==
 
The above examples show how to create a new database table in code. Using that table is even more simple.
 
 
 
Data aware components in Lazarus (such as the TDbEdit control) link to a TDataSource component using their "DataSource" and "DataField" properties. The TDataSource component handles communication between the database engine and the data aware components. A TDataSource then links to the TDbf component using it's "DataSet" property. The connection looks like this:
 
 
 
TDbEdit-------
 
              |
 
TDbEdit------|-->TDataSource-->TDbf
 
              |
 
TDbNavigator--
 
 
 
 
 
Be sure to set the FilePath (or FilePathFulll), TableLevel, and TableName properties of your TDbf component before calling
 
 
 
<syntaxhighlight>TDbf.Active := True;</syntaxhighlight>
 
 
 
 
 
== Master table relations ==
 
Real power in database programming begins when you have multiple tables that reference each other. While TDbf does not yet support referential integrity, it does support a master / detail relationship between TDbf's.
 
 
 
When there are two tables related, for instance:
 
 
 
[customers]
 
Id      <----|
 
Name          |
 
Phone        |
 
Address      |
 
              |  The CustID in invoices references a customer primary  field
 
[invoices]    |
 
Id            |
 
Amount        |
 
CustID  -----|  * This field indexed as "idxcustid"
 
 
 
 
 
If you wanted to display all invoices for a given customer, the detail table (invoices) can stay in sync with the master table (customers) automatically.
 
 
 
On the invoices TDbf component set the following:
 
 
 
<syntaxhighlight>
 
InvDbf.IndexName := 'idxcustid'; // our field that will match the customers table ID
 
InvDbf.MasterSource := dsCustomers; // datasource that is linked to the customers TDbf
 
InvDbf.MasterFields := 'Id'; // field on the customers table we are matching against our index
 
</syntaxhighlight>
 
 
 
===Searching and Displaying a data set by using filters===
 
[[user:svbfieldview.net|Simon Batty]]
 
 
 
In this example I wanted to search a database of books for all the titles an author has listed
 
and then display the list in a memo box
 
<syntaxhighlight>
 
Dbf1.FilePathFull := '/home/somelocatio/database_location/'; // path to the database directory
 
Dbf1.TableName := 'books.dbase';                            // database file (including extension)
 
DbF1.Open;
 
memo1.Clear;                                                // clear the memo box
 
 
Dbf1.FilterOptions := [foCaseInsensitive];
 
Df1.Filter := 'AUTHOR=' + QuotedStr('anauthor');            // AUTHOR is the field name containing the authors
 
                                                            // Note that we need to surround the value with double quotes: "James Joyce"
 
 
Dbf1.Filtered := true;      // This selects the filtered set
 
Dbf1.First;                  // moves the the first filtered data
 
while not dbf1.EOF do        // prints the titles that match the author to the memo box
 
begin
 
  memo1.Append(Dbf1.FieldByName('TITLE').AsString);          // TITLE is the field name for titles
 
  dbf1.next;                                                // use .next here NOT .findnext!
 
end;
 
Dbf1.Close;
 
</syntaxhighlight> 
 
 
 
Note that you can use Ddf1.FindFirst to get the first record in the filtered set, then use
 
Dbf1.Next to move though the data. I found that using Dbf1.FindNext just causes the program to hang. 
 
 
 
== Filters ==
 
There are two ways to use filters:
 
* use filter expressions (see examples above)
 
* write an event handler for OnFilterRecord; if you want to show the record, set Accept to true.
 
 
 
In both cases, set the Filtered property to true to enable filtering.
 
 
 
Example: imagine you have a database that identifies the products by a code, for example, B019, and you need to filter only those products that are of this code so that a report is generated with LazReport.
 
Let's assume that the field with product codes is called "product_no"
 
 
 
=== Expression filters ===
 
Implement filtering using a filter expression like this:
 
<syntaxhighlight>
 
TDBF1.Filter := 'product_no' = "B019"';
 
TDBF1.Filtered := true;
 
</syntaxhighlight>
 
 
 
Please see the section [[#Expressions]] above for the list of supported expressions.
 
 
 
{{Warning|Apparently, expression filtering does not work with LazReport; use the OnFilterRecord method instead. Please verify this and correct this warning.}}
 
 
 
=== OnFilterRecords ===
 
To use this method, you need to write a function
 
 
 
In your main code, write something like:
 
<syntaxhighlight>
 
OnFilterRecord := OnFilterProductcode; //instruct tdbf to lead all records past your OnFilterProductCode procedure
 
Filtered := True; //actually enable filtering
 
</syntaxhighlight>
 
 
 
Then write your event handler:
 
<syntaxhighlight>
 
procedure TTestCursorDBBasics.TestOnFilterProc(DataSet: TDataSet; var Accept: Boolean);
 
begin
 
  // Filter out everything except product_no=B019
 
  Accept := Copy (Trim (TDBF1.FieldByName ('product_no'). AsString),1,4) = 'B019';
 
end;
 
</syntaxhighlight>
 
All records that give a false value for Accept are filtered out i.e. are not displayed in the report.
 
 
 
You can use this method to show only empty values, e.g. for dates.
 
Assuming you have a date field called orderdate:
 
<syntaxhighlight>
 
procedure TTestCursorDBBasics.TestOnFilterProc(DataSet: TDataSet; var Accept: Boolean);
 
begin
 
  // Show only empty order dates
 
  Accept := TDBF1.FieldByName('orderdate').IsNull;
 
end;
 
  
 
== Packing and rebuilding the tables ==
 
== Packing and rebuilding the tables ==

Revision as of 11:14, 15 October 2013

Deutsch (de) English (en) español (es) français (fr) português (pt) русский (ru) 中文(中国大陆)‎ (zh_CN)

Overview

This tutorial is about basic database development using the TDbf component (by Micha Nelissen) with Lazarus.

In Lazarus, the DbfLaz component makes use of the FPC TDbf code.

Documentation

For official TDbf documentation PDF go to the SourceForge site. It may be useful to keep that pdf alongside this document while reading.

This wiki article serves as additional documentation for new features since the original documentation.


Alternatives and known problems

Alternatives for embedded database use include Firebird embedded (advantage: easily scalable to client/server), SQLite (advantage: easy deployment), or ZMSQL (using CSV files; advantage: only Pascal code required).

Known problems: The TDBF code lacked a maintainer for a long while. Currently (March 2013), both the Sourceforge upstream project and FPC database developers are looking into fixing TDBF bugs again.

Current issues: please see the bugs in the bug tracker, especially http://bugs.freepascal.org/view.php?id=22177

Limitations

  • TDBF sets database encoding based on OS language settings. Moving a DB to another computer that does not support that encoding may lead to a read-only database. Note: investigate whether this applies to Tablelevel 7 (Visual DBase) and 25/30 (Foxpro/Visual Foxpro)
  • Indexing a DBase file with [ixCaseInsensitive] currently does not work.
  • Currently there is no support for referential integrity/master-detail between multiple tables.
  • No support for internally encrypted .dbf files: the DbaseIV encryption mechanism is quite weak anyway. Please use one of the encryption methods mentioned below.

FPC 2.6.2 and lower:

  • On ARM processors, the TDBF code is known not to work due to bus alignment issues. This is being addressed in the FPC development/trunk version.
  • Choosing the FoxPro tablelevel (25) will produce files that cannot be read by Visual Foxpro drivers. This has been fixed in the FPC development/trunk version.

No need for installation

The DbfLaz package is installed by default in both FPC and Lazarus. The DBFLaz package uses the TDbf and associated units in the FreePascal Free Component Library. In other words, no need to install anything if you have a fairly recent version of Lazarus.

What the TDbf package provides

The TDbf package provides access to dBase and FoxPro database tables for Lazarus (and others). It allows for reading, writing and creation of dBase III+, dBase IV, Visual dBase VII and FoxPro tables. It does all of this without the need for additional libraries or database engines. Simply drop the TDbf on your form and you have instant access to a cross-platform database environment. The TDbf works in both Windows and Linux using Lazarus.

What to put in your uses section

If you use the visual DBF components in Lazarus, Lazarus will put Dbf in your uses clauses. If you use FPC (console mode) only, you can put it there yourself. Handy other units are db for DataSet support and Dbf_Common for things such as field type definitions.

So you'd get:

uses Dbf, db, Dbf_Common;

How to create a new database table

There is a "Database Desktop" application for Lazarus (open the project in your Lazarus directory/tools/LazDataDesktop). You could create a new data dictionary, then create code to set up DBF files based on the dictionary. As of now (July 2011), this process is still a bit convoluted.

Another option is to use OpenOffice or LibreOffice Calc and save your spreadsheet as a .dbf file.

You may also (on windows - works with wine too) try this application: http://www.dirfile.com/cdbf_explorer.htm

In any event, we shall illustrate creating a new database in code below.

Setting the path

It's a good idea to give your application's database its own directory. This simplifies making backups of the data.

There are two ways to set the path:

  • you can set the full path using the FilePathFull property
  • you can set a path relative to the current application path with FilePath.

For instance, setting "FilePath" at runtime to "data/" would use a data subdirectory just below the executable file. Setting the "FilePathFull" property to "/var/data/" would place all dbfs, index files etc in that exact folder, whatever the application location is.

An example:

MyDbf.FilePathFull := '/location/to/my/data'; //Directory where all .dbf files will be stored
MyDbf.TableName := 'customers.dbf'; // note: is the .dbf really required?

Choosing a TableLevel

By default, the TDbf components will create dBase IV tables. While this is very compatible, there are features you may wish to use that are not supported. E.g. to support auto-incrementing fields, you must use something newer. The table types are:

Table level *Base product compatibility Auto-increment field support Field length limit Remarks
Level 3 dBase III+ no 10 characters [1] Quite obsolete
Level 4 dBase IV no 10 characters [2] Used to be very popular. Basis for GIS shape files (.shp)
Level 7 Visual dBase VII yes 32 characters [3]
Level 25 FoxPro yes 10 characters [4]

[5]

FoxPro; compatible with FoxPro for DOS and Visual FoxPro. Note: implementation in FPC 2.6.2 produces files that cannot be read by Visual FoxPro drivers!
Level 30 Visual FoxPro yes Normally 10 characters [6]

[7] Not supported in FPC: when using a DBC (Database container) file: 27 characters

Not yet in FPC 2.6.2; only in current development FPC version

See bug [8] for more details on field length limitations.

You choose a table type by setting the TableLevel property appropriately:

MyDbf.TableLevel := 7; //Visual dBase 7

Adding fields

Creating fields for your new table at runtime pretty much follows the old Delphi standard. An example:

With MyDbf.FieldDefs do begin
  Add('Id', ftAutoInc, 0, True); //Autoincrement field called Id
  Add('Name', ftString, 80, True); //80 character string field called Name
End;

List of field types - bold types are currently supported by the DBF code. See Database_field_type for a explanation of field types:

  • ftUnknown
  • ftString
  • ftSmallInt
  • ftInteger
  • ftWord
  • ftBoolean
  • ftFloat
  • ftCurrency (only supported in TableLevel 25)
  • ftBCD (only supported in TableLevel 25)
  • ftDate
  • ftTime
  • ftDateTime: probably the recommended one for storing ftDate or ftDateTime values. At least on FoxPro, seems to store milliseconds as well.
  • ftBytes (only supported in TableLevel 25)
  • ftVarBytes
  • ftAutoInc (only supported in TableLevel 7 or 25)
  • ftBlob
  • ftMemo
  • ftGraphic
  • ftFmtMemo
  • ftParadoxOle
  • ftDBaseOle
  • ftTypedBinary
  • ftCursor
  • ftFixedChar
  • ftWideString
  • ftLargeInt
  • ftADT
  • ftArray
  • ftReference
  • ftDataSet
  • ftOraBlob
  • ftOraClob
  • ftVariant
  • ftInterface
  • ftIDispatch
  • ftGuid
  • ftTimeStamp
  • ftFMTBcd

Creating the table

Once you have defined the fields you wish to use in your new table, you can go ahead and create it with:

MyDbf.CreateTable;

How to add indexes to a table

If your database is larger than a few records, chances are you will want to have indexes defined to make searching faster. To change the index structure of a table, we need to have exclusive access to the table - which we would have while creating it anyway.

MyDbf.Exclusive := True;
MyDbf.Open;

Now, we just have to add the index - or two, as in this example:

MyDbf.AddIndex('custid', 'Id', [ixPrimary, ixUnique]);
MyDbf.AddIndex('custname','Name', [ixCaseInsensitive]);
MyDbf.Close;

Expressions

Indexes and filters can use expressions according to the TDbf documentation.

Index 100 character limit

Index expressions that return a string may be maximum 100 characters long; use SUBSTR to cut off extra characters, e.g.:

MyDbf.AddIndex('SOMEFANCYINDEX', 'DTOS(DATEFIELD)+SUBSTR(LONGFIELD ,1,10)+SUBSTR(LONGFIELD2 ,1,20)', []);

Index/filter on TDateTimes

To set an index on a date/time field, use an expression index and the DTOS function:

MyDbf.AddIndex('PUBLISHDATEIDX', 'DTOS(PUBLISHDATE)', []);

Filtering is similar - note: apparently you need to surround the string value you are comparing against with double quotes. See below for details on the DTOS function; basically you need to format the date as YYYYMMDD:

// we have a field named MODIFIED here:
MyDBF.Filter:='DTOS(MODIFIED) > "20051231"';
//adding some spaces also works:
//MyDBF.Filter:='DTOS(MODIFIED)>"20051231"';

// if you want to compare datetime, that can be done:
// everything later than 2 January 2006, 4:34PM
MyDBF.Filter:='DTOS(MODIFIED) > "20060102 163400"';

MyDBF.Filtered:=true;

Index/filter operators/functions

Supported operators (from the documentation mentioned above):

Operator/function Description
Concatenate two strings, or add two numbers
* Multiply two numbers
/ Divide two numbers
= Compare two strings, numbers or booleans for equality
<> Compare two strings, numbers or booleans for inequality
< True if the first argument is smaller than second argument
<= True if the first argument is smaller than or equal to the second argument
> True if the first argument is greater than the second argument
>= True if the first argument is greater than or equal to the second argument
NOT Negate the boolean argument to the right
AND Returns true if both the first and second boolean argument are true
OR Returs true if the first or second boolean argument is true
STR(num[,size,precision]) Converts a number num to a string with optional size and precision

num: number to convert to string size: the total number of characters to output precision: the number of digits to the right of the decimal point

DTOS(date) Converts date to a string with representation YYYYMMDD
SUBSTR(str,index,count) Extracts a substring from string str from position index and length count
UPPER(str) Returns the uppercase equivalent of string str
LOWER(str) Returns the lowercase equivalent of string str

Code example: creating a table/dbf file

The following sample creates a new table "customers" in code. This of course only needs done once, and after that you just OPEN the table, don't create it. ;-)

Light bulb  Note: When using Windows (at least it was so with XP Professional) you must create the relative data directory before trying to run this example, or

else this program will fail.
($MODE OBJFPC}
Program DatabaseTest;
{ We will require the following units be in the USES clause: }
uses Dbf, db, Dbf_Common, SysUtils;  
                                 
{ The Dbf is put there e.g. when you drop a TDbf component on a form...   }
{ but you will need db for the DataSet object and Dbf_Common              }
{ for things such as the field type definitions                           }
{ Finally, use SysUtils for ForceDirectories.                             }
var
  MyDbf: TDbf;
begin
  MyDbf := TDbf.Create(nil);
  try
    { make sure the data directory exists: }
    ForceDirectories('data');
    { use relative path to "data" directory }
    MyDbf.FilePath := 'data' + DirectorySeparator; 
    { we want to use Visual dBase VII compatible tables }
    // Note: November 2011: tablelevel 25 does not seem to work...
    MyDbf.TableLevel := 7;
    MyDbf.Exclusive := True;
    MyDbf.TableName := 'customers.dbf';
    With MyDbf.FieldDefs do begin
      Add('Id', ftAutoInc, 0, True);
      Add('Name', ftString, 80, True);
    End;
    MyDbf.CreateTable;
    MyDbf.Open;
    MyDbf.AddIndex('custid', 'Id', [ixPrimary, ixUnique]);
    { add a secondary index }
    MyDbf.AddIndex('custname','Name', [ixCaseInsensitive]);
    MyDbf.Close;
  finally
    MyDbf.Free;
  end;
end;

External Index Files

The TDbf component also supports storing secondary indexes in a separate file. This might be helpful if the database is expected to be very large. Secondary index files are created almost identically to normal indexes, but with the addition of the '.ndx' file extension:

MyDbf.AddIndex('custname.ndx','Name', [ixCaseInsensitive]);


Each time the TDbf is opened, the index file must be loaded:

MyDbf.OpenIndexFile('custname.ndx');


And indexes must be referenced including the extension:

MyDbf.IndexName := 'custname.ndx';


Index files can be packed (i.e. entries pointing to deleted data rows are removed) separately using:

MyDbf.CompactIndexFile('custname.ndx');

How to link the TDbf component to data-aware components

The above examples show how to create a new database table in code. Using that table is even more simple.

Data aware components in Lazarus (such as the TDbEdit control) link to a TDataSource component using their "DataSource" and "DataField" properties. The TDataSource component handles communication between the database engine and the data aware components. A TDataSource then links to the TDbf component using it's "DataSet" property. The connection looks like this:

TDbEdit-------
             |
TDbEdit------|-->TDataSource-->TDbf
             |
TDbNavigator--


Be sure to set the FilePath (or FilePathFulll), TableLevel, and TableName properties of your TDbf component before calling

TDbf.Active := True;


Master table relations

Real power in database programming begins when you have multiple tables that reference each other. While TDbf does not yet support referential integrity, it does support a master / detail relationship between TDbf's.

When there are two tables related, for instance:

[customers]
Id       <----|
Name          |
Phone         |
Address       |
              |  The CustID in invoices references a customer primary  field
[invoices]    |
Id            |
Amount        |
CustID   -----|  * This field indexed as "idxcustid"


If you wanted to display all invoices for a given customer, the detail table (invoices) can stay in sync with the master table (customers) automatically.

On the invoices TDbf component set the following:

InvDbf.IndexName := 'idxcustid'; // our field that will match the customers table ID
InvDbf.MasterSource := dsCustomers; // datasource that is linked to the customers TDbf
InvDbf.MasterFields := 'Id'; // field on the customers table we are matching against our index

Searching and Displaying a data set by using filters

Simon Batty

In this example I wanted to search a database of books for all the titles an author has listed and then display the list in a memo box

Dbf1.FilePathFull := '/home/somelocatio/database_location/'; // path to the database directory
Dbf1.TableName := 'books.dbase';                             // database file (including extension)
DbF1.Open;
memo1.Clear;                                                 // clear the memo box
 
Dbf1.FilterOptions := [foCaseInsensitive];
Df1.Filter := 'AUTHOR=' + QuotedStr('anauthor');             // AUTHOR is the field name containing the authors
                                                             // Note that we need to surround the value with double quotes: "James Joyce"
 
Dbf1.Filtered := true;       // This selects the filtered set
Dbf1.First;                  // moves the the first filtered data
while not dbf1.EOF do        // prints the titles that match the author to the memo box
begin
  memo1.Append(Dbf1.FieldByName('TITLE').AsString);          // TITLE is the field name for titles
  dbf1.next;                                                 // use .next here NOT .findnext!
end;
Dbf1.Close;

Note that you can use Ddf1.findfirst to get the first record in the filtered set, then use Dbf1.next to move though the data. I found that using Dbf1.Findnext just causes the program to hang.


How to Filter Date in Blanks

You can show only date fields that have no contents (are blank) e.g. in Lazreport.

The easiest way is using the OnFilterRecord event handler with code like the following:

if not YourDBFComponentName.FieldByName('YOURDATEFIELD').IsNull then Accept:=False;
Light bulb  Note: the following code would seem to be similar, but shorter:
Accept:=YourDBFComponentName.FieldByName('YOURDATEFIELD').IsNull;

When we put the value of the Accept False all records that do not meet that particular condition are disregarded, i.e, are not displayed in the report.

It is also necessary to put the Filtered property database to true, but outside of that event.

How to Filter a Value with TDBF

Imagine you have a database that identifies the products by a code, for example, B019, and you need to filter only those products that are of this code (B019, in this example so that a report is generated with LazReport. Component is TDbf called the data here. field that stores the value called "product_no"

We have two ways to filter. The first would be to use the following code:

dados.filter: = 'product_no' = "B019" '

It turns out that this works very well with several components, but does not work with LazReport.

To solve the problem with LazReport you can use the OnFilterRecord event handler and use the following code:

Accept: = Copy (Trim (dadosNot.FieldByName ('product_no'). AsString), 1,4) = 'B019';

It is necessary to colcoar dados.filtered to true, out of that event.

As said before, when the value is false Accept the registers are disregarded and this way you can show off all products with that code in the report LazReport.

Packing and rebuilding the tables

When a record is deleted, it's not truly removed from the physical table. Periodically you must "pack" a table to recover that lost space. This should be done with exclusive mode set. See above for remarks on packing external index files.

MyDbf.Exclusive := True;
MyDbf.Open;
MyDbf.PackTable;
// let's also rebuild all the indexes
MyDbf.RegenerateIndexes;
MyDbf.Close;
MyDbf.Exclusive := False;


Encryption

Apparently at least DBase IV and 5 support some kind of encryption (there is an encryption flag in the file structure), but it may well be that the password/encryption key is in the DBF itself (see [9]), making it insecure.

There are other options if you want to encrypt your data. See here discussion on DBase encryption

Operating system level encryption

You can use an encrypted filesystem such as TrueCrypt (containers) to encrypt the directory where the database files get stored.

Encrypt the DBF file

You can use streams (e.g. the Blowfish encyprtion stream code supplied with Lazarus/FPC) to encrypt the data (all tables etc) by storing the unencrypted file e.g. in memory, then writing that to disk:

  1. Open your file in a TFileStream.
  2. Decrypt TFileStream to a new stream, YourDecryptedStream
  3. Tdbf.UserStream:= YourDecryptedStream; //Note: this will not encrypt .dbt/memo files! Use UserMemoStream for this (currently - May 2013 - in FPC trunk only)
  4. Tdbf.Storage:=stoMemory;
  5. Tdbf.Open;

When finished

  1. Tdbf.Close
  2. Encrypt YourDecryptedStream to TFileStream
  3. Free stream(s)

Detailed code can be found in the forum thread mentioned above. People implementing this are welcome to adapt this article with working code.

Encrypt field data

You could also choose to encrypt field data by implementing the OnGetText and OnSetText events to encrypt/decrypt the field contents.

Third party software

  • MyDbf Studio is an open source admin tool to manage your dbase table entirely written under lazarus IDE and based on TDbf.
  • [10] are Delphi components for DBF files.

See also