Firebird Devel Guide 30EN
Firebird Devel Guide 30EN
Firebird Devel Guide 30EN
0 Developer's Guide
Release 1.0
Denis Simonov
Author of the written material and creator of the sample project on five
development platforms, originally as a series of magazine articles:
Copyright © 2017 Firebird Project and all contributing authors, under the Public Documentation License Version 1.0.
Please refer to the License Notice .
Abstract
This volume consists of chapters that walk through the development of a simple application for several language platforms,
notably Delphi, Microsoft Entity Framework and MVC.NET (“Model-View-Controller”) for web applications, PHP and
Java with the Spring framework. It is hoped that the work will grow in time, with contributions from authors using other
stacks with Firebird.
Table of Contents
1. About the Firebird Developer's Guide ................................................................................................. 1
About the Author ........................................................................................................................... 1
Translation ............................................................................................................................. 1
. . . and More Translation ....................................................................................................... 1
Acknowledgments .......................................................................................................................... 1
2. The examples.fdb Database ................................................................................................................ 3
Database Creation Script ................................................................................................................ 3
Database Aliases .................................................................................................................... 4
Creating the Database Objects ........................................................................................................ 5
Domains ................................................................................................................................ 5
Primary Tables ....................................................................................................................... 6
Secondary Tables ................................................................................................................... 8
Stored Procedures ................................................................................................................. 10
Roles and Privileges for Users .............................................................................................. 17
Saving and Running the Script ...................................................................................................... 18
Loading Test Data ........................................................................................................................ 19
3. Developing Firebird Applications in Delphi ....................................................................................... 20
Starting a Project ......................................................................................................................... 20
TFDConnection Component .......................................................................................................... 20
Path to the Client Library ............................................................................................................. 20
Developing for Embedded Applications ................................................................................. 21
Connection parameters ................................................................................................................. 21
Connection Parameters in a Configuration File ...................................................................... 23
Connecting to the database ................................................................................................... 24
Working with Transactions ........................................................................................................... 25
TFDTransaction Component ................................................................................................. 25
Datasets ....................................................................................................................................... 28
TFDQuery Component ......................................................................................................... 28
TFDUpdateSQL component .................................................................................................. 31
TFDCommand component ............................................................................................................ 34
Types of Command .............................................................................................................. 34
Creating the Primary Modules ...................................................................................................... 35
The Read-only Transaction ................................................................................................... 37
The Read/Write Transaction ................................................................................................. 38
Configuring the Customer Module for Editing ....................................................................... 39
Implementing the Customer Module ...................................................................................... 40
Using a RETURNING Clause to Acquire an Autoinc Value ................................................... 43
Creating a Secondary Module ....................................................................................................... 43
The Transactions for Invoice Data ........................................................................................ 44
A Filter for the Data ............................................................................................................ 44
Configuring the Module ....................................................................................................... 45
Doing the Work ................................................................................................................... 47
The Invoice Details .............................................................................................................. 51
The Result ................................................................................................................................... 56
Conclusion ................................................................................................................................... 56
Source Code ................................................................................................................................ 57
4. Developing Firebird Applications with Microsoft Entity Framework ................................................... 58
Methods of Interacting with a Database ......................................................................................... 58
iv
Firebird 3.0 Developer's Guide
v
Firebird 3.0 Developer's Guide
vi
List of Figures
2.1. Model of the examples.fdb database ................................................................................................. 3
3.1. TFDConnection property editor ...................................................................................................... 22
3.2. TFDUpdateSQL property editor ..................................................................................................... 32
3.3. TFDUpdateSQL SQL command editor ........................................................................................... 33
3.4. dCustomers datamodule ................................................................................................................. 36
3.5. Customers form, initial view .......................................................................................................... 37
3.6. The Invoice form tab ..................................................................................................................... 45
3.7. The Invoice data module tab .......................................................................................................... 46
3.8. The Customer input form ............................................................................................................... 49
3.9. Screenshot of the sample application .............................................................................................. 56
4.1. Choose data source for testing installation ...................................................................................... 61
4.2. Locate a database .......................................................................................................................... 62
4.3. Test and confirm the connection .................................................................................................... 62
4.4. Solution Explorer—>select NuGet packages ................................................................................... 63
4.5. Select and install packages from NuGet catalogue ........................................................................... 64
4.6. Solution Explorer - Add—>New Item ............................................................................................ 65
4.7. Add New Item wizard - select ADO.NET Entity Data Model ........................................................... 66
4.8. Add New Item wizard - select 'Code First from database' ................................................................ 66
4.9. Add New Item wizard - choose Connection .................................................................................... 67
4.10. Add Connection wizard - Connection properties ............................................................................ 68
4.11. Add Connection wizard - Advanced connection properties ............................................................. 69
4.12. EDM wizard - connection string storage ....................................................................................... 70
4.13. EDM wizard - select tables and views .......................................................................................... 72
4.14. A form for the Customer entity .................................................................................................... 75
4.15. Customer edit form ...................................................................................................................... 82
4.16. Invoice form ................................................................................................................................ 84
4.17. Product form ............................................................................................................................... 92
4.18. The result of the Entity Framework project ................................................................................... 96
5.1. Interaction between M-V-C parts ................................................................................................... 98
5.2. Create the FBMVCExample project ............................................................................................... 99
5.3. Change authentication setting ....................................................................................................... 100
5.4. Disable authentication for now ..................................................................................................... 100
5.5. Select Manage NuGet Packages ................................................................................................... 103
5.6. Select packages for installing ....................................................................................................... 104
5.7. Configuring connection string storage ........................................................................................... 105
5.8. Select Add—>Controller .............................................................................................................. 106
5.9. Creating a controller (1) ............................................................................................................... 106
5.10. Creating a controller (2) ............................................................................................................. 107
5.11. Customer list view ..................................................................................................................... 118
5.12. A customer selected for editing .................................................................................................. 119
6.1. Invoices display ........................................................................................................................... 191
6.2. Invoice editor .............................................................................................................................. 191
7.1. Folder structure for the template-based project .............................................................................. 194
7.2. Restarting the POM from NetBeans .............................................................................................. 199
7.3. Selecting a customer for invoicing ................................................................................................ 266
7.4. Editing an invoice header ............................................................................................................. 267
7.5. Editing an invoice line ................................................................................................................. 268
7.6. Selecting a product for an invoice line .......................................................................................... 269
vii
List of Tables
3.1. TFDConnection component main properties .................................................................................... 22
3.2. TFDTransaction component main properties ................................................................................... 25
3.3. TFDQuery component main properties ........................................................................................... 28
3.4. TFDUpdateSQL component main properties ................................................................................... 33
3.5. TFDCommand component main properties ..................................................................................... 34
5.1. Basic Structure of the MVC Project .............................................................................................. 101
6.1. Comparing the Firebird/InterBase and PDO Drivers ...................................................................... 164
viii
Chapter 1
This volume consists of chapters that walk through the development of a simple application for several language
platforms, notably Delphi, Microsoft Entity Framework and MVC.NET (“Model-View-Controller”) for web
applications, PHP and Java with the Spring framework. It is hoped that the work will grow in time, with contri-
butions from authors using other stacks with Firebird.
Translation
Development of the original Russian version was sponsored by IBSurgeon and Moscow Exchange Bank. A
crowd-funding campaign was launched by the Firebird Foundation in 2017 to fund the translation into English
to provide this document as the foundation for translation by Firebird Project document writers into other lan-
guages.
The campaign suceeded in raising enough to get the process under way.
Acknowledgments
We acknowledge these contributions of sponsors and donors with gratitude and thank you all for stepping up.
1
About the Firebird Developer's Guide
Moscow Exchange is the largest exchange holding in Russia and Eastern Europe, founded on De-
cember 19, 2011, through the consolidation of the MICEX (founded in 1992) and RTS (founded in
1995) exchange groups. Moscow Exchange ranks among the world's top 20 exchanges by trading
in bonds and by the total capitalization of shares traded, as well as among the 10 largest exchange
platforms for trading derivatives.
Technical support and developer of administrator tools for the Firebird DBMS.
Other Donors
Listed below are the names of companies and individuals whose cash contributions covered the costs for trans-
lation into English, editing of the raw, translated text and conversion of the whole into the Firebird Project's
standard DocBook 4 documentation source format.
2
Chapter 2
The applications work with a database based on the model illustrated in this diagram:
Disclaimer
This chapter does not attempt to provide a tutorial about database design or SQL syntax. The model is made
as simple as possible to avoid cluttering the application development techniques with topics about database
modeling and development. We hope some readers might be enlightened by our approach to maintaining inter-
related data using stored procedures. The scripts are all here for you to refer to as you work your way through
the projects.
The requirements for your real-life projects are undoubtedly different from and much more complicated than
those for our example projects.
3
The examples.fdb Database
We will assume that you are working in Windows. Obviously, the formats of path names will differ on other
file systems (Linux, Apple Mac, etc.) but the isql tool works the same on all platforms.
Run isql and enter the following script after the SQL> prompt appears :
Important
The straight single quotes around the user and password arguments are not optional in Firebird 2.5 and lower
versions because, in the CREATE DATABASE syntax, both are strings.
In Firebird 3, the rules changed. User names became identifiers and no longer require single quotes. They can
be made case-sensitive by enclosing the name in DOUBLE quotes, so you need to be aware of how that user
is registered in the security database. Passwords are still strings.
Quotes in the statement are not interchangeable with curly quotes, angle quotes or any other kind of quotes.
The user whose name and password are cited in the CREATE DATABASE statement becomes the owner of the
database and has full access to all metadata objects. It is not essential that SYSDBA be the owner of a database.
Any user can be the owner, which has the same access as SYSDBA in this database.
The actively supported versions of Firebird support the following page sizes: 4096, 8192 and 16384. The page
size of 8192 is good for most cases.
The optional DEFAULT CHARACTER SET clause specifies the default character set for string data types.
Character sets are applied to the CHAR, VARCHAR and BLOB TEXT data types. You can study the list of
available language encodings in an Appendix to the Firebird Language Reference manual. All up-to-date pro-
gramming languages support UTF8 so we choose this encoding.
Now we can exit the isql session by typing the following command:
EXIT;
Database Aliases
Databases are accessed locally and remotely by their physical file path on the server. Before you start to use a
database, it is useful and wise to register an alias for its file path and to use the alias for all connections. It saves
typing and, to some degree, it offers a little extra security from snoopers by obscuring the physical location of
your database file in the connection string.
In Firebird 2.5, the alias of a database is registered in the aliases.conf file as follows:
examples = D:\fbdata\2.5\examples.fdb
4
The examples.fdb Database
In Firebird 3.0, the alias of a database is registered in the databases.conf file. Along with the alias for the
database, some database-level parameters can be configured there: page cache size, the size of RAM for sorting
and several others, e.g.,
examples = D:\fbdata\3.0\examples.fdb
{
DefaultDbCachePages = 16K
TempCacheLimit = 512M
}
Tip
You can use an alias even before the database exists. It is valid to substitute the full file path with the alias in
the CREATE DATABASE statement.
Domains
First, we define some domains that we will use in column definitions.
BOOLEAN Type
In Firebird 3.0, there is a native BOOLEAN type. Some drivers do not support it, due to its relatively recent
appearance in Firebird's SQL lexicon. With that in mind,, our applications will be built on a database that will
work with either Firebird 2.5 or Firebird 3.0.
5
The examples.fdb Database
Important
Before Firebird 3, servers could connect clients to databases that were created under older Firebird versions.
Firebird 3 can connect only to databases that were created on or restored under Firebird 3.
Primary Tables
Now let us proceed to the primary tables. The first will be the CUSTOMER table. We will create a sequence (a
generator) for its primary key and a corresponding trigger for implementing it as an auto-incrementing column.
We will do the same for each of the tables.
SET TERM ^ ;
SET TERM ; ^
6
The examples.fdb Database
Note
• In Firebird 3.0, you can use IDENTITY columns as auto-incremental fields. The script for creating the table
would then be as follows:
• In Firebird 3.0, you need the USAGE privilege to use a sequence (generator) so you will have to add the
following line to the script:
SET TERM ^;
SET TERM ;^
7
The examples.fdb Database
'Price';
Note
In Firebird 3.0, you need to add the command for granting the USAGE privilege for a sequence (generator)
to the script:
Secondary Tables
The script for creating the INVOICE table:
SET TERM ^;
SET TERM ;^
8
The examples.fdb Database
The INVOICE_DATE column is indexed because we will be filtering invoices by date to enable the records to
be selected by a work period that will be application-defined by a start date and an end date.
Note
In Firebird 3.0, you need to add the command for granting the USAGE privilege for a sequence (generator)
to the script:
SET TERM ^;
SET TERM ;^
9
The examples.fdb Database
Note
In Firebird 3.0, you need to add the command for granting the USAGE privilege for a sequence (generator)
to the script:
Stored Procedures
Some parts of the business logic will be implemented by means of stored procedures.
SET TERM ^;
10
The examples.fdb Database
:INVOICE_ID,
:CUSTOMER_ID,
:INVOICE_DATE,
0,
0
);
END
^
SET TERM ;^
Editing an invoice
The procedure for editing an invoice is a bit more complicated. We will include a rule to block further editing
of an invoice once it is paid. We will create an exception that will be raised if an attempt is made to modify
a paid invoice.
UPDATE INVOICE
SET CUSTOMER_ID = :CUSTOMER_ID,
INVOICE_DATE = :INVOICE_DATE
WHERE INVOICE_ID = :INVOICE_ID;
END
^
11
The examples.fdb Database
SET TERM ;^
Note
In Firebird 3.0, the USAGE privilege is required for exceptions so we need to add the following line:
Deleting an invoice
The procedure SP_DELETE_INVOICE procedure checks whether the invoice is paid and raises an exception
if it is:
SET TERM ^ ;
SET TERM ;^
12
The examples.fdb Database
Note
In Firebird 3.0, the USAGE privilege is required for exceptions so we need to add the following line:
Paying an invoice
We will add one more procedure for paying an invoice:
SET TERM ^;
SET TERM ;^
Note
In Firebird 3.0, the USAGE privilege is required for exceptions so we need to add the following line:
13
The examples.fdb Database
SET TERM ^;
SELECT
price
FROM
product
WHERE
product_id = :product_id
INTO :sale_price;
END
^
SET TERM ;^
14
The examples.fdb Database
SET TERM ^;
15
The examples.fdb Database
SET TERM ;^
SET TERM ^;
16
The examples.fdb Database
SET TERM ;^
17
The examples.fdb Database
The user IVAN can assign the MANAGER role to other users.
https://github.com/sim1984/example-db_2_5/archive/1.0.zip
or https://github.com/sim1984/example-db_3_0/archive/1.0.zip
• OR download the ready-made database, complete with sample data. Links are provided at the end of this
chapter.
Warning
Do not split this command!
The argument "localhost:examples" uses an alias in place of the file path. It assumes that an alias named 'exam-
ples' actually exists, of course! The -i switch is an abbreviation of -input and its argument should be the
path to the script file you just saved.
18
The examples.fdb Database
• db_2_5.zip
• or db_3_0.zip
Reminder
A database built by Firebird 2.5 will not be accessible by a Firebird 3 server, nor vice versa. Make sure you
download the correct database for your needs.
19
Chapter 3
Developing Firebird
Applications in Delphi
This chapter will describe the process of developing applications for Firebird databases with the FireDac™ data
access components in the Embarcadero Delphi™ XE5 environment. FireDac™ is a standard set of components
for accessing various databases in Delphi XE3 and higher versions.
Starting a Project
Create a new project using File—>New—>VCL Forms Application - Delphi
Add a new data module using File—>New—>Other and selecting Delphi Projects—>Delphi Files—>Data Mod-
ule in the wizard. This will be the main data module in our project. It will contain some instances of global
access components that must be accessible to all forms that are intended to work with data. TFDConnection is
an example of this kind of component.
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 other FireDac components. The particular type of the database
to which the connection will be established depends on the value of the DriverName property. To access Firebird,
you need to set this property to FB.
For the connection to know exactly which access library it should work with, place the TFBPhysFBDriverLink
component in the main data module. Its VendorLib property enables the path to the client library to be specified
precisely. If it is not specified, the component will attempt to establish a connection via libraries registered in
the system, for example, in system32, which might not be what you want at all.
20
Developing Firebird Applications in Delphi
If you compile a 32-bit application, you should use the 32-bit fbclient.dll library. For a 64-bit application, it
should be the 64-bit library.
Along with the file fbclient.dll, it is advisable to place the following libraries in the same folder: msvcp80
.dll and msvcr80.dll (for Firebird 2.5) as well as msvcp100.dll and msvcr100.dll (for Firebird
3.0). These libraries are located either in the bin subfolder (Firebird 2.5) or in the root folder of the server
(Firebird 3.0).
For the application to show internal firebird errors correctly, it is necessary to copy the file firebird.msg
as well.
• For Firebird 2.5 or earlier, the libraries must be one level up from the folder with the client library, i.e., in
the application folder for our purposes.
• For Firebird 3, they must be in the same folder as the client library, i.e. in the fbclient folder.
It is not necessary to change anything for Firebird 3.0, in which the working mode depends on the connection
string and the value of the Providers parameter in the file firebird.conf/databases.conf.
TIP
Even if your application is intended to work with Firebird in the Embedded mode, it is advisable to attach to
the full server during development. The reason is that embedded Firebird runs in the same address space as the
application and any application connecting to a database in embedded mode must be able to obtain exclusive
access to that database. Once that connection succeeds, no other embedded connections are possible. When you
are connected to your database in the Delphi IDE, the established connection is in Delphi's application space,
thus preventing your application from being run successfully from the IDE.
Note, Firebird 3 embedded still requires exclusive access if the installed full server is in Super (Superserver)
mode.
Connection parameters
The Params property of the TFDConnection component contains the database connection parameters (username,
password, connection character set, etc.). If you invoke the TFDConnection property editor by double-clicking
on the component, you will see that those properties have been filled automatically. The property set depends
on the database type.
21
Developing Firebird Applications in Delphi
Property Purpose
Pooled Whether a connection pool is used
The path to the database or its alias as defined in the aliases.conf configura-
Database
tion file (or in databases.conf) of the Firebird server
User_Name Firebird user name. Not used if OSAuthent is True.
Password Firebird password. Not used if OSAuthent is True.
OSAuthent Whether operating system authentication is used
22
Developing Firebird Applications in Delphi
Property Purpose
Connection protocol. Possible values:
• Local—local protocol
Protocol • NetBEUI—named pipes, WNET
SPX—This property is for Novell's IPX/SPX protocol, which has never
•
been supported in Firebird
• TCPIP—TCP/IP
Server name or its IP address. If the server is run on a non-standard port, you
Server
also need to append the port number after a slash, e.g., localhost/3051
SQLDialect SQL Dialect. It must match that of the database
RoleName Role name, if required
CharacterSet Connection character set name
Additional Properties:
Used to manage the database connection or check the connection status. This
property must be set to True in order for the wizards of other FireDac com-
Connected ponents to work. If your application needs to request authentication data, it is
important to remember to reset this property to False before compiling your
application.
LoginPrompt Whether to request the username and password during a connection attempt
The TFDTransaction component that will be used as default to conduct vari-
ous TFDConnection transactions. If this property is not explicitly specified,
Transaction
TFDConnection will create its own TFDTransaction instance. Its parameters
can be configured in the TxOptions property.
The TFDTransaction component that is to be used as default for the Update-
Transaction property of TFDQuery components, unless explicitly specified
UpdateTransaction for the dataset. If this property is not specified explicitly, the value from the
Transaction property of the connection will be used, unless it is explicitly
specified for the dataset.
23
Developing Firebird Applications in Delphi
[connection]
DriverID=FB
Protocol=TCPIP
Server=localhost/3051
Database=examples
OSAuthent=No
RoleName=
CharacterSet=UTF8
You can get the contents of the connection section by copying the contents of the Params property of the TFD-
Connection component after the wizard finishes its work.
Note
Actually, the common settings are usually located in %AppData%\Manufacturer\AppName and are saved
to that location by the application installation software. However, it is convenient for the configuration file to
be stored somewhere closer during the development, for instance, in the application folder.
Note that if your application is installed into the Program Files folder and the configuration file is located there
as well, it is likely that the file will be virtualized in Program Data and issues could arise with modifying it
and reading the new settings subsequently.
A Little Modification
We will replace the standard database connection dialog box in our application and allow users to make three
mistakes while entering the authentication information. After three failures, the application will be closed.
To implement it, we will write the following code in the OnCreate event handler of the main data module.
24
Developing Firebird Applications in Delphi
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
TFDTransaction has three methods for managing a transaction explicitly: StartTransaction, Commit and Rollback.
The following table summarises the properties available to configure this component.
Property Purpose
Connection Reference to the FDConnection component
Controls the automatic start and end of a transaction, emulating Firebird's
Options.AutoCommit own transaction management. The default value is True. See note (1) below
for more details about behaviour if the Autocommit option is True.
Options.AutoStart Controls the automatic start of a transaction. The default value is True.
Options.AutoStop Controls the automatic end of a transaction. The default value is True.
Options.DisconnectAction
25
Developing Firebird Applications in Delphi
Property Purpose
The action that will be performed when the connection is closed while the
transaction is active. The default value is xdCommit—the transaction will be
committed. See note (2) below for details of the other options.
Controls nested transactions. The default value is True. Firebird does not
Options.EnableNested support nested transactions as such but FireDac can emulate them using
savepoints. For more details, see note(3) below.
Specifies the transaction isolation level. It is the most important transaction
property. The default value is xiReadCommitted. The other values that Fire-
Options.Isolation bird supports are xiSnapshot and xiUnspecified; also xiSerializable, to some
degree. For more details about the available isolation levels, see note (4) be-
low.
Firebird-specific transaction attributes that can be applied to refine the trans-
action parameters, overriding attributes applied by the standard implementa-
Options.Params
tion of the selected isolation level. For the attributes that can be set and the
“legal” combinations, see note (5) below.
Indicates whether it is a read-only transaction. The default value is False.
Setting it to True disables any write activity. Long-running read-only trans-
Options.ReadOnly actions in READ COMMITTED isolation are recommended for activities
that do not change anything in the database because they use fewer resources
and do not interfere with garbage collection.
Note 1: AutoCommit=True
• Starts a transaction (if required) before each SQL command and ends the transaction after the SQL command
completes execution
• If the command is successfully executed, the transaction will be ended by COMMIT. Otherwise, it will be
ended by ROLLBACK.
• If the application calls the StartTransaction method, automatic transaction management will be disabled
until that transaction is ended by Commit or Rollback.
Note 2: DisconnectAction
• xdNone—nothing will be done. The DBMS will perform its default action.
• xdCommit—the transaction will be committed
• xdRollback—the transaction will be rolled back
Note that, in some other data access components, the default value for the DisconnectAction property is xdRoll-
back and will need to be set manually with Firebird to match the FDTransaction setting.
Note 3: EnableNested
If StartTransaction is called from within an active transaction, FireDac will emulate a nested transaction by
creating a savepoint. Unless you are very confident in the effect of enabling nested transactions, set EnableN-
ested to False. With this setting, calling StartTransaction inside the transaction will raise an exception.
26
Developing Firebird Applications in Delphi
Note 4: Isolation
FireBird has three isolation levels: READ COMMITTED, SNAPSHOT (“concurrency”) and SNAPSHOT TA-
BLE STABILITY (“consistency”, rarely used). FireDac supports some but not all configurations for READ
COMMITTED and SNAPSHOT. It uses the third level partially to emulate the SERIALIZABLE isolation that
Firebird does not support.
• xiSnapshot—the SNAPSHOT (concurrency) isolation level. FireDac starts Snapshot transactions in Firebird
with the following parameters: read/write, wait
• xiUnspecified—Firebird's default isolation level (SNAPSHOT) with the following parameters: read/write,
wait
• xiSerializable—the SERIALIZABLE isolation level. Firebird does not support serializable isolation, but
FireDac emulates it by starting a SNAPSHOT TABLE STABILITY (“consistency”) transaction with the
following parameters: read/write, wait.
Other parameters, not supported by Firebird at all, are:
• xiDirtyRead—if this is selected (not a good idea!) READ COMMITTED will be used instead
• read write, the default read mode for all of the options.isolation selections—see note (4) above. Set write
off if you want read-only mode. Alternatively, you can set Options.ReadOnly to True to achieve the same
thing. There is no such thing as a “write-only” transaction.
• wait and nowait are conflict resolution settings, determining whether the transaction is to wait for a conflict
to resolve
• rec_version and no rec_version provide an option that is applicable only to READ COMMITTED transac-
tions. The default rec_version lets this transaction read the latest committed version of a record and over-
write it if the transaction ID of the latest committed version is newer (higher) than the ID of this transaction.
The no rec_version setting will block this transaction from reading the latest committed version if an update
is pending from any other transaction.
Multiple Transactions
Unlike many other DBMSs, Firebird allows as many TFDTransaction objects as you need to associate with the
same connection. In our application, we will use one common read transaction for all primary and secondary
modules and one read/write transaction for each dataset.
We do not want to rely on starting and ending transactions automatically: we want to have full control. That
is why Options.AutoCommit=False, Options.AutoStart=False and Options.AutoStop=False are set in all of our
transactions.
27
Developing Firebird Applications in Delphi
Datasets
The components TFDQuery, TFDTable, TFDStoredProc and TFDCommand are the components for working with
data in FireDac. TFDCommand does not deliver a dataset and, when TFDStoredProc is used with an executable
stored procedure, rather than a selectable one, it does not deliver a dataset, either.
Apart from datasets for working with the database directly, FireDac also has the TFDMemTable component for
working with in-memory datasets. It is functionally equivalent to TClientDataSet.
The main component for working with datasets, TFDQuery, can be used for practically any purpose. The
TFDTable and TFDStoredProc components are just variants, expanded or reduced to meet differences in func-
tionality. No more will be said about them and we will not be using them in our application. If you wish, you
can learn about them in the FireDac documentation.
The purpose of a dataset component is to buffer records retrieved by the SELECT statement, commonly for
displaying in a grid and providing for the current record in the buffer (grid) to be editable. Unlike the IBX
TIBDataSet component, TFDQuery component does not have the properties RefreshSQL, InsertSQL, UpdateSQL
and DeleteSQL. Instead, a separate TFDUpdateSQL object specifies the statement for dataset modifications and
the dataset component carries a reference to that component in its UpdateObject property.
RequestLive Property
Sometimes it is possible to make an FDQuery object editable without referring, through the UpdateOb-
ject property, to an FDUpdateSQL object that specifies queries for insert, update and delete. The property
UpdateOptions.RequestLive can be set to True for sets that are naturally updatable and the object will generate
the modification queries for you. However, because this approach puts strict limitations on the SELECT query,
it is not always useful to rely on it.
TFDQuery Component
Table 3.3. TFDQuery component main properties
Property Purpose
Connection Reference to the FDConnection object
If the dataset is to be used as detail to a master dataset, this proper-
MasterSource
ty refers to the data source (TDataSource) of the master set
If specified, refers to the transaction within which the query will be
Transaction executed. If not specified, the default transaction for the connec-
tion will be used.
Reference to the FDUpdateSQL object providing for the dataset
to be editable when the SELECT query does not meet the re-
UpdateObject
quirements for automatic generation of modification queries with
UpdateOptions.RequestLive=True.
UpdateTransaction
28
Developing Firebird Applications in Delphi
Property Purpose
The transaction within which modification queries will be execut-
ed. If the property is not specified the transaction from the Trans-
action property of the connection will be used.
If set to True (the default) FireDac controls the Required property
of the corresponding NOT NULL fields. If you keep it True and a
field with the Required=True has no value assigned to it, an excep-
UpdateOptions.CheckRequired
tion will be raised when the Post method is called. This might not
be what you want if a value is going to be assigned to this field lat-
er in BEFORE triggers.
Specifies whether a record can be deleted from the dataset. If
UpdateOptions.EnableDelete EnableDelete=False, an exception will be raised when the Delete
method is called.
Specifies whether a record can be inserted into the dataset. If
UpdateOptions.EnableInsert EnableInsert=False, an exception will be raised when the In-
sert/Append method is called.
Specifies whether a record can be inserted into the dataset. If
UpdateOptions.EnableInsert EnableInsert=False, an exception will be raised when the In-
sert/Append method is called.
Specifies whether a record can be edited in the dataset. If
UpdateOptions.EnableUpdate EnableUpdate=False, an exception will be raised when the Edit
method is called.
Controls the moment when the next value is fetched from the gen-
erator specified in the UpdateOptions.GeneratorName property
or in the GeneratorName property of the auto-incremental field
UpdateOptions.FetchGeneratorPoint AutoGenerateValue=arAutoInc. The default is gpDeferred, caus-
ing the next value to be fetched from the generator before a new
record is posted in the database, i.e., during Post or ApplyUpdates.
For the full set of possible values, see note (1) below.
The name of the generator from which the next value for an au-
UpdateOptions.GeneratorName
to-incremental field is to be fetched.
Specifies whether it is a read-only dataset. The default value is
False. If the value of this property is set to True, the EnableDelete,
UpdateOptions.ReadOnly
EnableInsert and EnableUpdate properties will be automatically
set to False.
Setting RequestLive to True makes a query editable, if possible.
Queries for insert, update and delete will be generated automatical-
UpdateOptions.RequestLive ly. This setting imposes strict limitations on the SELECT query. It
is supported for backward compatibility with the ancient BDE and
is not recommended.
Controls how to check whether a record has been modified. This
property allows control over possible overwriting of updates in
UpdateOptions.UpdateMode
cases where one user is taking a long time to edit a record while
another user has been editing the same record simultaneously and
29
Developing Firebird Applications in Delphi
Property Purpose
completes the update earlier. The default is upWhereKeyOnly. For
information about the available modes, see note (2) below.
Specifies whether the dataset cache defers changes in the dataset
buffer. If this property is set to True, any changes (Insert/Post, Up-
date/Post, Delete) are saved to a special log and the application
CachedUpdates
must apply them explicitly by calling the ApplyUpdates method.
All changes will be made within a small period of time and within
one short transaction. The default value of this property is False.
Contains the text of the SQL query. If this property is a SELECT
statement, execute it by calling the Open methold. Use the Exe-
SQL
cute or ExecSQL for executing a statement that does not return a
dataset.
Note 1: UpdateOptions.FetchGeneratorPoint
30
Developing Firebird Applications in Delphi
Note 2: UpdateOptions.UpdateMode
The user in a lengthy editing session could be unaware that a record has been updated one or more times
during his editing session, perhaps causing his own changes to overwrite someone else's updates. The
UpdateOptions.UpdateMode property allows a choice of behaviours to lessen or avoid this risk:
• upWhereAll—check whether a record exists by its primary key + check all columns for old values, e.g.,
With upWhereAll set, the update query will change content in a record only if the record has not been edited
by anyone else since our transaction started. It is especially important if there are dependencies between
values in columns, such as minimum and maximum wages, etc.
• upWhereChanged—check whether a record exists by its primary key + check for old values only in the
columns being edited.
• upWhereKeyOnly—check whether a record exists by its primary key. This check corresponds to the auto-
matically generated UpdateSQL query.
To avoid (or handle) update conflicts in a multi-user environment, typically you need to add WHERE con-
ditions manually. You would need a similar tactic, of course, to implement a process that emulates up-
WhereChanged, removing the unused column modifications from the update table set, leaving in the update
list only the columns that are actually modified. The update query could otherwise overwrite someone else's
updates of this record.
If you want to specify the settings for detecting update conflicts individually for each field, you can use the
ProviderFlags property for each field.
TFDUpdateSQL component
The TFDUpdateSQL component enables you to refine or redefine the SQL command that Delphi generates
automatically for updating a dataset. It can be used to update an FDQuery object, an FDTable object or data
underlying an FDStoredProc object.
Using TFDUpdateSQL is optional for TFDQuery and TFDTable because these components can generate state-
ments automatically, that can sometimes be used for posting updates from a dataset to the database. For updating
a dataset that is delivered into an FDStoredProc object, use of the TFDUpdateSQL is not optional. The devel-
oper must figure out a statement that will result in the desired updates. If only one table is updated, a direct
DML statement might be sufficient. Where multiple tables are affected, an executable stored procedure will be
unavoidable.
We recommend that you always use it, even in the simplest cases, to give yourself full control over the queries
that are requested from your application.
31
Developing Firebird Applications in Delphi
TFDUpdateSQL Properties
To specify the SQL DML statements at design time, double-click on the TFDUpdateSQL component in your
data module to open the property editor.
Important
Each component has its own design-time property editor. For multiple data-aware editors to run, FireDac needs
an active connection to the database (TFDConnection.Connected = True) and a transaction in the autostart mode
(TFDTransaction.Options.AutoStart = True) for each one.
Design-time settings could interfere with the way the application is intended to work. For instance, the user is
supposed to log in to the program using his username, but the FDConnection object connects to the database
as SYSDBA.
It is advisable to check the Connected property of the FDConnection object and reset it each time you use the
data-aware editors. AutoStart will have to be enabled and disabled for a a read-only transaction as well.
You can use the Generate tab to make writing Insert/Update/Delete/Refresh queries easier for yourself. Select
the table to be updated, its key fields, the fields to be updated and the fields that will be reread after the update
and click the Generate SQL button to have Delphi generate the queries automatically. You will be switched to
the SQL Commands tab where you can correct each query.
32
Developing Firebird Applications in Delphi
Note
Since product_id is not included in Updating Fields, it is absent from the generated Insert query. It is assumed
that this column is filled automatically by a generator call in a BEFORE INSERT trigger or, from Firebird
3.0 forward, it could be an IDENTITY column. When a value is fetched from the generator for this column
at the server side, it is recommended to add the PRODUCT_ID column manually to the RETURNING clause
of the INSERT statement.
The Options tab contains some properties that can affect the process of query generation. These properties are
not related to the TFDUpdateSQL component itself. Rather, for convenience, they are references to the Upda-
teOptions properties of the dataset that has the current TFDUpdateSQL specified in its UpdateObject property.
Property Purpose
Connection Reference to the TFDConnection component
DeleteSQL The SQL query for deleting a record
33
Developing Firebird Applications in Delphi
Property Purpose
The SQL query for returning a current record after it has been updated or in-
FetchRowSQL
serted—“RefreshSQL”
InsertSQL The SQL query for inserting a record
LockSQL The SQL query for locking a current record. (FOR UPDATE WITH LOCK)
ModifySQL The SQL query for modifying a record
UnlockSQL The SQL query for unlocking a current record. It is not used in Firebird.
Notice that, because the TFDUpdateSQL component does not execute modification queries directly, it has no
Transaction property. It acts as a replacement for queries automatically generated in the parent TFDRdbms-
DataSet.
TFDCommand component
The TFDCommand component is used to execute SQL queries. It is not descended from TDataSet so it is valid
to use only for executing SQL queries that do not return datasets.
Property Purpose
Connection Reference to the TFDConnection component
Transaction The transaction within which the SQL command will be executed
CommandKind Type of command. The types are described in the section below.
CommandText SQL query text
Types of Command
Usually, the command type is determined automatically from the text of the SQL statement. The following
values are available for the property TFDCommand.CommandKind to cater for cases where the internal parser
might be unable to make correct, unambiguous assumptions based on the statement text alone:
• skUnknown—unknown. Tells the internal parser to determine the command type automatically from its anal-
ysis of the text of the command
34
Developing Firebird Applications in Delphi
• skSelectForLock—a SELECT … WITH LOCK command for locking the selected rows
As our model for creating datasets, we will create the Customer dataset on the dCustomers datamodule:
35
Developing Firebird Applications in Delphi
On tabbing to the Customers form, this is the initial view. The DataSource component is not visible on the form
because it is located in the dCustomers datamodule.
36
Developing Firebird Applications in Delphi
We have placed the TFDQuery component in the dCustomers datamodule and named it qryCustomers. This
dataset will be referred to in the DataSet property of the DataSource data source in DCustomers. We specify
the read-only transaction trRead in the Transaction property, the trWritetransaction in the UpdateTransaction
property and, for the Connection property, the connection located in the main data module. We populate the
SQL property with the following query:
SELECT
customer_id,
name,
address,
zipcode,
phone
FROM
customer
ORDER BY name
37
Developing Firebird Applications in Delphi
Since this transaction is used only to read data, we set the Options.ReadOnly property to True. Thus, our trans-
action will have the following parameters: read read_committed rec_version.
Why?
A transaction with exactly these parameters can remain open in Firebird as long as necessary (days, weeks,
months) without locking other transactions or affecting the accumulation of garbage in the database because,
with these parameters, a transaction is started on the server as committed.
We set the property Options.DisconnectAction to xdCommit, which perfectly fits a read-only transaction. Finally,
the read transaction will have the following properties:
Options.AutoStart = False
Options.AutoCommit = False
Options.AutoStop = False
Options.DisconnectAction = xdCommit
Options.Isolations = xiReadCommitted
Options.ReadOnly = True
Important
Although we do not discuss reporting in this manual, be aware that you should not use such a transaction for
reports, especially if they use several queries in sequence. A transaction with READ COMMITTED isolation
will see all new committed changes when rereading data. The recommended configuration for reports is a
short read-only transaction with SNAPSHOT isolation (Options.Isolation = xiSnapshot and Options.ReadOnly=
True).
Options.AutoStart = False
Options.AutoCommit = False
Options.AutoStop = False
Options.DisconnectAction = xdRollback
Options.Isolations = xiSnapshot
Options.ReadOnly = False
38
Developing Firebird Applications in Delphi
DATE/DELETE query, it is advisable to use SNAPSHOT. The reason is that READ COMMITTED isolation
does not ensure the read consistency of the statement within one transaction, since the SELECT statement in this
isolation can return data that were committed to the database after the transaction began. In principle, SNAP-
SHOT isolation is recommended for short-running transactions.
InsertSQL
ModifySQL
UPDATE customer
SET name = :new_name,
address = :new_address,
zipcode = :new_zipcode,
phone = :new_phone
WHERE (customer_id = :old_customer_id)
DeleteSQL
39
Developing Firebird Applications in Delphi
FetchRowSQL
SELECT
customer_id,
name,
address,
zipcode,
phone
FROM
customer
WHERE customer_id = :old_customer_id
UpdateOptions.GeneratorName = GEN_CUSTOMER_ID
and
UpdateOptions.AutoIncFields = CUSTOMER_ID
Note
This method works only for autoinc fields that are populated by explicit generators (sequences). It is not appli-
cable to the IDENTITY type of autoinc key introduced in Firebird 3.0.
Another way to get the value from the generator is to return it after the INSERT is executed by means of a
RETURNING clause. This method, which works for IDENTITY fields as well, will be shown later, in the topic
Using a RETURNING Clause to Acquire an Autoinc Value.
The only way to switch the dataset to Insert/Edit mode is by starting a write transaction. So, if somebody opens
a form for adding a new record and leaves for a lunch break, we will have an active transaction hanging until the
user comes back from lunch and closes the form. This uncommitted edit can inhibit garbage collection, which
will reduce performance. There are two ways to solve this problem:
40
Developing Firebird Applications in Delphi
1. Use the CachedUpdates mode, which enables the transaction to be active just for a very short period (to
be exact, just for the time it takes for the changes to be applied to the database).
2. Give up using visual components that are data-aware. This approach requires some additional effort from
you to activate the data source and pass user input to it.
We will show how both methods are implemented. The first method is much more convenient to use. Let's
examine the code for editing a customer record:
We set the CachedUpdates mode for the dataset in the Edit method of the dCustomers module before switching
it to the edit mode:
procedure TdmCustomers.Edit;
begin
qryCustomer.CachedUpdates := True;
qryCustomer.Edit;
end;
The logic of handling the process of editing and adding a record is implemented in the OnClose event handler
for the modal edit form:
41
Developing Firebird Applications in Delphi
Customers.Post;
Customers.Save;
Action := caFree;
except
on E: Exception do
begin
Application.ShowException(E);
// It does not close the window give the user correct the error
Action := caNone;
end;
end;
end;
To understand the internal processes, we can study the code for the Cancel, Post and Save methods of the
dCustomer data module:
procedure TdmCustomers.Cancel;
begin
qryCustomer.Cancel;
qryCustomer.CancelUpdates;
qryCustomer.CachedUpdates := False;
end;
procedure TdmCustomers.Post;
begin
qryCustomer.Post;
end;
procedure TdmCustomers.Save;
begin
// We do everything in a short transaction
// In CachedUpdates mode an error does not interrupt the running code.
// The ApplyUpdates method returns the number of errors.
// The error can be obtained from the property RowError
try
trWrite.StartTransaction;
if (qryCustomer.ApplyUpdates = 0) then
begin
qryCustomer.CommitUpdates;
trWrite.Commit;
end
else
raise Exception.Create(qryCustomer.RowError.Message);
qryCustomer.CachedUpdates := False;
except
on E: Exception do
begin
if trWrite.Active then
trWrite.Rollback;
raise;
end;
end;
end;
Observe that the write transaction is not started at all until the OK button is clicked. Thus, the write transaction is
active only while the data are being transferred from the dataset buffer to the database. Since we access not more
than one record in the buffer, the transaction will be active for a very short time, which is exactly what we want.
42
Developing Firebird Applications in Delphi
SELECT
product_id,
name,
price,
description
FROM product
ORDER BY name
The RETURNING clause in this statement will return the value of the PRODUCT_ID field after it has been
populated by the BEFORE INSERT trigger. The client side in this case has no need to know the name of the
generator, since it all happens on the server. Leave the UpdateOptions.GeneratorName property as nil.
To acquire the autoinc value by this method also requires filling a couple of properties for the PRODUCT_ID
field because the value is being entered indirectly:
Required = False
and
ReadOnly = True
Everything else is set up similarly to the way it was done for the Customer module.
An invoice consists of a header where some general attributes are described (number, date, customer …) and
invoice lines with the list of products, their quantities, prices, etc. It is convenient to have two grids for such
documents: the main one (master) showing the data invoice header data and the detail one showing the invoice
lines.
We want to place two TDBGrid components on the invoice form and link a separate TDataSource to each of them
that will be linked to its respective TFDQuery. In our project, the dataset with the invoice headers (the master
set) will be called qryInvoice, and the one with the invoice lines (the detail set) will be called qryInvoiceLine.
43
Developing Firebird Applications in Delphi
Since the application could have more than one secondary dataset, it makes sense to add variables containing
the start and end dates of a work period to the global dmMain data module that is used by all modules working
with the database in one way or another. Once the application is started, the work period could be defined by the
start and end dates of the current quarter, or some other appropriate start/end date pair. The application could
allow the user to change the work period while working with the application.
44
Developing Firebird Applications in Delphi
45
Developing Firebird Applications in Delphi
Since the latest invoices are the most requested ones, it makes sense to sort them by date in reverse order. The
query will look like this in the SQL property of the qryInvoice dataset:
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
46
Developing Firebird Applications in Delphi
qryInvoice.ParamByName('date_begin').AsSqlTimeStamp := dmMain.BeginDateSt;
qryInvoice.ParamByName('date_end').AsSqlTimeStamp := dmMain.EndDateSt;
qryInvoice.Open;
For the purpose of illustration, we will use stored procedures to perform all operations on an invoice. Regular
INSERT/UPDATE/DELETE queries can be used when operations are simple and involve writing to only one
table in the database. We will execute each stored procedure as a separate query in TFDCommand objects. This
component is not descended from TFDRdbmsDataSet, does not buffer data and returns not more than one result
row. We are using it because it consumes fewer resources for queries that do not return data.
Since our stored procedures modify data, it is necessary to point the Transaction property of each TFDCommand
object to the trWrite transaction.
Tip
Another alternative is to place the stored procedure calls for inserting, editing and adding a record in the cor-
responding properties of a TFDUpdateSQL object.
qryAddInvoice.CommandText
qryEditInvoice.CommandText
47
Developing Firebird Applications in Delphi
qryDeleteInvoice.CommandText
qryPayForInvoice.CommandText
Since our stored procedures are not called from a TFDUpdateSQL object, we need to call qryInvoice.Refresh
after they are executed, in order to update the data in the grid.
Stored procedures that do not require input data from the user are called as follows:
procedure TdmInvoice.DeleteInvoice;
begin
// We do everything in a short transaction
trWrite.StartTransaction;
try
qryDeleteInvoice.ParamByName('INVOICE_ID').AsInteger :=
Invoice.INVOICE_ID.Value;
qryDeleteInvoice.Execute;
trWrite.Commit;
qryInvoice.Refresh;
except
on E: Exception do
begin
if trWrite.Active then
trWrite.Rollback;
raise;
end;
end;
end;
48
Developing Firebird Applications in Delphi
end;
end;
As the window for selecting a customer, we will use the same modal form that was created for adding customers.
The code for the button click handler for the TButtonedEdit component is as follows:
Since we are not using data-aware visual components, we need to initialize the customer code and name for
displaying during the call to the edit form:
49
Developing Firebird Applications in Delphi
Adding a new invoice and editing an existing one will be handled in the Close event of the modal form as it is
for the primary modules. However, we will not switch the dataset to CachedUpdates mode for these because the
updates carried out by stored procedures and we are not using data-aware visual components to capture input.
50
Developing Firebird Applications in Delphi
end;
try
Invoices.AddInvoice(xEditorForm.CustomerId, xEditorForm.InvoiceDate);
Action := caFree;
except
on E: Exception do
begin
Application.ShowException(E);
// It does not close the window give the user correct the error
Action := caNone;
end;
end;
end;
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
51
Developing Firebird Applications in Delphi
As with the invoice header, we will use stored procedures to perform all modifications. Let's examine the query
strings in the CommandText property of the commands that call the stored procedures.
qryAddInvoiceLine.CommandText
qryEditInvoiceLine.CommandText
qryDeleteInvoiceLine.CommandText
As with the header, the form for adding a new record and editing an existing one does not use data-aware visual
components. To select a product, we use the TButtonedEdit component again. The code for the on-click handler
for the button on the TButtonedEdit object is as follows:
52
Developing Firebird Applications in Delphi
Since we are not using data-aware visual components, again we will need to initialize the product code and name
and its price for displaying on the edit form.
We handle adding a new item and editing an existing one in the Close event of the modal form.
53
Developing Firebird Applications in Delphi
xEditorForm: TEditInvoiceLineForm;
begin
xEditorForm := TEditInvoiceLineForm.Create(Self);
try
xEditorForm.EditMode := emInvoiceLineEdit;
xEditorForm.OnClose := EditInvoiceLineEditorClose;
xEditorForm.Caption := 'Edit invoice line';
xEditorForm.InvoiceLineId := Invoices.InvoiceLine.INVOICE_LINE_ID.Value;
xEditorForm.SetProduct(
Invoices.InvoiceLine.PRODUCT_ID.Value,
Invoices.InvoiceLine.PRODUCTNAME.Value,
Invoices.InvoiceLine.SALE_PRICE.AsCurrency);
xEditorForm.Quantity := Invoices.InvoiceLine.QUANTITY.Value;
xEditorForm.ShowModal;
finally
xEditorForm.Free;
end;
end;
54
Developing Firebird Applications in Delphi
Action := caFree;
except
on E: Exception do
begin
Application.ShowException(E);
// It does not close the window give the user correct the error
Action := caNone;
end;
end;
end;
Now let's take a look at the code for the AddInvoiceLine and EditInvoiceLine procedures of the dmInvoice
data module:
55
Developing Firebird Applications in Delphi
raise;
end;
end;
end;
The Result
Conclusion
FireDac™ is a standard set of data-access and data-aware visual components for developing with various
database systems, including Firebird, starting from Delphi™ XE3. FireDac™ ships with the higher-end versions
of Delphi. Many independent sets of data access and data-aware visual components are available for working
56
Developing Firebird Applications in Delphi
with Firebird, some commercial, others distributed under a variety of licences, including open source and free-
ware. They include FibPlus, IBObjects, UIB, UniDAC, IBDac, Interbase Express (IBX) and more. The princi-
ples for developing Firebird applications in Delphi™ are the same, regardless of the components you choose.
All queries to a database are executed within a transaction. To guarantee that applications will work correctly
and efficiently with Firebird databases, it is advisable to manage transactions manually, by explicit calls to the
StartTransaction, Commit and Rollback methods of the TFDTransaction component. Transactions should be as
short as possible and you can use as many as the logic of your application requires.
The recommended configuration for a long-running, read-only transaction to view datasets is to use
READ_COMMITTED isolation with REC_VERSION for conflict resolution. An application can run many
datasets in one such transaction or one for each dataset, according to the requirements of the design.
To avoid holding an uncommitted transaction during an editing session, either use visual components that are
not data-aware or use the CachedUpdates mode. With CachedUpdates you can restrict writes to short bursts
of activity, keeping the read/write transaction active only for as long as it takes to post the most recent changes
to the database.
The TFDUpdateSQL component is necessary for editing most datasets. Update queries are governed by its In-
sertSQL, ModifySQL, DeleteSQL and FetchRowSQL properties. The queries for those properties can be gener-
ated automatically by a wizard but manual corrections or adjustments are often required.
Acquiring values for auto-incrementing primary keys can be handled in one of two ways:
• Getting the value from the generator beforehand by specifying the UpdateOptions.GeneratorName and
UpdateOptions.AutoIncFields properties for the TFDQuery component. This method cannot be used for au-
to-incrementing fields of the IDENTITY type that was introduced in Firebird 3.
• Getting the value by adding a RETURNING clause to the InsertSQL query. For this method you need to
specify Required=False and ReadOnly=True for the field because the value is not entered directly.
It is convenient and sometimes necessary to implement more complex business logic with stored procedures.
Using the TFDCommand component to execute stored procedures that do not return data reduces resource con-
sumption
.
Source Code
ObjectPascal source code for the sample project is available for download using the following link:
FireDacEx.zip.
For links to the database scripts and ready-to-use databases, refer to the final sections of the database chapter.
57
Chapter 4
Developing Firebird
Applications with Microsoft
Entity Framework
This chapter will describe the process of creating applications with a Firebird database using the Microsoft™
Entity Framework™ access components in the Visual Studio 2015™ environment.
ADO.NET Entity Framework (EF) combines an object-oriented data access technology with an object-relational
mapping (ORM) solution for the Microsoft .NET Framework. It enables interaction with objects by means of
both LINQ in the form of LINQ to Entities and with Entity SQL.
Database first:
Entity Framework creates a set of classes that reflect the model of an existing database.
Model first:
the developer creates a database model that Entity Framework later uses to create an actual database on the
server.
Code first:
the developer creates a class for the model of the data that will be stored in a database and then Entity
Framework uses this model to generate the database and its tables
Our sample application will use the Code first approach, but you could use one of the others just as easily.
Note
As we already have a database, we will just write the code that would result in creating that database.
• FirebirdSql.Data.FirebirdClient.dll
58
Developing Firebird Applications with Microsoft Entity Framework
• EntityFramework.Firebird.dll
There is nothing difficult in installing the first two. They are currently distributed and installed into a project
by means of the NuGet package manager. The DDEX Provider library, designed for operating Visual Studio
wizards, is not so easy to install and may take more time and effort.
Efforts have been made to automate the installation process and include all components in a single installer
package. However, you might need to install all of the components manually under some conditions. If so, you
can download the following:
• FirebirdSql.Data.FirebirdClient-4.10.0.0.msi
• EntityFramework.Firebird-4.10.0.0-NET45.7z
• DDEXProvider-3.0.2.0.7z
• DDEXProvider-3.0.2.0-src.7z
Because the installation involves operations in protected directories, you will need administrator privileges to
do it.
Steps
1. Install FirebirdSql.Data.FirebirdClient-4.10.0.0.msi
3. You need to install a Firebird build into the GAC. For your convenience, specify the path to the gacutil utility
for .NET Framework 4.5 in the environment variable %PATH%. In my case, the path is c:\Program
Files (x86)\Microsoft SDKs\Windows\v10.0A\bin\NETFX 4.6.1 Tools\
4. Run the command shell cmd.exe as administrator and go to the directory with the installed client, e.g.,
5. Now make sure that FirebirdSql.Data.FirebirdClient is installed into the GAC by typing the
following command:
gacutil /l FirebirdSql.Data.FirebirdClient
If FirebirdSql.Data.FirebirdClient has not been installed into the GAC, use the following command to do
it now:
gacutil /i FirebirdSql.Data.FirebirdClient.dll
59
Developing Firebird Applications with Microsoft Entity Framework
gacutil /i EntityFramework.Firebird.dll
7. Unpack DDEXProvider-3.0.2.0.7z to a directory convenient for you. Mine was unpacked to c:\Program
Files (x86)\FirebirdDDEX\.
8. Unpack the contents of the /reg_files/VS2015 subdirectory from the archive DDEXProvider-3.
0.2.0-src.7z there as well.
Author's remark
For some strange reason these files are absent from the archive with the compiled dll libraries, but they
are present in the source code archive.
9. Open the FirebirdDDEXProvider64.reg file in Notepad. Find the line that contains %path% and
change it to the full path to the file FirebirdSql.VisualStudio.DataTools.dll, e.g.,
10. Save this Registry file and run it. Click YES to the question about adding the information to the Registry.
11. Now you need to edit the machine.config file. In my installation, the path is as follows:
C:\Windows\Microsoft.NET\Framework\v4.0.30319\Config
<system.data>
<DbProviderFactories>
60
Developing Firebird Applications with Microsoft Entity Framework
Note
The settings we have configured here are valid for version 4.10.0.
To make sure that everything has been installed successfully, start Visual Studio 2015. Find the Server Explorer
and try to connect to an existing Firebird database.
61
Developing Firebird Applications with Microsoft Entity Framework
62
Developing Firebird Applications with Microsoft Entity Framework
Creating a Project
For our example in this chapter, we will create a Windows Forms application. Other types of applications differ
from it, but the principles of working with Firebird via Entity Framework remain the same.
• FirebirdSql.Data.FirebirdClient
• EntityFramework
• EntityFramework.Firebird
Right-click the project name in Solution Explorer and select Manage NuGet Packages from the drop-down list.
Find the packages listed above in the Nuget catalogue and install them in the package manager.
63
Developing Firebird Applications with Microsoft Entity Framework
To create an EDM, right-click the project name in Solution Explorer and select Add—>New Item from the menu.
64
Developing Firebird Applications with Microsoft Entity Framework
Next, in the Add New Item wizard, select ADO.NET Entity Data Model.
65
Developing Firebird Applications with Microsoft Entity Framework
Figure 4.7. Add New Item wizard - select ADO.NET Entity Data Model
Since we already have a database, we will generate the EDM from the database. Select the icon captioned Code
First from database.
Figure 4.8. Add New Item wizard - select 'Code First from database'
66
Developing Firebird Applications with Microsoft Entity Framework
Now we need to to select the connection the model will be created from. If the connection does not exist, it
will have to be created.
You might need to specify some advanced properties in addition to the main connection properties. You might
want to set the transaction isolation, for example, to a level different from the default Read Committed, or to
specify connection pooling, or something else that differs from defaults.
67
Developing Firebird Applications with Microsoft Entity Framework
68
Developing Firebird Applications with Microsoft Entity Framework
Tip
Snapshot is the recommended isolation level because Entity Framework and ADO.NET both use disconnected
data access—where each connection and each transaction is active only for a very short time.
Next, the Entity Data Model wizard will ask you how to store the connection string.
69
Developing Firebird Applications with Microsoft Entity Framework
For a web application or another three-tier architecture, where all users will be working with the database using
a single account, select Yes. If your application is going to request authentication for connecting to the database,
select No.
70
Developing Firebird Applications with Microsoft Entity Framework
Tip
It is much more convenient to work with wizards if you select Yes for each property. You can always change
the isolation level in the application when it is ready for testing and deployment by just editing the connection
string in the <AppName>.exe.conf application configuration file. The connection string will be stored in
the connectionStrings section and will look approximately like this:
<add name="DbModel"
connectionString="character set=UTF8; data source=localhost;
initial catalog=examples; port number=3050;
user id=sysdba; dialect=3; isolationlevel=Snapshot;
pooling=True; password=masterkey;"
providerName="FirebirdSql.Data.FirebirdClient" />
For the configuration file to stop storing the confidential information, just delete this parameter from the con-
nection string: password=masterkey;
Unfortunately, the current ADO.Net provider for Firebird (version 5.9.0.0) does not support network traffic
encryption, which is enabled by default in Firebird 3.0 and higher versions. If you want to work with Firebird
3.0, you need to change some settings in firebird.conf (or in databases.conf for a specific database)
to make Firebird to work without trying to use network encryption.
# WireCrypt = Enabled
to
WireCrypt = Disabled
making sure to delete the '#' comment marker. Remember that you must restart the server for configuration
changes to take effect.
Next, you will be asked which tables and views should be included in the model.
71
Developing Firebird Applications with Microsoft Entity Framework
For our project, select the four tables that are checked in the screenshot.
An Entity File
Let's take a look at the generated file describing the INVOICE entity:
72
Developing Firebird Applications with Microsoft Entity Framework
[Table("Firebird.INVOICE")]
public partial class INVOICE
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage",
"CA2214:DoNotCallOverridableMethodsInConstructors")]
public INVOICE()
{
INVOICE_LINES = new HashSet<INVOICE_LINE>();
}
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int INVOICE_ID { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage",
"CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<INVOICE_LINE> INVOICE_LINES { get; set; }
}
The class contains properties for each field of the INVOICE table. Each of these properties has attributes that
describe constraints. You can study the details of the various attributes in the Microsoft document, Code First
Data Annotations.
Two navigation properties are generated: CUSTOMER and INVOICE_LINES. The first one contains a ref-
erence to the customer entity. The second contains a collection of invoice lines. It is generated because the
INVOICE_LINE table has a foreign key to the INVOICE table. Of course, you can remove this property from
the INVOICE entity, but it is not really necessary. The CUSTOMER and INVOICE_LINES properties use “lazy
loading” which means that loading is not performed until the first access to an object. That way, the loading of
related data is avoided unless it is actually needed. Once the data are accessed via the navigation property, they
will be loaded from the database automatically.
Important
If lazy loading is in effect, classes that use it must be public and their properties must have the keywords
public and virtual.
73
Developing Firebird Applications with Microsoft Entity Framework
{
public DbModel()
: base("name=DbModel")
{
}
modelBuilder.Entity<CUSTOMER>()
.HasMany(e => e.INVOICES)
.WithRequired(e => e.CUSTOMER)
.WillCascadeOnDelete(false);
modelBuilder.Entity<PRODUCT>()
.HasMany(e => e.INVOICE_LINES)
.WithRequired(e => e.PRODUCT)
.WillCascadeOnDelete(false);
modelBuilder.Entity<INVOICE>()
.HasMany(e => e.INVOICE_LINES)
.WithRequired(e => e.INVOICE)
.WillCascadeOnDelete(false);
}
}
The properties coded here describe a dataset for each entity, along with advanced properties that are specified
for creating a model with Fluent API. A complete description of the Fluent API can be found in the Microsoft
document entitled Configuring/Mapping Properties and Types with the Fluent API.
We will use the Fluent API to specify precision and scale for properties of type DECIMAL in the OnModelCre-
ating method, by adding the following lines:
modelBuilder.Entity<PRODUCT>()
.Property(p => p.PRICE)
.HasPrecision(15, 2);
modelBuilder.Entity<INVOICE>()
.Property(p => p.TOTAL_SALE)
.HasPrecision(15, 2);
modelBuilder.Entity<INVOICE_LINE>()
.Property(p => p.SALE_PRICE)
.HasPrecision(15, 2);
modelBuilder.Entity<INVOICE_LINE>()
.Property(p => p.QUANTITY)
.HasPrecision(15, 0);
74
Developing Firebird Applications with Microsoft Entity Framework
Since both forms are similar in function and implementation, we will describe just one.
Getting a Context
To work with our model, we will need the method for getting a context (or a model). The following statement
is sufficient for that purpose:
75
Developing Firebird Applications with Microsoft Entity Framework
If no confidential data are stored in the connection string—for example, the password is absent because it will
be captured during the authentication process when the application is started—we will need a special method for
storing and recovering the connection string or for storing the previously created context. For that, we will create
a special class containing some application-level global variables, along with a method for getting a context.
A context might be the start and end dates of a work period, for example.
/// <summary>
/// Start date of the working period
/// </summary>
public static DateTime StartDate { get; set; }
/// <summary>
/// End date of the working period
/// </summary>
public static DateTime FinishDate { get; set; }
/// <summary>
/// Returns an instance of the model (context)
/// </summary>
/// <returns>Model</returns>
public static DbModel CreateDbContext() {
dbContext = dbContext ?? new DbModel();
return dbContext;
}
}
The connection string itself is applied after the authentication process completes successfully during the appli-
cation launch. We will add the following code to the Load event handler of the main form for that.
76
Developing Firebird Applications with Microsoft Entity Framework
}
}
else
Application.Exit();
}
1. The Load method loads all data from the CUSTOMER table to memory at once
2. Although lazy properties (INVOICES) are not loaded immediately, but only once they are accessed, they
will be loaded anyway when the records are shown in the grid and it will happen each time a group of
records is shown
To get around these drawbacks, we will use a feature of the LINQ (Language Integrated Query) technology,
LINQ to Entities. LINQ to Entities offers a simple and intuitive approach to getting data using C# statements
that are syntactically similar to SQL query statements. You can read about the LINQ syntax in LINQ to Entities.
77
Developing Firebird Applications with Microsoft Entity Framework
The IQueryable interface is in the System.Linq namespace. It provides remote access to the database and move-
ment through the data can be bi-directional. During the process of creating a query that returns an IQueryable
object, the query is optimized to minimise memory usage and network bandwidth.
The Local property returns the IEnumerable interface, through which we can create LINQ queries.
However, as this query will be executed on the data in memory, it is really useful only for small tables that do
not need to be filtered beforehand.
For a LINQ query to be converted into SQL and executed on the server, we need to access the
dbContext.CUSTOMERS directly instead of accessing the dbContext.CUSTOMERS.Local property in the LINQ
query. The prior call to dbContext.CUSTOMERS.Load(); to load the collection to memory is not required.
78
Developing Firebird Applications with Microsoft Entity Framework
// Disconnect all objects from the DbSet collection from the context
// Useful for updating the cache
public static void DetachAll<T>(this DbModel dbContext, DbSet<T> dbSet)
where T : class
{
foreach (var obj in dbSet.Local.ToList())
{
dbContext.Entry(obj).State = EntityState.Detached;
}
}
Other Extensions
NextValueFor
is used to get the next value from the generator.
dbContext.Database.SqlQuery
allows SQL queries to be executed directly and their results to be displayed on some entity (projection).
DetachAll
is used to detach all objects of the DBSet collection from the context. It is necessary to update the internal
cache, because all retrieved data are cached and are not retrieved from the database again. However, that is
not always useful because it makes it more difficult to get the latest version of records that were modified
in another context.
79
Developing Firebird Applications with Microsoft Entity Framework
Note
In web applications, a context usually exists for a very short period. A new context has an empty cache.
Refresh
is used to update the properties of an entity object. It is useful for updating the properties of an object after
it has been edited or added.
Adding a Customer
This is the code of the event handler for clicking the Add button:
80
Developing Firebird Applications with Microsoft Entity Framework
While adding the new record, we used the generator to get the value of the next identifier. We could have done
it without applying the value of the identifier, leaving the BEFORE INSERT trigger to fetch the next value of
the generator and apply it. However, that would leave us unable to update the added record.
Editing a Customer
The code of the event handler for clicking the Edit button is as follows:
81
Developing Firebird Applications with Microsoft Entity Framework
// display error
MessageBox.Show(ex.Message, "Error");
// Do not close the form to correct the error
fe.Cancel = true;
}
}
else
bindingSource.CancelEdit();
};
// show the modal form
editor.ShowDialog(this);
}
}
Deleting a Customer
The code of the event handler for clicking the Delete button is as follows:
82
Developing Firebird Applications with Microsoft Entity Framework
Secondary Modules
Our application will have only one secondary module, named “Invoices”. Secondary modules typically contain
larger numbers of records than primary ones and new records are added to them frequently.
An invoice consists of a title where some general attributes are described (number, date, customer …) and invoice
lines with the list of products, their quantities, prices, etc. It is convenient to have two grids for such documents:
the main one showing the invoice header data and the detail one for the list of products sold. We will need one
DataGridView component for each entity on the document form, binding the appropriate BindingSource to each.
83
Developing Firebird Applications with Microsoft Entity Framework
Filtering Data
Most secondary entities contain a field with the document creation date. To reduce the amount of retrieved data,
the concept of a work period is usually introduced to filter the data sent to the client. A work period is a range
of dates for which the records are required. Since the application can have more than one secondary entity, it
makes sense to add variables containing the start and end dates of a work period to the global AppVariables data
module (see Getting a Context that is used by all modules working with the database in one way or another.
Once the application is started, the work period is usually defined by the dates when the current quarter starts and
ends, although of course, other options are possible. While working with the application, the user can change
the work period.
Since the most recent records are the most requested, it makes sense to sort them by date in reverse order. As
with the primary modules, we will use LINQ to retrieve data.
84
Developing Firebird Applications with Microsoft Entity Framework
var invoices =
from invoice in dbContext.INVOICES
where (invoice.INVOICE_DATE >= AppVariables.StartDate) &&
(invoice.INVOICE_DATE <= AppVariables.FinishDate)
orderby invoice.INVOICE_DATE descending
select new InvoiceView
{
Id = invoice.INVOICE_ID,
Cusomer_Id = invoice.CUSTOMER_ID,
Customer = invoice.CUSTOMER.NAME,
Date = invoice.INVOICE_DATE,
Amount = invoice.TOTAL_SALE,
Payed = (invoice.PAYED == 1) ? "Yes" : "No"
};
masterBinding.DataSource = invoices.ToBindingList();
}
To simplify type casting, we define an InvoiceView class, rather than use some anonymous type. The definition
is as follows:
85
Developing Firebird Applications with Microsoft Entity Framework
this.Date = invoiceView.Date;
this.Amount = invoiceView.Amount;
this.Payed = invoiceView.Payed;
}
}
The Load method allows us to update one added or updated record in the grid quickly, instead of completely
reloading all records. Here is the code of the event handler for clicking the Add button:
In our primary modules, the similarly-named method called dbContext.Refresh but, here, a record is updated by
by calling the Load method of the InvoiceView class. The reason for the difference is that dbContext.Refresh is
used to update entity objects, not the objects that can be produced by complex LINQ queries.
The code of the event handler for clicking the Edit button:
86
Developing Firebird Applications with Microsoft Entity Framework
}
using (InvoiceEditorForm editor = new InvoiceEditorForm()) {
editor.Text = "Edit invoice";
editor.Invoice = invoice;
// Form Close Handler
editor.FormClosing += delegate (object fSender, FormClosingEventArgs fe) {
if (editor.DialogResult == DialogResult.OK) {
try {
// trying to save the changes
dbContext.SaveChanges();
// refresh
CurrentInvoice.Load(invoice.INVOICE_ID);
masterBinding.ResetCurrentItem();
}
catch (Exception ex) {
// display error
MessageBox.Show(ex.Message, "Error");
// Do not close the form to correct the error
fe.Cancel = true;
}
}
};
editor.ShowDialog(this);
}
}
Here we needed to find an entity by the identifier provided in the current record. The CurrentInvoice is used to
retrieve the invoice selected in the grid. This is how we code it:
Using the same approach, you can implement deleting the invoice header yourself.
Paying an Invoice
Besides adding, editing and deleting, we want one more operation for invoices: payment. Here is code for a
method implementing this operation:
87
Developing Firebird Applications with Microsoft Entity Framework
masterBinding.ResetCurrentItem();
}
catch (Exception ex) {
// display error
MessageBox.Show(ex.Message, "Error");
}
}
1. Getting data for each invoice from the INVOICE_LINE navigation property and displaying the contents
of this complex property in the detail grid, probably with LINQ transformations
2. Getting the data for each invoice with a separate LINQ query that will be re-executed when the cursor
moves to another record in the master grid
The first one assumes that we want to retrieve all invoices at once for the specified period together with the bound
data from the invoice lines when the invoice form is opened. Although it is done with one SQL query, it may
take quite a while and requires a large amount of random-access memory. It is better suited to web applications
where records are usually displayed page by page.
The second one is a bit more difficult to implement, but it allows the invoice form to be opened quickly and
requires less resource. However, each time the cursor in the master grid moves, an SQL query will be executed,
generating network traffic, albeit with only a small volume of data.
For our application we will use the second approach. We need an event handler for the BindingSource component
for editing the current record:
88
Developing Firebird Applications with Microsoft Entity Framework
Price = line.SALE_PRICE,
Total = Math.Round(line.QUANTITY * line.SALE_PRICE, 2)
};
detailBinding.DataSource = lines.ToBindingList();
}
Note
Unlike the InvoiceView class, this one has no method for loading one current record. In our example, the
speed of reloading the detail grid it is not crucial, because one document does not contain thousands of items.
Implementing this method is optional.
Now we will add a special property for retrieving the current line of the document selected in the detail grid.
89
Developing Firebird Applications with Microsoft Entity Framework
With our example, an update of the master grid record will be needed because one of its fields (TotalSale)
contains aggregated information derived from the detail lines of the document. This is how we do that:
90
Developing Firebird Applications with Microsoft Entity Framework
91
Developing Firebird Applications with Microsoft Entity Framework
92
Developing Firebird Applications with Microsoft Entity Framework
A click on the button next to the TextBox will open a modal form with a grid for selecting products. The
same modal form created for displaying the products is used for selecting them. The click handler code for the
embedded button that initiates the form is:
Suppose we need to make a discount on goods selected in the grid. Without explicit transaction management,
the code would be as follows:
93
Developing Firebird Applications with Microsoft Entity Framework
Let's say we select 10 products. Ten implicit transactions will be used for finding the products by their identifiers.
One more transaction will be used to save the changes.
If we control transactions explicitly, we can use just one transaction for the same piece of work. For example:
Our code starts the transaction with the default parameters. To specify your own parameters for a transaction,
you should use the UseTransaction method.
94
Developing Firebird Applications with Microsoft Entity Framework
That's it. Now only one transaction is used for the entire set of updates and there are no unnecessary commands
for finding data.
All that is left to do is to add a dialog box for entering the value of the discount and code to update data in the
grid. Try to do it on your own.
95
Developing Firebird Applications with Microsoft Entity Framework
The Result
Figure 4.18. The result of the Entity Framework project
Source Code
You can get the source code for the sample application using this link: FBFormAppExample.zip.
96
Chapter 5
We examine the specifics of creating a web application with this framework. The basic principles for working
with Entity Framework and Firebird are described in the previous chapter, Creating Applications with Microsoft
Entity Framework.
Controller
Controllers work with the model and provide interaction with the user. They also provide view options for
displaying the user interface. In an MVC application, views only display data while the controller handles
the input and responds to user activities.
As an example, the controller can process string values in a query and send them to the model, which can
use these values to send a query to the database.
View
the visual part of application's user interface. The user interface is usually created to reflect the data from
the model.
Model
Model objects are the parts of the application that implement the logic for working with the application data.
Model objects typically receive the status of the model and save it in the database.
97
Creating Web Applications in Entity Framework with MVC
Model-View-Controller Interaction
Interaction between these components is illustrated in the following general diagram:
The MVC pattern supports the creation of applications whose logical aspects—input, business and interface—
are separated but interact closely with one another. The diagram illustrates the location of each logic type in
the application:
This separation allows you to work with complex structures while developing the application because it ensures
discrete implementation of each aspect. The developer can focus on creating a view separately from implement-
ing the business logic.
More comprehensive information about the ASP.NET MVC technology can be found at the website of the
ASP.NET community.
Software Stack
Along with the libraries for working with Firebird, Entity Framework and MVC.NET, you will need a number
of JavaScript libraries to support a responsive interface, such as jquery, jquery-ui, Bootstrap, jqGrid. In this
example, we have tried to make a web application whose interface is similar to a desktop UI, by employing
grids for views and modal windows for data input.
98
Creating Web Applications in Entity Framework with MVC
Creating a Project
The Following topics will show how to use the Visual Studio wizards to create the framework of an MVC.NET
application.
Open File—>New—>Project in Visual Studio 2015 and create a new project named FBMVCExample.
99
Creating Web Applications in Entity Framework with MVC
For now, we will create a web application with no authentication, so click the Change Authentication button to
disable authentication. We will get back to this issue a bit later.
100
Creating Web Applications in Entity Framework with MVC
101
Creating Web Applications in Entity Framework with MVC
• FirebirdSql.Data.FirebirdClient
• EntityFramework.Firebird
• jQuery.UI.Combined
• Newtonsoft.Json
• Trirand.jqGrid
Note
Not all packages provided by NuGet are the latest version of the libraries. It is especially true for JavaScript
libraries. You can install the latest versions of JavaScript libraries using a content delivery network (CDN) or
by just downloading them and replacing the libraries provided by NuGet.
Right-click the project name in Solution Explorer and select the Manage NuGet Packages item in the drop-
down menu.
102
Creating Web Applications in Entity Framework with MVC
103
Creating Web Applications in Entity Framework with MVC
Creating an EDM
If you already have a Windows Forms application that uses Entity Framework, you can just model classes to the
Models folder. Otherwise, you have to create them from scratch. The process of creating an EDM is described
in the previous chapter in the topic Creating an Entity Data Model (EDM).
There is one more small difference: your response to the EDM wizard's question about how to store the con-
nection string:
104
Creating Web Applications in Entity Framework with MVC
When we create a web application, all users will work with the database using a single account, so select Yes
for this question. Any user with enough privileges can be specified as the username. It is advisable not to use
the SYSDBA user because it has more privileges than are required for a web application to work.
You can always change the username in the application when it is ready for testing and deployment, by just
editing the connection string in the AppName.exe.conf application configuration file.
The connection string will be stored in the connectionStrings section and will look approximately as follows:
<add name="DbModel"
connectionString="character set=UTF8; data source=localhost;
initial catalog=examples; port number=3050;
user id=sysdba; dialect=3; isolationlevel=Snapshot;
pooling=True; password=masterkey;"
providerName="FirebirdSql.Data.FirebirdClient" />
105
Creating Web Applications in Entity Framework with MVC
106
Creating Web Applications in Entity Framework with MVC
Once it is done, the controller CustomerController will be created, along with five views displaying:
Since the Ajax technology and the jqGrid library will be used extensively in our project, the first view, for
displaying the customer list as a table, will be enough for our purposes. The rest of the operations will be
performed with jqGrid.
Limiting Overhead
We want to be aware of ways to limit the overhead involved in passing data and connections back and forth over
the wide-area network. There are techniques that can help us with this.
The customer list may turn out to be quite big. The entire list from a big table is usually not returned in web
applications because it could make the process of loading the page seriously slow. Instead, the data are usually
split into pages or are dynamically loaded when the user scrolls down to the end of the page (or grid). We will
use the first option in our project.
107
Creating Web Applications in Entity Framework with MVC
Limiting Connections
Another characteristic of web applications is that they do not keep any permanent connections to the database
because the life of the page generation script is no longer than the time it takes to generate a response to the user
request. A connection to the database is actually a rather expensive resource so we have to save it. Of course,
there is a connection pool for reducing the time it takes to establish a connection to the database, but it is still
advisable to make a connection to the database only when it is really necessary.
// Display view
public ActionResult Index()
{
return View();
}
108
Creating Web Applications in Entity Framework with MVC
109
Creating Web Applications in Entity Framework with MVC
{
// check the correctness of the model
if (ModelState.IsValid)
{
// get a new identifier using a generator
customer.CUSTOMER_ID = db.NextValueFor("GEN_CUSTOMER_ID");
// add the model to the list
db.CUSTOMERS.Add(customer);
// save model
db.SaveChanges();
// return success in JSON format
return Json(true);
}
else {
// join model errors in one string
string messages = string.Join("; ", ModelState.Values
.SelectMany(x => x.Errors)
.Select(x => x.ErrorMessage));
// return error in JSON format
return Json(new { error = messages });
}
}
// Editing supplier
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(
[Bind(Include = "CUSTOMER_ID,NAME,ADDRESS,ZIPCODE,PHONE")] CUSTOMER customer)
{
// check the correctness of the model
if (ModelState.IsValid)
{
// mark the model as modified
db.Entry(customer).State = EntityState.Modified;
// save model
db.SaveChanges();
// return success in JSON format
return Json(true);
}
else {
// join model errors in one string
string messages = string.Join("; ", ModelState.Values
.SelectMany(x => x.Errors)
.Select(x => x.ErrorMessage));
// return error in JSON format
return Json(new { error = messages });
}
}
// Deleting supplier
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Delete(int id)
{
// find supplier by id
CUSTOMER customer = db.CUSTOMERS.Find(id);
// delete supplier
db.CUSTOMERS.Remove(customer);
110
Creating Web Applications in Entity Framework with MVC
// save model
db.SaveChanges();
// return success in JSON format
return Json(true);
}
The Index method is used to display the Views/Customer/Index.cshtml view. The view itself will be
presented a bit later. This view is actually an html page template with markup and JavaScript for initiating
jqGrid. The data itself will be obtained asynchronously in the JSON format, using the Ajax technology. The
selected type of sorting, the page number and the search parameters will determine the format of an HTTP
request that will be handled by the GetData action. The parameters of the HTTP request are displayed in the
input parameters of the GetData method. We generate a LINQ query based on these parameters and send the
retrieved result in the JSON format.
Note
Various libraries can assist with parsing the parameters of a query generated by jqGrid and make it easier to
build the model. We have not used them in our examples so the code might be somewhat cumbersome. You
can always improve it, of course.
The Create method is used to add a new customer record. The method has the [HttpPost] attribute specified
for it to indicate that the parameters of the HTTP POST request () are to be displayed on the Customer
model. Examine the following line:
Here Bind specifies which parameters of the HTTP request are to be displayed in the properties of the model.
This parameter is automatically added to each form where the @Html.AntiForgeryToken() helper is spec-
ified. However, the jqGrid library uses dynamically generated Ajax requests rather than previously created web
forms. To fix that, we need to change the shared view Views/Shared/_Layout.cshtml as follows:
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
111
Creating Web Applications in Entity Framework with MVC
function GetAntiForgeryToken() {
var tokenField =
$("input[type='hidden'][name$='RequestVerificationToken']");
if (tokenField.length == 0) {
return null;
} else {
return {
name: tokenField[0].name,
value: tokenField[0].value
};
}
}
112
Creating Web Applications in Entity Framework with MVC
@Scripts.Render("~/bundles/bootstrap")
@RenderSection("scripts", required: false)
</body>
</html>
Bundles
Bundles are used to make it easier to link JavaScript scripts and CSS files. You can link CSS bundles with the
Styles.Render helper and script bundles with the Scripts.Render helper.
Bundles are registered in the BundleConfig.cs file located in the App_Start folder:
113
Creating Web Applications in Entity Framework with MVC
"~/Scripts/bootstrap.js",
"~/Scripts/respond.js"));
bundles.Add(new StyleBundle("~/Content/css").Include(
"~/Content/jquery-ui.min.css",
"~/Content/themes/ui-darkness/jquery-ui.min.css",
"~/Content/themes/ui-darkness/theme.css",
"~/Content/bootstrap.min.css",
"~/Content/Site.css"
));
}
The RegisterBundles method adds all created bundles to the bundles collection. A bundle is declared in the
following way:
new ScriptBundle("~/bundles/jquery").Include("~/Scripts/jquery-{version}.js")
The virtual path of the bundle is passed to the ScriptBundle construct. Specific script files are included in this
bundle using the Include method.
The “~/Scripts/jquery.validate*” expression fills out the rest of the string with the asterisk character
as a wildcard. For example, the expression will include two files at once in the bundle: jquery.validate.js
and jquery.validate.unobtrusive.js, along with their minimized versions, because their names both
start with “jquery.validate”.
The same applies when creating CSS bundles, using the StyleBundle class.
Important
The full versions of the scripts and cascading style sheets should be used in the debug mode and the minimized
ones in the release mode. Bundles allow you to solve this problem. When you run the application in the debug
mode, the web.config files have the <compilation debug="true"> parameter. When you set this
parameter to false (the Release mode), the minimized version of JavaScript modules and CSS files will be used
instead of the full ones.
Views
Since we need only the View/Customer/Index.cshtml view out of the five created for the Customer
controller, you can delete the others from the folder.
You can see that the entire view consists of the header, the jqg table and the jqg-pager block for displaying the
navigation bar. The rest is occupied by the script for initiating the grid, the navigation bar and the dialog box
for editing records.
@{
ViewBag.Title = "Index";
}
114
Creating Web Applications in Entity Framework with MVC
<h2>Customers</h2>
<table id="jqg"></table>
<div id="jqg-pager"></div>
<script type="text/javascript">
$(document).ready(function () {
var dbGrid = $("#jqg").jqGrid({
url: '@Url.Action("GetData")', // URL to retrieve data
datatype: "json", // data format
mtype: "GET", // http type request
// model description
colModel: [
{
label: 'Id',
name: 'CUSTOMER_ID', // field name
key: true,
hidden: true
},
{
label: 'Name',
name: 'NAME',
width: 250,
sortable: true,
editable: true,
edittype: "text", // field type in the editor
search: true,
searchoptions: {
sopt: ['eq', 'bw', 'cn'] // allowed search operators
},
// size and maximum length for the input field
editoptions: { size: 30, maxlength: 60 },
// mandatory field
editrules: { required: true }
},
{
label: 'Address',
name: 'ADDRESS',
width: 300,
sortable: false, // prohibit sorting
editable: true,
search: false, // prohibit searching
edittype: "textarea",
editoptions: { maxlength: 250, cols: 30, rows: 4 }
},
{
label: 'Zip Code',
name: 'ZIPCODE',
width: 30,
sortable: false,
editable: true,
search: false,
edittype: "text",
editoptions: { size: 30, maxlength: 10 },
},
{
label: 'Phone',
name: 'PHONE',
115
Creating Web Applications in Entity Framework with MVC
width: 80,
sortable: false,
editable: true,
search: false,
edittype: "text",
editoptions: { size: 30, maxlength: 14 },
}
],
rowNum: 500, // number of rows displayed
loadonce: false, // load only once
sortname: 'NAME', // sort by default by NAME column
sortorder: "asc",
width: window.innerWidth - 80, // grid width
height: 500, // grid height
viewrecords: true, // display the number of records
caption: "Customers",
pager: 'jqg-pager' // navigation item id
});
dbGrid.jqGrid('navGrid', '#jqg-pager', {
search: true,
add: true,
edit: true,
del: true,
view: true,
refresh: true,
// button labels
searchtext: "Find",
addtext: "Add",
edittext: "Edit",
deltext: "Delete",
viewtext: "View",
viewtitle: "Selected record",
refreshtext: "Refresh"
},
update("edit"),
update("add"),
update("del")
);
116
Creating Web Applications in Entity Framework with MVC
params.url = '@Url.Action("Edit")';
postdata.CUSTOMER_ID = selectedRow;
break;
case "del":
params.url = '@Url.Action("Delete")';
postdata.CUSTOMER_ID = selectedRow;
break;
}
},
// processing results of sending forms (operations)
afterSubmit: function (response, postdata) {
var responseData = response.responseJSON;
// check the result for error messages
if (responseData.hasOwnProperty("error")) {
if (responseData.error.length) {
return [false, responseData.error];
}
}
else {
// refresh grid
$(this).jqGrid(
'setGridParam',
{
datatype: 'json'
}
).trigger('reloadGrid');
}
return [true, "", 0];
}
};
};
});
</script>
It is important to configure the model properties correctly in order to display the grid properly, position input
items on the edit form, configure validation for input forms and configure the sorting and search options. This
configuration is not simple and has a lot of parameters. In the comments I have tried to describe the parameters
being used. The full description of the model parameters can be found in the documentation for the jqGrid library
in the ColModel API section.
Note that jqGrid does not automatically add hidden grid columns to the input form, though I think it would make
sense at least for key fields. Consequently, we have to add the customer identifier to the request parameters for
editing and deleting:
case "edit":
params.url = '@Url.Action("Edit")';
postdata.CUSTOMER_ID = selectedRow;
break;
case "del":
params.url = '@Url.Action("Delete")';
postdata.CUSTOMER_ID = selectedRow;
break;
The working page with the list of customers will look like this:
117
Creating Web Applications in Entity Framework with MVC
118
Creating Web Applications in Entity Framework with MVC
The controller and view for the product UI are implemented in a similar way. We will not describe them here
in detail. You can either write them yourself or use the source code linked at the end of this chapter.
An invoice consists of a header where some general attributes are described (number, date, customer …) and
invoice detail lines with the list of products sold, their quantities, prices, etc. To save space on the page, we will
hide the detail grid and display it only in response to a click on the icon with the '+' sign on it. Thus, our detail
grid will be embedded in the main one.
[Authorize(Roles = "manager")]
public class InvoiceController : Controller
{
119
Creating Web Applications in Entity Framework with MVC
// display view
public ActionResult Index()
{
return View();
}
120
Creating Web Applications in Entity Framework with MVC
invoicesQuery = invoicesQuery.Where(
c => c.INVOICE_DATE == dateValue);
break;
case "lt": // <
invoicesQuery = invoicesQuery.Where(
c => c.INVOICE_DATE < dateValue);
break;
case "le": // <=
invoicesQuery = invoicesQuery.Where(
c => c.INVOICE_DATE <= dateValue);
break;
case "gt": // >
invoicesQuery = invoicesQuery.Where(
c => c.INVOICE_DATE > dateValue);
break;
case "ge": // >=
invoicesQuery = invoicesQuery.Where(
c => c.INVOICE_DATE >= dateValue);
break;
}
}
if (searchField == "PAID")
{
int iVal = (searchString == "on") ? 1 : 0;
invoicesQuery = invoicesQuery.Where(c => c.PAID == iVal);
}
// get the total number of invoices
int totalRows = invoicesQuery.Count();
// add sorting
switch (sord)
{
case "asc":
invoicesQuery = invoicesQuery.OrderBy(
invoice => invoice.INVOICE_DATE);
break;
case "desc":
invoicesQuery = invoicesQuery.OrderByDescending(
invoice => invoice.INVOICE_DATE);
break;
}
// get invoice list
var invoices = invoicesQuery
.Skip(offset)
.Take(limit)
.ToList();
// calculate the total number of pages
int totalPages = totalRows / limit + 1;
// create the result for jqGrid
var result = new
{
page = pageNo,
total = totalPages,
records = totalRows,
rows = invoices
};
// convert the result to JSON
return Json(result, JsonRequestBehavior.AllowGet);
}
121
Creating Web Applications in Entity Framework with MVC
122
Creating Web Applications in Entity Framework with MVC
}
catch (Exception ex)
{
// return error in JSON format
return Json(new { error = ex.Message });
}
}
else {
string messages = string.Join("; ", ModelState.Values
.SelectMany(x => x.Errors)
.Select(x => x.ErrorMessage));
// return error in JSON format
return Json(new { error = messages });
}
}
// Edit invoice
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Edit(
[Bind(Include = "INVOICE_ID,CUSTOMER_ID,INVOICE_DATE")] INVOICE invoice)
{
// check the correctness of the model
if (ModelState.IsValid)
{
try
{
var INVOICE_ID = new FbParameter("INVOICE_ID", FbDbType.Integer);
var CUSTOMER_ID = new FbParameter("CUSTOMER_ID", FbDbType.Integer);
var INVOICE_DATE = new FbParameter("INVOICE_DATE",
FbDbType.TimeStamp);
// initialize parameters query
INVOICE_ID.Value = invoice.INVOICE_ID;
CUSTOMER_ID.Value = invoice.CUSTOMER_ID;
INVOICE_DATE.Value = invoice.INVOICE_DATE;
// execute stored procedure
db.Database.ExecuteSqlCommand(
"EXECUTE PROCEDURE SP_EDIT_INVOICE(@INVOICE_ID, @CUSTOMER_ID, @INVOICE_DATE)",
INVOICE_ID,
CUSTOMER_ID,
INVOICE_DATE);
// return success in JSON format
return Json(true);
}
catch (Exception ex)
{
// return error in JSON format
return Json(new { error = ex.Message });
}
}
else {
string messages = string.Join("; ", ModelState.Values
.SelectMany(x => x.Errors)
.Select(x => x.ErrorMessage));
// return error in JSON format
return Json(new { error = messages });
}
}
123
Creating Web Applications in Entity Framework with MVC
// Delete invoice
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Delete(int id)
{
try
{
var INVOICE_ID = new FbParameter("INVOICE_ID", FbDbType.Integer);
// initialize parameters query
INVOICE_ID.Value = id;
// execute stored procedure
db.Database.ExecuteSqlCommand(
"EXECUTE PROCEDURE SP_DELETE_INVOICE(@INVOICE_ID)",
INVOICE_ID);
// return success in JSON format
return Json(true);
}
catch (Exception ex)
{
// return error in JSON format
return Json(new { error = ex.Message });
}
}
// Payment of invoice
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Pay(int id)
{
try
{
var INVOICE_ID = new FbParameter("INVOICE_ID", FbDbType.Integer);
// initialize parameters query
INVOICE_ID.Value = id;
// execute stored procedure
db.Database.ExecuteSqlCommand(
"EXECUTE PROCEDURE SP_PAY_FOR_INOVICE(@INVOICE_ID)",
INVOICE_ID);
// return success in JSON format
return Json(true);
}
catch (Exception ex)
{
// return error in JSON format
return Json(new { error = ex.Message });
}
}
124
Creating Web Applications in Entity Framework with MVC
try
{
var INVOICE_ID = new FbParameter("INVOICE_ID", FbDbType.Integer);
var PRODUCT_ID = new FbParameter("PRODUCT_ID", FbDbType.Integer);
var QUANTITY = new FbParameter("QUANTITY", FbDbType.Integer);
// initialize parameters query
INVOICE_ID.Value = invoiceLine.INVOICE_ID;
PRODUCT_ID.Value = invoiceLine.PRODUCT_ID;
QUANTITY.Value = invoiceLine.QUANTITY;
// execute stored procedure
db.Database.ExecuteSqlCommand(
""EXECUTE PROCEDURE SP_ADD_INVOICE_LINE(@INVOICE_ID, @PRODUCT_ID, @QUANTITY)",
INVOICE_ID,
PRODUCT_ID,
QUANTITY);
// return success in JSON format
return Json(true);
}
catch (Exception ex)
{
// return error in JSON format
return Json(new { error = ex.Message });
}
}
else {
string messages = string.Join("; ", ModelState.Values
.SelectMany(x => x.Errors)
.Select(x => x.ErrorMessage));
// return error in JSON format
return Json(new { error = messages });
}
}
125
Creating Web Applications in Entity Framework with MVC
return Json(true);
}
catch (Exception ex)
{
// return error in JSON format
return Json(new { error = ex.Message });
}
}
else {
string messages = string.Join("; ", ModelState.Values
.SelectMany(x => x.Errors)
.Select(x => x.ErrorMessage));
// return error in JSON format
return Json(new { error = messages });
}
}
The GetDetailData method for retrieving the list of lines in an invoice lacks the code for page-by-page navigation.
Realistically, a typical invoice does not have enough lines to justify using page-by-page navigation for them.
Omitting it simplifies and speeds up the code.
126
Creating Web Applications in Entity Framework with MVC
In our project, all data modification operations are performed in stored procedures, but you could do the same
work using Entity Framework. DDL code for the stored procedures can be found in the database creation script
in an earlier chapter and also in the .zip archives of all the DDL scripts:
https://github.com/sim1984/example-db_2_5/archive/1.0.zip
or https://github.com/sim1984/example-db_3_0/archive/1.0.zip
@{
ViewBag.Title = "Index";
}
<h2>Invoices</h2>
<table id="jqg"></table>
<div id="jpager"></div>
<script type="text/javascript">
/**
* The code to work with jqGrid
*/
</script>
To begin with, we will take the code for working with the main grid. All we have to write into it is the properties
of the model (field types and sizes, search, sorting, visibility parameters. etc.).
// invoice grid
var dbGrid = $("#jqg").jqGrid({
url: '@Url.Action("GetData")', URL to retrieve data
datatype: "json", // format data
mtype: "GET", // type of http request
// model description
colModel: [
{
label: 'Id',
name: 'INVOICE_ID',
key: true,
hidden: true
},
{
label: 'CUSTOMER_ID',
name: 'CUSTOMER_ID',
hidden: true,
editrules: { edithidden: true, required: true },
editable: true,
edittype:'custom', // own type
editoptions: {
custom_element: function (value, options) {
// add hidden input
127
Creating Web Applications in Entity Framework with MVC
return $("<input>")
.attr('type', 'hidden')
.attr('rowid', options.rowId)
.addClass("FormElement")
.addClass("form-control")
.val(value)
.get(0);
}
}
},
{
label: 'Date',
name: 'INVOICE_DATE',
width: 60,
sortable: true,
editable: true,
search: true,
edittype: "text", // type of input
align: "right",
formatter: 'date', // formatted as date
sorttype: 'date', // sorted as date
formatoptions: { // date format
srcformat: 'd.m.Y H:i:s',
newformat: 'd.m.Y H:i:s'
},
editoptions: {
// initializing the form element for editing
dataInit: function (element) {
// create datepicker
$(element).datepicker({
id: 'invoiceDate_datePicker',
dateFormat: 'dd.mm.yy',
minDate: new Date(2000, 0, 1),
maxDate: new Date(2030, 0, 1)
});
}
},
searchoptions: {
// initializing the form element for searching
dataInit: function (element) {
// create datepicker
$(element).datepicker({
id: 'invoiceDate_datePicker',
dateFormat: 'dd.mm.yy',
minDate: new Date(2000, 0, 1),
maxDate: new Date(2030, 0, 1)
});
},
searchoptions: { // searching types
sopt: ['eq', 'lt', 'le', 'gt', 'ge']
},
}
},
{
label: 'Customer',
name: 'CUSTOMER_NAME',
width: 250,
editable: true,
128
Creating Web Applications in Entity Framework with MVC
edittype: "text",
editoptions: {
size: 50,
maxlength: 60,
readonly: true
},
editrules: { required: true },
search: true,
searchoptions: {
sopt: ['eq', 'bw', 'cn']
},
},
{
label: 'Amount',
name: 'TOTAL_SALE',
width: 60,
sortable: false,
editable: false,
search: false,
align: "right",
formatter: 'currency', // format as currency
sorttype: 'number',
searchrules: {
"required": true,
"number": true,
"minValue": 0
}
},
{
label: 'Paid',
name: 'PAID',
width: 30,
sortable: false,
editable: true,
search: true,
searchoptions: {
sopt: ['eq']
},
edittype: "checkbox",
formatter: "checkbox",
stype: "checkbox",
align: "center",
editoptions: {
value: "1",
offval: "0"
}
}
],
rowNum: 500, // number of rows displayed
loadonce: false,
sortname: 'INVOICE_DATE', // sort by default by NAME column
sortorder: "desc",
width: window.innerWidth - 80, // grid width
height: 500, // grid height
viewrecords: true, // display the number of records
caption: "Invoices", // grid caption
pager: '#jpager', // pagination element
subGrid: true, // show subgrid
129
Creating Web Applications in Entity Framework with MVC
We'll add one more “custom” button to the main grid, for paying the invoice.
130
Creating Web Applications in Entity Framework with MVC
// refresh grid
$("#jqg").jqGrid(
'setGridParam',
{
datatype: 'json'
}
).trigger('reloadGrid');
}
}
});
}
}
});
To enable customer selection, we will create a read-only field with a button at its right hand side for opening
the form displaying the customer selection grid.
131
Creating Web Applications in Entity Framework with MVC
Now we will write a function for opening the customer module that invokes the Bootstrap library to create a
dialog box containing the grid from which a customer can be selected. It is actually the same grid we used earlier
but, this time, it is enclosed by a dialog box. A click on the OK button will place the customer identifier and the
customer name into the input fields of the parent dialog box for editing invoices.
/**
* Display a window for selecting a customer
*/
function showCustomerWindow() {
// the main block of the dialog
132
Creating Web Applications in Entity Framework with MVC
// title
$("<h5>").addClass("modal-title")
.html("Select customer")
.appendTo(dlgHeader);
// body of dialogue
var dlgBody = $('<div>')
.addClass("modal-body")
.appendTo(dlgContent);
// button "OK"
$("<button>")
.attr('type', 'button')
.addClass('btn')
.html('OK')
.on('click', function () {
var rowId = $("#jqgCustomer").jqGrid("getGridParam", "selrow");
var row = $("#jqgCustomer").jqGrid("getRowData", rowId);
// To save the identifier and customer name
// to the input elements of the parent form
$('#dlgEditInvoice input[name=CUSTOMER_ID]').val(rowId);
$('#dlgEditInvoice input[name=CUSTOMER_NAME]').val(row["NAME"]);
dlg.modal('hide');
})
.appendTo(dlgFooter);
133
Creating Web Applications in Entity Framework with MVC
// button "Cancel"
$("<button>")
.attr('type', 'button')
.addClass('btn')
.html('Cancel')
.on('click', function () { dlg.modal('hide'); })
.appendTo(dlgFooter);
dlg.on('hidden.bs.modal', function () {
dlg.remove();
});
// show dialog
dlg.modal();
134
Creating Web Applications in Entity Framework with MVC
name: 'ADDRESS',
width: 300,
sortable: false,
editable: true,
search: false,
edittype: "textarea",
editoptions: { maxlength: 250, cols: 30, rows: 4 }
},
{
label: 'Zip Code',
name: 'ZIPCODE',
width: 60,
sortable: false,
editable: true,
search: false,
edittype: "text",
editoptions: { size: 30, maxlength: 10 },
},
{
label: 'Phone',
name: 'PHONE',
width: 85,
sortable: false,
editable: true,
search: false,
edittype: "text",
editoptions: { size: 30, maxlength: 14 },
}
],
loadonce: false,
pager: '#jqgCustomerPager',
rowNum: 500, // number of rows displayed
sortname: 'NAME', // sort by default by NAME column
sortorder: "asc",
height: 500
});
dbGrid.jqGrid('navGrid', '#jqgCustomerPager',
{
search: true,
add: false,
edit: false,
del: false,
view: false,
refresh: true,
searchtext: "Search",
viewtext: "View",
viewtitle: "Selected record",
refreshtext: "Refresh"
}
);
}
All that is left to write for the invoice module is the showChildGrid function that enables the invoice lines to
be displayed and edited. Our function will create a grid with invoice lines dynamically after a click on the '+'
button to show the details.
135
Creating Web Applications in Entity Framework with MVC
Loading data for the lines requires passing the primary key from the selected invoice header.
$('<div>')
.attr('id', childGridPagerID)
.addClass('scroll')
.appendTo($('#' + parentRowID));
136
Creating Web Applications in Entity Framework with MVC
},
{
label: 'Product ID',
name: 'PRODUCT_ID',
hidden: true,
editrules: { edithidden: true, required: true },
editable: true,
edittype: 'custom',
editoptions: {
custom_element: function (value, options) {
// create hidden input
return $("<input>")
.attr('type', 'hidden')
.attr('rowid', options.rowId)
.addClass("FormElement")
.addClass("form-control")
.val(value)
.get(0);
}
}
},
{
label: 'Product',
name: 'Product',
width: 300,
editable: true,
edittype: "text",
editoptions: {
size: 50,
maxlength: 60,
readonly: true
},
editrules: { required: true }
},
{
label: 'Price',
name: 'Price',
formatter: 'currency',
editable: true,
editoptions: {
readonly: true
},
align: "right",
width: 100
},
{
label: 'Quantity',
name: 'Quantity',
align: "right",
width: 100,
editable: true,
editrules: { required: true, number: true, minValue: 1 },
editoptions: {
dataEvents: [
{
type: 'change',
fn: function (e) {
var quantity = $(this).val() - 0;
137
Creating Web Applications in Entity Framework with MVC
var price =
$('#dlgEditInvoiceLine input[name=Price]').val() - 0;
$('#dlgEditInvoiceLine input[name=Total]').val(quantity * price);
}
}
],
defaultValue: 1
}
},
{
label: 'Total',
name: 'Total',
formatter: 'currency',
align: "right",
width: 100,
editable: true,
editoptions: {
readonly: true
}
}
],
loadonce: false,
width: '100%',
height: '100%',
pager: "#" + childGridPagerID
});
138
Creating Web Applications in Entity Framework with MVC
139
Creating Web Applications in Entity Framework with MVC
};
};
}
Now we are done with creating the invoice module. Although the showProductWindow function that is used
to select a product from the list while filling out invoice lines is not examined here, it is totally similar to the
showCustomerWindow function that we examined earlier to implement the selection of customers from the
customer module.
An observant reader might have noticed that the functions for displaying the selection from the module and for
displaying the module itself were almost identical. Something you could do yourself to improve the code is to
move these functions into separate .js script files.
Authentication
The ASP.NET technology has a powerful mechanism for managing authentication in .NET applications called
ASP.NET Identity. The infrastructure of OWIN and AspNet Identity make it possible to perform both standard
authentication and authentication via external services through accounts in Google, Twitter, Facebook, et al.
The description of the ASP.NET Identity technology is quite comprehensive and goes beyond the scope of this
publication but you can read about it at http://www.asp.net/identity.
For our application, we will take a less complicated approach based on form authentication. Enabling form
authentication entails some changes in the web.config configuration file. Find the <system.web> section and
insert the following subsection inside it:
<authentication mode="Forms">
<forms name="cookies" timeout="2880" loginUrl="~/Account/Login"
defaultUrl="~/Invoice/Index"/>
</authentication>
Setting mode="Forms" enables form authentication. Some parameters need to follow it. The following list of
parameters is available:
cookieless
specifies whether cookie sets are used and how they are used. It can take the following values:
• UseCookies—specifies that the cookie sets will always be used, regardless of the device
• UseUri—cookies sets are never used
• AutoDetect—if the device supports cookie sets, they are used, otherwise, they are not used; a test deter-
mining their support is run for this setting.
• UseDeviceProfile—if the device supports cookie sets, they are used, otherwise, they are not used; no
detection test is run. Used by default.
defaultUrl
specifies the URL to redirect to after authentication
domain
specifies cookie sets for the entire domain, allowing for the same cookie sets to be used for the main domain
and its sub-domains. By default, its value is an empty string.
140
Creating Web Applications in Entity Framework with MVC
loginUrl
the URL for user authentication. The default value is "~/Account/Login".
name
specifies the name for the cookie set. The default value is ".ASPXAUTH".
path
specifies the path for the cookie set. The default value is "/".
requireSSL
specifies whether an SSL connection is required for sending cookie sets. The default value is false
timeout
specifies the timeout for cookies in minutes.
In our application, we will store authentication data in the same database that stores all other data to avoid the
need for an additional connection string.
[Table("Firebird.WEBUSER")]
public partial class WEBUSER
{
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage",
"CA2214:DoNotCallOverridableMethodsInConstructors")]
public WEBUSER()
{
WEBUSERINROLES = new HashSet<WEBUSERINROLE>();
}
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int WEBUSER_ID { get; set; }
[Required]
[StringLength(63)]
public string EMAIL { get; set; }
[Required]
[StringLength(63)]
public string PASSWD { get; set; }
[System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Usage",
"CA2227:CollectionPropertiesShouldBeReadOnly")]
public virtual ICollection<WEBUSERINROLE> WEBUSERINROLES { get; set; }
}
We'll add two more models: one for the description of roles (WEBROLE) and another one for bi
[Table("Firebird.WEBROLE")]
public partial class WEBROLE
{
141
Creating Web Applications in Entity Framework with MVC
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int WEBROLE_ID { get; set; }
[Required]
[StringLength(63)]
public string NAME { get; set; }
}
[Table("Firebird.WEBUSERINROLE")]
public partial class WEBUSERINROLE
{
[Key]
[DatabaseGenerated(DatabaseGeneratedOption.None)]
public int ID { get; set; }
[Required]
public int WEBUSER_ID { get; set; }
[Required]
public int WEBROLE_ID { get; set; }
We will use the Fluent API to specify relations between WEBUSER and WEBUSERINROLE in the DbM
…
public virtual DbSet<WEBUSER> WEBUSERS { get; set; }
public virtual DbSet<WEBROLE> WEBROLES { get; set; }
public virtual DbSet<WEBUSERINROLE> WEBUSERINROLES { get; set; }
…
protected override void OnModelCreating(DbModelBuilder modelBuilder)
{
modelBuilder.Entity<WEBUSER>()
.HasMany(e => e.WEBUSERINROLES)
.WithRequired(e => e.WEBUSER)
.WillCascadeOnDelete(false);
…
}
…
Since we use the Database First technology, tables in the database can be created automatically. I prefer to
control the process so here is a script for creating the additional tables:
142
Creating Web Applications in Entity Framework with MVC
SET TERM ^;
SET TERM ;^
143
Creating Web Applications in Entity Framework with MVC
Usually, some hash from the password, rather than the actual password, is stored in an open form, using the
md5 algorithm, for example. For our example, we have simplified authentication somewhat.
Our code will not interact directly with the WebUser model during registration and authentication. Instead, we
will add some special models to the project:
namespace FBMVCExample.Models
{
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
using System.Data.Entity.Spatial;
// Login model
public class LoginModel
{
[Required]
public string Name { get; set; }
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
}
[Required]
[DataType(DataType.Password)]
public string Password { get; set; }
[Required]
[DataType(DataType.Password)]
[Compare("Password", ErrorMessage = " Passwords do not match ")]
144
Creating Web Applications in Entity Framework with MVC
These models will be used for the authentication and registration views, respectively. The authentication view
is coded as follows:
@model FBMVCExample.Models.LoginModel
@{
ViewBag.Title = "Login";
}
<h2>Login</h2>
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<div class="form-horizontal">
@Html.ValidationSummary(true)
<div class="form-group">
<div class="form-group">
@Html.LabelFor(model => model.Password,
new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Password)
@Html.ValidationMessageFor(model => model.Password)
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Logon" class="btn btn-default" />
</div>
</div>
</div>
}
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}
@{
145
Creating Web Applications in Entity Framework with MVC
ViewBag.Title = "Registration";
}
<h2>???????????</h2>
@using (Html.BeginForm())
{
@Html.AntiForgeryToken()
<div class="form-horizontal">
@Html.ValidationSummary(true)
<div class="form-group">
@Html.LabelFor(model => model.Name,
new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Name)
@Html.ValidationMessageFor(model => model.Name)
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.Password,
new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.Password)
@Html.ValidationMessageFor(model => model.Password)
</div>
</div>
<div class="form-group">
@Html.LabelFor(model => model.ConfirmPassword,
new { @class = "control-label col-md-2" })
<div class="col-md-10">
@Html.EditorFor(model => model.ConfirmPassword)
@Html.ValidationMessageFor(model => model.ConfirmPassword)
</div>
</div>
<div class="form-group">
<div class="col-md-offset-2 col-md-10">
<input type="submit" value="Register"
class="btn btn-default" />
</div>
</div>
</div>
}
@section Scripts {
@Scripts.Render("~/bundles/jqueryval")
}
146
Creating Web Applications in Entity Framework with MVC
The model, views and controllers for user authentication and registration are made as simple as possible in this
example. A user usually has a lot more attributes than just a username and a password.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Mvc;
using System.Web.Security;
using FBMVCExample.Models;
namespace FBMVCExample.Controllers
{
public class AccountController : Controller
{
public ActionResult Login()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Login(LoginModel model)
{
if (ModelState.IsValid)
{
// search user in db
WEBUSER user = null;
using (DbModel db = new DbModel())
{
user = db.WEBUSERS.FirstOrDefault(
u => u.EMAIL == model.Name &&
u.PASSWD == model.Password);
}
// if you find a user with a login and password,
// then remember it and do a redirect to the start page
if (user != null)
{
FormsAuthentication.SetAuthCookie(model.Name, true);
return RedirectToAction("Index", "Invoice");
}
else
{
ModelState.AddModelError("",
" A user with such a username and password does not exist ");
}
}
return View(model);
}
[Authorize(Roles = "admin")]
public ActionResult Register()
{
147
Creating Web Applications in Entity Framework with MVC
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public ActionResult Register(RegisterModel model)
{
if (ModelState.IsValid)
{
WEBUSER user = null;
using (DbModel db = new DbModel())
{
user = db.WEBUSERS.FirstOrDefault(u => u.EMAIL == model.Name);
}
if (user == null)
{
// create a new user
using (DbModel db = new DbModel())
{
// get a new identifier using a sequence
int userId = db.NextValueFor("SEQ_WEBUSER");
db.WEBUSERS.Add(new WEBUSER {
WEBUSER_ID = userId,
EMAIL = model.Name,
PASSWD = model.Password
});
db.SaveChanges();
user = db.WEBUSERS.Where(u => u.WEBUSER_ID == userId)
.FirstOrDefault();
// find the role of manager
// This role will be the default role, i.e.
// will be issued automatically upon registration
var defaultRole =
db.WEBROLES
.Where(r => r.NAME == "manager")
.FirstOrDefault();
// Assign the default role to the newly added user
if (user != null && defaultRole != null)
{
db.WEBUSERINROLES.Add(new WEBUSERINROLE
{
WEBUSER_ID = user.WEBUSER_ID,
WEBROLE_ID = defaultRole.WEBROLE_ID
});
db.SaveChanges();
}
}
// if the user is successfully added to the database
if (user != null)
{
FormsAuthentication.SetAuthCookie(model.Name, true);
return RedirectToAction("Login", "Account");
}
}
else
{
ModelState.AddModelError("",
"User with such login already exists");
148
Creating Web Applications in Entity Framework with MVC
}
}
return View(model);
}
Note the attribute [Authorize(Roles = "admin")] to stipulate that only a user with the admin role can
perform the user registration operation. This mechanism is called an authentication filter. We will get back to
it a bit later.
FormsAuthentication.SetAuthCookie(model.Name, true);
All information about a user in Asp.Net MVC is stored in the proprty HttpContext.User that implements the
IPrincipal interface defined in the System.Security.Principal namespace.
The IPrincipal interface defines the Identity property that stores the object of the IIdentity interface describing
the current user.
To determine whether a user is logged in, ASP.NET MVC receives cookies from the browser and if the user
is logged in, the property IIdentity.IsAuthenticated is set to true and the Name property gets the username as
its value.
Next, we will add authentication items using the universal providers mechanism.
Universal Providers
Universal providers offer a ready-made authentication functionality. At the same time, these providers are flex-
ible enough that we can redefine them to work in whatever way we need them to. It is not necessary to redefine
and use all four providers. That is handy if we do not need all of the fancy ASP.NET Identity features, but just
a very simple authentication system.
So, our next step is to redefine the role provider. To do this, we need to add the Microsoft.AspNet.Providers
package using NuGet.
149
Creating Web Applications in Entity Framework with MVC
To define the role provider, first we add the Providers folder to the project and then add a new MyRoleProvider
class to it:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Security;
using FBMVCExample.Models;
namespace FBMVCExample.Providers
{
public class MyRoleProvider : RoleProvider
{
/// <summary>
/// Returns the list of user roles
/// </summary>
/// <param name="username">Username</param>
/// <returns></returns>
public override string[] GetRolesForUser(string username)
{
string[] roles = new string[] { };
using (DbModel db = new DbModel())
{
// Get the user
WEBUSER user = db.WEBUSERS.FirstOrDefault(
u => u.EMAIL == username);
if (user != null)
{
// fill in an array of available roles
int i = 0;
roles = new string[user.WEBUSERINROLES.Count];
foreach (var rolesInUser in user.WEBUSERINROLES)
{
roles[i] = rolesInUser.WEBROLE.NAME;
i++;
}
}
}
return roles;
}
/// <summary>
/// Creating a new role
/// </summary>
/// <param name="roleName">Role name</param>
public override void CreateRole(string roleName)
{
using (DbModel db = new DbModel())
{
WEBROLE newRole = new WEBROLE() { NAME = roleName };
db.WEBROLES.Add(newRole);
db.SaveChanges();
150
Creating Web Applications in Entity Framework with MVC
}
}
/// <summary>
/// Returns whether the user role is present
/// </summary>
/// <param name="username">User name</param>
/// <param name="roleName">Role name</param>
/// <returns></returns>
public override bool IsUserInRole(string username, string roleName)
{
bool outputResult = false;
using (DbModel db = new DbModel())
{
var userInRole =
from ur in db.WEBUSERINROLES
where ur.WEBUSER.EMAIL == username &&
ur.WEBROLE.NAME == roleName
select new { id = ur.ID };
outputResult = userInRole.Count() > 0;
}
return outputResult;
}
151
Creating Web Applications in Entity Framework with MVC
To use the role provider in the application, we need to add its definition to the configuration file. Open the
web.config file and remove the definition of providers added automatically during the installation of the
Microsoft.AspNet.Providers package.
<system.web>
<authentication mode="Forms">
<forms name="cookies" timeout="2880" loginUrl="~/Account/Login"
defaultUrl="~/Invoice/Index"/>
</authentication>
<roleManager enabled="true" defaultProvider="MyRoleProvider">
<providers>
<add name="MyRoleProvider"
type="FBMVCExample.Providers.MyRoleProvider" />
</providers>
</roleManager>
</system.web>
[Authorize(Roles = "admin")]
public ActionResult Register()
{…
152
Creating Web Applications in Entity Framework with MVC
This filter can be used at two levels: on a controller as a whole and on an individual operation of a controller. We
will set different rights for our main controllers: CustomerController, InvoiceController and ProductController.
In our project, a user with the MANAGER role can view and edit data in all three tables. Setting a filter for the
InvoiceController controller would be coded as follows:
[Authorize(Roles = "manager")]
public class InvoiceController : Controller
{
private DbModel db = new DbModel();
// Show view
public ActionResult Index()
{
return View();
}
…
Source Code
The source code for the sample application can be obtained from FBMVCExample.zip.
153
Chapter 6
If your server supports PHP, you just create your .php files, put them in your web directory and the server will
automatically parse them for you. PHP-enabled files are simply HTML files with a whole language of custom
tags embedded in them. There is nothing to compile.
For the drivers to work with the Windows PATH system variable, the fbclient.dll DLL file must be
available. Copying the DLL file from the PHP directory or a Firebird installation to the Windows system folder
would work, because the system directory is in the PATH variable by default. However, it is not recommended.
The more robust way to do it is to prepend the file path to the PATH variable explicitly yourself, using the
Windows advanced administration tool.
154
Developing Web Applications with PHP and Firebird
Make sure you have the matching release version of the Firebird client for your Firebird server.
To install the extension, uncomment this line in the php.ini configuration file:
extension=php_interbase.dll
extension=php_interbase.so
In Linux, one of the following commands should work. The one you use depends on the distribution package
and the versions it supports:
Tip
You might need to enable third party repositories if you find you have unresolvable dependency problems.
Programming Style
The Firebird/InterBase extension uses a procedural approach to developing programs. Functions with the ibase_
prefix can return or accept the identifier (ID) of a connection, transaction, prepared query or cursor (the result
of the SELECT query) as one of their parameters. This identifier is a server-allocated resource which, like all
allocated resources, should be released immediately it is no longer needed.
The PHP functions will not be described in detail here. You can study their descriptions at http://php.net/ibase.
Several small examples with comments will be provided instead.
<?php
$db = 'localhost:example';
$username = 'SYSDBA';
$password = 'masterkey';
// Connect to database
$dbh = ibase_connect($db, $username, $password);
155
Developing Web Applications with PHP and Firebird
The ibase_pconnect function, that creates so-called “persistent connections”, could be used instead of
ibase_connect. A call to ibase_close on this style of connection does not close it but all resources allocated to it
will be released. The default transaction is committed, while any others are rolled back. This type of connection
can be re-used in another session if the connection parameters match.
Persistent connections can increase the performance of a web application, sometimes considerably. It is espe-
cially noticeable if establishing a connection involves a lot of traffic. They allow a child process to use the same
connection throughout its entire lifetime instead of creating a connection every time a page interacts with the
Firebird server. Persistent connections are not unlike working with a connection pool.
Need to know
Many ibase_ functions cannot accommodate the identifier of a connection, transaction or prepared query. Those
functions use the identifier of the last established connection or last started transaction instead of the relevant
identifier. Ii is not a recommended practice, especially if your web application can use more than one connec-
tion.
ibase_query
The ibase_query function executes an SQL query and returns the identifier of the result or True if the query
returns no data set. Along with the connection or transaction ID and the text of the SQL query, this function can
accept a variable number of parameters to populate the SQL query parameters. For example,
// …
$sql = 'SELECT login, email FROM users WHERE id=?';
$id = 1;
// Execute query
$rc = ibase_query($dbh, $sql, $id);
// Get the result row by row as object
if ($row = ibase_fetch_object($rc)) {
echo $row->email, "\n";
}
// Release the handle associated with the result of the query
ibase_free_result($rc);
// …
156
Developing Web Applications with PHP and Firebird
Parameterized queries are typically used multiple times with fresh sets of parameter values each time. Pre-
pared queries are recommended for this style of usage. The identifier of a query is returned by the function
ibase_prepare and then the prepared query is executed using the function ibase_execute.
// …
$sql = 'SELECT login, email FROM users WHERE id=?';
// Prepare statement
$sth = ibase_prepare($dbh, $sql);
$id = 1;
// Execute statement
$rc = ibase_execute($sth, $id);
// Get the result row by row as object
if ($row = ibase_fetch_object($rc)) {
echo $row->email, "\n";
}
// Release the handle associated with the result of the query
ibase_free_result($rc);
// Release the prepared statement
ibase_free_query($sth);
Prepared queries are very often used when a large amount of data input is anticipated.
// …
$sql = 'INSERT INTO users(login, email) VALUES(?, ?)';
// Prepare statement
$sth = ibase_prepare($dbh, $sql);
$users = [["user1", "[email protected]"], ["user2", "[email protected]"]];
// Execute statement
foreach ($users as $user)) {
ibase_execute($sth, $user[0], $user[1]);
}
// Release the prepared statement
ibase_free_query($sth);
// …
It is actually a disadvantage of this extension that functions can take a variable number of parameters. It less than
ideal for parameterized queries, as the last example demonstrates. It is especially noticeable if you try to write a
universal class for executing any query. It would be much more useful to be able to send parameters in one array.
157
Developing Web Applications with PHP and Firebird
ibase_trans
By default, the Fb/IB extension commits the transaction automatically after executing each SQL query, mak-
ing it necessary to start a transaction with the function ibase_trans if you need to control transactions explic-
itly. An explicit transaction is started with the following parameters if none are provided: IBASE_WRITE |
IBASE_CONCURRENCY | IBASE_WAIT. You can find the description of predefined constants for specifying
the parameters of a transaction here. A transaction must be completed by either ibase_commit or ibase_rollback.
This extension supports the COMMIT RETAIN and ROLLBACK RETAIN parameters directly if you use the
functions ibase_commit_ret or ibase_rollback_ret, respectively, instead.
Note
The default transaction parameters are good for most cases and it is really rarely that you need to change them.
A connection to the database, along with all resources allocated to it, exists for no longer than it takes for the
PHP script to complete. Even if you use persistent connections, all allocated resources will be released after
the ibase_close function is called. Even so, I strongly recommend releasing all allocated resources explicitly
by calling the corresponding ibase_ functions.
I advise strongly against using the ibase_commit_ret and ibase_rollback_ret functions because they have no
place in a web application. The purpose of COMMIT RETAIN and ROLLBACK RETAIN is to keep cursors
open in desktop applications when a transaction ends.
Warning
ibase_ functions raise no exception if an error occurs, although an error will cause some to return False. Note
that it is essential to use the === strict relational operator to compare the result to False. Calling any ibase
function could result in an error.
The function ibase_errmsg is available to discover an error message and the function ibase_errcode can provide
the error code.
158
Developing Web Applications with PHP and Firebird
The Fb/IB extension can interact with the Firebird server by way of functions that wrap calls to the Ser-
vices API: ibase_service_attach, ibase_service_detach, ibase_server_info, ibase_maintain_db, ibase_db_info,
ibase_backup, ibase_restore. They can return information about the Firebird server, initiate a backup or restore
or get statistics. We are not examining them in detail, since they are required mainly to administer a database,
a topic that is outside the scope of this project.
Firebird Events
The Firebird/Interbase extension also supports working with Firebird events by means of a set of functions:
ibase_set_event_handler, ibase_free_event_handler, ibase_wait_event.
PDO and all basic drivers are built into PHP as extensions. To use them, just enable them by editing the php.ini
file as follows:
extension=php_pdo.dll
Note
This step is optional for PHP versions 5.3 and higher because DLLs are no longer needed for PDO to work.
Firebird-specific Library
The other requirement is for database-specific DLLs to be configured; or else loaded during execution by means
of the dl() function; or else included in php.ini following php_pdo.dll. For example:
extension=php_pdo.dll
extension=php_pdo_firebird.dll
In Linux, one of the following commands should work. The one you use depends on the distribution package
and the versions it supports:
159
Developing Web Applications with PHP and Firebird
Programming Style
PDO uses an object-oriented approach to developing programs. The DSN (Data Source Name), a.k.a. connection
string, determines which specific driver will be used in PDO. The DSN consists of a prefix that determines the
database type and a set of parameters in the form of <key>=<value> separated by semicolons. The valid set
of parameters depends on the database type.
To be able to work with Firebird, the connection string must start with the firebird: prefix and conform to
the format described in the PDO_FIREBIRD DSN section of the documentation.
Making Connections
Connections are established automatically during creation of the PDO from its abstract class. The class construc-
tor accepts parameters to specify the data source (DSN) and also the optional username and password, if any. A
fourth parameter can be used to pass an array of driver-specific connection settings in the key=value format.
$dsn = 'firebird:dbname=localhost:example;charset=utf8;';
$username = 'SYSDBA';
$password = 'masterkey';
try {
// Connect to database
$dbh = new \PDO($dsn, $username, $password,
[\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]);
$sql = 'SELECT login, email FROM users';
// Execute query
$query = $dbh->query($sql);
// Get the result row by row as object
while ($row = $query->fetch(\PDO::FETCH_OBJ)) {
echo $row->email, "\n";
}
$query->closeCursor();
} catch (\PDOException $e) {
echo $e->getMessage();
}
Persistent connections
For PDO to use persistent connections, the array of attributes must be passed to the PDO constructor with
PDO::ATTR_PERSISTENT => true.
Exception Handling
The PDO driver is much more friendly than the Firebird/InterBase extension with respect to exception handling.
Setting the \PDO::ATTR_ERRMODE attribute to the value \PDO::ERRMODE_EXCEPTION specifies a mode in
which any error, including a database connection error, will raise the exception \PDOException.
This is superior to the laborious procedure of checking whether an error has occurred each time an ibase_
function is called.
160
Developing Web Applications with PHP and Firebird
Querying
The query method executes an SQL query and returns the result set in the form of a \PDOStatement object.
A fetch to this method can return the result in more than one form: it could be a column, an instance of the
specified class, an object.
For executing an SQL query that returns no data set, you can use the exec method that returns the number of
affected rows.
Parameterized Queries
If there are parameters in the query, prepared queries must be used. For this, the prepare method is called
instead of the query method. The prepare method returns an object of the \PDOStatement class that encapsulates
methods for working with prepared queries and their results. Executing the query requires calling the execute
method that can accept as its parameter an array of named or unnamed parameters.
The result of executing a SELECT query can be obtained with one the following methods: fetch, fetchAll, fetch-
Column, fetchObject. The fetch and fetchAll methods can return results in various forms: an associative array,
an object or an instance of a particular class. The class instance option is quite often used in the MVC pattern
during work with models.
$dsn = 'firebird:dbname=localhost:example;charset=utf8;';
$username = 'SYSDBA';
$password = 'masterkey';
try {
// Connect to database
$dbh = new \PDO($dsn, $username, $password,
[\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]);
$sql = 'INSERT INTO users(login, email) VALUES(?, ?)';
$users = [
["user1", "[email protected]"],
["user2", "[email protected]"]
];
// Prepare statement
$query = $dbh->prepare($sql);
// Execute statement
foreach ($users as $user)) {
$query->execute($user);
}
} catch (\PDOException $e) {
echo $e->getMessage();
}
161
Developing Web Applications with PHP and Firebird
$dsn = 'firebird:dbname=localhost:example;charset=utf8;';
$username = 'SYSDBA';
$password = 'masterkey';
try {
// Connect to database
$dbh = new \PDO($dsn, $username, $password,
[\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]);
$sql = 'INSERT INTO users(login, email) VALUES(:login, :email)';
$users = [
[":login" => "user1", ":email" => "[email protected]"],
[":login" => "user2", ":email" => "[email protected]"]
];
// Prepare statement
$query = $dbh->prepare($sql);
// Execute statement
foreach ($users as $user)) {
$query->execute($user);
}
} catch (\PDOException $e) {
echo $e->getMessage();
}
Note
In order to support named parameters, PDO preprocesses the query and replaces parameters of the :paramname
type with "?", retaining the array of correspondence between the parameter names and their left-to-right posi-
tions in the query. For that reason, the EXECUTE BLOCK statement will not work if there are colon-prefixed
variables. Currently, PDO offers no workaround to support a parameterized EXECUTE BLOCK statement,
such as by specifying an alternative prefix for parameters as has been implemented in some access components.
Another Way to Do It
An alternative way to pass parameters to a query is by using “binding”. The bindValue method binds a value
to a named or unnamed parameter. The bindParam method binds a variable to a named or unnamed parameter.
The bindParam method is especially useful for stored procedures that return a value via the OUT or IN OUT
parameter, which is different to the mechanism for returning values from stored procedures in Firebird.
$dsn = 'firebird:dbname=localhost:example;charset=utf8;';
$username = 'SYSDBA';
$password = 'masterkey';
try {
// Connect to database
$dbh = new \PDO($dsn, $username, $password,
[\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]);
$sql = 'INSERT INTO users(login, email) VALUES(:login, :email)';
$users = [
["user1", "[email protected]"],
["user2", "[email protected]"]
];
// Prepare statement
$query = $dbh->prepare($sql);
// Execute statement
foreach ($users as $user)) {
$query->bindValue(":login", $user[0]);
162
Developing Web Applications with PHP and Firebird
$query->bindValue(":email", $user[1]);
$query->execute();
}
} catch (\PDOException $e) {
echo $e->getMessage();
}
Caution
The numbers associated with unnamed parameters for the bindParam and bindValue methods start from 1.
$dsn = 'firebird:dbname=localhost:example;charset=utf8;';
$username = 'SYSDBA';
$password = 'masterkey';
try {
// Connect to database
$dbh = new \PDO($dsn, $username, $password,
[\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]);
$sql = 'INSERT INTO users(login, email) VALUES(?, ?)';
$users = [
["user1", "[email protected]"],
["user2", "[email protected]"]
];
// Prepare statement
$query = $dbh->prepare($sql);
// Execute statement
foreach ($users as $user)) {
$query->bindValue(1, $user[0]);
$query->bindValue(2, $user[1]);
$query->execute();
}
} catch (\PDOException $e) {
echo $e->getMessage();
}
Transactions
By default, PDO commits the transaction automatically after executing each SQL query. If you want to control
transactions explicitly, you need to start a transaction with the method \PDO::beginTransaction. By default, a
transaction is started with the following parameters: CONCURRENCY | WAIT | READ_WRITE. A transaction
can be ended with the \PDO::commit or \PDO::rollback method.
$username = 'SYSDBA';
$password = 'masterkey';
try {
// Connect to database
$dbh = new \PDO($dsn, $username, $password,
[\PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION]);
// Start the transaction to ensure consistency between statements
$dbh->beginTransaction();
// Get users from one table
$users_stmt = $dbh->prepare('SELECT login, email FROM old_users');
163
Developing Web Applications with PHP and Firebird
$users_stmt->execute();
$users = $users_stmt->fetchAll(\PDO::FETCH_OBJECT);
$users_stmt->closeCursor();
// And insert into another table
$sql = 'INSERT INTO users(login, email) VALUES(?, ?)';
// Prepapre statemenet
$query = $dbh->prepare($sql);
// Execute statememt
foreach ($users as $user)) {
$query->bindValue(1, $user->LOGIN);
$query->bindValue(2, $user->EMAIL]);
$query->execute();
}
// Commit transaction
$dbh->commit();
} catch (\PDOException $e) {
// Rollback transaction
if ($dbh && $dbh->inTransaction())
$dbh->rollback();
echo $e->getMessage();
}
Unfortunately, the beginTransaction method does not permit transaction parameters to be changed, but you can
do the trick by specifying transaction parameters in the SQL statement SET TRANSACTION.
164
Developing Web Applications with PHP and Firebird
From these comparisons we can conclude that PDO is better equipped than the FB/IB extension for most frame-
works.
Having decided to use the MVC pattern, we do have a few issues to think about. Development of an application
modeled on this pattern is not so easy as it may seem, especially if we do not use third-party libraries. If you write
everything on your own, you will have to solve a lot of problems: automatically loading .php files enabling
the definition of classes, routing, and so on.
Several frameworks have been created for solving these problems, such as Yii, Laravel, Symphony, Kohana and
many more. My personal preference is Laravel, so the development of the application described here is going
to use this framework.
Installing Laravel
Before installing Laravel, make sure that your system environment meets the requirements.
165
Developing Web Applications with PHP and Firebird
• PDO extension
• MCrypt extension
• OpenSSL extension
• Mbstring extension
• Tokenizer extension
Installing Composer
Laravel uses Composer to manage dependencies. Install Composer first and then install Laravel.
The easiest way to install Composer on Windows is by downloading and running the installation file: Compos-
er-Setup.exe. The installation wizard will install Composer and configure PATH so that you can run Composer
from the command line in any directory.
If you need to install Composer manually, go to https://getcomposer.org/download/ and pick up a fresh instal-
lation script that will do as follows:
Caution
Because this script changes with each new version of the installer, you will always need to have the latest
version when reinstalling.
After you run the script, the composer.phar file will appear. The .phar extension marks an archive but,
actually, it is a PHP script that can understand only a few commands (install, update, ...) and can download and
unpack libraries.
Windows
If you are working in Windows, you can make it easier to work with Composer by creating the composer.
bat file. Run the following command:
Then set up your PATH so that you can just call composer from any directory in your command shell.
Installing Laravel
Now, to install Laravel:
166
Developing Web Applications with PHP and Firebird
Creating a Project
If the installation is successful, we can carry on with creating the project framework. Enter:
Wait until it finishes creating the project framework. A description of the directory structure can be found in
the Laravel documentation.
• app—the main directory of our application. Models will be located in the root directory. The Http subdi-
rectory contains everything that is related to working with the browser. The Http/Controllers subdi-
rectory contains our controllers.
• config—the directory with configuration files. You will discover more details about the configuration
process later.
• public—the root directory of the web application (DocumentRoot). It contains static files: css, js, images,
etc.
• resources—contains views, localization files and, if any, LESS files, SASS and js applications on such
frameworks as ReactJS, AngularJS or Ember that are later put together into the public folder with an external
tool.
• The root directory of our application contains the composer.json file that describes the packages our
application will need besides those that are already present in Laravel.
We will need two such packages: zofe/rapyd-laravel for building a quick interface with grids and edit dialog
boxes, and sim1984/laravel-firebird, an extension for working with Firebird databases.
Caution
Remember to set the minimum-stability parameter to 'dev' because the package is not stable enough to publish
at https://packagist.org. You will need to modify the composer.json file (see below) to add a reference to the
gitHub repository.
167
Developing Web Applications with PHP and Firebird
"repositories": [
{
"type": "package",
"package": {
"version": "dev-master",
"name": "sim1984/laravel-firebird",
"source": {
"url": "https://github.com/sim1984/laravel-firebird",
"type": "git",
"reference": "master"
},
"autoload": {
"classmap": [""]
}
}
}
],
Use the require section to add the required packages in the following way:
"zofe/rapyd": "2.2.*",
"sim1984/laravel-firebird": "dev-master"
Now you can start updating the packages with the following command, which must be started in the root directory
of the web application:
composer update
On completion of that command, the new packages will be installed in your application.
Configuration
Now we can get down to configuration. To get it started, execute the following command to create additional
configuration files for the zofe/rapyd package:
We add two new providers to the file config/app.php by adding two new entries to the providers key:
Zofe\Rapyd\RapydServiceProvider::class,
Firebird\FirebirdServiceProvider::class,
We proceed to the file config/databases.conf (not to be confused with databases.conf in your Firebird
server root!) that contains the database connection settings. Add the following lines to the connections key:
168
Developing Web Applications with PHP and Firebird
'firebird' => [
'driver' => 'firebird',
'host' => env('DB_HOST', 'localhost'),
'port' => env('DB_PORT', '3050'),
'database' => env('DB_DATABASE', 'examples'),
'username' => env('DB_USERNAME', 'SYSDBA'),
'password' => env('DB_PASSWORD', 'masterkey'),
'charset' => env('DB_CHARSET', 'UTF8'),
'engine_version' => '3.0.0',
],
Since we will use our connection as the default connection, specify the following:
Pay attention to the env function that is used to read the environment variables of the application from the
special .env file located in the root directory of the project. Correct the following lines in the .env file:
DB_CONNECTION=firebird
DB_HOST=localhost
DB_PORT=3050
DB_DATABASE=examples
DB_USERNAME=SYSDBA
DB_PASSWORD=masterkey
Edit the config/rapyd.php configuration file to change the date and time formats to match those used in
your locale:
'fields' => [
'attributes' => ['class' => 'form-control'],
'date' => [
'format' => 'Y-m-d',
],
'datetime' => [
'format' => 'Y-m-d H:i:s',
'store_as' => 'Y-m-d H:i:s',
],
],
That completes the initial configuration. Now we can start building the logic of the web application.
Creating Models
The Laravel framework supports the Eloquent ORM, an elegant and simple implementation of the ActiveRecord
pattern for working with a database. Each table has a corresponding class model that works with it. Models
169
Developing Web Applications with PHP and Firebird
enable the application to read data from tables and write data to a table. The model we are going to work with
complies fully with the one illustrated earlier, at the beginning of the Database chapter.
namespace App;
use Firebird\Eloquent\Model;
/**
* Primary key of the model
*
* @var string
*/
protected $primaryKey = 'CUSTOMER_ID';
/**
* Our model does not have a timestamp
*
* @var bool
*/
public $timestamps = false;
/**
* The name of the sequence for generating the primary key
*
* @var string
*/
protected $sequence = 'GEN_CUSTOMER_ID';
}
Notice that we use the modified Firebird\Eloquent\Model model from the sim1984/laravel-firebird
package as the basis. It allows us to use the sequence specified in the $sequence attribute to generate values
for the primary key ID.
170
Developing Web Applications with PHP and Firebird
namespace App;
use Firebird\Eloquent\Model;
/**
* Primary key of the model
*
* @var string
*/
protected $primaryKey = 'PRODUCT_ID';
/**
* Our model does not have a timestamp
*
* @var bool
*/
public $timestamps = false;
/**
* The name of the sequence for generating the primary key
*
* @var string
*/
protected $sequence = 'GEN_PRODUCT_ID';
}
namespace App;
use Firebird\Eloquent\Model;
/**
* Table associated with the model
*
* @var string
*/
protected $table = 'INVOICE';
/**
* Primary key of the model
*
* @var string
*/
171
Developing Web Applications with PHP and Firebird
/**
* Our model does not have a timestamp
*
* @var bool
*/
public $timestamps = false;
/**
* The name of the sequence for generating the primary key
*
* @var string
*/
protected $sequence = 'GEN_INVOICE_ID';
/**
* Customer
*
* @return \App\Customer
*/
public function customer() {
return $this->belongsTo('App\Customer', 'CUSTOMER_ID');
}
/**
* Invoice lines
* @return \App\InvoiceLine[]
*/
public function lines() {
return $this->hasMany('App\InvoiceLine', 'INVOICE_ID');
}
/**
* Payed
*/
public function pay() {
$connection = $this->getConnection();
$attributes = $this->attributes;
$connection->executeProcedure('SP_PAY_FOR_INOVICE',
[$attributes['INVOICE_ID']]);
}
}
You'll observe some additional functions in this model. The customer function returns the customer that relates
to the invoice header via the CUSTOMER_ID field. The belongsTo method is used for establishing this relation.
The name of the model class and the name of the relation field are passed to this method.
The function lines returns items from the invoice that are represented by a collection of InvoiceLine models,
described later. To establish the one-to-many relation in the lines function, the name of the class model and the
relation field are passed to the hasMany method.
You can find more details about specifying relations between entities in the Relationships section of the Laravel
documentation.
The pay function performs payment of an invoice by calling the stored procedure SP_PAY_FOR_INVOICE, pass-
ing the identifier of the invoice header. The value of any field (model attribute) can be obtained from the attribute
attribute. The executeProcedure method calls the stored procedure.
172
Developing Web Applications with PHP and Firebird
Note
namespace App;
use Firebird\Eloquent\Model;
use Illuminate\Database\Eloquent\Builder;
/**
* Table associated with the model
*
* @var string
*/
protected $table = 'INVOICE_LINE';
/**
* Primary key of the model
*
* @var string
*/
protected $primaryKey = 'INVOICE_LINE_ID';
/**
* Our model does not have a timestamp
*
* @var bool
*/
public $timestamps = false;
/**
* The name of the sequence for generating the primary key
*
* @var string
*/
protected $sequence = 'GEN_INVOICE_LINE_ID';
/**
* Array of names of computed fields
*
* @var array
*/
protected $appends = ['SUM_PRICE'];
/**
* Product
*
* @return \App\Product
173
Developing Web Applications with PHP and Firebird
*/
public function product() {
return $this->belongsTo('App\Product', 'PRODUCT_ID');
}
/**
* Amount by item
*
* @return double
*/
public function getSumPriceAttribute() {
return $this->SALE_PRICE * $this->QUANTITY;
}
/**
* Adding a model object to the database
* Override this method, because in this case, we work with a stored procedure
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param array $options
* @return bool
*/
protected function performInsert(Builder $query, array $options = []) {
if ($this->fireModelEvent('creating') === false) {
return false;
}
$connection = $this->getConnection();
$attributes = $this->attributes;
$connection->executeProcedure('SP_ADD_INVOICE_LINE', [
$attributes['INVOICE_ID'],
$attributes['PRODUCT_ID'],
$attributes['QUANTITY']
]);
// We will go ahead and set the exists property to true,
// so that it is set when the created event is fired, just in case
// the developer tries to update it during the event. This will allow
// them to do so and run an update here.
$this->exists = true;
$this->wasRecentlyCreated = true;
$this->fireModelEvent('created', false);
return true;
}
/**
* Saving changes to the current model instance in the database
* Override this method, because in this case, we work with a stored procedure
*
* @param \Illuminate\Database\Eloquent\Builder $query
* @param array $options
* @return bool
*/
protected function performUpdate(Builder $query, array $options = []) {
$dirty = $this->getDirty();
if (count($dirty) > 0) {
// If the updating event returns false, we will cancel
// the update operation so developers can hook Validation systems
// into their models and cancel this operation if the model does
// not pass validation. Otherwise, we update.
174
Developing Web Applications with PHP and Firebird
/**
* Deleting the current model instance from the database
* Override this method, because in this case, we work with a stored procedure
*
* @return void
*/
protected function performDeleteOnModel() {
$connection = $this->getConnection();
$attributes = $this->attributes;
$connection->executeProcedure('SP_DELETE_INVOICE_LINE',
[$attributes['INVOICE_LINE_ID']]);
}
}
The product function in this model returns the product, actually the App/Product model that was specified as the
invoice item. The relation is established through the PRODUCT_ID field by the belongsTo method.
The SumPrice is a calculated field, calculated by the function getSumPriceAttribute. For a calculated field to be
available in the model, its name must be specified in the $appends array that stores the names of calculated fields.
Operations
In this model, we redefined the insert, update and delete operations so that they are performed through stored
procedures. Along with performing the insert, update and delete operations, these stored procedures recalculate
the total in the invoice header. We could have avoided doing that, but then we would have had to modify several
models in one transaction. Later, we will examine how to do it that way.
$customers = DB::table('CUSTOMER')->get();
This query constructor is quite a powerful tool for building and executing SQL queries. You can also direct it
to filter, sort and merge tables. For example:
175
Developing Web Applications with PHP and Firebird
DB::table('users')
->join('contacts', function ($join) {
$join->on('users.id', '=', 'contacts.user_id')->orOn(...);
})
->get()
Nevertheless, models are more convenient to work with. You can find the description of Eloquent ORM models
and the syntax for querying them at https://laravel.com/docs/5.2/eloquent.
As an example, to retrieve all elements from the collection of customers would require executing the following
query:
$customers = Customer::all();
$customers = App\Customer::select()
->orderBy('name')
->take(20)
->get();
Complex Models
When a model is more complex, its relationships or relationship collections can be retrieved via dynamic at-
tributes. The following query, for example, returns the items of the invoice that has the identifier 1:
$lines = Invoice::find(1)->lines;
Records are added by creating an instance of the model, initiating its attributes and saving the model using the
save method:
Updating a record involves finding it, accepting changes to the appropriate attributes and saving it with the save
method:
$flight = App\Flight::find(1);
$flight->name = 'New Flight Name';
$flight->save();
To delete a record, involves finding it and calling the delete method.
$flight = App\Flight::find(1);
$flight->delete();
176
Developing Web Applications with PHP and Firebird
The destroy method allows a record to be deleted more rapidly by its key value, without needing to retrieve
its instance:
App\Flight::destroy(1);
There are other ways of deleting records, for instance, “soft” deletion. You can read more about deletion methods
at https://laravel.com/docs/5.2/eloquent#deleting-models.
Transactions
Now let us talk a little about transactions. Without going into the fine detail, I will demonstrate how transactions
and the Eloquent ORM can be used together.
DB::transaction(function () {
// Create a new position in the invoice
$line = new App\InvoiceLine();
$line->CUSTOMER_ID = 45;
$line->PRODUCT_ID = 342;
$line->QUANTITY = 10;
$line->COST = 12.45;
$line->save();
// add the sum of the line item to the amount of the invoice
$invoice = App\Invoice::find($line->CUSTOMER_ID);
$invoice->INVOICE_SUM += $line->SUM_PRICE;
$invoice->save();
});
Every parameter of the transaction method that is located inside the callback function is executed within one
transaction.
Route::get('/', function () {
return 'Hello World';
});
Route::post('foo/bar', function () {
return 'Hello World';
});
In the first example, we register the handler of the GET request for the website root for the POST request with
the route /foo/bar in the second.
177
Developing Web Applications with PHP and Firebird
You can register a route for several types of HTTP requests. For example:
You can extract some part of the URL from the route for use as a parameter in the handling function:
You can find more details about routing configuration in the Routing chapter of the documentation. Routes are
configured in the app/Http/routes.php file in Laravel 5.2 and in the routes/wep.php file in Laravel
5.3.
All Laravel controllers must extend the basic class of the controller App\Http\Controllers\Controller
that exists in Laravel by default. You can read more details about writing controllers at https://laravel.com/docs/
5.2/controllers.
A Customer Controller
First, we'll write our Customer controller.
<?php
/*
* Customer controller
*/
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Customer;
178
Developing Web Applications with PHP and Firebird
// sorted alphabetically
$customers = Customer::select()
->orderBy('NAME')
->take(20)
->get();
var_dump($customers);
}
}
Now we have to link the controller methods to the route. For this, add the following line to routes.php
(web.php):
Route::get('/customers', 'CustomerController@showCustomers');
The controller name is separated from the method name with the @ character.
To build a quick interface with grids and edit dialog boxes, we will use the zofe/rapyd package that was
enabled earlier. Classes from the zofe/rapyd package take up the role of building standard queries to Eloquent
ORM models. We will change the customer controller so that it shows data on the grid, allows filtering and
record insertions, updates and deletes by way of the edit dialog boxes.
<?php
/*
* Customer Controller
*/
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Customer;
/**
* Displays the list of customers
*
* @return Response
*/
public function showCustomers() {
// Connect widget for search
$filter = \DataFilter::source(new Customer);
// Search will be by the name of the supplier
$filter->add('NAME', 'Name', 'text');
// Set capture for search button
$filter->submit('Search');
// Add the filter reset button and assign it caption
$filter->reset('Reset');
// Create a grid to display the filtered data
$grid = \DataGrid::source($filter);
// output columns
// Field, label, sorted
$grid->add('NAME', 'Name', true);
$grid->add('ADDRESS', 'Address');
$grid->add('ZIPCODE', 'Zip Code');
$grid->add('PHONE', 'Phone');
// Add buttons to view, edit and delete records
179
Developing Web Applications with PHP and Firebird
/**
* Add, edit and delete a customer
*
* @return Response
*/
public function editCustomer() {
if (\Input::get('do_delete') == 1)
return "not the first";
// create an editor
$edit = \DataEdit::source(new Customer());
// Set title of the dialog, depending on the type of operation
switch ($edit->status) {
case 'create':
$edit->label('Add customer');
break;
case 'modify':
$edit->label('Edit customer');
break;
case 'do_delete':
$edit->label('Delete customer');
break;
case 'show':
$edit->label("Customer's card");
// add a link to go back to the list of customers
$edit->link('customers', 'Back', 'TR');
break;
}
// set that after the operations of adding, editing and deleting,
// you need to return to the list of customers
$edit->back('insert|update|do_delete', 'customers');
// We add editors of a certain type, assign them a label and
// associate them with the attributes of the model
$edit->add('NAME', 'Name', 'text')->rule('required|max:60');
$edit->add('ADDRESS', 'Address', 'textarea')
->attributes(['rows' => 3])
->rule('max:250');
$edit->add('ZIPCODE', 'Zip code', 'text')->rule('max:10');
$edit->add('PHONE', 'Phone', 'text')->rule('max:14');
// display the template customer_edit and pass it to the editor
return $edit->view('customer_edit', compact('edit'));
}
}
180
Developing Web Applications with PHP and Firebird
blade Templates
By default, Laravel uses the blade template engine. The view function finds the necessary template in the re-
sources/views directory, makes the necessary changes to it and returns the text of the HTML page, at the
same time passing to it any variables that are supplied in the template. You can find the description of the blade
template syntax at https://laravel.com/docs/5.2/blade.
@extends('example')
@section('title', 'Customers')
@section('body')
<h1>Customers</h1>
<p>
{!! $filter !!}
{!! $grid !!}
</p>
@stop
This template is inherited from the example template and redefines its body section. The $filter and $grid vari-
ables contain the HTML code for filtering and displaying data on the grid. The example template is common
for all pages.
@extends('master')
@section('title', 'Example of working with Firebird')
@section('body')
<h1>??????</h1>
@if(Session::has('message'))
<div class="alert alert-success">
{!! Session::get('message') !!}
</div>
@endif
<p>Example of working with Firebird.<br/>
</p>
@stop
@section('content')
@include('menu')
@yield('body')
@stop
This template is itself inherited from the master template and also enables the menu template. The menu is quite
simple and consists of three items: Customers, Products and Invoices.
181
Developing Web Applications with PHP and Firebird
The master template enables css styles and JavaScript files with libraries.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>@yield('title', 'An example of a Web application on Firebird')</title>
<meta name="description" content="@yield('description',
'An example of a Web application on Firebird')" />
@section('meta', '')
<link href="http://fonts.googleapis.com/css?family=Bitter" rel="stylesheet"
type="text/css" />
<link href="//netdna.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css"
rel="stylesheet">
<link href="//maxcdn.bootstrapcdn.com/font-awesome/4.1.0/css/font-awesome.min.css"
rel="stylesheet">
{!! Rapyd::styles(true) !!}
</head>
<body>
<div id="wrap">
<div class="container">
<br />
<div class="row">
<div class="col-sm-12">
@yield('content')
</div>
</div>
</div>
</div>
<div id="footer">
</div>
<script src="//ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js">
182
Developing Web Applications with PHP and Firebird
</script>
<script src="//netdna.bootstrapcdn.com/bootstrap/3.2.0/js/bootstrap.min.js">
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery.pjax/1.9.6/jquery.pjax.min.js">
<script src="https://cdnjs.cloudflare.com/ajax/libs/riot/2.2.4/riot+compiler.min.js"></scr
{!! Rapyd::scripts() !!}
</body>
</html>
@extends('example')
@section('title', 'Edit customer')
@section('body')
<p>
{!! $edit !!}
</p>
@stop
A Product Controller
Implementation of the product controller is similar to what we did for the customer controller:
<?php
/*
* Product Controller
*/
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Product;
/**
* Displays a list of products
*
* @return Response
*/
public function showProducts() {
// Connect widget for search
$filter = \DataFilter::source(new Product);
// The search will be by product name
$filter->add('NAME', 'Name', 'text');
$filter->submit('Search');
$filter->reset('Reset');
// Create a grid to display the filtered data
$grid = \DataGrid::source($filter);
// output grid columns
// Field, label, sorting
$grid->add('NAME', 'Name', true);
183
Developing Web Applications with PHP and Firebird
/**
* Add, edit and delete products
*
* @return Response
*/
public function editProduct() {
if (\Input::get('do_delete') == 1)
return "not the first";
// create editor
$edit = \DataEdit::source(new Product());
// Set the title of the dialog, depending on the type of operation
switch ($edit->status) {
case 'create':
$edit->label('Add product');
break;
case 'modify':
$edit->label('Edit product');
break;
case 'do_delete':
$edit->label('Delete product');
break;
case 'show':
$edit->label("Product's card");
$edit->link('products', 'Back', 'TR');
break;
}
// set that after the operations of adding, editing and deleting,
// you need to return to the list of products
$edit->back('insert|update|do_delete', 'products');
// We add editors of a certain type, assign them a label and
// associate them with the attributes of the model
$edit->add('NAME', 'Name', 'text')->rule('required|max:100');
$edit->add('PRICE', 'Price', 'text')->rule('max:19');
$edit->add('DESCRIPTION', 'Description', 'textarea')
->attributes(['rows' => 8])
->rule('max:8192');
// display the template product_edit and pass it to the editor
return $edit->view('product_edit', compact('edit'));
}
}
184
Developing Web Applications with PHP and Firebird
<?php
/*
* Invoice controller
*/
namespace App\Http\Controllers;
use App\Http\Controllers\Controller;
use App\Invoice;
use App\Customer;
use App\Product;
use App\InvoiceLine;
/**
* Show invoice list
*
* @return Response
*/
public function showInvoices() {
// The invoice model will also select the related suppliers
$invoices = Invoice::with('customer');
// Add a widget for search.
$filter = \DataFilter::source($invoices);
// Let's filter by date range
$filter->add('INVOICE_DATE', 'Date', 'daterange');
// and filter by customer name
$filter->add('customer.NAME', 'Customer', 'text');
$filter->submit('Search');
$filter->reset('Reset');
// Create a grid to display the filtered data
$grid = \DataGrid::source($filter);
// output grid columns
// Field, caption, sorted
// For the date we set an additional function that converts
// the date into a string
$grid->add('INVOICE_DATE|strtotime|date[Y-m-d H:i:s]', 'Date', true);
// for money we will set a format with two decimal places
$grid->add('TOTAL_SALE|number_format[2,., ]', 'Amount');
$grid->add('customer.NAME', 'Customer');
// Boolean printed as Yes/No
$grid->add('PAID', 'Paid')
->cell(function( $value, $row) {
return $value ? 'Yes' : 'No';
});
// set the function of processing each row
$grid->row(function($row) {
185
Developing Web Applications with PHP and Firebird
$grid->orderBy('INVOICE_DATE', 'desc');
// set the number of records per page
$grid->paginate(10);
// display the customer template and pass the filter and grid to it
return view('invoice', compact('filter', 'grid'));
}
/**
* Add, edit and delete invoice
*
* @return Response
*/
public function editInvoice() {
// get the text of the saved error, if it was
$error_msg = \Request::old('error_msg');
// create an invoice invoice editor
$edit = \DataEdit::source(new Invoice());
// if the invoice is paid, then we generate an error when trying to edit it
if (($edit->model->PAID) && ($edit->status === 'modify')) {
$edit->status = 'show';
$error_msg = 'Editing is not possible. The account has already been paid.';
}
// if the invoice is paid, then we generate an error when trying to delete it
if (($edit->model->PAID) && ($edit->status === 'delete')) {
$edit->status = 'show';
$error_msg = 'Deleting is not possible. The account has already been paid.';
}
// Set the label of the dialog, depending on the type of operation
switch ($edit->status) {
case 'create':
$edit->label('Add invoice');
break;
case 'modify':
$edit->label('Edit invoice');
break;
case 'do_delete':
$edit->label('Delete invoice');
break;
case 'show':
$edit->label('Invoice');
$edit->link('invoices', 'Back', 'TR');
// If the invoice is not paid, we show the pay button
if (!$edit->model->PAID)
186
Developing Web Applications with PHP and Firebird
$edit->link('invoice/pay/' . $edit->model->INVOICE_ID,
'Pay', 'BL');
break;
}
// set that after the operations of adding, editing and deleting,
// we return to the list of invoices
$edit->back('insert|update|do_delete', 'invoices');
// set the "date" field, that it is mandatory
// The default is the current date
$edit->add('INVOICE_DATE', '????', 'datetime')
->rule('required')
->insertValue(date('Y-m-d H:i:s'));
// add a field for entering the customer. When typing a customer name,
// a list of prompts will be displayed
$edit->add('customer.NAME', 'Customer', 'autocomplete')
->rule('required')
->options(Customer::lists('NAME', 'CUSTOMER_ID')
->all());
// add a field that will display the invoice amount, read-only
$edit->add('TOTAL_SALE', 'Amount', 'text')
->mode('readonly')
->insertValue('0.00');
// add paid checkbox
$paidCheckbox = $edit->add('PAID', 'Paid', 'checkbox')
->insertValue('0')
->mode('readonly');
$paidCheckbox->checked_output = 'Yes';
$paidCheckbox->unchecked_output = 'No';
// create a grid to display the invoice line rows
$grid = $this->getInvoiceLineGrid($edit->model, $edit->status);
// we display the invoice_edit template and pass the editor and grid to
// it to display the invoice invoice items
return $edit->view('invoice_edit', compact('edit', 'grid', 'error_msg'));
}
/**
* Payment of invoice
*
* @return Response
*/
public function payInvoice($id) {
try {
// find the invoice by ID
$invoice = Invoice::findOrFail($id);
// call the payment procedure
$invoice->pay();
} catch (\Illuminate\Database\QueryException $e) {
// if an error occurs, select the exclusion text
$pos = strpos($e->getMessage(), 'E_INVOICE_ALREADY_PAYED');
if ($pos !== false) {
// redirect to the editor page and display the error there
return redirect('invoice/edit?show=' . $id)
->withInput(['error_msg' => 'Invoice already paid']);
} else
throw $e;
}
// redirect to the editor page
return redirect('invoice/edit?show=' . $id);
187
Developing Web Applications with PHP and Firebird
/**
* Returns the grid for the invoice item
* @param \App\Invoice $invoice
* @param string $mode
* @return \DataGrid
*/
private function getInvoiceLineGrid(Invoice $invoice, $mode) {
// Get invoice items
// For each ivoice item, the associated product will be initialized
$lines = InvoiceLine::with('product')
->where('INVOICE_ID', $invoice->INVOICE_ID);
// Create a grid for displaying invoice items
$grid = \DataGrid::source($lines);
// output grid columns
// Field, caption, sorted
$grid->add('product.NAME', 'Name');
$grid->add('QUANTITY', 'Quantity');
$grid->add('SALE_PRICE|number_format[2,., ]', 'Price')
->style('min-width: 8em;');
$grid->add('SUM_PRICE|number_format[2,., ]', 'Amount')
->style('min-width: 8em;');
// set the function of processing each row
$grid->row(function($row) {
$row->cell('QUANTITY')->style("text-align: right");
// The monetary values are pressed to the right
$row->cell('SALE_PRICE')->style("text-align: right");
$row->cell('SUM_PRICE')->style("text-align: right");
});
if ($mode == 'modify') {
// Add buttons to view, edit and delete records
$grid->edit('/invoice/editline', '??????????????', 'modify|delete');
// Add a button to add an invoice item
$grid->link('/invoice/editline?invoice_id=' . $invoice->INVOICE_ID,
"Add item", "TR");
}
return $grid;
}
/**
* Add, edit and delete invoice items
*
* @return Response
*/
public function editInvoiceLine() {
if (\Input::get('do_delete') == 1)
return "not the first";
$invoice_id = null;
// create the editor of the invoice item
$edit = \DataEdit::source(new InvoiceLine());
// Set the label of the dialog, depending on the type of operation
switch ($edit->status) {
case 'create':
$edit->label('Add invoice item');
$invoice_id = \Input::get('invoice_id');
break;
188
Developing Web Applications with PHP and Firebird
case 'modify':
$edit->label('Edit invoice item');
$invoice_id = $edit->model->INVOICE_ID;
break;
case 'delete':
$invoice_id = $edit->model->INVOICE_ID;
break;
case 'do_delete':
$edit->label('Delete invoice item');
$invoice_id = $edit->model->INVOICE_ID;
break;
}
// make url to go back
$base = str_replace(\Request::path(), '', strtok(\Request::fullUrl(), '?'));
$back_url = $base . 'invoice/edit?modify=' . $invoice_id;
// set the page to go back
$edit->back('insert|update|do_delete', $back_url);
$edit->back_url = $back_url;
// add a hidden field with an invoice code
$edit->add('INVOICE_ID', '', 'hidden')
->rule('required')
->insertValue($invoice_id)
->updateValue($invoice_id);
// Add a field for entering the goods. When you type the product name,
// a list of prompts is displayed.
$edit->add('product.NAME', 'Name', 'autocomplete')
->rule('required')
->options(Product::lists('NAME', 'PRODUCT_ID')->all());
// Field for input quantity
$edit->add('QUANTITY', 'Quantity', 'text')
->rule('required');
// display the template invoice_line_edit and pass it to the editor
return $edit->view('invoice_line_edit', compact('edit'));
}
}
@extends('example')
@section('title','Edit invoice')
@section('body')
<div class="container">
{!! $edit->header !!}
@if($error_msg)
<div class="alert alert-danger">
<strong>??????!</strong> {{ $error_msg }}
</div>
189
Developing Web Applications with PHP and Firebird
@endif
{!! $edit->message !!}
@if(!$edit->message)
<div class="row">
<div class="col-sm-4">
{!! $edit->render('INVOICE_DATE') !!}
{!! $edit->render('customer.NAME') !!}
{!! $edit->render('TOTAL_SALE') !!}
{!! $edit->render('PAID') !!}
</div>
</div>
{!! $grid !!}
@endif
{!! $edit->footer !!}
</div>
@stop
Route::get('/', 'InvoiceController@showInvoices');
Route::get('/customers', 'CustomerController@showCustomers');
Route::any('/customer/edit', 'CustomerController@editCustomer');
Route::get('/products', 'ProductController@showProducts');
Route::any('/product/edit', 'ProductController@editProduct');
Route::get('/invoices', 'InvoiceController@showInvoices');
Route::any('/invoice/edit', 'InvoiceController@editInvoice');
Route::any('/invoice/pay/{id}', 'InvoiceController@payInvoice');
Route::any('/invoice/editline', 'InvoiceController@editInvoiceLine');
Here the /invoice/pay/{id} route picks up the invoice identifier from the URL and sends it to the payIn-
voice method. The rest of the routes should be self-explanatory.
The Result
Some screenshots from the web application we developed in this project.
190
Developing Web Applications with PHP and Firebird
191
Developing Web Applications with PHP and Firebird
Source Code
You can download the source code for this project from phpfbexample.zip
192
Chapter 7
Creating an Application
with jOOQ and Spring MVC
This chapter will describe how to create a web application in the Java language using the Spring MVC frame-
work, the jOOQ library and a Firebird sample database.
To make development easier, you can use one of the popular IDEs for Java (NetBeans, IntelliJ IDEA, Eclipse,
JDeveloper and others). I used NetBeans.
For testing and debugging purposes, we will also need to install one of the web servers or application servers
(Apache Tomcat or GlassFish). We are basing our project on the Maven web application templates.
3. Create the jsp, jspf and resources folders inside the WEB-INF folder
193
Creating an Application with jOOQ and Spring MVC
The WEB-INF/jsp folder will contain jsp pages and the jspf folder will contain page fragments that will be
added to other pages using the following directive:
The resources folder is used to store static web resources—the WEB-INF/resources/css folder for cas-
cading style sheet files, the WEB-INF/resources/fonts folder for font files, the WEB-INF/resources/
js folder for JavaScript files and third-party JavaScript libraries.
Now, we modify the pom.xml file and add the general properties of the application, dependencies on library
packages (Spring MVC, Jaybird, JDBC pool, JOOQ) and the properties of the JDBC connection.
194
Creating an Application with jOOQ and Spring MVC
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>ru.ibase</groupId>
<artifactId>fbjavaex</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>war</packaging>
<properties>
<endorsed.dir>${project.build.directory}/endorsed</endorsed.dir>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<spring.version>4.3.4.RELEASE</spring.version>
<jstl.version>1.2</jstl.version>
<javax.servlet.version>3.0.1</javax.servlet.version>
<db.url>jdbc:firebirdsql://localhost:3050/examples</db.url>
<db.driver>org.firebirdsql.jdbc.FBDriver</db.driver>
<db.username>SYSDBA</db.username>
<db.password>masterkey</db.password>
</properties>
<dependencies>
<dependency>
<groupId>javax</groupId>
<artifactId>javaee-web-api</artifactId>
<version>7.0</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>${javax.servlet.version}</version>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>jstl</groupId>
<artifactId>jstl</artifactId>
<version>${jstl.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.8.5</version>
</dependency>
195
Creating an Application with jOOQ and Spring MVC
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.8.5</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-webmvc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>${spring.version}</version>
</dependency>
<dependency>
<groupId>org.firebirdsql.jdbc</groupId>
<artifactId>jaybird-jdk18</artifactId>
<version>3.0.0</version>
</dependency>
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.4</version>
</dependency>
196
Creating an Application with jOOQ and Spring MVC
<dependency>
<groupId>org.jooq</groupId>
<artifactId>jooq</artifactId>
<version>3.9.2</version>
</dependency>
<dependency>
<groupId>org.jooq</groupId>
<artifactId>jooq-meta</artifactId>
<version>3.9.2</version>
</dependency>
<dependency>
<groupId>org.jooq</groupId>
<artifactId>jooq-codegen</artifactId>
<version>3.9.2</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>${spring.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.1</version>
<configuration>
<source>1.7</source>
<target>1.7</target>
<compilerArguments>
<endorseddirs>${endorsed.dir}</endorseddirs>
</compilerArguments>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-war-plugin</artifactId>
<version>2.3</version>
<configuration>
<failOnMissingWebXml>false</failOnMissingWebXml>
</configuration>
197
Creating an Application with jOOQ and Spring MVC
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<version>2.6</version>
<executions>
<execution>
<phase>validate</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<outputDirectory>${endorsed.dir}</outputDirectory>
<silent>true</silent>
<artifactItems>
<artifactItem>
<groupId>javax</groupId>
<artifactId>javaee-endorsed-api</artifactId>
<version>7.0</version>
<type>jar</type>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>
What is a POM?
A Project Object Model or POM is the fundamental unit of work in Maven. It is an XML file that contains
information about the project and configuration details used by Maven to build the project. More details can
be found at http://maven.apache.org/guides/introduction/introduction-to-the-pom.
After all the necessary dependencies have been fulfilled, a restart of the POM is recommended, to load all the
necessary libraries and avoid errors that might otherwise occur while you are working on the project. This is
how it is done in NetBeans:
198
Creating an Application with jOOQ and Spring MVC
199
Creating an Application with jOOQ and Spring MVC
I am creating Java configuration classes here as I am not a big fan of doing configuration in XML.
package ru.ibase.fbjavaex.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter;
import org.springframework.web.servlet.view.JstlView;
import org.springframework.web.servlet.view.UrlBasedViewResolver;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import java.util.List;
@Configuration
@ComponentScan("ru.ibase.fbjavaex")
@EnableWebMvc
public class WebAppConfig extends WebMvcConfigurerAdapter {
@Override
public void configureMessageConverters(
List<HttpMessageConverter<?>> httpMessageConverters) {
MappingJackson2HttpMessageConverter jsonConverter =
new MappingJackson2HttpMessageConverter();
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS,
false);
jsonConverter.setObjectMapper(objectMapper);
httpMessageConverters.add(jsonConverter);
}
@Bean
public UrlBasedViewResolver setupViewResolver() {
UrlBasedViewResolver resolver = new UrlBasedViewResolver();
resolver.setPrefix("/WEB-INF/jsp/");
resolver.setSuffix(".jsp");
resolver.setViewClass(JstlView.class);
return resolver;
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/resources/**")
200
Creating an Application with jOOQ and Spring MVC
.addResourceLocations("/WEB-INF/resources/");
}
}
Start-up Code—WebInitializer
Now we'll get rid of the Web.xml file and create the WebInitializer.java class in its place:
package ru.ibase.fbjavaex.config;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.ServletRegistration.Dynamic;
import org.springframework.web.WebApplicationInitializer;
import org.springframework.web.context.support.AnnotationConfigWebApplicationContext;
import org.springframework.web.servlet.DispatcherServlet;
@Override
public void onStartup(ServletContext servletContext) throws ServletException {
AnnotationConfigWebApplicationContext ctx =
new AnnotationConfigWebApplicationContext();
ctx.register(WebAppConfig.class);
ctx.setServletContext(servletContext);
Dynamic servlet = servletContext.addServlet("dispatcher",
new DispatcherServlet(ctx));
servlet.addMapping("/");
servlet.setLoadOnStartup(1);
}
All that is left to configure is IoC containers for injecting dependencies, a step we will return to later. We proceed
next to generating classes for working with the database via Java Object-Oriented Querying (jOOQ).
201
Creating an Application with jOOQ and Spring MVC
jOOQ Classes
jOOQ classes for working with the database are generated on the basis of the database schema described in the
earlier chapter, The examples.fdb Database.
To generate jOOQ classes for working with our database, you will need to download these binary files at http://
www.jooq.org/download or via the maven repository:
• jooq-3.9.2.jarThe main library included in our application for working with jOOQ
• jooq-meta-3.9.2.jar—The tool included in your build for navigating the database schema via generated objects
• jooq-codegen-3.9.2.jar—The tool included in your build for generating the database schema
Along with those, of course, you will need to download the Jaybird driver for connecting to the Firebird database
via JDBC: jaybird-full-3.0.0.jar.
<generator>
<name>org.jooq.util.JavaGenerator</name>
<database>
<!-- The type of the database. Format:
org.util.[database].[database]Database -->
<name>org.jooq.util.firebird.FirebirdDatabase</name>
<inputSchema></inputSchema>
<!-- Objects that are excluded when generating from your schema.
202
Creating an Application with jOOQ and Spring MVC
<target>
<!-- The name of the package to which the generated -->
<packageName>ru.ibase.fbjavaex.exampledb</packageName>
You can find more details about the process of generating classes at https://www.jooq.org/doc/3.9/manual-sin-
gle-page/#code-generation.
Dependency Injection
Dependency injection is a process whereby objects define their dependencies, that is, the other objects they
work with. It is done only through constructor arguments, arguments to a factory method, or properties set or
returned using a factory method. The container then injects those dependencies when it creates the bean. You
can find more details about dependency injection at http://docs.spring.io/spring/docs/current/spring-framework-
reference/htmlsingle/#beans.
As before, we will avoid xml configuration and base our approach on annotations and Java configuration.
The main attributes and parts of the Java configuration of an IoC container are classes with the @Configuration
annotation and methods with the @Bean annotation.
203
Creating an Application with jOOQ and Spring MVC
/**
* IoC container configuration
* to implement dependency injection.
*/
package ru.ibase.fbjavaex.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
import org.apache.commons.dbcp.BasicDataSource;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.jdbc.datasource.TransactionAwareDataSourceProxy;
import org.jooq.impl.DataSourceConnectionProvider;
import org.jooq.DSLContext;
import org.jooq.impl.DefaultDSLContext;
import org.jooq.impl.DefaultConfiguration;
import org.jooq.SQLDialect;
import org.jooq.impl.DefaultExecuteListenerProvider;
import ru.ibase.fbjavaex.exception.ExceptionTranslator;
import ru.ibase.fbjavaex.managers.*;
import ru.ibase.fbjavaex.jqgrid.*;
/**
* The Spring IoC configuration class of the container
*/
@Configuration
public class JooqConfig {
/**
* Return connection pool
*
* @return
*/
204
Creating an Application with jOOQ and Spring MVC
@Bean(name = "dataSource")
public DataSource getDataSource() {
BasicDataSource dataSource = new BasicDataSource();
// ?????????? ???????????? ???????????
dataSource.setUrl("jdbc:firebirdsql://localhost:3050/examples");
dataSource.setDriverClassName("org.firebirdsql.jdbc.FBDriver");
dataSource.setUsername("SYSDBA");
dataSource.setPassword("masterkey");
dataSource.setConnectionProperties("charSet=utf-8");
return dataSource;
}
/**
* Return transaction manager
*
* @return
*/
@Bean(name = "transactionManager")
public DataSourceTransactionManager getTransactionManager() {
return new DataSourceTransactionManager(getDataSource());
}
@Bean(name = "transactionAwareDataSource")
public TransactionAwareDataSourceProxy getTransactionAwareDataSource() {
return new TransactionAwareDataSourceProxy(getDataSource());
}
/**
* Return connection provider
*
* @return
*/
@Bean(name = "connectionProvider")
public DataSourceConnectionProvider getConnectionProvider() {
return new DataSourceConnectionProvider(getTransactionAwareDataSource());
}
/**
* Return exception translator
*
* @return
*/
@Bean(name = "exceptionTranslator")
public ExceptionTranslator getExceptionTranslator() {
return new ExceptionTranslator();
}
/**
* Returns the DSL context configuration
*
* @return
*/
@Bean(name = "dslConfig")
public org.jooq.Configuration getDslConfig() {
DefaultConfiguration config = new DefaultConfiguration();
// ?????????? ??????? SQL ???? Firebird
config.setSQLDialect(SQLDialect.FIREBIRD);
config.setConnectionProvider(getConnectionProvider());
205
Creating an Application with jOOQ and Spring MVC
DefaultExecuteListenerProvider listenerProvider =
new DefaultExecuteListenerProvider(getExceptionTranslator());
config.setExecuteListenerProvider(listenerProvider);
return config;
}
/**
* Return DSL context
*
* @return
*/
@Bean(name = "dsl")
public DSLContext getDsl() {
org.jooq.Configuration config = this.getDslConfig();
return new DefaultDSLContext(config);
}
/**
* Return customer manager
*
* @return
*/
@Bean(name = "customerManager")
public CustomerManager getCustomerManager() {
return new CustomerManager();
}
/**
* Return customer grid
*
* @return
*/
@Bean(name = "customerGrid")
public JqGridCustomer getCustomerGrid() {
return new JqGridCustomer();
}
/**
* Return product manager
*
* @return
*/
@Bean(name = "productManager")
public ProductManager getProductManager() {
return new ProductManager();
}
/**
* Return product grid
*
* @return
*/
@Bean(name = "productGrid")
public JqGridProduct getProductGrid() {
return new JqGridProduct();
}
/**
206
Creating an Application with jOOQ and Spring MVC
/**
* Return invoice grid
*
* @return
*/
@Bean(name = "invoiceGrid")
public JqGridInvoice getInvoiceGrid() {
return new JqGridInvoice();
}
/**
* Return invoice items grid
*
* @return
*/
@Bean(name = "invoiceLineGrid")
public JqGridInvoiceLine getInvoiceLineGrid() {
return new JqGridInvoiceLine();
}
/**
* Return working period
*
* @return
*/
@Bean(name = "workingPeriod")
public WorkingPeriod getWorkingPeriod() {
return new WorkingPeriod();
}
The org.jooq.impl.DSL class is the main one from which jOOQ objects are created. It acts as a static factory for
table expressions, column (or field) expressions, conditional expressions and many other parts of a query.
DSLContext references the org.jooq.Configuration object that configures the behavior of jOOQ during the exe-
cution of queries. Unlike with static DSL, with DSLContext you can to create SQL statements that are already
“configured” and ready for execution.
207
Creating an Application with jOOQ and Spring MVC
In our application, DSLContext is created in the getDsl method of the JooqConfig configuration class. Configu-
ration for DSLContext is returned by the getDslConfig method. In this method we specify the Firebird dialect
that we will use, the connection provider that determines how we get a connection via JDBC and the SQL query
execution listener.
jOOQ uses an informal BNF notation modelling a unified SQL dialect suitable for most database engines. Unlike
other, simpler frameworks that use the Fluent API or the chain method, the jOOQ-based BNF interface does
not permit bad query syntax.
Result<Record> result =
dsl.select()
.from(AUTHOR.as("a"))
.join(BOOK.as("b")).on(a.ID.equal(b.AUTHOR_ID))
.where(a.YEAR_OF_BIRTH.greaterThan(1920)
.and(a.FIRST_NAME.equal("Paulo")))
.orderBy(b.TITLE)
.fetch();
The AUTHOR and BOOK classes describing the corresponding tables must be generated beforehand. The process
of generating jOOQ classes according to the specified database schema was described earlier.
We specified table aliases for the AUTHOR and BOOK tables using the AS clause. Here is the same query in
DSL without aliases:
Result<Record> result =
dsl.select()
.from(AUTHOR)
.join(BOOK).on(AUTHOR.ID.equal(BOOK.AUTHOR_ID))
.where(AUTHOR.YEAR_OF_BIRTH.greaterThan(1920)
.and(AUTHOR.FIRST_NAME.equal("Paulo")))
.orderBy(BOOK.TITLE)
.fetch();
Now we take a more complex query with aggregate functions and grouping:
208
Creating an Application with jOOQ and Spring MVC
In jOOQ:
Note
'Dialect' in the jOOQ context represents not just the SQL dialect of the database but also the major version
number of the database engine. The field 'limit', limiting the number of records returned, will be generated
according to the SQL syntax available to the database engine. The example above used FIREBIRD_3_0, which
supports OFFSET … FETCH. If we had specified the FIREBIRD_2_5 or just the FIREBIRD dialect, the ROWS
clause would have been used instead.
You can build a query in parts. This will allow you to change it dynamically, to change the sort order or to add
additional filter conditions.
SelectFinalStep<?> select
= dsl.select()
.from(PRODUCT);
209
Creating an Application with jOOQ and Spring MVC
case "asc":
query.addOrderBy(PRODUCT.NAME.asc());
break;
case "desc":
query.addOrderBy(PRODUCT.NAME.desc());
break;
}
return query.fetchMaps();
dsl.select()
.from(BOOK)
.where(BOOK.ID.equal(5))
.and(BOOK.TITLE.equal("Animal Farm"))
.fetch();
dsl.select()
.from(BOOK)
.where(BOOK.ID.equal(val(5)))
.and(BOOK.TITLE.equal(val("Animal Farm")))
.fetch();
SELECT *
FROM BOOK
WHERE BOOK.ID = ?
AND BOOK.TITLE = ?
You need not concern yourself with the index position of the field value that corresponds to a parameter, as the
values will be bound to the appropriate parameter automatically. The index of the parameter list is 1-based. If
you need to change the value of a parameter, you just select it by its index number.
Select<?> select =
dsl.select()
.from(BOOK)
.where(BOOK.ID.equal(5))
.and(BOOK.TITLE.equal("Animal Farm"));
Param<?> param = select.getParam("2");
Param.setValue("Animals as Leaders");
210
Creating an Application with jOOQ and Spring MVC
Another way to assign a new value to a parameter is to call the bind method:
Query query1 =
dsl.select()
.from(AUTHOR)
.where(LAST_NAME.equal("Poe"));
query1.bind(1, "Orwell");
jOOQ supports named parameters, too. They need to be created explicitly using org.jooq.Param:
// Create a query with a named parameter. You can then use that name for
// accessing the parameter again
Query query1 =
dsl.select()
.from(AUTHOR)
.where(LAST_NAME.equal(param("lastName", "Poe")));
Param<?> param1 = query.getParam("lastName");
// You can now change the bind value directly on the Param reference:
param2.setValue("Orwell");
Another way to assign a new value to a parameter is to call the bind method:
// Or, with named parameters
Query query2 =
dsl.select()
.from(AUTHOR)
.where(LAST_NAME.equal(param("lastName", "Poe")));
query2.bind("lastName", "Orwell");
For our example, we will return the data to a map list (the fetchMaps method) which is handy to use for serializing
a result for JSON.
211
Creating an Application with jOOQ and Spring MVC
In jOOQ:
dsl.insertInto(AUTHOR,
AUTHOR.ID, AUTHOR.FIRST_NAME, AUTHOR.LAST_NAME)
.values(100, "Hermann", "Hesse")
.execute();
UPDATE AUTHOR
SET FIRST_NAME = 'Hermann',
LAST_NAME = 'Hesse'
WHERE ID = 3;
In jOOQ:
dsl.update(AUTHOR)
.set(AUTHOR.FIRST_NAME, "Hermann")
.set(AUTHOR.LAST_NAME, "Hesse")
.where(AUTHOR.ID.equal(3))
.execute();
In jOOQ:
dsl.delete(AUTHOR)
.where(AUTHOR.ID.equal(100))
.execute();
More complex update queries can be built in jOOQ, such as a MERGE query, for example.
212
Creating an Application with jOOQ and Spring MVC
spAddInvoice(dsl.configuration(),
invoiceId,
customerId,
invoiceDate);
is equivalent to getting the next value of the generator using the following SQL query:
jOOQ also provides tools to build simple DDL queries, but we do not cover them here.
Explicit Transactions
In jOOQ you have several ways to control transactions explicitly. Since we are going to develop our application
using the Spring Framework, we will use the transaction manager specified in the configuration (JooqConfig).
You can get the transaction manager by declaring the txMgr property in the class as follows:
@Autowired
private DataSourceTransactionManager txMgr;
The standard scenario for using this technique with a transaction would be coded like this:
213
Creating an Application with jOOQ and Spring MVC
dsl.insertInto(BOOK)
.set(BOOK.ID, 5)
.set(BOOK.AUTHOR_ID, 1)
.set(BOOK.TITLE, "Book 5")
.execute();
// transaction commit
txMgr.commit(tx);
}
catch (DataAccessException e) {
// transaction rollback
txMgr.rolback(tx);
}
However, Spring enables that scenario to be implemented much more easily using the @Transactional annotation
specified before the method of the class. Thereby, all actions performed by the method will be wrapped in the
transaction.
/**
* Delete customer
*
* @param customerId
*/
@Transactional(propagation = Propagation.REQUIRED,
isolation = Isolation.REPEATABLE_READ)
public void delete(int customerId) {
this.dsl.deleteFrom(CUSTOMER)
.where(CUSTOMER.CUSTOMER_ID.eq(customerId))
.execute();
}
Transaction Parameters
Propagation
The propagation parameter defines how to work with transactions if our method is called from an external
transaction.
• Propagation.REQUIRED—execute in the existing transaction if there is one. Otherwise, create a new one.
• Propagation.MANDATORY—execute in the existing transaction if there is one. Otherwise, raise an ex-
ception.
• Propagation.SUPPORTS—execute in the existing transaction if there is one. Otherwise, execute outside
the transaction.
• Propagation.NOT_SUPPORTED—always execute outside the transaction. If there is an existing one, it
will be suspended.
• Propagation.REQUIRES_NEW—always execute in a new independent transaction. If there is an existing
one, it will be suspended until the new transaction is ended.
• Propagation.NESTED—if there is an existing transaction, execute in a new so-called “nested” transaction.
If the nested transaction is rolled back, it will not affect the external transaction; if the external transaction
is rolled back, the nested one will be rolled back as well. If there is no existing transaction, a new one
is simply created.
• Propagation.NEVER—always execute outside the transaction. Raise an exception if there is an existing
one.
214
Creating an Application with jOOQ and Spring MVC
Isolation Level
The isolation parameter defines the isolation level. Five values are supported: DEFAULT,
READ_UNCOMMITTED, READ_COMMITTED, REPEATABLE_READ, SERIALIZABLE. If the DE-
FAULT value of the isolation parameter is specified, that level will be used.
The other isolation levels are taken from the SQL standard, not all of them supported exactly by Firebird.
Only the READ_COMMITED level corresponds in all of the criteria, so JDBC READ_COMMITTED
is mapped into read_committed in Firebird. REPEATABLE_READ is mapped into concurrency (SNAP-
SHOT) and SERIALIZABLE is mapped into consistency (SNAPSHOT TABLE STABILITY).
Firebird supports additional transaction parameters besides isolation level, viz. NO RECORD_VERSION/
RECORD_VERSION (applicable only to a transaction with READ COMMITTED isolation) and WAIT/
NO WAIT. The standard isolation levels can be mapped to Firebird transaction parameters by specifying
the properties of the JDBC connection (see more details in the Using Transactions chapter of Jaybird 2.1
JDBC driver Java Programmer's Manual.
If your transaction works with more than one query, it is recommended to use the REPEATABLE_READ
isolation level to maintain data consistency.
Read Mode
By default, a transaction is in the read-write mode. The readOnly property in the @Transactional annotation
can be used to specify that it is to be read-only.
To display data and page-by-page navigation elements in this grid, we need to return data in the JSON format,
the structure of which looks like this:
{
total: 100,
page: 3,
records: 3000,
rows: [
{id: 1, name: "Ada"},
{id: 2, name: "Smith"},
…
]
}
where
215
Creating an Application with jOOQ and Spring MVC
package ru.ibase.fbjavaex.jqgrid;
import java.util.List;
import java.util.Map;
/**
* A class describing the structure that is used in jqGrid
* Designed for JSON serialization
*
* @author Simonov Denis
*/
public class JqGridData {
/**
* Total number of pages
*/
private final int total;
/**
* The current page number
*/
private final int page;
/**
* Total number of records
*/
private final int records;
/**
* The actual data
*/
private final List<Map<String, Object>> rows;
/**
* Constructor
*
* @param total
* @param page
* @param records
* @param rows
*/
public JqGridData(int total, int page, int records,
List<Map<String, Object>> rows) {
this.total = total;
this.page = page;
this.records = records;
this.rows = rows;
}
/**
* Returns the total number of pages
*
* @return
216
Creating an Application with jOOQ and Spring MVC
*/
public int getTotal() {
return total;
}
/**
* Returns the current page
*
* @return
*/
public int getPage() {
return page;
}
/**
* Returns the total number of records
*
* @return
*/
public int getRecords() {
return records;
}
/**
* Return list of map
* This is an array of data to display in the grid
*
* @return
*/
public List<Map<String, Object>> getRows() {
return rows;
}
}
Now we will write an abstract class that will return that structure depending on the search and sorting conditions.
It will be a parent class for the entity-specific classes that return similar structures.
/*
* Abstract class for working with JqGrid
*/
package ru.ibase.fbjavaex.jqgrid;
import java.util.Map;
import java.util.List;
import org.jooq.DSLContext;
import org.springframework.beans.factory.annotation.Autowired;
/**
* Working with JqGrid
*
* @author Simonov Denis
*/
public abstract class JqGrid {
@Autowired(required = true)
217
Creating an Application with jOOQ and Spring MVC
/**
* Returns the total number of records
*
* @return
*/
public abstract int getCountRecord();
/**
* Returns the structure for JSON serialization
*
* @return
*/
public JqGridData getJqGridData() {
int recordCount = this.getCountRecord();
List<Map<String, Object>> records = this.getRecords();
int total = 0;
if (this.limit > 0) {
total = recordCount / this.limit + 1;
}
/**
* Returns the number of records per page
*
* @return
*/
public int getLimit() {
return this.limit;
}
/**
* Returns the offset to retrieve the first record on the page
*
* @return
*/
public int getOffset() {
return this.offset;
218
Creating an Application with jOOQ and Spring MVC
/**
* Returns field name for sorting
*
* @return
*/
public String getIdx() {
return this.sIdx;
}
/**
* Returns the sort order
*
* @return
*/
public String getOrd() {
return this.sOrd;
}
/**
* Returns the current page number
*
* @return
*/
public int getPageNo() {
return this.pageNo;
}
/**
* Returns an array of records as a list of maps
*
* @return
*/
public abstract List<Map<String, Object>> getRecords();
/**
* Returns field name for search
*
* @return
*/
public String getSearchField() {
return this.searchField;
}
/**
* Returns value for search
*
* @return
*/
public String getSearchString() {
return this.searchString;
}
/**
* Returns the search operation
*
* @return
219
Creating an Application with jOOQ and Spring MVC
*/
public String getSearchOper() {
return this.searchOper;
}
/**
* Sets the limit on the number of display records
*
* @param limit
*/
public void setLimit(int limit) {
this.limit = limit;
}
/**
* Sets the number of records to skip
*
* @param offset
*/
public void setOffset(int offset) {
this.offset = offset;
}
/**
* Sets the sorting
*
* @param sIdx
* @param sOrd
*/
public void setOrderBy(String sIdx, String sOrd) {
this.sIdx = sIdx;
this.sOrd = sOrd;
}
/**
* Sets the current page number
*
* @param pageNo
*/
public void setPageNo(int pageNo) {
this.pageNo = pageNo;
this.offset = (pageNo - 1) * this.limit;
}
/**
* Sets the search condition
*
* @param searchField
* @param searchString
* @param searchOper
*/
public void setSearchCondition(String searchField, String searchString,
String searchOper) {
this.searchFlag = true;
this.searchField = searchField;
this.searchString = searchString;
this.searchOper = searchOper;
}
220
Creating an Application with jOOQ and Spring MVC
Note
Notice that this class contains the DSLContext dsl property that will be used to build jOOQ queries for retrieving
data.
First, we implement a class for working with jqGrid, inheriting it from our abstract class
ru.ibase.fbjavaex.jqgrid.JqGrid. It will be able to search and sort by the NAME field in reversing order. Track
the source code below for explanatory comments.
package ru.ibase.fbjavaex.jqgrid;
import org.jooq.*;
import java.util.List;
import java.util.Map;
/**
* Customer grid
*
* @author Simonov Denis
*/
public class JqGridCustomer extends JqGrid {
/**
* Adding a search condition
*
* @param query
*/
private void makeSearchCondition(SelectQuery<?> query) {
switch (this.searchOper) {
case "eq":
// CUSTOMER.NAME = ?
query.addConditions(CUSTOMER.NAME.eq(this.searchString));
break;
case "bw":
// CUSTOMER.NAME STARTING WITH ?
query.addConditions(CUSTOMER.NAME.startsWith(this.searchString));
break;
case "cn":
// CUSTOMER.NAME CONTAINING ?
query.addConditions(CUSTOMER.NAME.contains(this.searchString));
break;
}
221
Creating an Application with jOOQ and Spring MVC
/**
* Returns the total number of records
*
* @return
*/
@Override
public int getCountRecord() {
// query that returns the number of records
SelectFinalStep<?> select
= dsl.selectCount()
.from(CUSTOMER);
/**
* Returns the grid records
*
* @return
*/
@Override
public List<Map<String, Object>> getRecords() {
// Basic selection query
SelectFinalStep<?> select =
dsl.select()
.from(CUSTOMER);
if (this.offset != 0) {
query.addOffset(this.offset);
}
222
Creating an Application with jOOQ and Spring MVC
CustomerManager Class
The CustomerManager class that is defined next is a kind of business layer between the corresponding controller
and the database. We will use it for adding, editing and deleting a customer. All operations in this layer will be
performed in a SNAPSHOT-level transaction.
package ru.ibase.fbjavaex.managers;
import org.jooq.DSLContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Isolation;
/**
* Customer manager
*
* @author Simonov Denis
*/
public class CustomerManager {
@Autowired(required = true)
private DSLContext dsl;
/**
* Adding a customer
*
* @param name
* @param address
* @param zipcode
* @param phone
*/
@Transactional(propagation = Propagation.REQUIRED,
isolation = Isolation.REPEATABLE_READ)
public void create(String name, String address, String zipcode, String phone) {
if (zipcode != null) {
if (zipcode.trim().isEmpty()) {
zipcode = null;
}
}
this.dsl
.insertInto(CUSTOMER,
CUSTOMER.CUSTOMER_ID,
223
Creating an Application with jOOQ and Spring MVC
CUSTOMER.NAME,
CUSTOMER.ADDRESS,
CUSTOMER.ZIPCODE,
CUSTOMER.PHONE)
.values(
customerId,
name,
address,
zipcode,
phone
)
.execute();
}
/**
* Editing a customer
*
* @param customerId
* @param name
* @param address
* @param zipcode
* @param phone
*/
@Transactional(propagation = Propagation.REQUIRED,
isolation = Isolation.REPEATABLE_READ)
public void edit(int customerId, String name, String address,
String zipcode, String phone) {
if (zipcode != null) {
if (zipcode.trim().isEmpty()) {
zipcode = null;
}
}
this.dsl.update(CUSTOMER)
.set(CUSTOMER.NAME, name)
.set(CUSTOMER.ADDRESS, address)
.set(CUSTOMER.ZIPCODE, zipcode)
.set(CUSTOMER.PHONE, phone)
.where(CUSTOMER.CUSTOMER_ID.eq(customerId))
.execute();
}
/**
* Deleting a customer
*
* @param customerId
*/
@Transactional(propagation = Propagation.REQUIRED,
isolation = Isolation.REPEATABLE_READ)
public void delete(int customerId) {
this.dsl.deleteFrom(CUSTOMER)
.where(CUSTOMER.CUSTOMER_ID.eq(customerId))
.execute();
}
}
224
Creating an Application with jOOQ and Spring MVC
• The method attribute specifies the HTTP request method (PUT, GET, POST, DELETE)
• The index method will be the input point of our controller. It is responsible for displaying the JSP page (view)
that contains the layout for displaying the grid, the tool bar and the navigation bar.
Data for display are loaded asynchronously by the jqGrid component. The path is /customer/getdata, to
which the getData method is connected.
getData Method
The getData method contains the additional @ResponseBody annotation for indicating that our method returns
the object for serialization into a specific format. The annotation @RequestMapping contains the attribute pro-
duces = MediaType.APPLICATION_JSON, directing that the returned object be serialized into the JSON format.
It is in the getData method that we work with the JqGridCustomer class described earlier. The @RequestParam
annotation enables the value of the parameter to be retrieved from the HTTP request. This class method works
with GET requests.
• The value attribute in the @RequestParam annotation defines the name of the parameter to be retrieved from
the HTTP request.
• The Required attribute can designate the HTTP request parameter as mandatory.
• The defaultValue attribute supplies the value that is to be used if the HTTP parameter is not specified.
The editCustomer method is connected with the /customer/edit path. The deleteCustomer method is con-
nected with the /customer/delete path. Both methods operate on existing customer records.
package ru.ibase.fbjavaex.controllers;
import java.util.HashMap;
import java.util.Map;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
225
Creating an Application with jOOQ and Spring MVC
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RequestParam;
import javax.ws.rs.core.MediaType;
import org.springframework.beans.factory.annotation.Autowired;
import ru.ibase.fbjavaex.managers.CustomerManager;
import ru.ibase.fbjavaex.jqgrid.JqGridCustomer;
import ru.ibase.fbjavaex.jqgrid.JqGridData;
/**
* Customer Controller
*
* @author Simonov Denis
*/
@Controller
public class CustomerController {
@Autowired(required = true)
private JqGridCustomer customerGrid;
@Autowired(required = true)
private CustomerManager customerManager;
/**
* Default action
* Returns the JSP name of the page (view) to display
*
* @param map
* @return name of JSP template
*/
@RequestMapping(value = "/customer/", method = RequestMethod.GET)
public String index(ModelMap map) {
return "customer";
}
/**
* Returns JSON data for jqGrid
*
* @param rows number of entries per page
* @param page page number
* @param sIdx sorting field
* @param sOrd sorting order
* @param search should the search be performed
* @param searchField search field
* @param searchString value for searching
* @param searchOper search operation
* @return JSON data for jqGrid
*/
@RequestMapping(value = "/customer/getdata",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON)
@ResponseBody
public JqGridData getData(
226
Creating an Application with jOOQ and Spring MVC
return customerGrid.getJqGridData();
}
@RequestMapping(value = "/customer/create",
method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON)
@ResponseBody
public Map<String, Object> addCustomer(
@RequestParam(value = "NAME", required = true,
defaultValue = "") String name,
@RequestParam(value = "ADDRESS", required = false,
defaultValue = "") String address,
@RequestParam(value = "ZIPCODE", required = false,
defaultValue = "") String zipcode,
@RequestParam(value = "PHONE", required = false,
defaultValue = "") String phone) {
Map<String, Object> map = new HashMap<>();
try {
customerManager.create(name, address, zipcode, phone);
map.put("success", true);
} catch (Exception ex) {
map.put("error", ex.getMessage());
}
return map;
227
Creating an Application with jOOQ and Spring MVC
@RequestMapping(value = "/customer/edit",
method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON)
@ResponseBody
public Map<String, Object> editCustomer(
@RequestParam(value = "CUSTOMER_ID", required = true,
defaultValue = "0") int customerId,
@RequestParam(value = "NAME", required = true,
defaultValue = "") String name,
@RequestParam(value = "ADDRESS", required = false,
defaultValue = "") String address,
@RequestParam(value = "ZIPCODE", required = false,
defaultValue = "") String zipcode,
@RequestParam(value = "PHONE", required = false,
defaultValue = "") String phone) {
Map<String, Object> map = new HashMap<>();
try {
customerManager.edit(customerId, name, address, zipcode, phone);
map.put("success", true);
} catch (Exception ex) {
map.put("error", ex.getMessage());
}
return map;
}
@RequestMapping(value = "/customer/delete",
method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON)
@ResponseBody
public Map<String, Object> deleteCustomer(
@RequestParam(value = "CUSTOMER_ID", required = true,
defaultValue = "0") int customerId) {
Map<String, Object> map = new HashMap<>();
try {
customerManager.delete(customerId);
map.put("success", true);
} catch (Exception ex) {
map.put("error", ex.getMessage());
}
return map;
}
}
Customer Display
The JSP page for displaying the customer module contains nothing special: the layout with the main parts of the
page, the table for displaying the grid and the block for displaying the navigation bar. JSP templates are fairly
unsophisticated. If you wish, you can replace them with other template systems that support inheritance.
The ../jspf/head.jspf file contains common scripts and styles for all website pages and the ../jspf/
menu.jspf file contains the website's main menu. Their code is not reproduced here: it is quite simple and
you can examine it in the project's source if you are curious.
228
Creating an Application with jOOQ and Spring MVC
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>An example of a Spring MVC application using Firebird
and jOOQ</title>
<h2>Customers</h2>
<table id="jqGridCustomer"></table>
<div id="jqPagerCustomer"></div>
<hr/>
<footer>
<p>© 2016 - An example of a Spring MVC application
using Firebird and jOOQ</p>
</footer>
</div>
<script type="text/javascript">
$(document).ready(function () {
JqGridCustomer({
baseAddress: '${cp}'
});
});
</script>
</body>
</html>
The basic logic on the client side is concentrated in the /resources/js/jqGridCustomer.js JavaScript
module.
229
Creating an Application with jOOQ and Spring MVC
}, options),
// return model description
getColModel: function () {
return [
{
label: 'Id',
name: 'CUSTOMER_ID', // field name
key: true,
hidden: true
},
{
label: 'Name',
name: 'NAME',
width: 240,
sortable: true,
editable: true,
edittype: "text", // input field type in the editor
search: true,
searchoptions: {
// allowed search operators
sopt: ['eq', 'bw', 'cn']
},
// size and maximum length for the input field
editoptions: {size: 30, maxlength: 60},
editrules: {required: true}
},
{
label: 'Address',
name: 'ADDRESS',
width: 300,
sortable: false, // prohibit sorting
editable: true,
search: false, // prohibit search
edittype: "textarea", // Memo field
editoptions: {maxlength: 250, cols: 30, rows: 4}
},
{
label: 'Zip Code',
name: 'ZIPCODE',
width: 30,
sortable: false,
editable: true,
search: false,
edittype: "text",
editoptions: {size: 30, maxlength: 10}
},
{
label: 'Phone',
name: 'PHONE',
width: 80,
sortable: false,
editable: true,
search: false,
edittype: "text",
editoptions: {size: 30, maxlength: 14}
}
];
},
230
Creating an Application with jOOQ and Spring MVC
// grid initialization
initGrid: function () {
// url to retrieve data
var url = jqGridCustomer.options.baseAddress
+ '/customer/getdata';
jqGridCustomer.dbGrid = $("#jqGridCustomer").jqGrid({
url: url,
datatype: "json", // data format
mtype: "GET", // request type
colModel: jqGridCustomer.getColModel(),
rowNum: 500, // number of rows displayed
loadonce: false, // load only once
sortname: 'NAME', // Sorting by NAME by default
sortorder: "asc",
width: window.innerWidth - 80,
height: 500,
viewrecords: true, // display the number of records
guiStyle: "bootstrap",
iconSet: "fontAwesome",
caption: "Customers",
// navigation item
pager: 'jqPagerCustomer'
});
},
// editing options
getEditOptions: function () {
return {
url: jqGridCustomer.options.baseAddress + '/customer/edit',
reloadAfterSubmit: true,
closeOnEscape: true,
closeAfterEdit: true,
drag: true,
width: 400,
afterSubmit: jqGridCustomer.afterSubmit,
editData: {
// In addition to the values from the form, pass the key field
CUSTOMER_ID: function () {
// get the current row
var selectedRow = jqGridCustomer.dbGrid.getGridParam("selrow");
// get the value of the field CUSTOMER_ID
var value = jqGridCustomer.dbGrid.getCell(selectedRow,
'CUSTOMER_ID');
return value;
}
}
};
},
// Add options
getAddOptions: function () {
return {
url: jqGridCustomer.options.baseAddress + '/customer/create',
reloadAfterSubmit: true,
closeOnEscape: true,
closeAfterAdd: true,
drag: true,
width: 400,
afterSubmit: jqGridCustomer.afterSubmit
};
231
Creating an Application with jOOQ and Spring MVC
},
// Edit options
getDeleteOptions: function () {
return {
url: jqGridCustomer.options.baseAddress + '/customer/delete',
reloadAfterSubmit: true,
closeOnEscape: true,
closeAfterDelete: true,
drag: true,
msg: "Delete the selected customer?",
afterSubmit: jqGridCustomer.afterSubmit,
delData: {
// pass the key field
CUSTOMER_ID: function () {
var selectedRow = jqGridCustomer.dbGrid.getGridParam("selrow");
var value = jqGridCustomer.dbGrid.getCell(selectedRow,
'CUSTOMER_ID');
return value;
}
}
};
},
// initializing the navigation bar with editing dialogs
initPagerWithEditors: function () {
jqGridCustomer.dbGrid.jqGrid('navGrid', '#jqPagerCustomer',
{
// buttons
search: true,
add: true,
edit: true,
del: true,
view: true,
refresh: true,
// button captions
searchtext: "Search",
addtext: "Add",
edittext: "Edit",
deltext: "Delete",
viewtext: "View",
viewtitle: "Selected record",
refreshtext: "Refresh"
},
jqGridCustomer.getEditOptions(),
jqGridCustomer.getAddOptions(),
jqGridCustomer.getDeleteOptions()
);
},
// initialize the navigation bar without editing dialogs
initPagerWithoutEditors: function () {
jqGridCustomer.dbGrid.jqGrid('navGrid', '#jqPagerCustomer',
{
// buttons
search: true,
add: false,
edit: false,
del: false,
view: false,
refresh: true,
232
Creating an Application with jOOQ and Spring MVC
// button captions
searchtext: "Search",
viewtext: "View",
viewtitle: "Selected record",
refreshtext: "Refresh"
}
);
},
// initialize the navigation bar
initPager: function () {
if (jqGridCustomer.options.showEditorPanel) {
jqGridCustomer.initPagerWithEditors();
} else {
jqGridCustomer.initPagerWithoutEditors();
}
},
// initialize
init: function () {
jqGridCustomer.initGrid();
jqGridCustomer.initPager();
},
// processor of the results of processing forms (operations)
afterSubmit: function (response, postdata) {
var responseData = response.responseJSON;
// check the result for error messages
if (responseData.hasOwnProperty("error")) {
if (responseData.error.length) {
return [false, responseData.error];
}
} else {
// if an error was not returned, refresh the grid
$(this).jqGrid(
'setGridParam',
{
datatype: 'json'
}
).trigger('reloadGrid');
}
return [true, "", 0];
}
};
jqGridCustomer.init();
return jqGridCustomer;
};
})(jQuery);
Visual Elements
Each column in jqGrid has a number of properties available. The source code contains comments explain-
ing column properties. You can read more details about configuring the model of jqGrid columns in the
ColModel API section of the documentation for the jqGrid project.
233
Creating an Application with jOOQ and Spring MVC
The url property defines the URL to which the data will be submitted after the OK button in clicked in the
dialog box.
The afterSubmit property marks the event that occurs after the data have been sent to the server and a response
has been received back.
The afterSubmit method checks whether the controller returns an error. The grid is updated if no error is
returned; otherwise, the error is shown to the user.
Note
The editData property allows you to specify the values of additional fields that are not shown in the edit
dialog box. Edit dialog boxes do not show the values of hidden fields and it is rather tedious if you want
to display automatically generated keys.
package ru.ibase.fbjavaex.config;
import java.sql.Timestamp;
import java.time.LocalDateTime;
/**
* Working period
*
* @author Simonov Denis
*/
public class WorkingPeriod {
/**
* Constructor
*/
234
Creating an Application with jOOQ and Spring MVC
WorkingPeriod() {
// in real applications is calculated from the current date
this.beginDate = Timestamp.valueOf("2015-06-01 00:00:00");
this.endDate = Timestamp.valueOf(LocalDateTime.now().plusDays(1));
}
/**
* Returns the start date of the work period
*
* @return
*/
public Timestamp getBeginDate() {
return this.beginDate;
}
/**
* Returns the end date of the work period
*
* @return
*/
public Timestamp getEndDate() {
return this.endDate;
}
/**
* Setting the start date of the work period
*
* @param value
*/
public void setBeginDate(Timestamp value) {
this.beginDate = value;
}
/**
* Setting the end date of the work period
*
* @param value
*/
public void setEndDate(Timestamp value) {
this.endDate = value;
}
/**
* Setting the working period
*
* @param beginDate
* @param endDate
*/
public void setRangeDate(Timestamp beginDate, Timestamp endDate) {
this.beginDate = beginDate;
this.endDate = endDate;
}
}
In our project we have only one secondary module called "Invoices". An invoice consists of a header where
some general attributes are described (number, date, customer …) and one or more invoice items (product name,
235
Creating an Application with jOOQ and Spring MVC
quantity, price, etc.). The invoice header is displayed in the main grid while items can be viewed in a detail grid
that is opened with a click on the "+" icon of the selected document.
We implement a class, inherited from the ru.ibase.fbjavaex.jqgrid.JqGrid abstract class described earlier, for
viewing the invoice headers via jqGrid. Searching can be by customer name or invoice date and reversible date
order is supported, too.
package ru.ibase.fbjavaex.jqgrid;
import java.sql.*;
import org.jooq.*;
import java.util.List;
import java.util.Map;
import org.springframework.beans.factory.annotation.Autowired;
import ru.ibase.fbjavaex.config.WorkingPeriod;
/**
* Grid handler for the invoice journal
*
* @author Simonov Denis
*/
public class JqGridInvoice extends JqGrid {
@Autowired(required = true)
private WorkingPeriod workingPeriod;
/**
* Adding a search condition
*
* @param query
*/
private void makeSearchCondition(SelectQuery<?> query) {
// adding a search condition to the query,
// if it is produced for different fields,
// different comparison operators are available when searching.
if (this.searchString.isEmpty()) {
return;
}
if (this.searchField.equals("CUSTOMER_NAME")) {
switch (this.searchOper) {
case "eq": // equal
query.addConditions(CUSTOMER.NAME.eq(this.searchString));
break;
case "bw": // starting with
query.addConditions(CUSTOMER.NAME.startsWith(this.searchString));
break;
case "cn": // containing
query.addConditions(CUSTOMER.NAME.contains(this.searchString));
break;
}
}
if (this.searchField.equals("INVOICE_DATE")) {
236
Creating an Application with jOOQ and Spring MVC
switch (this.searchOper) {
case "eq": // =
query.addConditions(INVOICE.INVOICE_DATE.eq(dateValue));
break;
case "lt": // <
query.addConditions(INVOICE.INVOICE_DATE.lt(dateValue));
break;
case "le": // <=
query.addConditions(INVOICE.INVOICE_DATE.le(dateValue));
break;
case "gt": // >
query.addConditions(INVOICE.INVOICE_DATE.gt(dateValue));
break;
case "ge": // >=
query.addConditions(INVOICE.INVOICE_DATE.ge(dateValue));
break;
}
}
}
/**
* Returns the total number of records
*
* @return
*/
@Override
public int getCountRecord() {
SelectFinalStep<?> select
= dsl.selectCount()
.from(INVOICE)
.where(INVOICE.INVOICE_DATE.between(
this.workingPeriod.getBeginDate(),
this.workingPeriod.getEndDate()));
if (this.searchFlag) {
makeSearchCondition(query);
}
/**
* Returns the list of invoices
*
* @return
*/
@Override
public List<Map<String, Object>> getRecords() {
SelectFinalStep<?> select = dsl.select(
INVOICE.INVOICE_ID,
INVOICE.CUSTOMER_ID,
CUSTOMER.NAME.as("CUSTOMER_NAME"),
237
Creating an Application with jOOQ and Spring MVC
INVOICE.INVOICE_DATE,
INVOICE.PAID,
INVOICE.TOTAL_SALE)
.from(INVOICE)
.innerJoin(CUSTOMER).on(CUSTOMER.CUSTOMER_ID.eq(INVOICE.CUSTOMER_ID))
.where(INVOICE.INVOICE_DATE.between(
this.workingPeriod.getBeginDate(),
this.workingPeriod.getEndDate()));
return query.fetchMaps();
}
}
Invoice Items
We make the class for viewing the invoice items via jqGrid a little simpler. Its records are filtered by invoice
header code and user-driven search and sort options are not implemented.
package ru.ibase.fbjavaex.jqgrid;
import org.jooq.*;
import java.util.List;
import java.util.Map;
/**
* The grid handler for the invoice items
238
Creating an Application with jOOQ and Spring MVC
*
* @author Simonov Denis
*/
public class JqGridInvoiceLine extends JqGrid {
/**
* Returns the total number of records
*
* @return
*/
@Override
public int getCountRecord() {
SelectFinalStep<?> select
= dsl.selectCount()
.from(INVOICE_LINE)
.where(INVOICE_LINE.INVOICE_ID.eq(this.invoiceId));
/**
* Returns invoice items
*
* @return
*/
@Override
public List<Map<String, Object>> getRecords() {
SelectFinalStep<?> select = dsl.select(
INVOICE_LINE.INVOICE_LINE_ID,
INVOICE_LINE.INVOICE_ID,
INVOICE_LINE.PRODUCT_ID,
PRODUCT.NAME.as("PRODUCT_NAME"),
INVOICE_LINE.QUANTITY,
INVOICE_LINE.SALE_PRICE,
INVOICE_LINE.SALE_PRICE.mul(INVOICE_LINE.QUANTITY).as("TOTAL"))
.from(INVOICE_LINE)
.innerJoin(PRODUCT).on(PRODUCT.PRODUCT_ID.eq(INVOICE_LINE.PRODUCT_ID))
.where(INVOICE_LINE.INVOICE_ID.eq(this.invoiceId));
239
Creating an Application with jOOQ and Spring MVC
InvoiceManager Class
The ru.ibase.fbjavaex.managers.InvoiceManager class is a kind of business layer that will be used to direct
adding, editing and deleting invoices and their items, along with invoice payment. All operations in this layer
will be performed in a SNAPSHOT transaction. We have chosen to have our application perform all of the
invoice management options in this class by calling stored procedures. It is not mandatory to do it this way, of
course. It is just one option.
package ru.ibase.fbjavaex.managers;
import java.sql.Timestamp;
import org.jooq.DSLContext;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Isolation;
/**
* Invoice manager
*
* @author Simonov Denis
*/
public class InvoiceManager {
@Autowired(required = true)
private DSLContext dsl;
/**
* Add invoice
*
* @param customerId
* @param invoiceDate
*/
@Transactional(propagation = Propagation.REQUIRED,
isolation = Isolation.REPEATABLE_READ)
public void create(Integer customerId,
Timestamp invoiceDate) {
int invoiceId = this.dsl.nextval(GEN_INVOICE_ID).intValue();
spAddInvoice(this.dsl.configuration(),
invoiceId,
customerId,
invoiceDate);
}
240
Creating an Application with jOOQ and Spring MVC
/**
* Edit invoice
*
* @param invoiceId
* @param customerId
* @param invoiceDate
*/
@Transactional(propagation = Propagation.REQUIRED,
isolation = Isolation.REPEATABLE_READ)
public void edit(Integer invoiceId,
Integer customerId,
Timestamp invoiceDate) {
spEditInvoice(this.dsl.configuration(),
invoiceId,
customerId,
invoiceDate);
}
/**
* Payment of invoices
*
* @param invoiceId
*/
@Transactional(propagation = Propagation.REQUIRED,
isolation = Isolation.REPEATABLE_READ)
public void pay(Integer invoiceId) {
spPayForInovice(this.dsl.configuration(),
invoiceId);
}
/**
* Delete invoice
*
* @param invoiceId
*/
@Transactional(propagation = Propagation.REQUIRED,
isolation = Isolation.REPEATABLE_READ)
public void delete(Integer invoiceId) {
spDeleteInvoice(this.dsl.configuration(),
invoiceId);
}
/**
* Add invoice item
*
* @param invoiceId
* @param productId
* @param quantity
*/
@Transactional(propagation = Propagation.REQUIRED,
isolation = Isolation.REPEATABLE_READ)
public void addInvoiceLine(Integer invoiceId,
Integer productId,
Integer quantity) {
spAddInvoiceLine(this.dsl.configuration(),
invoiceId,
productId,
241
Creating an Application with jOOQ and Spring MVC
quantity);
}
/**
* Edit invoice item
*
* @param invoiceLineId
* @param quantity
*/
@Transactional(propagation = Propagation.REQUIRED,
isolation = Isolation.REPEATABLE_READ)
public void editInvoiceLine(Integer invoiceLineId,
Integer quantity) {
spEditInvoiceLine(this.dsl.configuration(),
invoiceLineId,
quantity);
}
/**
* Delete invoice item
*
* @param invoiceLineId
*/
@Transactional(propagation = Propagation.REQUIRED,
isolation = Isolation.REPEATABLE_READ)
public void deleteInvoiceLine(Integer invoiceLineId) {
spDeleteInvoiceLine(this.dsl.configuration(),
invoiceLineId);
}
}
Data for displaying invoice headers are loaded asynchronously by the jqGrid component (the path is /invoice/
getdata). The getData method is connected with this path, similarly to the primary modules. Invoice items
are returned by the getDetailData method (the path is /invoice/getdetaildata). The primary key of the
invoice whose detail grid is currently open is passed to this method.
The methods implemented are addInvoice, editInvoice, deleteInvoice, payInvoice for invoice headers and addIn-
voiceLine, editInvoiceLine, deleteInvoiceLine for invoice line items.
package ru.ibase.fbjavaex.controllers;
import java.sql.Timestamp;
import java.util.HashMap;
import java.util.Map;
import java.util.Date;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.beans.PropertyEditorSupport;
242
Creating an Application with jOOQ and Spring MVC
import javax.ws.rs.core.MediaType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.ModelMap;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.InitBinder;
import org.springframework.web.bind.WebDataBinder;
import ru.ibase.fbjavaex.jqgrid.JqGridInvoice;
import ru.ibase.fbjavaex.jqgrid.JqGridInvoiceLine;
import ru.ibase.fbjavaex.managers.InvoiceManager;
import ru.ibase.fbjavaex.jqgrid.JqGridData;
/**
* Invoice controller
*
* @author Simonov Denis
*/
@Controller
public class InvoiceController {
@Autowired(required = true)
private JqGridInvoice invoiceGrid;
@Autowired(required = true)
private JqGridInvoiceLine invoiceLineGrid;
@Autowired(required = true)
private InvoiceManager invoiceManager;
/**
* Describe how a string is converted to a date
* from the input parameters of the HTTP request
*
* @param binder
*/
@InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(Timestamp.class,
new PropertyEditorSupport() {
@Override
public void setAsText(String value) {
try {
if ((value == null) || (value.isEmpty())) {
setValue(null);
} else {
Date parsedDate = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss")
.parse(value);
setValue(new Timestamp(parsedDate.getTime()));
}
} catch (ParseException e) {
throw new java.lang.IllegalArgumentException(value);
243
Creating an Application with jOOQ and Spring MVC
}
}
});
}
/**
* Default action
* Returns the JSP name of the page (view) to display
*
* @param map
* @return JSP page name
*/
@RequestMapping(value = "/invoice/", method = RequestMethod.GET)
public String index(ModelMap map) {
return "invoice";
}
/**
* Returns a list of invoices in JSON format for jqGrid
*
* @param rows number of entries per page
* @param page current page number
* @param sIdx sort field
* @param sOrd sorting order
* @param search search flag
* @param searchField search field
* @param searchString search value
* @param searchOper comparison operation
* @param filters filter
* @return
*/
@RequestMapping(value = "/invoice/getdata",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON)
@ResponseBody
public JqGridData getData(
@RequestParam(value = "rows", required = false,
defaultValue = "20") int rows,
@RequestParam(value = "page", required = false,
defaultValue = "1") int page,
@RequestParam(value = "sidx", required = false,
defaultValue = "") String sIdx,
@RequestParam(value = "sord", required = false,
defaultValue = "asc") String sOrd,
@RequestParam(value = "_search", required = false,
defaultValue = "false") Boolean search,
@RequestParam(value = "searchField", required = false,
defaultValue = "") String searchField,
@RequestParam(value = "searchString", required = false,
defaultValue = "") String searchString,
@RequestParam(value = "searchOper", required = false,
defaultValue = "") String searchOper,
@RequestParam(value = "filters", required = false,
defaultValue = "") String filters) {
if (search) {
invoiceGrid.setSearchCondition(searchField, searchString, searchOper);
244
Creating an Application with jOOQ and Spring MVC
}
invoiceGrid.setLimit(rows);
invoiceGrid.setPageNo(page);
invoiceGrid.setOrderBy(sIdx, sOrd);
return invoiceGrid.getJqGridData();
}
/**
* Add invoice
*
* @param customerId customer id
* @param invoiceDate invoice date
* @return
*/
@RequestMapping(value = "/invoice/create",
method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON)
@ResponseBody
public Map<String, Object> addInvoice(
@RequestParam(value = "CUSTOMER_ID", required = true,
defaultValue = "0") Integer customerId,
@RequestParam(value = "INVOICE_DATE", required = false,
defaultValue = "") Timestamp invoiceDate) {
Map<String, Object> map = new HashMap<>();
try {
invoiceManager.create(customerId, invoiceDate);
map.put("success", true);
} catch (Exception ex) {
map.put("error", ex.getMessage());
}
return map;
}
/**
* Edit invoice
*
* @param invoiceId invoice id
* @param customerId customer id
* @param invoiceDate invoice date
* @return
*/
@RequestMapping(value = "/invoice/edit",
method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON)
@ResponseBody
public Map<String, Object> editInvoice(
@RequestParam(value = "INVOICE_ID", required = true,
defaultValue = "0") Integer invoiceId,
@RequestParam(value = "CUSTOMER_ID", required = true,
defaultValue = "0") Integer customerId,
@RequestParam(value = "INVOICE_DATE", required = false,
defaultValue = "") Timestamp invoiceDate) {
Map<String, Object> map = new HashMap<>();
try {
invoiceManager.edit(invoiceId, customerId, invoiceDate);
map.put("success", true);
245
Creating an Application with jOOQ and Spring MVC
/**
* Pays an invoice
*
* @param invoiceId invoice id
* @return
*/
@RequestMapping(value = "/invoice/pay",
method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON)
@ResponseBody
public Map<String, Object> payInvoice(
@RequestParam(value = "INVOICE_ID", required = true,
defaultValue = "0") Integer invoiceId) {
Map<String, Object> map = new HashMap<>();
try {
invoiceManager.pay(invoiceId);
map.put("success", true);
} catch (Exception ex) {
map.put("error", ex.getMessage());
}
return map;
}
/**
* Delete invoice
*
* @param invoiceId invoice id
* @return
*/
@RequestMapping(value = "/invoice/delete",
method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON)
@ResponseBody
public Map<String, Object> deleteInvoice(
@RequestParam(value = "INVOICE_ID", required = true,
defaultValue = "0") Integer invoiceId) {
Map<String, Object> map = new HashMap<>();
try {
invoiceManager.delete(invoiceId);
map.put("success", true);
} catch (Exception ex) {
map.put("error", ex.getMessage());
}
return map;
}
/**
* Returns invoice item
*
* @param invoice_id invoice id
* @return
*/
246
Creating an Application with jOOQ and Spring MVC
@RequestMapping(value = "/invoice/getdetaildata",
method = RequestMethod.GET,
produces = MediaType.APPLICATION_JSON)
@ResponseBody
public JqGridData getDetailData(
@RequestParam(value = "INVOICE_ID", required = true) int invoice_id) {
invoiceLineGrid.setInvoiceId(invoice_id);
return invoiceLineGrid.getJqGridData();
/**
* Add invoice item
*
* @param invoiceId invoice id
* @param productId product id
* @param quantity quantity of products
* @return
*/
@RequestMapping(value = "/invoice/createdetail",
method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON)
@ResponseBody
public Map<String, Object> addInvoiceLine(
@RequestParam(value = "INVOICE_ID", required = true,
defaultValue = "0") Integer invoiceId,
@RequestParam(value = "PRODUCT_ID", required = true,
defaultValue = "0") Integer productId,
@RequestParam(value = "QUANTITY", required = true,
defaultValue = "0") Integer quantity) {
Map<String, Object> map = new HashMap<>();
try {
invoiceManager.addInvoiceLine(invoiceId, productId, quantity);
map.put("success", true);
} catch (Exception ex) {
map.put("error", ex.getMessage());
}
return map;
}
/**
* Edit invoice item
*
* @param invoiceLineId invoice item id
* @param quantity quantity of products
* @return
*/
@RequestMapping(value = "/invoice/editdetail",
method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON)
@ResponseBody
public Map<String, Object> editInvoiceLine(
@RequestParam(value = "INVOICE_LINE_ID", required = true,
defaultValue = "0") Integer invoiceLineId,
@RequestParam(value = "QUANTITY", required = true,
defaultValue = "0") Integer quantity) {
247
Creating an Application with jOOQ and Spring MVC
/**
* Delete invoice item
*
* @param invoiceLineId invoice item id
* @return
*/
@RequestMapping(value = "/invoice/deletedetail",
method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON)
@ResponseBody
public Map<String, Object> deleteInvoiceLine(
@RequestParam(value = "INVOICE_LINE_ID", required = true,
defaultValue = "0") Integer invoiceLineId) {
Map<String, Object> map = new HashMap<>();
try {
invoiceManager.deleteInvoiceLine(invoiceLineId);
map.put("success", true);
} catch (Exception ex) {
map.put("error", ex.getMessage());
}
return map;
}
}
The invoice controller is very similar to the primary module controllers except for two things:
1. The controller displays and works with the data of both the main grid and the detail grid
2. Invoices are filtered by the date field so that only those invoices that are included in the work period are
displayed
The java.sql.Timestamp type in Java supports precision up to nanoseconds whereas the maximum precision of
the TIMESTAMP type in Firebird is one ten-thousandth of a second. That is not really a significant problem.
Date and time types in Java support working with time zones. Firebird does not currently support the TIMES-
TAMP WITH TIMEZONE type. Java works on the assumption that dates in the database are stored in the time
zone of the server. However, time will be converted to UTC during serialization into JSON. It must be taken
into account when processing time data in JavaScript.
248
Creating an Application with jOOQ and Spring MVC
Attention!
Java takes the time offset from its own time zone database, not from the operating system. This practice con-
siderably increases the need to keep up with the latest version of JDK. If you have some old version of JDK
installed, working with date and time may be incorrect.
By default, a date is serialized into JSON in as the number of nanoseconds since January 1, 1970, which is
not always what is wanted. A date can be serialized into a text representation, by setting to False the date
conversion configuration property SerializationFeature.WRITE_DATES_AS_TIMESTAMPS date conversion in
the configureMessageConverters method of the WebAppConfig class.
@Configuration
@ComponentScan("ru.ibase.fbjavaex")
@EnableWebMvc
public class WebAppConfig extends WebMvcConfigurerAdapter {
@Override
public void configureMessageConverters(
List<HttpMessageConverter<?>> httpMessageConverters) {
MappingJackson2HttpMessageConverter jsonConverter =
new MappingJackson2HttpMessageConverter();
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS,
false);
jsonConverter.setObjectMapper(objectMapper);
httpMessageConverters.add(jsonConverter);
}
…
}
The initBinder method of the InvoiceController controller describes how the text representation of a date sent by
the browser is converted into a value of type Timestamp.
<!DOCTYPE html>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>An example of a Spring MVC application using Firebird and jOOQ</title>
249
Creating an Application with jOOQ and Spring MVC
<h2>Invoices</h2>
<table id="jqGridInvoice"></table>
<div id="jqPagerInvoice"></div>
<hr />
<footer>
<p>© 2016 - An example of a Spring MVC application using
Firebird and jOOQ</p>
</footer>
</div>
<script type="text/javascript">
var invoiceGrid = null;
$(document).ready(function () {
invoiceGrid = JqGridInvoice({
baseAddress: '${cp}'
});
});
</script>
</body>
</html>
The basic logic on the client side is concentrated in the /resources/js/jqGridInvoice.js JavaScript
module.
250
Creating an Application with jOOQ and Spring MVC
},
{
label: 'Customer Id'
name: 'CUSTOMER_ID',
hidden: true,
editrules: {edithidden: true, required: true},
editable: true,
edittype: 'custom', // custom type
editoptions: {
custom_element: function (value, options) {
// add hidden input
return $("<input>")
.attr('type', 'hidden')
.attr('rowid', options.rowId)
.addClass("FormElement")
.addClass("form-control")
.val(value)
.get(0);
}
}
},
{
label: 'Date',
name: 'INVOICE_DATE',
width: 60,
sortable: true,
editable: true,
search: true,
edittype: "text", // input type
align: "right",
// format as date
formatter: jqGridInvoice.dateTimeFormatter,
sorttype: 'date', // sort as date
formatoptions: {
srcformat: 'Y-m-d\TH:i:s', // input format
newformat: 'Y-m-d H:i:s' // output format
},
editoptions: {
// initializing the form element for editing
dataInit: function (element) {
// creating datepicker
$(element).datepicker({
id: 'invoiceDate_datePicker',
dateFormat: 'dd.mm.yy',
minDate: new Date(2000, 0, 1),
maxDate: new Date(2030, 0, 1)
});
}
},
searchoptions: {
// initializing the form element for searching
dataInit: function (element) {
// create datepicker
$(element).datepicker({
id: 'invoiceDate_datePicker',
dateFormat: 'dd.mm.yy',
minDate: new Date(2000, 0, 1),
maxDate: new Date(2030, 0, 1)
251
Creating an Application with jOOQ and Spring MVC
});
},
searchoptions: { // search types
sopt: ['eq', 'lt', 'le', 'gt', 'ge']
}
}
},
{
label: 'Customer',
name: 'CUSTOMER_NAME',
width: 250,
editable: true,
edittype: "text",
editoptions: {
size: 50,
maxlength: 60,
readonly: true
},
editrules: {required: true},
search: true,
searchoptions: {
sopt: ['eq', 'bw', 'cn']
}
},
{
label: 'Amount',
name: 'TOTAL_SALE',
width: 60,
sortable: false,
editable: false,
search: false,
align: "right",
// foramt as currency
formatter: 'currency',
sorttype: 'number',
searchrules: {
"required": true,
"number": true,
"minValue": 0
}
},
{
label: 'Paid',
name: 'PAID',
width: 30,
sortable: false,
editable: true,
search: true,
searchoptions: {
sopt: ['eq']
},
edittype: "checkbox",
formatter: "checkbox",
stype: "checkbox",
align: "center",
editoptions: {
value: "1",
offval: "0"
252
Creating an Application with jOOQ and Spring MVC
}
}
];
},
initGrid: function () {
// url to retrieve data
var url = jqGridInvoice.options.baseAddress + '/invoice/getdata';
jqGridInvoice.dbGrid = $("#jqGridInvoice").jqGrid({
url: url,
datatype: "json", // data format
mtype: "GET", // http request type
// model description
colModel: jqGridInvoice.getInvoiceColModel(),
rowNum: 500, // number of rows displayed
loadonce: false, // load only once
// default sort by INVOICE_DATE column
sortname: 'INVOICE_DATE',
sortorder: "desc", // sorting order
width: window.innerWidth - 80,
height: 500,
viewrecords: true, // display the number of entries
guiStyle: "bootstrap",
iconSet: "fontAwesome",
caption: "Invoices",
// pagination element
pager: '#jqPagerInvoice',
subGrid: true, // show subGrid
// javascript function to display the child grid
subGridRowExpanded: jqGridInvoice.showChildGrid,
subGridOptions: {
// load only once
reloadOnExpand: false,
// load the subgrid string only when you click on the "+"
selectOnExpand: true
}
});
},
// date format function
dateTimeFormatter: function(cellvalue, options, rowObject) {
var date = new Date(cellvalue);
return date.toLocaleString().replace(",", "");
},
// returns a template for the editing dialog
getTemplate: function () {
var template = "<div style='margin-left:15px;' id='dlgEditInvoice'>";
template += "<div>{CUSTOMER_ID} </div>";
template += "<div> Date: </div><div>{INVOICE_DATE}</div>";
// customer input field with a button
template += "<div> Customer <sup>*</sup>:</div>";
template += "<div>";
template += "<div style='float: left;'>{CUSTOMER_NAME}</div> ";
template += "<a style='margin-left: 0.2em;' class='btn' ";
template += "onclick='invoiceGrid.showCustomerWindow(); ";
template += "return false;'>";
template += "<span class='glyphicon glyphicon-folder-open'>";
template += "</span>Select</a> ";
template += "<div style='clear: both;'></div>";
template += "</div>";
253
Creating an Application with jOOQ and Spring MVC
254
Creating an Application with jOOQ and Spring MVC
searchtext: "Search",
addtext: "Add",
255
Creating an Application with jOOQ and Spring MVC
edittext: "Edit",
deltext: "Delete",
viewtext: "View",
viewtitle: "Selected record",
refreshtext: "Refresh"
},
jqGridInvoice.getEditInvoiceOptions(),
jqGridInvoice.getAddInvoiceOptions(),
jqGridInvoice.getDeleteInvoiceOptions()
);
// Add a button to pay the invoice
var urlPay = jqGridInvoice.options.baseAddress + '/invoice/pay';
jqGridInvoice.dbGrid.navButtonAdd('#jqPagerInvoice',
{
buttonicon: "glyphicon-usd",
title: "Pay",
caption: "Pay",
position: "last",
onClickButton: function () {
// get the id of the current record
var id = jqGridInvoice.dbGrid.getGridParam("selrow");
if (id) {
$.ajax({
url: urlPay,
type: 'POST',
data: {INVOICE_ID: id},
success: function (data) {
// Check if an error has occurred
if (data.hasOwnProperty("error")) {
jqGridInvoice.alertDialog('??????',
data.error);
} else {
// refresh grid
$("#jqGridInvoice").jqGrid(
'setGridParam',
{
datatype: 'json'
}
).trigger('reloadGrid');
}
}
});
}
}
}
);
},
init: function () {
jqGridInvoice.initGrid();
jqGridInvoice.initPager();
},
afterSubmit: function (response, postdata) {
var responseData = response.responseJSON;
// Check if an error has occurred
if (responseData.hasOwnProperty("error")) {
if (responseData.error.length) {
return [false, responseData.error];
}
256
Creating an Application with jOOQ and Spring MVC
} else {
// refresh grid
$(this).jqGrid(
'setGridParam',
{
datatype: 'json'
}
).trigger('reloadGrid');
}
return [true, "", 0];
},
getInvoiceLineColModel: function (parentRowKey) {
return [
{
label: 'Invoice Line ID',
name: 'INVOICE_LINE_ID',
key: true,
hidden: true
},
{
label: 'Invoice ID',
name: 'INVOICE_ID',
hidden: true,
editrules: {edithidden: true, required: true},
editable: true,
edittype: 'custom',
editoptions: {
custom_element: function (value, options) {
// create hidden input
return $("<input>")
.attr('type', 'hidden')
.attr('rowid', options.rowId)
.addClass("FormElement")
.addClass("form-control")
.val(parentRowKey)
.get(0);
}
}
},
{
label: 'Product ID',
name: 'PRODUCT_ID',
hidden: true,
editrules: {edithidden: true, required: true},
editable: true,
edittype: 'custom',
editoptions: {
custom_element: function (value, options) {
// create hidden input
return $("<input>")
.attr('type', 'hidden')
.attr('rowid', options.rowId)
.addClass("FormElement")
.addClass("form-control")
.val(value)
.get(0);
}
}
257
Creating an Application with jOOQ and Spring MVC
},
{
label: 'Product',
name: 'PRODUCT_NAME',
width: 300,
editable: true,
edittype: "text",
editoptions: {
size: 50,
maxlength: 60,
readonly: true
},
editrules: {required: true}
},
{
label: 'Price',
name: 'SALE_PRICE',
formatter: 'currency',
editable: true,
editoptions: {
readonly: true
},
align: "right",
width: 100
},
{
label: 'Quantity',
name: 'QUANTITY',
align: "right",
width: 100,
editable: true,
editrules: {required: true, number: true, minValue: 1},
editoptions: {
dataEvents: [{
type: 'change',
fn: function (e) {
var quantity = $(this).val() - 0;
var price =
$('#dlgEditInvoiceLine input[name=SALE_PRICE]').val()-0;
var total = quantity * price;
$('#dlgEditInvoiceLine input[name=TOTAL]').val(total);
}
}],
defaultValue: 1
}
},
{
label: 'Total',
name: 'TOTAL',
formatter: 'currency',
align: "right",
width: 100,
editable: true,
editoptions: {
readonly: true
}
}
];
258
Creating an Application with jOOQ and Spring MVC
},
// returns the options for editing the invoice item
getEditInvoiceLineOptions: function () {
return {
url: jqGridInvoice.options.baseAddress + '/invoice/editdetail',
reloadAfterSubmit: true,
closeOnEscape: true,
closeAfterEdit: true,
drag: true,
modal: true,
top: $(".container.body-content").position().top + 150,
left: $(".container.body-content").position().left + 150,
template: jqGridInvoice.getTemplateDetail(),
afterSubmit: jqGridInvoice.afterSubmit,
editData: {
INVOICE_LINE_ID: function () {
var selectedRow = jqGridInvoice.detailGrid
.getGridParam("selrow");
var value = jqGridInvoice.detailGrid
.getCell(selectedRow, 'INVOICE_LINE_ID');
return value;
},
QUANTITY: function () {
return $('#dlgEditInvoiceLine input[name=QUANTITY]').val();
}
}
};
},
// returns options for adding an invoice item
getAddInvoiceLineOptions: function () {
return {
url: jqGridInvoice.options.baseAddress + '/invoice/createdetail',
reloadAfterSubmit: true,
closeOnEscape: true,
closeAfterAdd: true,
drag: true,
modal: true,
top: $(".container.body-content").position().top + 150,
left: $(".container.body-content").position().left + 150,
template: jqGridInvoice.getTemplateDetail(),
afterSubmit: jqGridInvoice.afterSubmit,
editData: {
INVOICE_ID: function () {
var selectedRow = jqGridInvoice.dbGrid.getGridParam("selrow");
var value = jqGridInvoice.dbGrid
.getCell(selectedRow, 'INVOICE_ID');
return value;
},
PRODUCT_ID: function () {
return $('#dlgEditInvoiceLine input[name=PRODUCT_ID]').val();
},
QUANTITY: function () {
return $('#dlgEditInvoiceLine input[name=QUANTITY]').val();
}
}
};
},
// returns the option to delete the invoice item
259
Creating an Application with jOOQ and Spring MVC
getDeleteInvoiceLineOptions: function () {
return {
url: jqGridInvoice.options.baseAddress + '/invoice/deletedetail',
reloadAfterSubmit: true,
closeOnEscape: true,
closeAfterDelete: true,
drag: true,
msg: "Delete the selected item?",
afterSubmit: jqGridInvoice.afterSubmit,
delData: {
INVOICE_LINE_ID: function () {
var selectedRow = jqGridInvoice.detailGrid
.getGridParam("selrow");
var value = jqGridInvoice.detailGrid
.getCell(selectedRow, 'INVOICE_LINE_ID');
return value;
}
}
};
},
// Event handler for the parent grid expansion event
// takes two parameters: the parent record identifier
// and the primary record key
showChildGrid: function (parentRowID, parentRowKey) {
var childGridID = parentRowID + "_table";
var childGridPagerID = parentRowID + "_pager";
// send the primary key of the parent record
// to filter the entries of the invoice items
var childGridURL = jqGridInvoice.options.baseAddress
+ '/invoice/getdetaildata';
childGridURL = childGridURL + "?INVOICE_ID="
+ encodeURIComponent(parentRowKey);
// add HTML elements to display the table and page navigation
// as children for the selected row in the master grid
$('<table>')
.attr('id', childGridID)
.appendTo($('#' + parentRowID));
$('<div>')
.attr('id', childGridPagerID)
.addClass('scroll')
.appendTo($('#' + parentRowID));
// create and initialize the child grid
jqGridInvoice.detailGrid = $("#" + childGridID).jqGrid({
url: childGridURL,
mtype: "GET",
datatype: "json",
page: 1,
colModel: jqGridInvoice.getInvoiceLineColModel(parentRowKey),
loadonce: false,
width: '100%',
height: '100%',
guiStyle: "bootstrap",
iconSet: "fontAwesome",
pager: "#" + childGridPagerID
});
// displaying the toolbar
$("#" + childGridID).jqGrid(
'navGrid', '#' + childGridPagerID,
260
Creating an Application with jOOQ and Spring MVC
{
search: false,
add: true,
edit: true,
del: true,
refresh: true
},
jqGridInvoice.getEditInvoiceLineOptions(),
jqGridInvoice.getAddInvoiceLineOptions(),
jqGridInvoice.getDeleteInvoiceLineOptions()
);
},
// returns a template for the invoice item editor
getTemplateDetail: function () {
var template = "<div style='margin-left:15px;' ";
template += "id='dlgEditInvoiceLine'>";
template += "<div>{INVOICE_ID} </div>";
template += "<div>{PRODUCT_ID} </div>";
// input field with a button
template += "<div> Product <sup>*</sup>:</div>";
template += "<div>";
template += "<div style='float: left;'>{PRODUCT_NAME}</div> ";
template += "<a style='margin-left: 0.2em;' class='btn' ";
template += "onclick='invoiceGrid.showProductWindow(); ";
template += "return false;'>";
template += "<span class='glyphicon glyphicon-folder-open'>";
template += "</span> Select</a> ";
template += "<div style='clear: both;'></div>";
template += "</div>";
template += "<div> Quantity: </div><div>{QUANTITY} </div>";
template += "<div> Price: </div><div>{SALE_PRICE} </div>";
template += "<div> Total: </div><div>{TOTAL} </div>";
template += "<hr style='width: 100%;'/>";
template += "<div> {sData} {cData} </div>";
template += "</div>";
return template;
},
// Display selection window from the goods directory.
showProductWindow: function () {
var dlg = $('<div>')
.attr('id', 'dlgChooseProduct')
.attr('aria-hidden', 'true')
.attr('role', 'dialog')
.attr('data-backdrop', 'static')
.css("z-index", '2000')
.addClass('modal')
.appendTo($('body'));
261
Creating an Application with jOOQ and Spring MVC
.addClass("close")
.attr('type', 'button')
.attr('aria-hidden', 'true')
.attr('data-dismiss', 'modal')
.html("×")
.appendTo(dlgHeader);
$("<h5>").addClass("modal-title")
.html("Select product")
.appendTo(dlgHeader);
var dlgBody = $('<div>')
.addClass("modal-body")
.appendTo(dlgContent);
var dlgFooter = $('<div>').addClass("modal-footer")
.appendTo(dlgContent);
$("<button>")
.attr('type', 'button')
.addClass('btn')
.html('OK')
.on('click', function () {
var rowId = $("#jqGridProduct")
.jqGrid("getGridParam", "selrow");
var row = $("#jqGridProduct")
.jqGrid("getRowData", rowId);
$('#dlgEditInvoiceLine input[name=PRODUCT_ID]')
.val(row["PRODUCT_ID"]);
$('#dlgEditInvoiceLine input[name=PRODUCT_NAME]')
.val(row["NAME"]);
$('#dlgEditInvoiceLine input[name=SALE_PRICE]')
.val(row["PRICE"]);
var price = $('#dlgEditInvoiceLine input[name=SALE_PRICE]')
.val()-0;
var quantity = $('#dlgEditInvoiceLine input[name=QUANTITY]')
.val()-0;
var total = Math.round(price * quantity * 100) / 100;
$('#dlgEditInvoiceLine input[name=TOTAL]').val(total);
dlg.modal('hide');
})
.appendTo(dlgFooter);
$("<button>")
.attr('type', 'button')
.addClass('btn')
.html('Cancel')
.on('click', function () {
dlg.modal('hide');
})
.appendTo(dlgFooter);
$('<table>')
.attr('id', 'jqGridProduct')
.appendTo(dlgBody);
$('<div>')
.attr('id', 'jqPagerProduct')
.appendTo(dlgBody);
dlg.on('hidden.bs.modal', function () {
dlg.remove();
});
262
Creating an Application with jOOQ and Spring MVC
dlg.modal();
jqGridProductFactory({
baseAddress: jqGridInvoice.options.baseAddress
});
},
// Display the selection window from the customer's directory.
showCustomerWindow: function () {
// the main block of the dialog
var dlg = $('<div>')
.attr('id', 'dlgChooseCustomer')
.attr('aria-hidden', 'true')
.attr('role', 'dialog')
.attr('data-backdrop', 'static')
.css("z-index", '2000')
.addClass('modal')
.appendTo($('body'));
// block with the contents of the dialog
var dlgContent = $("<div>")
.addClass("modal-content")
.css('width', '730px')
.appendTo($('<div>')
.addClass('modal-dialog')
.appendTo(dlg));
// block with dialog header
var dlgHeader = $('<div>').addClass("modal-header")
.appendTo(dlgContent);
// button "X" for closing
$("<button>")
.addClass("close")
.attr('type', 'button')
.attr('aria-hidden', 'true')
.attr('data-dismiss', 'modal')
.html("×")
.appendTo(dlgHeader);
// title of dialog
$("<h5>").addClass("modal-title")
.html("Select customer")
.appendTo(dlgHeader);
// body of dialog
var dlgBody = $('<div>')
.addClass("modal-body")
.appendTo(dlgContent);
// footer of dialog
var dlgFooter = $('<div>').addClass("modal-footer")
.appendTo(dlgContent);
// "OK" button
$("<button>")
.attr('type', 'button')
.addClass('btn')
.html('OK')
.on('click', function () {
var rowId = $("#jqGridCustomer")
.jqGrid("getGridParam", "selrow");
var row = $("#jqGridCustomer")
.jqGrid("getRowData", rowId);
// Keep the identifier and the name of the customer
// in the input elements of the parent form.
263
Creating an Application with jOOQ and Spring MVC
$('#dlgEditInvoice input[name=CUSTOMER_ID]')
.val(rowId);
$('#dlgEditInvoice input[name=CUSTOMER_NAME]')
.val(row["NAME"]);
dlg.modal('hide');
})
.appendTo(dlgFooter);
// "Cancel" button
$("<button>")
.attr('type', 'button')
.addClass('btn')
.html('Cancel')
.on('click', function () {
dlg.modal('hide');
})
.appendTo(dlgFooter);
// add a table to display the customers in the body of the dialog
$('<table>')
.attr('id', 'jqGridCustomer')
.appendTo(dlgBody);
// add the navigation bar
$('<div>')
.attr('id', 'jqPagerCustomer')
.appendTo(dlgBody);
dlg.on('hidden.bs.modal', function () {
dlg.remove();
});
// display dialog
dlg.modal();
jqGridCustomerFactory({
baseAddress: jqGridInvoice.options.baseAddress
});
},
// A window for displaying the error.
alertDialog: function (title, error) {
var alertDlg = $('<div>')
.attr('aria-hidden', 'true')
.attr('role', 'dialog')
.attr('data-backdrop', 'static')
.addClass('modal')
.appendTo($('body'));
var dlgContent = $("<div>")
.addClass("modal-content")
.appendTo($('<div>')
.addClass('modal-dialog')
.appendTo(alertDlg));
var dlgHeader = $('<div>').addClass("modal-header")
.appendTo(dlgContent);
$("<button>")
.addClass("close")
.attr('type', 'button')
.attr('aria-hidden', 'true')
.attr('data-dismiss', 'modal')
.html("×")
.appendTo(dlgHeader);
$("<h5>").addClass("modal-title")
.html(title)
.appendTo(dlgHeader);
264
Creating an Application with jOOQ and Spring MVC
$('<div>')
.addClass("modal-body")
.appendTo(dlgContent)
.append(error);
alertDlg.on('hidden.bs.modal', function () {
alertDlg.remove();
});
alertDlg.modal();
}
};
jqGridInvoice.init();
return jqGridInvoice;
};
})(jQuery, JqGridProduct, JqGridCustomer);
The items are filtered by the primary key of the invoice. Along with the main buttons on the navigation bar, a cus-
tom button for paying for the invoice is added to the invoice header using the jqGridInvoice.dbGrid.navButtonAdd
function (see the initPager method).
Dialog Boxes
Dialog boxes for editing secondary modules are much more complicated than their primary counterparts. They
often use options selected from other modules. For that reason, these edit dialog boxes cannot be built automat-
ically using jqGrid. However, this library has an option to build dialog boxes using templates, which we use.
The dialog box template is returned by the getTemplate function. The invoiceGrid.showCustomerWindow()
function opens the customer module for selecting a customer. It uses the functions of the JqGridCustomer
module described earlier. After the customer is selected in the modal window, its key is inserted into the
CUSTOMER_ID field. Fields that are to be sent to the server using pre-processing or from hidden fields are
described in the editData property of the Edit and Add options.
Processing Dates
To get back to processing dates: as we already know, the InvoiceController controller returns the date in UTC.
Because we want to display it in the current time zone, we specify the jqGridInvoice.dateTimeFormatter date
formatting function via the formatter property of the corresponding INVOICE_DATE field.
When sending data to the server, we need the reverse operation—convert time from the current time zone to
UTC. The convertToUTC function is responsible for that.
The custom template returned by the getTemplateDetail function is also used for editing invoice items. The
invoiceGrid.showProductWindow() function opens a window for selecting a product from the product list. This
function uses the functions of the JqGridProduct module.
265
Creating an Application with jOOQ and Spring MVC
The code for the JqGridInvoice module contains detailed comments and more explanation so that you can un-
derstand the logic of its workings.
The Result
Some screenshots from the web application we have developed in our project.
266
Creating an Application with jOOQ and Spring MVC
267
Creating an Application with jOOQ and Spring MVC
268
Creating an Application with jOOQ and Spring MVC
Source Code
You can download the source code from the link fbjavaex.zip.
269
Appendix A:
License notice
The contents of this Documentation are subject to the Public Documentation License Version 1.0 (the “Li-
cense”); you may only use this Documentation if you comply with the terms of this License. Copies of the Li-
cense are available at http://www.firebirdsql.org/pdfmanual/pdl.pdf (PDF) and http://www.firebirdsql.org/man-
ual/pdl.html (HTML).
Copyright (C) 2017. All Rights Reserved. Initial Writers contact: paul at vinkenoog dot nl.
Included portions are Copyright (C) 2001-2017 by the author. All Rights Reserved.
270
Appendix B:
Document History
Revision History
0.90 27 November H.E.M.B. Pre-beta, not published
2017
1.00 25 February H.E.M.B. Initial publication, with links to English-language versions of sample
2018 applications.
271