📄 tiofp documentation - the visitor pattern and sql databases.htm
字号:
FbDirty: boolean;
protected
function AcceptVisitor : boolean ; override ;
public
procedure Execute( pVisited : TVisitedAbs ) ; override ;
property Dirty : boolean read FbDirty write FbDirty ;
end ;</PRE>
<P>And the implementation looks like this:</P><PRE>function TVisPerObjIsDirty.AcceptVisitor : boolean;
begin
result := ( Visited is TPerObjAbs ) and ( not Dirty ) ;
end;</PRE><PRE>procedure TVisPerObjIsDirty.Execute(pVisited: TVisitedAbs);
begin
Inherited Execute( pVisited ) ;
if not AcceptVisitor then
exit ; //==>
Dirty := TPerObjAbs( pVisited ).ObjectState in
[ posCreate,
posUpdate,
posDelete
]
end;</PRE>
<P>The call to this Visitor is wrapped up in the TPerObjAbs.GetDirty method like
this:</P><PRE>function TPerObjAbs.GetDirty: boolean;
var
lVis : TVisPerObjIsDirty ;
begin
lVis := TVisPerObjIsDirty.Create ;
try
self.Iterate( lVis ) ;
result := lVis.Dirty ;
finally
lVis.Free ;
end ;
end;</PRE>
<P>This lets us extend the application’s main form by adding an ActionList.
Double click the ActionList and add three actions as shown below. Move the code
from the three save buttons OnClick methods to the actions then hook the save
buttons up to the actions.</P>
<P>In the ActionList&apos;s OnUpdate method add the following code to check
the Dirty state of the data hierarchy and enable or disable the save buttons as
necessary:</P><PRE>procedure TFormMain_VisitorManager.ALUpdate(Action: TBasicAction;
var Handled: Boolean);
var
lDirty : boolean ;
begin
lDirty := FPeople.Dirty ;
aSaveToInterbase.Enabled := lDirty ;
aSaveToCSV.Enabled := lDirty ;
aSaveToTXT.Enabled := lDirty ;
Handled := true ;
end;</PRE>
<P>Now the save buttons are disabled when the object hierarchy is clean, and
disabled when the object hierarchy is dirty like this:</P>
<TABLE cellSpacing=0 cellPadding=0>
<TBODY>
<TR class=Normal>
<TD vAlign=top width=295><B>The object Hierarchy is clean</B></TD>
<TD vAlign=top width=295><B>The Object Hierarchy is dirty</B></TD></TR>
<TR class=Normal>
<TD vAlign=top width=295>
<P align=center><IMG height=175
src="tiOFP Documentation - The Visitor pattern and SQL databases_files/3_TheVisitorAndSQLDatabases_clip_image002_0001.jpg"
width=286></P></TD>
<TD vAlign=top width=295>
<P align=center><IMG height=175
src="tiOFP Documentation - The Visitor pattern and SQL databases_files/3_TheVisitorAndSQLDatabases_clip_image004.jpg"
width=286></P></TD></TR></TBODY></TABLE>
<H2>Adding more database constraints</H2>
<P>Now let’s create a slightly more realistic database schema by adding a unique
key on the People table. This can be done by modifying the create SQL as shown
below: </P><PRE>create table People
( OID Integer not null,
Name VarChar( 20 ),
EMailAdrs VarChar( 60 ),
Primary Key ( OID )) ;</PRE><PRE>create unique index People_uk on People ( name, EMailAdrs ) ;</PRE><PRE>insert into People values ( 1, "Peter Hinrichsen", "peter_hinrichsen@techinsite.com.au");
insert into People values ( 2, "Don Macrae", "don@xpro.com.au" ) ;
insert into People values ( 3, "Malcolm Groves", "malcolm@madrigal.com.au" ) ;</PRE>
<P>Run the application and deliberately try to insert duplicate name records to
see how the framework handles a database error. Add two duplicate records then
click ‘Save to Interbase’. You will get unique key error like the one shown
below:</P>
<P><IMG height=229
src="tiOFP Documentation - The Visitor pattern and SQL databases_files/3_TheVisitorAndSQLDatabases_clip_image001_0008.gif"
width=521> </P>
<P>Click the ‘Read from Interbase’ button and you will find that one record was
saved, while the other was not. The record that was not saved is still in the
client with an ObjectState of posCreate. What we clearly want here is some
transaction management so either all objects are saved, or none are saved.
Before we can setup transaction management, we must modify the framework so all
the visitors share the same database connection.</P>
<H2>Share the database connection between Visitors</H2>
<P>The First step towards providing transaction support is to change the
framework so all calls to the database are funneled through the same database
connection and transaction object. You will remember that the TVisSQLAbs class
owned an instance of a TIBDatabase and TIBTransaction. This meant that every
visitor was processed within its own database connection and transaction.
Transactions would be implicitly started and committed by Delphi around each SQL
statement. What we want is for all visitors to share the same database
connection, and for the Visitor Manager to have control over the database
transaction.</P>
<P></P>To achieve this, we will do four things:
<P></P>
<OL>
<LI>Move the database and transaction objects out of the TVisSQLAbs class and
wrap them up in an object we will call TtiDatabase. (This will become
especially useful when we start work on building a swappable persistence
layer. Also, one of the slowest things you can do to a database is connect to
it, so sharing a database connection between visitor will improve the
application’s performance.)
<LI>Modify the TVisSQLAbs class with a Database property. The SetDatabase
method will assign a field variable for later reference, and hook the Query
object up to the database connection at the same time.
<LI>Create a single instance (Singleton pattern) of the database object that
has application wide visibility.
<LI>Modify the Visitor Manager so it sets each Visitors Database property
before executing, and then clears it when done. </LI></OL>
<P>These four steps are detailed below.</P>
<P>Firstly, we will move the database and transaction objects into their own
class. The interface of TtiDatabase is shown here:</P><PRE>TtiDatabase = class( TObject )
private
FDB : TIBDatabase ;
FTransaction : TIBTransaction ;
public
constructor Create ;
destructor Destroy ; override ;
property DB : TIBDatabase read FDB ;
end ;</PRE>
<P>And the implementation of TtiDatabase is shown here:</P><PRE>constructor TtiDatabase.Create;
begin
inherited ;
FDB := TIBDatabase.Create( nil ) ;
FDB.DatabaseName := 'C:\TechInsite\OPFPresentation\Source\3_SQLVisitors\Test.gdb' ;
FDB.Params.Add( 'user_name=SYSDBA' ) ;
FDB.Params.Add( 'password=masterkey' ) ;
FDB.LoginPrompt := False ;
FDB.Connected := True ;
FTransaction := TIBTransaction.Create( nil ) ;
FTransaction.DefaultDatabase := FDB ;
FDB.DefaultTransaction := FTransaction ;
end;</PRE><PRE>destructor TtiDatabase.Destroy;
begin
FTransaction.Free ;
FDB.Free ;
inherited;
end;</PRE>
<P>As well as moving the database connection out of the Visitor class, and
allowing it to be shared between all visitors in the application, we have
wrapped the TIBDatabase component in our own code. This is an important step
towards using the Adaptor pattern to make our framework independent of any
one-database vendor’s API.</P>
<P>Next, we modify the TVisSQLAbs class. The owned database and transaction
objects are removed and a field variable is added to hold a pointer to the
shared database object. A database property with a SetDatabase method is added
and SetDatabase is responsible for hooking the query object up to the database
connection. The interface of TVisSQLAbs is shown below:</P><PRE>TVisSQLAbs = class( TVisPerObjAwareAbs )
private
FDatabase: TtiDatabase;
procedure SetDatabase(const Value: TtiDatabase);
public
constructor Create ; override ;
destructor Destroy ; override ;
property Database : TtiDatabase read FDatabase write SetDatabase ;
end ;</PRE>
<P>The implementation of TVisSQLAbs.SetDatabase is shown below. Notice how there
is protection against Value being passed as nil.</P><PRE>procedure TVisSQLAbs.SetDatabase(const Value: TtiDatabase);
begin
FDatabase := Value;
if FDatabase <> nil then
FQuery.Database := FDatabase.DB
else
FQuery.Database := nil ;
end;</PRE>
<P>Thirdly we require a globally visible, single instance of the database
connection. (This is not how we will ultimately implement a database connection
– we will use a thread safe database connection pool, but this is sufficient to
get us started.) I use something I call a poor man’s Singleton, a unit wide
variable hiding behind a globally visible function. This implementation does not
come close to providing what a GoF singleton requires, but it does the job and
is quick to implement. The code for the single instance database connection is
shown below:</P><PRE>interface
function gDBConnection : TtiDatabase ;
implementation
var
uDBConnection : TtiDatabase ;
function gDBConnection : TtiDatabase ;
begin
if uDBConnection = nil then
uDBConnection := TtiDatabase.Create ;
result := uDBConnection ;
end ;</PRE>
<P>The final changes we must make are to extend TVisitorMgr.Execute with some
code to set the database property on the visitor before calling
TVisitor.Execute. There is a complication here because not all Visitors will
have a database property, only those that descend from TVisSQLAbs. As a work
around we will check the type of the Visitor, and if it descends from
TVisSQLAbs, assume it has a database property and hook it up to the default
application database object. We also clear the Visitors database property after
the visitor has executed. (This is a bit of a hack and is not how we solve the
problem in the framework. We actually have another class called a
TVisitorController that is responsible for performing tasks before and after
visitors execute. Much more elegant, but rather more complex too.) The modified
TVisitorMgr.Execute method is shown below:</P><PRE>procedure TVisitorMgr.Execute(const pCommand: string; const pData: TVisited);
var
i : integer ;
lVisitor : TVisitor ;
begin
for i := 0 to FList.Count - 1 do
if SameText( pCommand, TVisitorMapping( FList.Items[i] ).Command ) then
begin
lVisitor := TVisitorMapping( FList.Items[i] ).VisitorClass.Create ;
try
if ( lVisitor is TVisSQLAbs ) then
TVisSQLAbs( lVisitor ).Database := gDBConnection ;
pData.Iterate( lVisitor ) ;
if ( lVisitor is TVisSQLAbs ) then
TVisSQLAbs( lVisitor ).Database := nil ;
finally
lVisitor.Free ;
end ;
end ;
end;</PRE>
<P>The changes we have made to this section ensure that all database activity is
channeled through a single database connection. The next step is to modify
TVisitorMgr.Execute so all SQL statements are called within a single database
transaction.</P>
<H2>Manage transactions</H2>
<P>To add transaction support, we must to two things:</P>
<OL>
<LI>1. Expose transaction support through the procedures StartTransaction,
Commit and RollBack on the database wrapper class TtiDatabase
<LI>2. Extend the Visitor Manager’s Execute method with the ability to start a
transaction when a group of objects is passed to Execute and commit or roll
back the transaction when processing has either finished successfully, or
failed. </LI></OL>
<P>Firstly, we extended interface of TtiDatabase, with the additional methods
StartTransaction, Commit and RollBack as in the code below:</P><PRE>TtiDatabase = class( TObject )
private
FDB : TIBDatabase ;
FTransaction : TIBTransaction ;
public
constructor Create ;
destructor Destroy ; override ;
property DB : TIBDatabase read FDB ;
procedure StartTransaction ;
procedure Commit ;
procedure Rollback ;
end ;
</PRE>
<P>The implementation of TtiDatabase is shown next. You can see that the
StartTransaction, Commit and RollBack methods simply delegate the call to the
owned TIBTransaction object. The reasons for this shall be looked at in more
detail when we study swappable persistence layers and the Adaptor pattern.</P><PRE>procedure TtiDatabase.StartTransaction;
begin
FTransaction.StartTransaction ;
end;
procedure TtiDatabase.Commit;
begin
FTransaction.Commit ;
end;
procedure TtiDatabase.Rollback;
begin
FTransaction.RollBack ;
end;
</PRE>
<P>Secondly, we extend the TVisitorManager.Execute method transaction support.
This means changes in three places. Firstly we start the transaction after
assigning the database connection to the visitor’s Database property. Next we
wrap the call to pData. Iterate( lVisitor ) up in a try except block and call
RollBack if an exception is raised. We re-raise the exception after calling
RollBack so it will bubble to the top of any exception handling code we have
ad
⌨️ 快捷键说明
复制代码
Ctrl + C
搜索代码
Ctrl + F
全屏模式
F11
切换主题
Ctrl + Shift + D
显示快捷键
?
增大字号
Ctrl + =
减小字号
Ctrl + -