Вы находитесь на странице: 1из 17

2004

Delphi

Informant

Magazine

Bonus

Content

INSIDE
BONUS CONTENT
Active Delphi

ASP.NET: Part III

In the last two installments of this column we examined only the surface
of a very simple ASP.NET application in Delphi 8. We saw what an ASP.NET
application looked like from the developers perspective that is, what
the application looked like on the surface. As Nick Hodges tells us,
there is more than cosmetics to even an empty, single-page ASP.NET
application. This series wrap-up takes a look at the global.asax/pas file
and the web.config file.

Columns & Rows

ADO.NET Data Access Components: Part VIII

Continuing his exploration of ADO.NET, Bill Todd explains the basics of


sending changes from the DataSet back to the database. The examples
use the Country table from the sample Employee database that ships with
InterBase.

Columns & Rows

12 ADO.NET Data Access Components: Part IX

Completing his exploration of ADO.NET, Bill Todd looks at updating the


database using stored procedures and applying updates for tables with
a one-to-many relationship. Youll see how using stored procedures for
updates gives you power and control youve never had. Youll also see how
complex and laborious updating related tables is in ADO.NET compared to
Delphi for Win32.

DELPHI INFORMANT MAGAZINE | Bonus Content

A C T I V E
ASP.NET

D E L P H I

DELPHI 8

By Nick Hodges

ASP.NET
Part III: global.asax/pas & web.config

uying a car is a complicated task. No one


wants to feel like he got a bad deal, so
you would certainly look a car over very

closely before making a decision to buy it. You


might investigate a car on the Internet, but that
would only provide pictures and a description. At
the very least, you would want to see the car in
person and sit in the thing. You would probably
want to test drive it, as well.
In the last two installments of this column we havent done
much more than look at a few pictures on the Web of an
ASP.NET application (see Part I and Part II). We have only
examined the surface of a very simple ASP.NET application
in Delphi 8. We saw what an ASP.NET application looked
like from the developers perspective that is, what the
application looked like on the surface.
However, there is more than just cosmetics to even an
empty, single-page ASP.NET application. To get a better feel,
we need to kick the tires a bit and take a walk around the
outside. There is quite a bit of infrastructure that goes along
with the act of dragging and dropping components and
hooking code to events. In this issue, well take a look at
two things that are somewhat under the hood but nevertheless happen to be right there in the Project Manager: the
global.asax/pas file and the web.config file.
The global.asax/pas file contains the code for managing application-wide code and events. It implements a
descendant of System.Web.HttpApplication. This class
2

DELPHI INFORMANT MAGAZINE | Bonus Content

is analogous to TApplication in the VCL; it provides the


infrastructure for your whole ASP.NET application, allowing you to hook code to events that take place for all
incoming requests. The web.config file contains configuration information for your application, much as a *.ini
file does for a regular client application. Both of these
entities are an important part of an ASP.NET application,
but neither are ever visible to your users. In this article,
well take a closer look at these two files, and show how
they can be used to add power and functionality to your
ASP.NET applications.
The Global Unit
The global module consists of two files, global.asax and
global.pas. The main purpose of the global module is to
provide a container for application-level code and event
handlers. The ASP.NET runtime automatically compiles
the global.asax file when the first request is made to the
application, and executes the code in the module for every
request that comes into the application.
Note: By default, ASP.NET is configured to never allow an
end user to see any file that has the *.asax extension. The
aspnet_isapi.dll has an http handler built into it for the
*.asax extension that forbids the end user to see asax pages.
The same is true for *.config files. Unless you alter your
IIS settings, or allow directory listings of your virtual directories, theres no way for end users to see the contents of
these files, so you need not worry about them sitting in the
root directory. Requesting them via the browser results in a
This type of page is not served error.
The global.asax file, by default, contains this single line:
<%@ Application Codebehind="Global.pas" Inherits="Global.Global"%>

Active

Delphi

ASP.NET

Event

Description

Application_Start

This event is fired once and only once whenever the


application itself is started. Or, put another way, it is
fired only when you start up IIS, and it is never fired
again. Use it to initialize variables that are considered global to the entire application. Dont use it to
initialize variables that are used on a per request/per
instance basis. For that, override the Init method or
use the InitializeComponents method provided with the
TGlobal class.

Session_Start

This event fires whenever a new, unrecognized visitor comes to your site. An unrecognized visitor is one
that doesnt have a valid session cookie. Every time a
new visitor makes a request, a new session is started,
and this event is fired. Use it to write code you want to
occur each time a new user shows up.

Application_BeginRequest

This event is fired at the very beginning of each individual HTTP request. You can use this code to do
any initialization that you need to do to get ready to
respond to the request.

Application_EndRequest

This event fires when the processing for each HTTP


request has been completed. This event is the last
chance to influence what gets sent back as the result
of an individual request.

Application_AuthenticateRequest

This event fires when the ASP.NET security system has


identified a specific user.

Application_Error

This event fires when an exception that isnt otherwise


handled by your code is raised.

Session_End

This fires when an individual users session expires via


time-out, or whenever a users session is ended, e.g.
via a logout.

Application_End

This occurs when the application is shut down. Just


like the Application_Start event, this event fires once
and only once in an applications lifetime, normally
when IIS is shutdown or restarted.

Figure 1: TGlobal class events

The Application attribute simply defines the unit of code


that will provide the code-behind for the global application
object. The global.asax file must reside in the root folder
of your application. You can, if you wish, add <script>based event handlers to the global.asax file, but its much
easier to manage such code in the global.pas code-behind
file. (Delphi 8 doesnt support the use of Delphi in *.as?x
pages.) In addition, you can declare server-side object tags
in this file. You can also add server-side include references
to the file, and they function as youd expect an include
directive to function. (See the MSDN documentation for
more information on these last two features, as they wont
be covered here.)
As Delphi developers, we want to do most of our coding
inside real, no-kidding *.pas files. Thus, the global.pas
file is a regular Delphi code file that contains, by default,
a declaration of the TGlobal class that, as mentioned
above, descends from System.Web.HttpApplication. The
main function of this class from the developers perspective is to provide events that occur during the lifecycle
3

DELPHI INFORMANT MAGAZINE | Bonus Content

of a single request. The default unit


includes stubs for eight applicationlevel events, though there are more,
but less common, events available for
your use. Again, the important thing
to note is that the event handlers in
the TGlobal class are application-level
events. Some events will fire for every
individual request, some will fire for
each request for a given user, and
some will fire once for each instance
of the application object, but they are
all horizontal events, i.e. the code in
the event handlers will cut across all
pages, and none of the code written
in them will be specific to any specific
Web page.
ASP.NET will manage and cache a
collection of instances of the TGlobal
object. (If youre a WebBroker/
WebSnap user, the TGlobal object is
managed and cached in much the same
way that WebModules were.) Thus,
when adding code to these events, you
need to remember that the class may
be dirty, i.e. the instance of the class
that you get for use may have been
used before, and it may have been left
in a state that needs resetting.
TGlobal is like any other Delphi class;
you can add methods, events, and
properties to it. If you click over to the
Design tab, you can drop non-visual
components onto the designer, and they
will display just like any other component, allowing you to set their properties in the Object Inspector. For example, the TGlobal class is a good place to
manage your database connections.

The TGlobal class exposes a number of events for which you


can write event handler code. The table in Figure 1 discusses
the eight default events and their functions.
There is more to the TGlobal object than the event handlers,
however. The class also exposes a number of very valuable
properties that contain critical application-level information
(see Figure 2).
The Web.Config File
Also included in your application is a file called web.config.
The web.config file contains configuration information for your
application. You can think of it as the *.ini file for your application, though it is a bit more useful than the standard *.ini file.
For starters, it is an XML file, so while its a bit more
complicated than a standard *.ini file, it certainly is more
powerful and can contain all sorts of information. A
web.config file has a standard schema that contains infor-

Active

Delphi

ASP.NET

Property

Description

Tag Name

Description

Application

This points to the Application object, which contains information available to all users throughout
the application. In other words, the information
held here is global, in that all users see the same
information.

<compilation>

Context

This points to an instance of the HTTPContext class


for the given request.

Modules

Points to a collection of all the modules that are


current to the application.

This section determines how the framework


will treat the code in your application. The
most important attribute here is the debug
attribute, which, when set to true, will provide
a thorough error message and stack trace for
any errors that occur in your application. This
section can also be configured to use alternative dynamic scripting languages such as Perl
or Delphi.

Request

This points to an instance of the HTTPRequest class


that represents the current request. You can access
this information as needed to properly respond to
the current request.

<customErrors>

Response

This points to an instance of the HTTPResponse


class that represents the current response. You can
set the properties of this class to respond to the
request as needed.

Server

This points to an instance of the HTTPServerUtility


class, which contains information about the physical server that is running the application.

This tag defines how the framework is


to deal with custom errors. If the mode
attribute is set to On, then a generic error
message is returned. If Off, then a detailed
error message that is useful to developers is returned. If set to RemoteOnly, then
the detailed error is sent only to the local
machine, and remote users see the generic
error message. You can add sub-tags here
that control exactly what users see for specific HTTP errors as well.

Session

This points to the current session object, which


contains user unique information. In other words,
the same variables are available to all users, but
the values for those variables will vary on a per
user basis.

<authentication>

User

This points to the instance of the IPrincipal interface


that contains information about the current user.

This tag determines how users are authenticated within your application. It can only
be defined in application-level or higher
config files. You tell your application to
use Windows authentication, Forms-based
authentication, Passport authentication, or
no authentication.

<trace>

This section determines whether tracing is on


or off for an application. When on, a complete trace of your application will be augmented onto a given pages response. This
is obviously useful during development.

<sessionState>

This section determines how the application


will deal with user session information. Session information can be stored in-process, in
a separate process designed to handle session information, in SQL Server, or not at all.

<globalization>

This section defines the globalization setting


for an application. You can determine the
encoding methods for the incoming requests
and outgoing responses, as well as set the
culture for the application.

Figure 2: TGlobal class properties.

mation that the framework will look for and use. In addition, you can add your own data into the file.
A web.config file is also specific not only to the application
as a whole, but it can be used to provide specific configuration information to specific portions of an application.
For instance, all ASP.NET applications require a web.config
file to reside in the root directory of the application. You
can also place a web.config file in a directory below your
root directory, and provide customized configuration information for that directory and all sub-directories below it.
web.config files in a subdirectory can both override and
augment the web.config files in parent directories. Note
that this inheritance hierarchy is not based on the physical
directories on your hard-drive, but on the virtual directory
structure defined by your application and the Web server.
For example, you may have an administrator function in
your application you want to be accessible only to properly
authenticated users. To make that subdirectory and all directories below it available only to authenticated users, you can
add a specialized web.config file in that directory with the
following entry in it:
<system.Web>
<authorization>
<deny users="?"/>
</authorization>
</system.Web>

DELPHI INFORMANT MAGAZINE | Bonus Content

Figure 3: Default settings in web.config.

The web.config file is an XML file, and therefore must contain


properly formed XML in order for the framework to properly
read it. For instance, all element tags must be in the proper
case. (Sadly, the case for these files is the very un-Delphiish camelCase.) The information in your applications main
web.config file can both augment and override the settings in
your machine.config file. The configuration information for a
given application can be accessed via the ConfigurationSettings
class in the System.Configuration namespace.
The default web.config file for a Delphi ASP.NET application
looks like this:

Active

Delphi

ASP.NET

<?xml version="1.0" encoding="utf-8" ?>


<configuration>
<system.Web>
<compilation debug="true" defaultLanguage="c#">
</compilation>
<customErrors mode="RemoteOnly"/>
<authentication mode="Windows" />
<trace
enabled="false"
requestLimit="10"
pageOutput="false"
traceMode="SortByTime"
localOnly="true"/>
<sessionState
mode="InProc"
stateConnectionString="tcpip=127.0.0.1:42424"
sqlConnectionString="data source=127.0.0.1;userid= sa;
password="
cookieless="false"
timeout="20"/>
<globalization requestEncoding="utf-8"
responseEncoding="utf-8"/>
</system.Web>
</configuration>

The settings in the default web.config provide configuration


information about six basic pieces of information, which are
outlined in the table shown in Figure 3.
There are a couple of other items to note about web.config
files. First, like global.asax files, the ASP.NET framework forbids the viewing of these files via a client browser. Second,
the ASP.NET framework caches these files for faster access,
but it will notice if the file is updated and reload and apply
it when the file changes. This allows you to change the configuration of your application without shutting down and
restarting the server.

Nick Hodges is the Chief Technology Officer for Lemanix Corporation


(www.lemanix.com), a Borland Solutions Partner in the Twin Cities of
Minnesota. Nick is a member of TeamB (www.teamb.com) and a frequent
speaker at the annual Borland Conference. Nick lives in St. Paul with his
wife and three children. He can be reached at nickhodges@yahoo.com.

DELPHI INFORMANT MAGAZINE | Bonus Content

C O L U M N S
ADO.NET

&

R O W S

DELPHI 8 FOR THE MICROSOFT .NET FRAMEWORK

By Bill Todd

ADO.NET Data Access


Components
Part VIII: Sending Changes Back to the Database

revious articles in this series demonstrated


how to use a DataAdapter to load data
into one or more DataTables in a DataSet,

bind user interface objects, such as a DataGrid, to


the DataTables, and edit the data (see Part VII).
But the changes you make are only changing the
data held in local memory in the DataSet object.
The next trick is to send the changes back to the
database server to update the database. In Delphi
for Win32 this is almost always pretty easy. Using
ADO.NET, the process ranges from fairly simple
to preposterously complex. On the other hand,
ADO.NET gives you much more control of the
update process than Delphi for Win32, and this
lets you do some very useful things.
A Simple Example
The first sample project for this article is named Update801
and uses the Country table from the InterBase Employee
database (the sample projects are available for download;
see end of article for details). Figure 1 shows the main form,
which contains a MainMenu, DataGrid, BdpConnection,
BdpDataAdapter, and DataSet. The BdpConnection and the
BdpDataAdapter were added by dragging the Country table
from the Data Explorer to the form. The File menu contains
two choices, Open Country and Exit. The OnClick event handler
for the Open Country menu item calls the OpenCountry procedure shown in Figure 2. This code is almost identical to the
6

DELPHI INFORMANT MAGAZINE | Bonus Content

Figure 1: The sample application form.

OpenCountry method in the sample application from Part


VII; youll find a detailed explanation there.
Now that all of the components are on the form it is time
to configure the DataAdapter. Before you can use the
DataAdapter.Update method to update the database you
must provide the SQL statements that the DataAdapter
will use to process updates, inserts, and deletes. The

Columns

&

Rows

ADO.NET Data Access Components

procedure CountryForm.OpenCountry;
var
CountryPrimaryKey: array of DataColumn;
CountryDataView: DataView;
begin
if (EmployeeConn.State = ConnectionState.Closed) then
EmployeeConn.Open;
// Set name of Table that fill will create to Country.
if (not CountryAdapter.TableMappings.Contains('Table1')) then
CountryAdapter.TableMappings.Add('Table1', 'Country');
CountryAdapter.Fill(EmpDataSet, 'Country');
// Create dynamic array of DataColumn objects, add Country
// column to the array, and set the PrimaryKey property of
// the DataTable.
SetLength(CountryPrimaryKey, 1);
CountryPrimaryKey[0] :=
EmpDataSet.Tables['Country'].Columns['Country'];
EmpDataSet.Tables['Country'].PrimaryKey := CountryPrimaryKey;
// Bind the DataGrid to the DataTable.
CountryDataView := EmpDataSet.Tables['Country'].DefaultView;
CountryGrid.DataSource := CountryDataView;
CountryDataView.Sort := 'Country';
end;

Figure 2: The OpenCountry method.

DataAdapter from the Tool Palette or if you need to


change the SQL that was generated automatically,
simply right-click the DataAdapter and choose Configure DataAdapter to display the DataAdapter Configuration dialog box shown in Figure 3.
Use the controls on the Command tab to create or alter
the SELECT statement. Click the Generate SQL button
and the SQL statements corresponding to the checked
checkboxes in the SQL Commands groupbox will be
generated automatically. You can click the Update,
Insert, and Delete tabs to view and alter the individual
SQL statements.
The Optimize checkbox is the mystery control in this
dialog box because the name is completely misleading
and so is the online help. If you look at the Update
tab, shown in Figure 4, youll see that all the fields
are included in the WHERE clause with parameters for
their values. When you call DataAdapter.Update the
DataAdapter assigns the original value of each field
to the corresponding parameter in the WHERE clause
and assigns the current value of each field to the corresponding parameter in the UPDATE clause. This
ensures that the update will fail if another user has changed
the row since you read it. If youve worked with DataSnap
or dbExpress in Delphi for Win32 this gives you the same
behavior that youd get by setting the DataSetProviders
UpdateMode property to upWhereAll.
If you check the Optimize checkbox then click the Generate
SQL button again, youll see the Currency field vanish from
the WHERE clause in the UPDATE statement. What the
Optimize checkbox really does is change the WHERE clause
so that only the primary key field or fields are included.
This ensures that your update or delete will succeed,
unless another user has changed the primary key. This provides the same behavior as the upWhereKeyOnly option of
the DataSetProvider in Delphi for Win32.

Figure 3: The DataAdapter Configuration dialog box.

DataAdapter has three properties, named DeleteCommand,


InsertCommand, and UpdateCommand. Each of these properties holds a reference to a BdpCommand object. Each
Command object holds the corresponding SQL statement.
One way to create the insert, update, and delete statements
is to add three BdpCommand objects from the Tool Palette
to your form and set the CommandText property of each
Command object to the appropriate SQL statement. Use the
Object Inspector to set the InsertCommand, DeleteCommand,
and UpdateCommand properties of the DataAdpater to the
appropriate Command object and you are ready to go. However, there is an easier way.
If you drag a table from the Data Explorer to create the
Connection and DataAdapter components, the Delphi IDE
will create the insert, update, and delete Command objects
and their SQL statements for you. If you have added the
7

DELPHI INFORMANT MAGAZINE | Bonus Content

The Delphi for Win32 DataSetProvider provides a third


option, upWhereChanged. When you use this option the
DataSetProvider generates an INSERT or DELETE statement
whose WHERE clause contains only the primary key field(s)
and any fields whose value you have changed. This is possible since the DataSetProvider is creating the UPDATE and
DELETE statements on-the-fly. Unfortunately, there is no
corresponding option in ADO.NET.
After you click the Generate SQL button and close the dialog
box by clicking OK, you can access the SQL statements for
each of the Command objects by selecting the DataAdapter
then expanding the SelectCommand, DeleteCommand,
InsertCommand, or UpdateCommand properties in the
Object Inspector. Expanding one of the DataAdapter
Command properties gives you access to all the properties
of the Command object, including the CommandText property that holds the SQL statement.
There are two problems with the SQL produced by the
Generate SQL button. If you dont check the Optimize box all
the non-blob fields are included in the WHERE clause. This

Columns

&

Rows

ADO.NET Data Access Components

is intended to ensure that your update or delete operaprocedure CountryForm.UpdateDatabaseItem_Click(


tions will fail if the data has been changed by another
sender: System.Object; e: System.EventArgs);
user since you read the record. However, this will not
var
work reliably for tables that include blob columns. Since
CountryGridCurrencyMgr: CurrencyManager;
blobs cannot be compared for equality, this method will
begin
CountryGridCurrencyMgr := CountryGrid.BindingContext[
fail if another user changes only blob columns. If you are
EmpDataSet.Tables['Country'].DefaultView] as CurrencyManager;
working with a table that contains blob columns a betCountryGridCurrencyMgr.EndCurrentEdit;
ter solution is to add a NUMERIC(18,0) column named
// If there are pending changes, update the database.
UPDATE_NUMBER. Use a before insert trigger to set the
if (EmpDataSet.Tables['Country'].GetChanges.Rows.Count > 0)
then
value to zero. Create a generator for each table to supply
CountryAdapter.Update(EmpDataSet, 'Country');
update numbers and use a before update trigger to get the
end;
next value from the generator and assign it to the column.
This ensures that the UPDATE_NUMBER column will be
Figure 4: The Update Database Click event handler (version 1).
changed whenever any other column, including a blob
column, is changed. As long as the UPDATE_NUMBER
column is included in the WHERE clause of your UPDATE
and DELETE statements you will reliably detect changes
made by other users. In fact, the UPDATE_NUMBER column
and the primary key are the only columns you need in the
WHERE clause.
The second problem occurs when you have a column that
allows nulls. The UPDATE statement for the Country table is:
UPDATE
SET
WHERE
AND

COUNTRY
COUNTRY = ?, CURRENCY = ?
COUNTRY = ?
CURRENCY = ?

Although the Currency column in the Employee database


does not allow nulls, pretend for a moment that it does and
that you have just entered the currency for a country whose
Currency field was null. Replacing the parameters with their
values would give an UPDATE statement like this:
UPDATE
SET
WHERE
AND

COUNTRY
COUNTRY = 'USA', CURRENCY = 'Dollar'
COUNTRY = 'USA'
CURRENCY = NULL

This statement will fail because CURRENCY = NULL is not


true. Null is never equal to anything. The correct SQL is:
UPDATE
SET
WHERE
AND

COUNTRY
COUNTRY = 'USA', CURRENCY = 'Dollar'
COUNTRY = 'USA'
CURRENCY IS NULL

This SQL works if the original value of the Currency field


is null, but does not work if the original value was not
null. The SQL that should be generated for any column
that allows nulls is:
UPDATE
SET
WHERE
AND

COUNTRY
COUNTRY = ?, CURRENCY = ?
COUNTRY = ?
(CURRENCY = ? OR (? IS NULL AND CURRENCY IS NULL))

If you are working with a table that has columns that allow
nulls you will have to change the WHERE clause of the
UPDATE and DELETE statements generated by the Generate
SQL button manually to allow for a null original value.
8

DELPHI INFORMANT MAGAZINE | Bonus Content

Figure 5: The DataGridTableStyle Collection Editor.

Updating the Database


In theory, all you have to do to apply the changes you
have made to a DataTable to the database is call the
DataAdapter.Update method. In fact, its much more difficult. Start by adding an Edit item to the forms main menu,
then add an Update Database choice below Edit. Create a Click
event handler and add the following line:
if (EmpDataSet.Tables['Country'].GetChanges.Rows.Count > 0) then
CountryAdapter.Update(EmpDataSet, 'Country');

The if statement is not necessary. Nothing bad happens if you


call Update and there are no pending changes. I included the
if statement to show how to tell if a DataTable has pending
changes in case you need to know. This statement will work
only if you move to a different row in the grid after making
changes and before calling Update. If you dont move off the
row that contains the changes, the changes will not be written to the DataTable and will not be applied to the database.
The solution is to call the DataGrids CurrencyManagers
EndEdit method. Figure 4 shows the Update Database menu
items Click event handler with this code added. The first
statement gets a reference to the DataGrids CurrencyManager
and assigns it to the CountryGridCurrencyMgr variable. The
second line calls the CurrencyManager.EndEdit method. See
Part VII of this series for a more detailed explanation of the
CurrencyManager class.

Columns

&

Rows

ADO.NET Data Access Components

With the addition of the call to the


CurrencyManager.EndEdit method, the user no
longer has to move off the current row before calling Update but the user still must move off the
last field that he/she changed or the change to
that field will not be applied to the database. The
solution is to call the DataGrid.EndEdit method.
Unfortunately, the DataGrid.EndEdit method
requires a DataGridColumnStyle as a parameter, so
before changing the code you first have to add a
DataGridTableStyle and two DataGridColumnStyles
to the DataGrid.
Select the DataGrid, then click the ellipsis button
for the TableStyles property in the Object Inspector.
This will open the DataGridTableStyle Collection
Editor shown in Figure 5. Click the Add button to
add a new DataGridTableStyle. Next, scroll down
to the GridColumnStyles property and click the
ellipsis button to open the DataGridColumnStyle
Collection Editor. Click the Add button twice to add
a DataGridColumnStyle for each column in the
Country table. Click the OK button to close both
collection editors.
Modify the Update Database menu items Click event
handler as shown in Figure 6. This code begins by
getting the number of the column that contains the
current cell (the cell that has focus). The next line
gets a reference to the DataGridColumnStyle for
the column that contains the current cell. The third
line calls the DataGrid.EndEdit method, passing the
DataGridColumnStyle for the current cell as its first
parameter. The second parameter is the row number of the row that contains the current cell. The
third parameter is set to True if you want to abort
the changes to the current cell and False if you want
to keep the changes.
At last we have a method that will update the
database with all the changes the user has
made without requiring the user to remember to
move off the last row that was changed before
updating the database. Compare the 12 lines of
code (including variable declarations) in Figure
6 with the following code that does the same
thing using DataSnap or dbExpress in Delphi for
Win32:

procedure CountryForm.UpdateDatabaseItem_Click(
sender: System.Object; e: System.EventArgs);
var
CountryGridCurrencyMgr: CurrencyManager;
ColStyle: DataGridColumnStyle;
ColNumber: Integer;
begin
// Call DataGrid.EndEdit to post changes to current field.
ColNumber := CountryGrid.CurrentCell.ColumnNumber;
ColStyle :=
CountryGrid.TableStyles[0].GridColumnStyles[ColNumber];
CountryGrid.EndEdit(ColStyle, CountryGrid.CurrentCell.RowNumber, False);
// Call CurrencyManaager.EndEdit to post changes to current row.
CountryGridCurrencyMgr := CountryGrid.BindingContext[
EmpDataSet.Tables['Country'].DefaultView] as CurrencyManager;
CountryGridCurrencyMgr.EndCurrentEdit;
// If there are pending changes, update the database.
if (EmpDataSet.Tables['Country'].GetChanges.Rows.Count > 0) then
CountryAdapter.Update(EmpDataSet, 'Country');
end;

Figure 6: The Update Database Click event handler (version 2).

procedure CountryForm.UpdateDatabaseItem_Click(
sender: System.Object; e: System.EventArgs);
var
CountryGridCurrencyMgr: CurrencyManager;
ColStyle: DataGridColumnStyle;
ColNumber: Integer;
CurrentCountry: String;
begin
// Call DataGrid.EndEdit to post changes to the current field.
ColNumber := CountryGrid.CurrentCell.ColumnNumber;
ColStyle := CountryGrid.TableStyles[0].GridColumnStyles[ColNumber];
CountryGrid.EndEdit(
ColStyle, CountryGrid.CurrentCell.RowNumber, False);
// Call CurrencyManaager.EndEdit to post changes to the current row.
CountryGridCurrencyMgr := CountryGrid.BindingContext[
EmpDataSet.Tables['Country'].DefaultView] as CurrencyManager;
CountryGridCurrencyMgr.EndCurrentEdit;
// If there are pending changes, update the database.
if (EmpDataSet.Tables['Country'].GetChanges.Rows.Count > 0) then
CountryAdapter.Update(EmpDataSet, 'Country');
// Reread the data to see changes made by other users.
RowNumber := CountryGridCurrencyMgr.Position;
CurrentCountry := EmpDataSet.Tables[
'Country'].DefaultView[RowNumber]['Country'].ToString;
EmpDataSet.Tables['Country'].Clear;
CountryAdapter.Fill(EmpDataSet, 'Country');
CountryGridCurrencyMgr.Position :=
EmpDataSet.Tables['Country'].DefaultView.Find(CurrentCountry);
end;

Figure 7: The Update Database Click event handler (final version).

ClientDataSet.Post;
ClientDataSet.ApplyUpdates(0);

After updating the database you may want to refresh your


view of the data to show any changes made by other users.
Figure 7 shows the complete code for the Update Database
Click event handler. The first line of code after the call to
Update saves the current row number. The next line saves
the value of the Country field. The call to EmpDataSet.Tab
les['Country'].Clear removes all records from the Country DataTable and the call to the DataAdapter.Fill method
rereads the data and loads it into the Country DataTable.
The last statement finds the row that contains the country
9

DELPHI INFORMANT MAGAZINE | Bonus Content

the cursor was on originally and restores the cursor to that


row by setting the CurrencyManagers Position property.
Using CommandBuilder
If your SELECT statement returns data from a single table, the
table has a primary key, and the primary key is included in the
SELECT clause, you can use the CommandBuilder to automatically generate the INSERT, UPDATE, and DELETE statements at
run time. The Update802 sample application is almost identical
to the Update801 application, except that it includes a BdpCommandBuilder that was added from the Tool Palette.
After adding the CommandBuilder, which is named
CountryCmdBldr, select the DataAdapter, then right click

Columns

&

Rows

ADO.NET Data Access Components

and choose Configure DataAdapter. Uncheck the Insert,


Update, and Delete checkboxes, then click the Generate SQL button. This removes the INSERT, UPDATE,
and DELETE SQL statements. Next, select the CommandBuilder, then go to Object Inspector, drop down
the list for the DataAdapter property, and choose
DataAdapter. Now move to the UpdateMode property
and choose either All or Key. UpdateMode controls
which columns are included in the WHERE clause for
the UPDATE and DELETE statements. When you call
the DataAdapter.Update method the CommandBuilder
will automatically generate the INSERT, UPDATE, and
DELETE Command objects and their respective SQL
statements.
You can get a reference to the INSERT, UPDATE, and
DELETE Command objects that the CommandBuilder
creates by calling its GetInsertCommand,
GetUpdateCommand, or GetDeleteCommand method.
The Update802 sample application has a View menu
that lets you display the SQL statements generated by
the CommandBuilder. Figure 8 shows the code that
displays the INSERT Command statement.

procedure CountryForm.ViewInsertItem_Click(
sender: System.Object; e: System.EventArgs);
begin
MessageBox.Show(
CountryCmdBldr.GetInsertCommand.CommandText,
'Insert Statement');
end;

Figure 8: Showing the INSERT Command.

if (EmpDataSet.Tables['Country'].GetChanges.Rows.Count > 0) then


CountryAdapter.AutoUpdate(
EmpDataSet, 'Country', BdpUpdateMode.All);

Figure 9: Using AutoUpdate.

function CountryForm.GetTransaction: BdpTransaction;


begin
Result := EmployeeConn.BeginTransaction(
IsolationLevel.ReadCommitted, 'CountryTrans');
CountryAdapter.DeleteCommand.Transaction := Result;
CountryAdapter.InsertCommand.Transaction := Result;
CountryAdapter.UpdateCommand.Transaction := Result;
end;

Why use the CommandBuilder when the IDE will generate the INSERT, UPDATE, and DELETE statements for Figure 10: The GetTransaction method.
you at design time? The only reason is when you dont
know the SELECT statement at design time. If your appliBut what about cases where you need to call Fill or Update
cation lets the user generate a SELECT statement and the
for more than one DataAdapter in the same transaction?
fields in the SELECT clause can vary, your application must
For example, how do you update both a master and a detail
generate matching INSERT, UPDATE, and DELETE statetable in a single transaction so that the updates to both
ments after the SELECT statement is known.
tables succeed or fail together?
The disadvantage of using the CommandBuilder is performance. The CommandBuilder has to query the database to
get the metadata it needs to construct the SQL statements.
The metadata query takes time and network bandwidth and
increases the load on the database server.
Using AutoUpdate
If you really want to take the easy way out, and
you are using a BdpDataAdapter, you can call the
BdpDataAdapter.AutoUpdate method. The Update803 sample application uses this technique, as shown in Figure
9. The Update Database Click event handler in this example
is identical to the one in Figure 7, except for the code
shown in Figure 9.
When you call AutoUpdate it creates a BdpCommandBuilder
and uses it to generate the INSERT, UPDATE, and DELETE
statements. Notice that the parameters passed to AutoUpdate
are identical to those passed to Update, with the addition of
a parameter to specify the update mode. The two modes are
BdpUpdateMode.All and BdpUpdateMode.Key.
Using Transactions
Because you cannot access data in an InterBase database in
any way outside the context of a transaction, it is obvious
that the BDP InterBase driver starts and commits transactions each time you call the BdpDataAdapters Fill or Update
methods. That makes transaction control easy and automatic
when each call to Fill or Update can use its own transaction.
10

DELPHI INFORMANT MAGAZINE | Bonus Content

The answer is to use explicit transaction control. To start


a transaction call the Connection objects BeginTransaction
method. BeginTransaction creates a Transaction object and
returns a reference to the new transaction object instance.
The Command object has a Transaction property. When
a Command object executes the SQL statement in its
CommandText property, it does so within the transaction
assigned to its Transaction property. To end a transaction,
call the Transaction objects Commit or Rollback method.
The Update804 sample application is identical to
Update801, except that it uses explicit transaction control.
Figure 10 shows the GetTransaction method. This method
returns a BdpTransaction object. The first line of code calls
BeginTransaction and returns the BdpTransaction instance.
The first parameter specifies the transaction isolation level
and the second the name of the transaction. Unfortunately,
the BdpConnection component only allows one transaction
per connection in Delphi 8 and C#Builder 1, so the name is
not necessary. The next three lines assign the BdpTransaction instance to the Transaction properties of the DELETE,
INSERT, and UPDATE Command objects of the BdpDataAdapter for the Country table.
Figure 11 shows the modified code for the Update Database
menu items Click event handler. If there are changes pending
for the Country table the code calls the GetTransaction method in Figure 10 to start a transaction and assign the
BdpTransaction object to the DataAdapters Command

Columns

&

Rows

ADO.NET Data Access Components

if (EmpDataSet.Tables['Country'].GetChanges.Rows.Count > 0) then


begin
Trans := GetTransaction;
try
CountryAdapter.Update(EmpDataSet, 'Country');
Trans.Commit;
except
on E: Exception do begin
Trans.Rollback;
MessageBox.Show(E.Message, 'Update Error');
end;
end;
end;

Figure 11: Calling Update with explicit transaction control.

function CountryForm.GetTransaction: BdpTransaction;


begin
Result := EmployeeConn.BeginTransaction(
IsolationLevel.ReadCommitted, 'CountryTrans');
CountryCmdBldr.GetUpdateCommand.Transaction := Result;
CountryCmdBldr.GetInsertCommand.Transaction := Result;
CountryCmdBldr.GetDeleteCommand.Transaction := Result;
end;

Figure 12: Starting a transaction when using a CommandBuilder.

objects. The next statement calls the DataAdapters Update


method and the following line commits the transaction by
calling the BdpTransaction objects Commit method. If the call
to Update raises an exception the except block rolls back the
transaction and displays the error message.
Note that transaction support for InterBase is very limited in
Delphi 8 and C#Builder 1. There is no support for multiple
simultaneous transactions on the same connection and there
is no support for any of the InterBase transaction options
such as read only, wait/nowait, record version/no record
version, the consistency transaction isolation level, or table
reservations.
Using Transactions with
CommandBuilder
Using transactions with CommandBuilder is a bit different
because the CommandBuilder object does not create the
UPDATE, INSERT, and DELETE Command objects until you
call the DataAdapter.Update method. This creates a problem
because there is no opportunity to set the Command objects
Transaction property. There is an easy solution, since calling
the CommandBuilders GetDeleteCommand, GetInsertCommand, or GetUpdateCommand method will force the CommandBuilder to create all three Command objects.
The Update805 sample application uses this technique
in the GetTransaction method shown in Figure 12. This
method calls BeginTransaction to create a BdpTransaction object exactly like the example in Figure 10. The next
three lines use the Get...Command methods to instantiate the Command objects and assign the BdpTransaction
object to their Transaction properties. The code for the
Update Database menu items Click event handler is identical to Figure 11. Note that there is no way to use explicit
transaction control with the BdpDataAdapters
11

DELPHI INFORMANT MAGAZINE | Bonus Content

AutoUpdate method because you have no access to the


CommandBuilder that it creates internally.
Conclusion
ADO.NET is more complex than any of the data access
technologies in Delphi for Win32. On the plus side,
ADO.NET provides a disconnected data model, which
ensures short transactions and is ideal for multi-tier
applications. It also provides much more control of
the updating process by letting you customize the
SQL statements that are used to apply updates to the
database, something you cannot do with dbExpress or
DataSnap. The ADO.NET DataSet component is also a
big step up from TClientDataSet because it allows you
to model the relationships between multiple tables on
the client side.

Unfortunately these benefits come at a heavy price. In


Delphi for Win32 you can write a working database
application with almost no code. Based on my experience it seems that you will have to write about five
times more code to create a database application in
ADO.NET than you would need to create the same
application in Delphi for Win32. That means your organization or your clients are in for a severe case of sticker shock. ADO.NET is also frustrating to use. A great
example is the hurdles we had to jump in this article to get
all the changes the user made in the DataGrid to post automatically before updating the database. Another example
is that calling DataAdapter.Update will not always open
the connection. You have to remember to open the connection and close it again if you want it closed. Yet another
example is the complexity of using the CurrencyManager to
interact with the data the user is working with through the
user interface.
Part IX in this series will look at updating the database using
stored procedures and applying updates for tables with a
one-to-many relationship. You will see how using stored
procedures for updates gives you power and control that
you have not had before. You will also see how complex and
laborious updating related tables is in ADO.NET compared
to Delphi for Win32.
The example projects referenced in this article are available
for download on the Delphi Informant Magazine Complete
Works CD located in INFORM\2004\SEP\DI200409BT.

Bill Todd is president of The Database Group, Inc., a database consulting


and development firm based near Phoenix. He is co-author of four database
programming books, author of more than 100 articles, a contributing editor
to Delphi Informant Magazine, and a member of Team B, which provides
technical support on the Borland Internet newsgroups. Bill is also an
internationally known trainer and frequent speaker at Borland Developer
Conferences in the United States and Europe. Readers may reach him at
btarticle@dbginc.com.

C O L U M N S
ADO.NET

&

R O W S

DELPHI 8 FOR THE MICROSOFT .NET FRAMEWORK

By Bill Todd

ADO.NET Data Access


Components
Part IX: Stored Procedures and Command Objects

n Part VIII we covered the basics of sending the changes


from the DataSet back to the database. The examples
used the Country table from the sample Employee database that comes with InterBase. Most applications have to
deal with two issues that make the update process more
complex.
The first issue is columns whose value is assigned on
the server. This not only applies to auto-incrementing
primary keys, but to any field whose value is assigned on
the database server. Examples are identity and timestamp
fields in Microsoft SQL Server databases, fields populated
by triggers using generators in InterBase databases, or
any field that is assigned a default value or has its value
assigned in a trigger. When you insert or update a record
and field values are assigned on the server, you need to
get those values back to the records in the DataSet so the
user can see them.
One solution is to refresh the data by removing the
records in the table, then calling the DataAdapter.Fill
method again to re-fetch the data from the server. This
works, but causes more network traffic and server load
than is necessary. There are two other ways to solve this
problem. The best technique is to use stored procedures.
This is the method recommended in the books I have
read, and the one that provides the best performance. It
works particularly well in multi-tier applications or applications that run over a low-speed network, because it
minimizes network traffic.
Unfortunately it isnt implemented in the Borland Data
Providers (BDP) for ADO.NET. In spite of that, I am going
to demonstrate this technique for two reasons. First, its so
important that I have to believe that Borland will implement
it in the next release of BDP. Second, it works with standard
ADO.NET providers, such as those from Microsoft.
12

DELPHI INFORMANT MAGAZINE | Bonus Content

CREATE TABLE COMPANY(


COMPANY_ID INTEGER NOT NULL,
COMPANY_NAME VARCHAR(40) NOT NULL,
COMPANY_ADDRESS VARCHAR(40),
COMPANY_CITY VARCHAR(30),
COMPANY_STATE VARCHAR(2),
COMPANY_ZIP VARCHAR(10),
COMPANY_VERSION INTEGER
);

Figure 1: The COMPANY table.

ALTER TABLE COMPANY


ADD CONSTRAINT COMPANY_PRIMARY_KEY PRIMARY KEY (COMPANY_ID);

Figure 2: The COMPANY tables primary key.

The second technique uses DataAdapter events. This


method is somewhat slower than using stored procedures,
and it generates a bit more network traffic, but it does work
with the version of the BDP that ships with Delphi 8 and
C#Builder 1.
The code samples in this article use a simple InterBase
database named Contact that contains a single table named
COMPANY. Figure 1 shows the SQL statement to create the
COMPANY table. Figure 2 shows the SQL that adds the primary key constraint.
The COMPANY_ID column is the COMPANY tables primary
key. Note that the data type for COMPANY_ID is an integer.
Normally you would make a field that gets its value from a
generator NUMERIC(18,0) so it can contain the full range
of values that the generator can supply. However, the BDP
InterBase driver doesnt support the NUMERIC or DECIMAL
data types. This is not only a limitation for any field popu-

Columns

&

Rows

ADO.NET Data Access Components

lated by a generator, it also means theres no fixed point


data type that you can use to hold monetary data accurately.
As I mentioned in Part VIII, theres one problem with the
technique normally used to determine if another user has
updated the record you are trying to update after you read
it. The most common way to do this is to include the original value of all the columns in the WHERE clause of the
UPDATE statement. This means that the record wont be
found if another user has changed the value of any field.
The problem with this technique is that you cant include a
blob field in the WHERE clause. This means that if the only
field the other user changes is a blob field, your application
wont be able to detect the change.

tion uses the COMPANY table to show how to get the values for COMPANY_ID and COMPANY_VERSION that are
assigned by the triggers. Figure 5 shows the applications
form at design time. The tray contains a BdpConnection,
a BdpDataAdapter, DataSet, and MainMenu component.
The BdpConnection is connected to the Contact database
and the DataAdapter selects all rows and columns from
the COMPANY table. The form contains a ToolBar and
DataGrid. The DataGrid is connected to the Company
DataTable in the ContactDataSet DataSet object.

The solution is easy: Add a field to the table that is updated by a trigger using a generator each time the record is
updated. All you have to do is include that field in the
WHERE clause of the UPDATE statement, and you will
accurately detect changes made by another user to any
field including a blob. Although the COMPANY table does
not contain any blob fields, I have included a column
named COMPANY_VERSION to demonstrate this technique.
Using Stored Procedures
If you use stored procedures to insert and update rows, you
need triggers to assign the next value to the primary key and
version columns. Figure 3 shows the before insert trigger that
sets the value of the COMPANY_ID and COMPANY_VERSION
fields. The COMPANY_ID field is assigned a value from the
COMPANY_ID_GEN generator, but only if the COMPANY_ID
column is null or negative in the new row. This gives you the
option to get the next value from the generator and assign it
to the COMPANY_ID column in a client application, as you
will see later in this article.
Figure 4 shows the before update trigger for the COMPANY
table. This trigger generates a new value for the
COMPANY_VERSION column each time the table is updated.
The sample application is called GetValues and it comes in
two versions. The one in the GetValuesSP folder demonstrates using stored procedures; the one in GetValuesCMD
shows how to get the value of server-generated fields
using Command objects. The GetValues sample applicaCREATE TRIGGER COMPANY_ID FOR COMPANY
ACTIVE BEFORE INSERT POSITION 10 AS BEGIN
IF ((NEW.COMPANY_ID IS NULL) OR (NEW.COMPANY_ID < 0)) THEN
NEW.COMPANY_ID = GEN_ID(COMPANY_ID_GEN, 1);
NEW.COMPANY_VERSION = GEN_ID(COMPANY_VERSION_GEN, 1);
END;

Figure 3: The COMPANY tables before insert trigger.

CREATE TRIGGER COMPANY_VERSION FOR COMPANY


ACTIVE BEFORE UPDATE POSITION 10 AS BEGIN
NEW.COMPANY_VERSION = GEN_ID(COMPANY_VERSION_GEN, 1);
END;

Figure 4: The COMPANY tables before update trigger.

13

DELPHI INFORMANT MAGAZINE | Bonus Content

Figure 5: The GetValues demo application.


CREATE PROCEDURE COMPANY_INSERT(
COMPANY_NAME VARCHAR(40),
COMPANY_ADDRESS VARCHAR(40),
COMPANY_CITY VARCHAR(30),
COMPANY_STATE VARCHAR(2),
COMPANY_ZIP VARCHAR(10)
)
RETURNS(
COMPANY_ID INTEGER,
COMPANY_VERSION INTEGER
)
AS BEGIN
COMPANY_ID = GEN_ID(COMPANY_ID_GEN, 1);
COMPANY_VERSION = GEN_ID(COMPANY_VERSION_GEN, 1);
INSERT INTO COMPANY (
COMPANY_ID,
COMPANY_NAME,
COMPANY_ADDRESS,
COMPANY_CITY,
COMPANY_STATE,
COMPANY_ZIP,
COMPANY_VERSION
)
VALUES (
:COMPANY_ID,
:COMPANY_NAME,
:COMPANY_ADDRESS,
:COMPANY_CITY,
:COMPANY_STATE,
:COMPANY_ZIP,
:COMPANY_VERSION
);
END;

Figure 6: The COMPANY_INSERT stored procedure.

Columns

&

Rows

ADO.NET Data Access Components

CREATE PROCEDURE COMPANY_UPDATE(


COMPANY_ID INTEGER,
COMPANY_NAME VARCHAR(40),
COMPANY_ADDRESS VARCHAR(40),
COMPANY_CITY VARCHAR(30),
COMPANY_STATE VARCHAR(2),
COMPANY_ZIP VARCHAR(10),
COMPANY_VERSION INTEGER
)
RETURNS (
COMPANY_VERSION_NEW INTEGER
)
AS BEGIN
COMPANY_VERSION_NEW = GEN_ID(COMPANY_VERSION_GEN, 1);

Figure 7: InsertCommand configured for a stored procedure.

UPDATE COMPANY
SET COMPANY_NAME = :COMPANY_NAME,
COMPANY_ADDRESS = :COMPANY_ADDRESS,
COMPANY_CITY = :COMPANY_CITY,
COMPANY_STATE = :COMPANY_STATE,
COMPANY_ZIP = :COMPANY_ZIP,
COMPANY_VERSION = :COMPANY_VERSION_NEW
WHERE COMPANY_ID = :COMPANY_ID
AND COMPANY_VERSION = :COMPANY_VERSION;
END;

Figure 9: The COMPANY_UPDATE stored procedure.

Figure 8 shows the BdpParameter Collection Editor with


the parameters for the stored procedure. When you call the
DataAdapter.Update method to apply updates to the database the current value of each column will be assigned to
the input parameter whose SourceColumn property is set to
the columns name. After the stored procedure executes the
values returned in the two output parameters, COMPANY_ID
and COMPANY_VERSION, will be assigned to their source
columns in the data record.

Figure 8: The insert parameters collection.

The secret to getting the values assigned by the server to


the COMPANY_ID and COMPANY_VERSION columns is
to use stored procedures to insert and update new rows.
Figure 6 shows the COMPANY_INSERT stored procedure.
The procedure takes five input parameters corresponding to the five columns whose values are supplied by the
client application. It returns two output parameters that
contain the value the server assigns to COMPANY_ID and
COMPANY_VERSION. The stored procedure body is quite
simple. It gets the value for COMPANY_ID and
COMPANY_VERSION from the COMPANY_ID_GEN and
COMPANY_VERSION_GEN generators, then inserts the
new row into the COMPANY table.
Figure 7 shows part of the Object Inspector with the
CompanyAdapter DataAdapter selected and the
InsertCommand property expanded. The CommandType is
set to StoredProcedure and the CommandText property is
set to COMPANY_INSERT, which is the stored procedure
that should be called when a new row is inserted. The
UpdateRowSource property is set to OutputParameters.
This tells the DataAdapter to get new column values from
the stored procedures output parameters.
14

DELPHI INFORMANT MAGAZINE | Bonus Content

Figure 9 shows the COMPANY_UPDATE stored procedure that


is called by the CompanyAdapter.UpdateCommand object.
Figure 10 shows the UpdateDatabase method that is
called by the Save Changes button. The first block of code
attempts to post any unposted changes in the DataGrid in
case the user has changed the current row and not moved
off of it. The if statement calls the DataTable.GetChanges
method to determine if there are any pending changes
that need to be applied to the database. If there are
changes a call to the GetTransaction method, shown in
Figure 11, starts a transaction.
The three calls to the DataAdapter.Update method show
how you can use the DataTable.Select method to return just
the rows that have been updated, deleted, or inserted. This
reduces the number of rows that are sent to the server and
also lets you control the order in which the updates are
processed. This is important when you are updating two or
more related tables. With related tables you must process
the master tables inserts before the detail tables inserts and
you must process the detail tables deletes before the master
tables deletes to avoid violating referential integrity.
This technique works fine when the user inserts records
but not when the user updates an existing record due to
another feature that is not implemented yet in the BDP.

Columns

&

Rows

ADO.NET Data Access Components

procedure TCompanyForm.UpdateDatabase;
var
Trans: BdpTransaction;
CompanyGridCurrencyMgr: CurrencyManager;
begin
// Post the current record so it will be included in the updates.
CompanyGrid.EndEdit(CompanyGrid.TableStyles[0].GridColumnStyles[
CompanyGrid.CurrentCell.ColumnNumber],
CompanyGrid.CurrentCell.RowNumber, False);
CompanyGridCurrencyMgr := CompanyGrid.BindingContext[
ContactDataSet.Tables['Company'].DefaultView] as
CurrencyManager;
CompanyGridCurrencyMgr.EndCurrentEdit;
// If there are pending changes, update the database.
if (ContactDataSet.Tables['Company'].GetChanges <> nil) then
begin
Trans := GetTransaction;
try
CompanyAdapter.Update(
CompanyTable.Select('', '', DataViewRowState.Added));
CompanyAdapter.Update(
CompanyTable.Select('', '', DataViewRowState.ModifiedCurrent));
CompanyAdapter.Update(
CompanyTable.Select('', '', DataViewRowState.Deleted));
Trans.Commit;
except
on E: DBConcurrencyException do begin
// If this is the BDP update stored procedure bug,
// eat the error, otherwise display it.
if (E.Message = 'Concurrency violation: the UpdateCommand affected 0 records.') then
begin
Trans.Commit;
if (E.Row.HasErrors) then
E.Row.ClearErrors;
end
else
begin
Trans.Rollback;
MessageBox.Show(E.Message, 'Concurrency Error');
end;
end;
on E: Exception do begin
Trans.Rollback;
MessageBox.Show(E.Message, 'Update Error');
end;
end;
end;
end;

Figure 10: The UpdateDatabase method.

function TCompanyForm.GetTransaction: BdpTransaction;


begin
Result := ContactConn.BeginTransaction(
IsolationLevel.ReadCommitted, 'ContactTrans');
CompanyAdapter.UpdateCommand.Transaction := Result;
CompanyAdapter.InsertCommand.Transaction := Result;
CompanyAdapter.DeleteCommand.Transaction := Result;
end;

Figure 11: The GetTransaction method.

To understand why you need to remember that the normal way to see if the record was updated by another user
is to include the original value of the fields in the WHERE
clause of the UPDATE statement. The BDP executes
each update statement then checks the number of rows
affected by the update. If no rows were affected the BDP
assumes that the row was not found because another
15

DELPHI INFORMANT MAGAZINE | Bonus Content

user changed it so the BDP raises


a DBConcurrencyException. The
problem is that the BDP still checks
the number of rows affected even
though the UpdateCommand object
is using a stored procedure, not a
SQL statement. The result is that the
number of rows affected is always
zero and the BDP always raises a
DBConcurrencyException.
The code in Figure 9 gets around
this bug by catching the
DBConcurrencyException in the
try..except block. If the error message is Concurrency violation: the
UpdateCommand affected 0 records.
the code in the except block commits the transaction and sets the
DataRow.RowError property to a null
string. While this gets around the
BDP problem it also means that you
wont get an error message if the
update fails because another user
changed the row. If any other error
occurs the except block rolls back
the transaction and displays the
error message.

If primary key values are going to be


assigned by the database server what
should you use for a primary key
value when you insert a new row into
the DataTable object in the DataSet?
You have to pick something that cannot conflict with any of the rows you
have selected. The answer is to use
positive values for the primary keys
assigned by the server and negative
values for the temporary keys used in
the DataTable. Figure 12 shows the
Columns Collection Editor showing
the properties of the COMPANY_ID
column. Note that the AutoIncrement
property is set to true and the AutoIncrementSeed and
AutoIncrementStep properties are both set to -1. This will
cause the local keys to start with -1 and increment by -1 as
you insert new rows.
Using Command Objects
Since you cannot use stored procedures with the BDP the
next best choice is to use the DataAdapter.RowUpdating
event handler. When you use this technique with InterBase you do not need the triggers that assign values to
the COMPANY_ID and COMPANY_VERSION columns.
Instead you need the two stored procedures shown in
Figures 13 and 14. The stored procedure in Figure 13 gets
the next company id and the next company version and
returns both values. We will use this procedure when
inserting a new row. The stored procedure in Figure 14
returns the next company version value and will be used
when a row is updated.

Columns

&

Rows

ADO.NET Data Access Components

CREATE PROCEDURE GET_COMPANY_ID_VERSION


RETURNS (
COMPANY_ID INTEGER,
COMPANY_VERSION INTEGER
)
AS BEGIN
COMPANY_ID = GEN_ID(COMPANY_ID_GEN, 1);
COMPANY_VERSION = GEN_ID(COMPANY_VERSION_GEN, 1);
SUSPEND;
END;

Figure 13: The GET_COMPANY_ID_VERSION stored procedure.

CREATE PROCEDURE GET_COMPANY_VERSION


RETURNS (
COMPANY_VERSION INTEGER
)
AS BEGIN
COMPANY_VERSION = GEN_ID(COMPANY_VERSION_GEN, 1);
SUSPEND;
END;

Figure 12: The Columns Collection Editor.

Figure 14: The GET_COMPANY_VERSION stored procedure.

This version of the GetValues program uses SQL INSERT and


UPDATE statements to insert and update rows. Figure 15 shows
the main form at design time. Note that two BdpCommand
objects have been added to the tray. The CompanyIdVersionCmd Command object returns the next company id and company version by using the following SQL statement to call the
GET_COMPANY_ID_VERSION stored procedure:

the company id and company version values returned by the


Command object to their respective columns in the row that
is being inserted. This causes the new values to be immediately available in the DataTable.

The CompanyVersionCmd Command object uses a similar


statement to call the GET_COMPANY_VERSION stored
procedure.

If the row is being updated the


CompanyVersionCmd.ExecuteScaler method is called and
the returned value is assigned to the COMPANY_VERSION
column in the row being updated. Once again, this makes
the new value immediately available in the DataTable.
If you run the sample application and insert or update a
row you will see the new values appear immediately in
the DataGrid when you click the Save Changes button.

There are only two changes in the code for this version of
the sample program. First, the:

You can use this same approach with Oracle sequences by


using a Command object whose SQL statement is:

on E: DbConcurrencyException

SELECT ASEQUENCE.NEXTVALUE
FROM DUAL

SELECT COMPANY_ID, COMPANY_VERSION


FROM GET_COMPANY_ID_VERSION

block has been removed from the UpdateDatabase method


in Figure 10. Second, the RowUpdating
event handler for the CompanyAdapter,
shown in Figure 16, has been added.
This event handler is called once for
each row whose changes must be
applied to the database. The event
handler checks the StatementType
field of the BdpRowUpdatingEventArgs
parameter to determine if the row is
being inserted or updated. If the row is
being inserted the event handler calls
the CompanyIdVersionCmd Command
objects ExecuteReader method.
ExecuteReader executes the SQL statement and returns a BdpDataReader
that contains the result set. The call
to Rdr.Read reads the first row of the
result set and the next two lines assign
16

Figure 15: The GetValues form using Command objects.

DELPHI INFORMANT MAGAZINE | Bonus Content

Columns

&

Rows

ADO.NET Data Access Components

procedure TCompanyForm.CompanyAdapter_RowUpdating(
sender: System.Object;
e: Borland.Data.Provider.BdpRowUpdatingEventArgs);
var
Rdr: BdpDataReader;
begin
if (e.StatementType = StatementType.Insert) then
begin
Rdr := CompanyIdVersionCmd.ExecuteReader;
Rdr.Read;
e.Row['COMPANY_ID'] := Rdr['COMPANY_ID'];
e.Row['COMPANY_VERSION'] := Rdr['COMPANY_VERSION'];
end
else if (e.StatementType = StatementType.Update) then
begin
E.Row['COMPANY_VERSION'] := CompanyVersionCmd.ExecuteScalar;
end;
end;

Figure 16: The DataAdapter RowUpdating event handler.

but there is another approach that you may prefer.


Instead of putting your code in the RowUpdating event
handler you can put it in the RowUpdated event handler.
RowUpdated fires after the row has been updated. You
cannot use RowUpdated with InterBase because InterBase does not have a SQL function that returns the last
generator value retrieved by your connection. However,
with Oracle you can move your code to RowUpdated and
change the Command objects SQL to:
SELECT ASEQUENCE.CURRVAL
FROM DUAL

With Microsoft SQL Server identity fields there is no way to


get the next value before the row is inserted, so you must
put your code in the RowUpdated event handler. To get the
value to assign to the key field of the new row use:
SELECT SCOPE_IDENTITY()

So far using the RowUpdating or RowUpdated event may


seem like an acceptable substitute for the stored procedure technique that is not implemented in BDP. That is

17

DELPHI INFORMANT MAGAZINE | Bonus Content

not true in all cases. Consider a SQL Server timestamp field. If you want a field that changes each
time any column in the row changes a timestamp
is perfect. However, there is no function call to get
the last timestamp value. With a stored procedure
it is easy to return the new value. Using an event
handler there is no way to get the new value except
re-reading the row. The same applies to any value
assigned by a trigger or constraint. To fully support
the ADO.NET standard Borland needs to provide
stored procedure support for DataAdapters as soon
as possible.

Conclusion
If you are not using the BDP returning values assigned
on the server is easy. Just use stored procedures
to insert and update rows and return the server
assigned values in output parameters. With the BDP
the best alternative is to use the RowUpdating or
RowUpdated event and call a stored procedure that gets
the server assigned value. If you need to control the order
in which inserts, updates, and deletes are applied use the
DataTable.Select method to return a DataTable that contains
just the rows with the type of update you specify. This
makes it easy to control the update order among related
tables.
The example projects referenced in this article are available for
download on the Delphi Informant Magazine Complete Works
CD located in INFORM\2004\SEP\DI200409TB.

Bill Todd is president of The Database Group, Inc., a database consulting


and development firm based near Phoenix. He is co-author of four database
programming books, author of more than 100 articles, a contributing editor
to Delphi Informant Magazine, and a member of Team B, which provides
technical support on the Borland Internet newsgroups. Bill is also an
internationally known trainer and frequent speaker at Borland Developer
Conferences in the United States and Europe. Readers may reach him at
btarticle@dbginc.com.

Вам также может понравиться