Creating applications for Firebird DBMS using various components and drivers: FireDac

  • Tutorial
This article will describe the process of creating applications for Firebird DBMS using FireDac access components and Delphi XE5 environment. FireDac is the standard set of database access components since Delphi XE3.

Our application will work with the database model, which is presented in the figure below.

diagramm


At the end of this article are links to other articles that describe the process of creating a database with this model and a link to the database creation script.
Attention!

This model is just an example. Your subject area may be more complex, or completely different. The model used in this article is simplified as much as possible so as not to clutter up the description of working with components, the description of the creation and modification of the data model.

Create a new project File-> New-> VCL Forms Application - Delphi. In the new project, add a new date module File-> New-> Other, in the wizard that appears, select Delphi Projects-> Delphi Files-> Data Module. This date module will be central to our project. It will contain some instances of global access components that should be available to all forms that need to work with data. For example, such a component is TFDConnection.

TFDConnection Component


The TFDConnection component provides connectivity to various types of databases. We will specify an instance of this component in the Connection properties of the remaining FireDac components. Which type of database will be connected to depends on the value of the DriverName property. To access Firebird, we need to set this property to FB. In order for the connection to know which access library it is necessary to work with, we will place the TFDPhysFBDriverLink component in the main date module. Its VendorLib property allows you to specify the path to the client library. If it is not specified, then connection to Firebird will be through libraries registered in the system, for example, system32, which in some cases may be undesirable.

Client Library Path


We will place the necessary access library in the fbclient folder, which is located in the application folder. To do this, in the code for the OnCreate event the module date we will write the following code.

  // указываем путь до клиентской библиотеки
  xAppPath := ExtractFileDir(Application.ExeName) + PathDelim;
  FDPhysFBDriverLink.VendorLib := xAppPath + 'fbclient' + PathDelim + 'fbclient.dll';


Important!

If you are compiling a 32-bit application, then you should use the 32-bit fbclient.dll library. For 64 bit - 64 bit. In addition to the fbclient.dll file, it is advisable to place the msvcp80.dll and msvcr80.dll libraries (for Firebird 2.5), and msvcp100.dll and msvcr100.dll (for Firebird 3.0) in the same folder. These libraries can be found either in the bin subfolder (Firebird 2.5) or in the root folder of the server (Firebird 3.0).

In order for the application to correctly display its own firebird errors, you must also copy the firebird.msg file. For Firebird 2.5 and in earlier versions, it should be one level above the client library directory, i.e. in our case, in the application directory. For Firebird 3, it must be in the client library directory, i.e. in the fbclient directory.


If you need your application to work without the installed Firebird server, i.e. in Embedded mode, then for Firebird 2.5 you need to replace fbclient.dll with fbembed.dll. If desired, the library name can be moved to the configuration file of your application. For Firebird 3.0, nothing needs to be changed (the mode of operation depends on the connection string and the value of the Providers parameter in the firebird.conf / databases.conf file).

Tip

Even if your application will work with Firebird in Embedded mode, it is more convenient to develop under a full server. The fact is that in Embedded Firebird mode it works in the same address space with your application, which can lead to undesirable consequences when errors occur in your application. In addition, at the time of development, the Delphi environment and your application are separate applications using Embedded. Prior to version 2.5, they cannot work with one database at the same time.

Connection options


The TFDConnection component contains the database connection parameters in the Params property (username, password, connection character set, etc.). If you use the TFDConnection property editor (double-click on the component), then the mentioned properties will be filled automatically. The set of these properties depends on the type of database.

ParameterAppointment
PooledIs the connection pool used.
DatabaseThe path to the database or its alias defined in the aliases.conf (or databases.conf) configuration file of the Firebird server.
User_NameUsername.
PasswordPassword.
OSAuthentIs authentication used by the operating system?
ProtocolConnection protocol. The following values ​​are allowed:
  • Local - local protocol;
  • NetBEUI - named pipes
  • SPX - not supported in modern versions;
  • TCPIP - TCP / IP.

ServerServer name or its IP address. If the server runs on a non-standard port, then you must also specify the port through a slash, for example localhost / 3051.
SQLDialectDialect. Must match the dialect of the database.
RolenameThe name of the role.
CharactersetThe name of the connection character set.

FDConnection_params


Additional properties:

Connected - control the connection to the database, or check the status of the connection. This property must be set to True for the wizards of other FireDac components to work. If your application needs to request authorization data, it is important not to forget to reset this property to False before compiling your application.

LoginPrompt - whether to request a username and password when trying to connect.

Transaction - a component of TFDTransaction, which will be used as the default for performing various TFDConnection operations. If this property is not explicitly assigned, TFDConnection will create an instance of TFDTransaction on its own, its parameters can be specified in the TxOptions property.

UpdateTransaction- component TFDTransaction, which will be used as the default for the same properties of TFDQuery components. If this property is not explicitly assigned, the value from the Transaction property will be used.

Since the connection parameters, except for the username and password, usually do not change during the operation of the application, we will read them from the configuration file.

  // считываем параметры подключения
  xIniFile := TIniFile.Create(xAppPath + 'config.ini');
  try
    xIniFile.ReadSectionValues('connection', FDConnection.Params);
  finally
    xIniFile.Free;
  end;


The config.ini file contains approximately the following lines:
[connection]
DriverID=FB
Protocol=TCPIP
Server=localhost/3051
Database=examples
OSAuthent=No
RoleName=
CharacterSet=UTF8


The contents of the connection section can be obtained by copying the contents of the Params property of the TFDConnection component after the wizard.
Note

In fact, the general settings are usually found in% AppData% \ Manufacture \ AppName and saved there by the application installer. However, during development it is convenient that the settings file lay somewhere closer, for example, in the application folder.

Please note that if your application is installed in the Program Files folder and the configuration file is in the same place, either this file will be virtualized in Program Data, or there will be problems with its modification and subsequent reading of new settings.

Database Connection


To connect to the database, you must change the Connected property of the TFDConnection component to True or call the Open method. In the last method, you can pass the username and password as parameters. In our application, we will replace the standard database connection dialog. We will give the opportunity to make a mistake when entering registration information no more than three times, after which the application will be closed. To do this, we will write the following code in the OnCreate event handler of the main data module.

  // делаем максимум 3 попытки входа в систему, потом закрываем приложение
  xLoginCount := 0;
  xLoginPromptDlg := TLoginPromptForm.Create(Self);
  while (xLoginCount < MAX_LOGIN_COUNT) and 
        (not FDConnection.Connected) do
  begin
    try
      if xLoginPromptDlg.ShowModal = mrOK then
        FDConnection.Open(
          xLoginPromptDlg.UserName, xLoginPromptDlg.Password)
      else
        xLoginCount := MAX_LOGIN_COUNT;
    except
      on E: Exception do
      begin
        Inc(xLoginCount);
        Application.ShowException(E);
      end
    end;
  end;
  xLoginPromptDlg.Free;
  if not FDConnection.Connected then
    Halt;


TFDTransaction Component


The TFDTransaction component is designed to work explicitly with transactions.

The client part of Firebird allows performing any action only in the context of a transaction. Therefore, if you were able to access the data without explicitly calling TFDTransaction.StartTransaction, then somewhere in the bowels of FireDac this call occurred automatically. This behavior is highly discouraged. For applications to work correctly with the database, it is desirable to manage transactions manually, that is, explicitly call the StartTransaction, Commit and Rollback methods of the TFDTransaction component.

ParameterAppointment
ConnectionCommunication with the FDConnection component.
Options.AutoCommitControls the automatic start and end of a transaction. The default value is True.

If the value of this property is set to True, then FireDAC does the following:
  • A transaction is started (if required) before each SQL command is executed, and the transaction ends after an SQL command is executed. If the command succeeds, the transaction will complete as COMMIT, otherwise ROLLBACK.
  • If the application calls the StartTransaction method, then automatic transaction management will be disabled until the transaction completes as Commit or Rollback.

In Firebird, automatic transaction management is emulated by the FireDAC components themselves.
Options.AutoStartControls the automatic start of a transaction. Default is True.
Options.AutoStopControls the automatic completion of a transaction. Default is True.
Options.DisconnectActionThe action that will be performed when the connection is closed, if the transaction is active. The default value is xdCommit. The following options are possible:
  • xdNone - nothing will be done. The action will be left to the DBMS;
  • xdCommit - transaction confirmation;
  • xdRollback - transaction rollback.

In other access components, the default value for the similar xdRollback property is. Therefore, you must set this property manually to the value that is really required.
Options.EnableNestedManages nested transactions. The default value is True.

When a transaction is active, the next StartTransaction call will create a nested transaction. FireDAC emulates nested transactions using savepoints if the DBMS does not explicitly support nested transactions. To disable nested transactions, set EnableNested to False and the next call to StartTransaction will throw an exception.

Firebird does not support nested transactions explicitly.
Options.IsolationDefines the level of transaction isolation. This is the most important property of a transaction. The default value is xiReadCommited. The following options are possible:
  • xiUnspecified - the default isolation level for your DBMS is used (in Firebird it is SNAPSHOT, i.e. with read write concurrency wait parameters);
  • xiDirtyRead - this level of isolation does not exist in Firebird so READ COMMITED will be used instead;
  • xiReadCommited - READ COMMITED isolation level. In Firebird, such a transaction starts with read write read_committed rec_version nowait;
  • xiRepeatableRead - this level of isolation does not exist in Firebird so SNAPSHOT will be used instead;
  • xiSnapshot - SNAPSHOT isolation level. In Firebird, such a transaction starts with read write concurrency wait parameters;
  • xiSerializable - SERIALIZABLE isolation level. Actually, there is no transaction in Firebird with this isolation level, but it is emulated by starting a transaction with read write consistency wait parameters.

Options.ParamsDBMS-specific transaction parameters. Currently used only for Firebird and Interbase. Possible values:
  • read
  • write
  • read_committed
  • concurrency
  • consistency
  • wait
  • nowait
  • rec_version
  • no rec_version

Options.ReadOnlyIndicates whether the transaction is read-only. Default is False. If set to True, then any changes within the current transaction are not possible, in Firebird in this case there is no read value in the transaction parameters.

Setting this property to True allows the DBMS to optimize the use of resources.

Unlike other DBMSs, Firebird allows you to use as many TFDTransaction components tied to one connection. In our application, we will use one common reading transaction for all directories and online journals, and one writing transaction for each directory / journal.

In our application, we will not rely on automatic start and completion of transactions, and therefore in all transactions Options.AutoCommit = False, Options.AutoStart = False and Options.AutoStop = False.

Since the reading transaction is common to all directories and journals, it is convenient to place it in the main date module. For normal work (displaying data in a grid, etc.), the isolation mode READ COMMITED (Options.Isolation = xiReadCommited) is usually used, because it allows the transaction to see other people's committed database changes simply by re-executing the queries (re-reading the data). Since this transaction is read-only, set the Options.ReadOnly property to True. Thus, our transaction will have read read_commited rec_version parameters. A transaction with such parameters in Firebird can be open for an arbitrarily long time (days, weeks, months), without blocking other transactions or affecting the accumulation of garbage in the database (because, in fact,

Note

Such a transaction cannot be used for reports (especially if they use several consecutive queries), because a transaction with the READ COMMITED isolation mode during re-reading of data will see all new committed changes.

For reports, it is recommended that you use a short read-only transaction with SNAPSHOT isolation mode (Options.Isolation = xiSnapshot and Options.ReadOnly = True). In this example, working with reports is not considered.


We start the reading transaction immediately after successfully establishing a connection to the database by calling trRead.StartTransaction in the OnCreate event of the main data module, and we complete it before closing the connection by calling tRead.Commit in the OnDestroy event of the main data module. The value of the Options.DisconnectAction property is xdCommit by default, suitable for a read-only transaction.

The writing transaction will be separate for each directory / journal. We will place it on a form that relates directly to the desired magazine. The writing transaction should be as short as possible so as not to hold the Oldest Active Transaction, which does not allow garbage collection, which in turn leads to degradation of performance. Since the writing transaction is very short, we can use the SNAPSHOT isolation level. Thus, our writing transaction will have options.ReadOnly = False and Options.Isolation = xiSnapshot. For writing transactions, the value of the Options.DisconnectAction property is not suitable by default; it must be set to xdRollback.

Datasets


You can work with data in FireDac using the FDQuery, FDTable, FDStoredProc, FDCommand components, but FDCommand is not a dataset.

TFDQuery, TFDTable, and TFDStoredProc are inherited from TFDRdbmsDataSet. In addition to data sets for working directly with the database, in FireDac there is also a TFDMemTable component, which is designed to work with a data set in memory, which is an analog of TClientDataSet.

The main component for working with datasets is TFDQuery. The capabilities of this component are enough for almost any purpose. The components TFDTable and TFDStoredProc are just modifications, either slightly extended or truncated. We will not consider and apply them in our application. You can read them in the FireDac documentation if you wish.

The purpose of the component is to buffer the records selected by the SELECT statement to represent this data in the Grid, as well as to ensure the "edibility" of the record (current in the buffer (grid)). Unlike the IBX.IBDataSet component, the FDQuery component does not contain the RefreshSQL, InsertSQL, UpdateSQL, and DeleteSQL properties. Instead, “editable” is provided by the FDUpdateSQL component, which is set to the UpdateObject property.

Information

In some cases, you can make the FDQuery component editable without setting the UpdateObject property and writing Insert / Update / Delete queries, simply setting the UpdateOptions.RequestLive = True property, and modifying queries will be generated automatically. However, this approach has many limitations on the main SELECT query, so you should not rely on it.


ParameterAppointment
ConnectionCommunication with the FDConnection component.
MasterourceLink to the Master Data Source (TDataSource) for FDQuery used as Detail.
TransactionThe transaction within which the query specified in the SQL property will be executed. If the property is not specified, the default transaction will be used to connect.
UpdateobjectCommunication with the FDUpdateSQL component, which provides “editable” data sets when a SELECT query does not meet the requirements for automatically generating modifying queries when setting UpdateOptions.RequestLive = True.
UpdateTransactionA transaction within which modifying queries will be executed. If the property is not specified, the transaction from the Transaction property will be used.
UpdateOptions.CheckRequiredIf the CheckRequired property is set to True, then FireDac controls the Required property of the corresponding fields, i.e. fields with a NOT NULL constraint. The default is set to True.

If CheckRequired = True and the field with the Required = True property is not assigned a value, an exception will be thrown when the Post method is called. This may be undesirable if the value of this field can be assigned later in BEFORE triggers.
UpdateOptions.EnableDeleteDetermines whether deleting a record from a dataset is allowed. If EnableDelete = False, an exception will be thrown when the Delete method is called.
UpdateOptions.EnableInsertDetermines whether inserting a record into a dataset is allowed. If EnableInsert = False, an exception will be thrown when calling the Insert / Append method.
UpdateOptions.EnableUpdateDetermines whether changing a record in a dataset is allowed. If EnableUpdate = False, an exception will be thrown when calling the Edit method.
UpdateOptions.FetchGeneratorsPointControls when the next generator value specified in the UpdateOptions.GeneratorName property or the GeneratorName property of the auto-increment field AutoGenerateValue = arAutoInc is obtained. It has the following options:
  • gpNone - generator value is not retrieved;
  • gpImmediate - the next generator value is retrieved immediately after calling the Insert / Append method;
  • gpDeffered - the next generator value is retrieved before the publication of a new record in the database, i.e. during the execution of the Post or ApplyUpdates methods.

The default value is gpDeffered.
UpdateOptions.GeneratorNameThe name of the generator to retrieve the next auto-increment field value.
UpdateOptions.ReadOnlyIndicates whether the dataset is read-only. Default is False. If the value of this property is set to True, then the values ​​of the EnableDelete, EnableInsert and EnableUpdate properties will be automatically set to False.
UpdateOptions.RequestLiveSetting RequestLive to True makes the request “live”, i.e. editable if possible. In this case, Insert / Update / Delete queries will be generated automatically. This option imposes many restrictions on a SELECT query, introduced for backward compatibility with BDE, and is not recommended.
UpdateOptions.UpdateModeResponsible for checking record modification. This property made it possible to control the possible “overlap” of updates for cases when the user is editing the record “for a long time” and another user can manage to edit the same record and save it earlier. That is, the first user at the editing stage will not even know that the record has already changed, perhaps more than once, and will be able to “wipe” these updates with his own:
  • upWhereAll - check for the existence of a record by the primary key + check all columns for old values. for instance
    update table set ... 
    where pkfield = :old_ pkfield 
      and client_name = :old_client_name 
      and info = :old_info ... 
              

    That is, in this case, the request will change the information in the record only if no one managed to change the record before us.
    This is especially important if there are interdependencies between column values ​​- for example, minimum and maximum wages, etc.
  • upWhereCahnged - check record for existence by primary key + plus check for old values ​​of only mutable columns.
    update table set ... 
    where pkfield = :old_pkfield 
       and client_name = :old_client
              

  • upWhereKeyOnly (by default) - check the record for existence by the primary key.

The last check corresponds to the query generated automatically for UpdateSQL. Therefore, in case of possible update conflicts in a multi-user environment, it is necessary to append the conditions to where independently. And, of course, it is also necessary, when implementing the upWhereChanged analogue, to remove unnecessary column changes in the update table set ... - that is, leave only really changed columns in the set list, otherwise the request will overwrite other people's updates of this record. As you understand, this means the need to dynamically construct the UpdateSQL query.

If you want to configure update conflict detection detection individually for each field, then you can use the ProviderFlags property for each field.
CachedupdatesDetermines whether the data set will cache the changes without immediately making them to the database. If this property is set to True, then any changes (Insert / Post, Update / Post, Delete) are not immediately made to the database, but are saved in a special log.

An application must explicitly apply the changes by calling the ApplyUpdates method. In this case, all changes will be made within a short period of time and in one short transaction. The default value of this property is False.
SQLContains an SQL query. If this property contains a SELECT query, then it must be performed using the Open method. Otherwise, you must use the Execute or ExecSQL methods.


TFDUpdateSQL Component


The TFDUpdateSQL component allows you to override the SQL commands generated to automatically update a dataset. It can be used to update TFDQuery, TFDTable, and TFDStoredProc components. Using TFDUpdateSQL is optional for the TFDQuery and TFDTable components, because these components are able to automatically generate commands for publishing updates from a dataset to a DBMS. Using TFDUpdateSQL is required to be able to update the TFDStoredProc dataset. We recommend that you always use it, even for the simplest cases, in order to get full control over what requests are performed in your application.

To specify SQL commands at the design stage, use the TFDUpdateSQL editor of the design time, which is called by double-clicking on the component.

Note

For many FireDac design-time editors to work, they must have an active database connection (TFDConnection.Connected = True) and the transaction in auto-start mode (TFDTransaction.Options.AutoStart = True). But such settings may interfere with the application. For example, the user must enter the program under his username, and TFDConnection connects to the database under SYSDBA. Therefore, after each use of the design-time editors, we recommend that you check the TFDConnection.Connected property and reset it. In addition, you will have to enable and disable auto-read-only read-only transactions.

FDUpdateSQL_Generate


On the Generate tab, you can simplify your task of writing Insert / Update / Delete / Refresh queries. To do this, select the table for updating, its key fields, fields for updating, and the fields that will be re-read after the update, and click on the “Generate SQL” button.

After that, the queries will be generated automatically, and you will go to the “SQL Commands” tab, where you can correct each of the queries.

FDUpdateSQL_SQLCommands


Note

Since product_id is not included in Updating Fields, it is not in the generated insert request. It is assumed that this column is automatically populated with a trigger (with a generator), or this is an IDENTITY column (starting with Firebird 3.0). When retrieving the generator value for this column from the server, it is recommended that you manually add the PRODUCT_ID column to the RETURNING clause of the INSERT statement.


The Options tab contains some properties that may affect the generation of queries. These properties do not apply to the TFDUpdateSQL component itself, but are links to the UpdateOptions properties of the data set that has the current TFDUpdateSQL in the UpdateObject property. This is done solely for convenience.

ParameterAppointment
ConnectionCommunication with the FDConnection component.
DeleteSQLSQL query to delete a record.
FetchrowowSQL query to return one current (updated, inserted) record.
InsertSQLSQL query to insert records.
LockSQLSQL query to lock one current record. (FOR UPDATE WITH LOCK).
ModifySQLSQL query to modify the record.
UnlocklockSQL query to unlock the current record. Firebird does not apply.


As you already noticed, the TFDUpdateSQL component does not have the Transaction property. This is because the component does not execute modifying queries directly, but only replaces the automatically generated queries in the dataset, which is the ancestor of TFDRdbmsDataSet.

TFDCommand Component


The TFDCommand component is designed to execute SQL queries. It is not a successor to TDataSet, and therefore is convenient for executing SQL queries that do not return a dataset.

ParameterAppointment
ConnectionCommunication with the FDConnection component.
TransactionТранзакция, в рамках которой будет выполняться SQL команда.
CommandKindТип команды.
  • skUnknown – неизвестен. В этом случае тип команды будет определятся автоматически по тексту команды внутренним парсером;
  • skStartTransaction – команда для старта транзакции;
  • skCommit – команда завершения и подтверждения транзакции;
  • skRollback – команда завершения и отката транзакции;
  • skCreate – команда CREATE … для создания нового объекта метаданных;
  • skAlter – команда ALTER … для модификации объекта метаданных;
  • skDrop – команда DROP … для удаления объекта метаданных;
  • skSelect – команда SELECT для выборки данных;
  • skSelectForLock – команда SELECT … WITH LOCK для блокировки выбранных строк;
  • skInsert – команда INSERT … для вставки новой записи;
  • skUpdate – команда UPDATE … для модификации записей;
  • skDelete – команда DELETE … для удаления записей;
  • skMerge – команда MERGE INTO …
  • skExecute - EXECUTE PROCEDURE or EXECUTE BLOCK command;
  • skStoredProc - call the stored procedure;
  • skStoredProcNoCrs - A call to a stored procedure that does not return a cursor;
  • skStoredProcWithCrs - a call to a stored procedure that returns a cursor.

Typically, the type of command is automatically determined by the text of the SQL query.
CommandTextThe text of the SQL query.


Creating Directories


In our application we will create two reference books: a reference book of goods and a reference book of customers. Each of the directories is a form with a TDBGrid grid, a TDataSource data source, a TFDQuery dataset that writes TFDTransaction transactions.

Customers


Note

The trRead component is not visible because it is not on the form, but in the dmMain module.


Consider the creation of reference books on the example of a directory of customers.

Place the TFDQuery component on the form named qryCustomers. This dataset will be specified in the DataSet property of the DataSource. In the Transaction property, specify ReadOnly transaction trRead, which was created in the main project data module. In the UpdateTransaction property, we specify the trWrite transaction, in the Connection property - the connection located in the main data module. In the SQL property, write the following query:

SELECT
    customer_id,
    name,
    address,
    zipcode,
    phone
FROM
    customer
ORDER BY name


The write transaction trWrite should be as short as possible and have SNAPSHOT isolation mode. We will not rely on automatic start and completion of the transaction, but we will start and end the transaction explicitly. Thus, our transaction should have the following properties:

Options.AutoStart = False
Options.AutoCommit = False
Options.AutoStop = False
Options.DisconnectAction = xdRollback
Options.Isolations = xiSnapshot
Options.ReadOnly = False

In fact, it is not necessary to set the isolation mode SNAPSHOT for simple INSERT / UPDATE / DELETE. However, if the table has complex triggers, or instead of simple INSERT / UPDATE / DELETE queries, a stored procedure is called, it is advisable to use the SNAPSHOT isolation level.

The fact is that the isolation level of READ COMMITED does not ensure the atomicity of the operator within a single transaction (statement read consistency). Thus, a SELECT statement can return data that has entered the database after the start of the query. In principle, SNAPSHOT isolation mode can be recommended almost always if the transaction is short.

To be able to edit the dataset, you must fill in the InsertSQL, ModifySQL, DeleteSQL, and FetchRowSQL properties. These properties can be generated by the wizard, but after that some editing may be required. For example, you can add a RETURNING clause, delete a modification of some columns, or even replace an automatically generated request to call a stored procedure.

InsertSQL:

INSERT INTO customer (customer_id,
                      name,
                      address,
                      zipcode,
                      phone)
VALUES (:new_customer_id,
        :new_name,
        :new_address,
        :new_zipcode,
        :new_phone)


ModifySQL:

UPDATE customer
SET name = :new_name,
    address = :new_address,
    zipcode = :new_zipcode,
    phone = :new_phone
WHERE (customer_id = :old_customer_id)


DeleteSQL:

DELETE FROM customer
WHERE (customer_id = :old_customer_id)


FetchRowSQL:

SELECT
    customer_id,
    name,
    address,
    zipcode,
    phone
FROM
    customer
WHERE customer_id = :old_customer_id


In this reference, we will get the value of the generator before inserting the record into the table. To do this, set the properties of the TFDQuery component to the following values: UpdateOptions.GeneratorName = GEN_CUSTOMER_ID and UpdateOptions.AutoIncFields = CUSTOMER_ID. There is another way when the value of the generator (auto-increment field) is returned after executing an INSERT request using the RETURNING clause. This method will be shown later.

To add a new record and edit an existing one, it is customary to use modal forms, upon closing of which, with the result of mrOK, changes are made to the database. Typically, DBAware components are used to create such forms, which allow you to display the values ​​of a field in the current record and immediately make changes to the current record of the data set in Insert / Edit modes, i.e. to Post. But you can only switch the data set to Insert / Edit mode by starting a writing transaction. Thus, if someone opens the form for making a new entry and leaves for lunch without closing this form, we will have an active transaction hanging until the employee returns from lunch and closes the form. This in turn will cause the active transaction to hold garbage collection, which will later lead to reduced performance.
  1. Use the CachedUpdates mode, which allows you to keep a transaction active only for a very short period of time, namely, while making changes.
  2. Refuse DBAware components. However, this path will require additional efforts from you.

We show the application of both methods. For reference, it is much more convenient to use the first method. Consider the vendor record edit code
procedure TCustomerForm.actEditRecordExecute(Sender: TObject);
var
  xEditor: TEditCustomerForm;
begin
  xEditor := TEditCustomerForm.Create(Self);
  try
    xEditor.OnClose := CustomerEditorClose;
    xEditorForm.DataSource := DataSource;
    xEditor.Caption := 'Edit customer';
    qryCustomer.CachedUpdates := True;
    qryCustomer.Edit;
    xEditor.ShowModal;
  finally
    xEditor.Free;
  end;
end;

The code shows that before putting the data set into edit mode, we set it to CachedUpdates mode, and all the editing processing logic takes place in modal form.
procedure TCustomerForm.CustomerEditorClose (Sender: TObject;
  var Action: TCloseAction);
begin
  if TForm(Sender).ModalResult <> mrOK then
  begin
    // отменяем все изменения
    qryCustomer.Cancel;
    qryCustomer.CancelUpdates;
    // возвращаем набор данных в обычный режим обновления
    qryCustomer.CachedUpdates := False;
    // и позволяем закрыть форму
    Action := caFree;
    Exit;
  end;
  try
    // подтверждаем изменения на уровне набора данных
    qryCustomer.Post;
    // стартуем транзакцию
    trWrite.StartTransaction;
    // если в наборе данных есть изменения
    if (qryCustomer.ApplyUpdates = 0) then
    begin
      // записываем их в БД
      qryCustomer.CommitUpdates;
      // и подтверждаем транзакцию
      trWrite.Commit;
    end
    else begin
      raise Exception.Create(qryCustomer.RowError.Message);
    end;
    qryCustomer.CachedUpdates := False;
    Action := caFree;
  except
    on E: Exception do
    begin
      // откатываем транзакцию
      if trWrite.Active then
        trWrite.Rollback;
      Application.ShowException(E);
      // Не закрываем окно, даём возможность исправить ошибку
      Action := caNone;
    end;
  end;
end;

The code shows that until the OK button is pressed, the writing transaction does not start at all. Thus, a writing transaction is active only for the period of data transfer from the data set buffer to the database. Since we save no more than one record in the buffer, the transaction will be active for a very short time, as required.

The directory of goods is made similar to the directory of customers. However, in it we will demonstrate another way to obtain auto-increment values.

The main request will look like this:
SELECT
    product_id,
    name,
    price,
    description
FROM product
ORDER BY name

The component property TFDUpdateSQL.InsertSQL will contain the following query:
INSERT INTO PRODUCT
(NAME, PRICE, DESCRIPTION)
VALUES (:NEW_NAME, :NEW_PRICE, :NEW_DESCRIPTION)
RETURNING PRODUCT_ID

A RETURNING clause appeared in this query, which will return the value of the PRODUCT_ID field after changing it in the BEFORE INSERT trigger. In this case, it makes no sense to set the value of the UpdateOptions.GeneratorName property. In addition, the PRODUCT_ID field must be set to Required = False and ReadOnly = True, since the value of this property is not entered directly. Otherwise, everything is about the same as organized for the manufacturers directory.

Create Magazines


In our application there will be one journal “Invoices”. Unlike directories, journals contain a fairly large number of entries and are often updated.

Invoice - consists of a heading that describes common attributes (number, date, customer ...), and lines of the invoice with a list of goods, their quantity, cost, etc. For such documents, it is convenient to have two grids: in the main, data on the document header is displayed, and in the detail, the list of goods. Thus, on the form of the document we need to place two TDBGrid components, to each of which we will bind our TDataSource, which in turn will be bound to their TFDQuery. In our case, the data set with the document headers will be called qryInvoice, and with the document lines, qryInvoiceLine.

In the Transaction property of both datasets, we specify ReadOnly transaction trRead, which was created in the main project data module. In the UpdateTransaction property, we specify the trWrite transaction, in the Connection property - the connection located in the main data module.

Most logs contain a field with the date the document was created. To reduce the amount of data to be selected, it is usually customary to introduce such a thing as a working period in order to reduce the amount of data transmitted to the client. A work period is a date range within which work documents are required. Since the application may contain more than one log, it makes sense to place the variables containing the start and end dates of the working period in the global dmMain data module, which, in one way or another, is used by all modules working with the database. When the application starts, the working period is usually initialized by the start and end dates of the current quarter (there may be other options). During the application, you can change the working period at the request of the user.

Invoices


Since the most recently entered documents are most often required, it makes sense to sort them by date in reverse order. Based on the foregoing, in the SQL property of the qryInvoice dataset, the query will look like this:

SELECT
  invoice.invoice_id AS invoice_id,
  invoice.customer_id AS customer_id,
  customer.NAME AS customer_name,
  invoice.invoice_date AS invoice_date,
  invoice.total_sale AS total_sale,
  IIF(invoice.payed=1, 'Yes', 'No') AS payed 
FROM
  invoice
  JOIN customer ON customer.customer_id = invoice.customer_id 
WHERE invoice.invoice_date BETWEEN :date_begin AND :date_end
ORDER BY invoice.invoice_date DESC


When you open this data set, you will need to initialize the query parameters:

  qryInvoice.ParamByName('date_begin').AsSqlTimeStamp := dmMain.BeginDateSt;
  qryInvoice.ParamByName('date_end').AsSqlTimeStamp := dmMain.EndDateSt;
  qryInvoice.Open;


All operations on the invoice will be performed using stored procedures, although in simpler cases this can also be done using ordinary INSERT / UPDATE / DELETE queries.

We will execute each stored procedure as a separate request in the TFDCommand components. This component is not an ancestor of TFDRdbmsDataSet, it does not buffer data and returns a maximum of one row of result, therefore its use carries less overhead for queries that do not return data. Since our stored procedures perform data modification, the Transaction property of the TFDCommand components needs to set the transaction trWrite.

Note

Stored procedures for inserting, editing, and adding records can also be placed in the corresponding properties of the TFDUpdateSQL component.


Four operations are provided for working with the header of the invoice: adding, editing, deleting and setting the sign “paid”. As soon as the invoice is paid, we prohibit any modifications to it, both in the header and in the lines. This is done at the stored procedure level. Here are the query texts for calling stored procedures.

qryAddInvoice.CommandText:
EXECUTE PROCEDURE sp_add_invoice(
  NEXT VALUE FOR gen_invoice_id, 
  :CUSTOMER_ID, 
  :INVOICE_DATE
)


qryEditInvoice.CommandText:
EXECUTE PROCEDURE sp_edit_invoice(
  :INVOICE_ID, 
  :CUSTOMER_ID, 
  :INVOICE_DATE
)


qryDeleteInvoice.CommandText:
EXECUTE PROCEDURE sp_delete_invoice(:INVOICE_ID)


qryPayForInvoice.CommandText:
EXECUTE PROCEDURE sp_pay_for_inovice(:invoice_id)


Since our stored procedures are not called from the TFDUpdateSQL component, after executing them, you must call qryInvoice.Refresh to update the data in the grid.

Calling stored procedures for which data entry is not required is performed as follows:
  if MessageDlg('Вы действительно хотите удалить счёт фактуру?', mtConfirmation,
    [mbYes, mbNo], 0) = mrYes then
  begin
    // Стартуем транзакцию
    trWrite.StartTransaction;
    try
      qryDeleteInvoice.ParamByName('INVOICE_ID').AsInteger :=
        qryInvoice.FieldByName('INVOICE_ID').AsInteger;
      // выполнение хранимой процедуры
      qryDeleteInvoice.Execute;
      // подтверждение транзакции
      trWrite.Commit;
      // обновление данных в гриде
      qryInvoice.Refresh;
    except
      on E: Exception do
      begin
        if trWrite.Active then
          trWrite.Rollback;
        Application.ShowException(E);
      end;
    end;
  end;


To add a new record and edit an existing one, as in the case of directories, we will use modal forms. In this case, we will not use DBAware components. Another feature - we will use the TButtonedEdit component to select a customer. It will display the name of the current customer, and at the click of a button call up a modal form with a grid to select the customer. Of course, it would be possible to use something like TDBLookupCombobox, but, firstly, there can be a lot of customers and scrolling through such a drop-down list will be inconvenient, and secondly, a single name may not be enough to find the right customer.

Editinvoice


As a modal window for choosing a customer, we use the same form that was created to enter customers. The button click handler code in the TButtonedEdit component will look like this:

procedure TEditInvoiceForm.edtCustomerRightButtonClick(Sender: TObject);
var
  xSelectForm: TCustomerForm;
begin
  xSelectForm := TCustomerForm.Create(Self);
  try
    xSelectForm.Visible := False;
    if xSelectForm.ShowModal = mrOK then
    begin
      FCustomerId := xSelectForm.qryCustomer.FieldByName('CUSTOMER_ID')
        .AsInteger;
      edtCustomer.Text := xSelectForm.qryCustomer.FieldByName('NAME').AsString;
    end;
  finally
    xSelectForm.Free;
  end;
end;


Since we use non-DBAware components, when calling the edit form, we will need to initialize the customer code and its name for display.

procedure TInvoiceForm.actEditInvoiceExecute(Sender: TObject);
var
  xEditorForm: TEditInvoiceForm;
begin
  xEditorForm:= TEditInvoiceForm.Create(Self);
  try
    xEditorForm.OnClose := EditInvoiceEditorClose;
    xEditor.Caption := 'Редактирование счёт-фактуры';
    xEditorForm.InvoiceId := qryInvoice.FieldByName('INVOICE_ID').AsInteger;
    xEditorForm.SetCustomer(qryInvoice.FieldByName('CUSTOMER_ID').AsInteger,
                        qryInvoice.FieldByName('CUSTOMER_NAME').AsString);
    xEditorForm.InvoiceDate := qryInvoice.FieldByName('INVOICE_DATE').AsDateTime;
    xEditorForm.ShowModal;
  finally
    xEditorForm.Free;
  end;
end;

procedure TEditInvoiceForm.SetCustomer(ACustomerId: Integer;
  const ACustomerName: string);
begin
  FCustomerId := ACustomerId;
  edtCustomer.Text := ACustomerName;
end;


We will carry out the processing of adding a new invoice and editing the existing one in the closing event of the modal form, just as it was done for directories. However, here we will no longer transfer the data set to CachedUpdates mode, since the modification is performed using stored procedures, and we do not use DBAware components.

procedure TInvoiceForm.EditInvoiceEditorClose(Sender: TObject; var Action: TCloseAction);
var
  xEditorForm: TEditInvoiceForm;
begin
  xEditorForm := TEditInvoiceForm(Sender);
  // если форма закрыта не по нажатию кнопки OK, 
  // то вообще ничего не делаем. Транзакция не стартует.
  if xEditorForm.ModalResult <> mrOK then
  begin
    Action := caFree;
    Exit;
  end;
  // Выполняем всё в короткой транзакции
  trWrite.StartTransaction;
  try
    qryEditInvoice.ParamByName('INVOICE_ID').AsInteger := xEditorForm.InvoiceId;
    qryEditInvoice.ParamByName('CUSTOMER_ID').AsInteger :=
      xEditorForm.CustomerId;
    qryEditInvoice.ParamByName('INVOICE_DATE').AsSqlTimeStamp :=
      DateTimeToSQLTimeStamp(xEditorForm.InvoiceDate);
    qryEditInvoice.Execute();
    trWrite.Commit;
    qryInvoice.Refresh;
    Action := caFree;
  except
    on E: Exception do
    begin
      if trWrite.Active then
        trWrite.Rollback;
      Application.ShowException(E);
      // Не закрываем окно, даём возможность исправить ошибку
      Action := caNone;
    end;
  end;
end;


Now let's move on to the invoice positions. To the qryInvoiceLine data set, set the MasterSource = MasterSource property, which is bound to qryInvoice, and the MasterFields = INVOICE_ID property. In the SQL property, write the following query:

SELECT
    invoice_line.invoice_line_id AS invoice_line_id,
    invoice_line.invoice_id AS invoice_id,
    invoice_line.product_id AS product_id,
    product.name AS productname,
    invoice_line.quantity AS quantity,
    invoice_line.sale_price AS sale_price,
    invoice_line.quantity * invoice_line.sale_price AS total
FROM
    invoice_line
JOIN product ON product.product_id = invoice_line.product_id
WHERE invoice_line.invoice_id = :invoice_id


All modifications, as in the case with the header of the invoice, will be carried out using stored procedures. Here are the query texts for calling stored procedures.

qryAddInvoiceLine:
EXECUTE PROCEDURE sp_add_invoice_line(
  :invoice_id, 
  :product_id, 
  :quantity
)

qryEditInvoiceLine:
EXECUTE PROCEDURE sp_edit_invoice_line(
  :invoice_line_id,
  :quantity
)

qryDeleteInvoiceLine:
EXECUTE PROCEDURE sp_delete_invoice_line(
  :invoice_line_id
)


The form for adding a new record and editing an existing one, as in the case with the header, will not use DBAware. To select the product, we will use the TButtonedEdit component. The button click handler code in the TButtonedEdit component will look like this:

procedure TEditInvoiceLineForm.edtProductRightButtonClick(Sender: TObject);
var
  xSelectForm: TGoodsForm;
begin
  // не позволяем изменять товар в режиме редактирования
  // это можно сделать только при добавлении новой позиции
  if FEditMode = emInvoiceLineEdit then
    Exit;
  xSelectForm := TGoodsForm.Create(Self);
  try
    xSelectForm.Visible := False;
    if xSelectForm.ShowModal = mrOK then
    begin
      FProductId := xSelectForm.qryGoods.FieldByName('PRODUCT_ID')
        .AsInteger;
      edtProduct.Text := xSelectForm.qryGoods.FieldByName('NAME').AsString;
      // в данном случае мы копируем также цену по прайсу
      edtPrice.Text := xSelectForm.qryGoods.FieldByName('PRICE').AsString;
    end;
  finally
    xSelectForm.Free;
  end;
end;


Since we do not use DBAware components, when calling the editing form, we will need to initialize the product code, its name and cost for display.

procedure TInvoiceForm.actEditInvoiceLineExecute(Sender: TObject);
var
  xEditorForm: TEditInvoiceLineForm;
begin
  xEditorForm:= TEditInvoiceLineForm.Create(Self);
  try
    xEditorForm.OnClose := EditInvoiceLineEditorClose;
    xEditorForm.EditMode := emInvoiceLineEdit;
    xEditorForm.Caption := 'Редактирование позиции';
    xEditorForm.InvoiceLineId := qryInvoiceLine.FieldByName('INVOICE_LINE_ID').AsInteger;
    xEditorForm.SetProduct(qryInvoiceLine.FieldByName('PRODUCT_ID').AsInteger,
                       qryInvoiceLine.FieldByName('PRODUCTNAME').AsString,
                       qryInvoiceLine.FieldByName('SALE_PRICE').AsCurrency);
    xEditorForm.Quantity := qryInvoiceLine.FieldByName('QUANTITY').AsInteger;
    xEditorForm.ShowModal;
  finally
    xEditorForm.Free;
  end;
end;


procedure TEditInvoiceLineForm.SetProduct(AProductId: Integer;
  AProductName: string; APrice: Currency);
begin
  FProductId := AProductId;
  edtProduct.Text := AProductName;
  edtPrice.Text := CurrToStr(APrice);
end;


The processing of adding a new position and editing an existing one will be performed in the closing event of the modal form.

procedure TInvoiceForm.EditInvoiceLineEditorClose(Sender: TObject;
  var Action: TCloseAction);
var
  xCustomerId: Integer;
  xEditorForm: TEditInvoiceLineForm;
begin
  xEditorForm := TEditInvoiceLineForm(Sender);
  // если форма закрыта не по нажатию кнопки OK, 
  // то вообще ничего не делаем. Транзакция не стартует.
  if xEditorForm.ModalResult <> mrOK then
  begin
    Action := caFree;
    Exit;
  end;
  // Всё делаем в короткой транзакции
  trWrite.StartTransaction;
  try
    qryEditInvoiceLine.ParamByName('INVOICE_LINE_ID').AsInteger :=
      xEditorForm.InvoiceLineId;
    qryEditInvoiceLine.ParamByName('QUANTITY').AsInteger :=
      xEditorForm.Quantity;
    qryEditInvoiceLine.Execute();
    trWrite.Commit;
    qryInvoice.Refresh;
    qryInvoiceLine.Refresh;
    Action := caFree;
  except
    on E: Exception do
    begin
      if trWrite.Active then
        trWrite.Rollback;
      Application.ShowException(E);
      // Не закрываем окно редактирования. Позволяем пользователю исправить ошибку
      Action := caNone;
    end;
  end;
end;


That's it. I hope this article helped you understand the features of writing an application in Delphi using FireDac components when working with Firebird DBMS.

The finished application is as follows.

screenshot


Questions, comments and suggestions write in the comments, or in PM.

References


Source codes and sample database

Also popular now: