Oracle Objects for OLE Bound
Widget Library and C++ Class Library Workbook
This document contains information regarding the Oracle Objects for OLE Bound Widget libraries (MFC) and a library workbook explaining how to use the libraries.
There are two parts in this document
A. OMFC Bound Widget Libaray
B. Oracle Objects for OLE C++ Class
Library Workbook
A. OMFC BOUND WIDGET LIBRARY
OMFC 2.5 BOUND WIDGET LIBRARY
This file documents the OMFC library
provided with the Oracle Objects for OLE C++ Class Library. The
OMFC library contains classes you can use to build GUI programs
using Microsoft's MFC framework. The classes have been built using
Visual C++ 1.52 and are for MFC version 2.5.
How to build the sample program.
The sample program can be found in the [ORACLE_HOME]\oo4o\cpp\mfc\samples\empedit
directory (where [ORACLE_HOME]
is the directory your Oracle products for Windows 3.1 are installed
- usually c:\orawin). The project file is vsdept.mak and can
be loaded by Microsoft Visual C++ version 1.5x. There is also
another sample in [ORACLE_HOME]\oo4o\cpp\workbook\empedt
called empedt.mak. Microsoft store some the path information
to locate header files and libraries for all your projects in
the msvc.ini file and some are stored in the project make file.
To change the path information for all your projects, select Options
from the VC++ main menu. Select Directories from the popup
menu and a dalog appears containing five edit fields. The second
of these contains the path for include files. You will need to
add or modify references for the class library include files and
the omfc include files. These are installed into [ORACLE_HOME]\oo4o\cpp\include
and [ORACLE_HOME]\oo4o\cpp\mfc\include
respectively. Unfortunatly, there is an upper size to the path
of 128 bytes and it is quite easy to execed this limit if you
have many include directories. You may wish to copy all the Oracle
Objects include files to one place and just add that directory
to the path. The third edit field contains the library path and
should be modified to contain the paths to oraclm.lib and
omfc.lib. These are installed into [ORACLE_HOME]\oo4o\cpp\lib
and [ORACLE_HOME]\oo4o\cpp\mfc\lib
respectively. The other option is to add or change the project
include file path. Select Options from the VC++ main menu,
then select Project from the popup menu. Click on the Compiler
button and then select Preprocessor from the list. You
can then enter the search path in the Include Path edit
window.
In order to run the sample (or any application
you build), it will need to access the Class Library runtime DLL
- oraclm.dll. The easiest way to do this is to copy
it to a directory on your path (such as \orawin\bin) or your windows
system directory. The latter is recommended. You can find the
DLL in the [ORACLE_HOME]\oo4o\cpp\bin
directory, or if you want to debug in the class library, you should
use the version in [ORACLE_HOME]\oo4o\cpp\bin\dbg.
You will need to ensure that oraclm.lib is included in your project.
A debuggable version of omfc.lib is also supplied
in ORACLE_HOME]\oo4o\cpp\mfc\lib\dbg.
What problem do these classes solve?
The basic classes of the Oracle Objects
for OLE C++ Class Library enable you to access the data in an
Oracle database. You can fetch records, add records, edit records,
execute arbitrary SQL statements, and so forth. However, if you
want to write a GUI program that displays the database data, you
are on your own. You must fetch the data, push the data into your
GUI widgets, and repeat whenever the dynaset moves to another
record. If the widget is used to edit the data, you must execute
a StartEdit, SetValue, Update cycle.
The OBinder and OBound
classes make this work much easier. An OBinder instance
manages a dynaset. OBound instances are attached to fields
in the OBinder's dynaset and "bound" to the OBinder
instance. From then on, the OBinder and OBound
code do most of the tedious bookkeeping for you: the OBound
instance values are changed when needed, and when edits are made
through the OBound instances they are saved to the Oracle
database.
The C++ Class Library provides an implementation
of OBinder. However, it provides only a pure virtual OBound
class. To make use of the convenience of OBinder, you
need subclasses of OBound that implement the OBound
functionality.
The classes in this OMFC library are
subclasses of OBound. They provide GUI widget implementations
of the OBound functionality. As a result you can create
a form using Visual C++'s App Studio resource editor and, with
very few lines of code, you can hook those widgets to database
fields. You then have a working application.
Please see the Oracle Objects for
OLE C++ online help system for more discussion of OBinder
and OBound.
Note to Visual Basic users:
An OBinder object works like a data control (it has no
user interface but it performs all the bookkeeping). OBound
objects work like bound controls.
What kind of objects are available?
OMFC classes are provided for the following
kinds of user interface widgets:
edit controls: display (and edit) values as strings
static text controls: display values as strings
check box: display (and edit) values as "on" or "off"
radio button group: display (and edit) values as a setting from a set of choices
slider: a combination slider and text display that graphically displays
and edits a numeric value
How are these objects used?
This OMFC library (and in fact the entire
C++ Class Library) is used to build large model programs.
To make and set up instances of any
of these classes, you must go through several steps:
1. First, you must create the user interface
widget. In Visual C++ this is easiest to do using Microsoft's
App Studio resource editor. You can also create the widget programmatically.
2. Next, you must declare an OBinder
instance in your application, typically in the view class for
the window where the database form resides.
3. You must declare an OBound
subclass instance for each widget (or one for a group of radio
buttons). These instances are usually members of the view class
for the window where the database form resides.
4. You must call the "BindToControl"
method on each of the OBound subclass instances. You should
do this just once (for example, in the "OnInitialUpdate"
method of your view). The BindToControl method tells the
instance variable exactly which widget it is managing. The widget
is identified by window pointer (CWnd *) and an ID (for instance
IDC_WIDGET1).
5. You must call the "BindToBinder"
method on each of the OBound subclass instances. You can
do this multiple times, but normally you do it just once (for
example, in the "OnInitialUpdate" method of your
view). The BindToBinder method tells the instance variable
what OBinder to get its data from and what field it should
access in the OBinder's dynaset.
6. The final step in setting up the
instance is to open the OBinder. This creates a dynaset
and fetches records from the database.
Your application can now run. You need
to implement some way for the user to navigate through the records
of the dynaset. For example, you can create a button with the
label "Next" that calls OBinder::MoveNext. The
user can make changes in the widgets. Just before the dynaset
is navigated to another record (for example, in response to a
MoveNext call), all changes on the current record are saved.
7. Finally, when the program is exiting,
it is a good idea to call OBinder::Close explicitly. This
is not strictly necessary, but is good form. See the OBinder::Close
in the Oracle Objects for OLE C++ online help system.
The OBound subclass instances
manage the user interface widgets and keep them in synch with
data in the database. The instance hooks together the user's view
of the data and the database's view of the data. (Step 4 sets
up the user interface association; Step 5 sets up the database
data association.)
An example
An example is provided that uses the
OMFC classes. When you installed Oracle Objects for OLE the sample
was placed in the Samples subdirectory of the OMFC
directory (if you asked to install sample code). The example allows
the user to edit the emp2 table, which is an extended version
of the sample emp table provided with Oracle databases. The interesting
files are VSDEPVW.H (which declares the bound control variables
for the formview) and VSDEPVW.CPP (which uses the bound controls).
The OBinder that controls the
dynaset on the emp2 table is a member of a special subclass.
It is subclassed so that the instance can override the PostMove
and PostAdd trigger methods. The PostMove trigger
is overridden so that when a new employee is selected, the department
OBinder can be updated to the appropriate department.
The PostAdd trigger is overridden so that when a new employee
record is created, a department number and employee ID number
are filled in automatically.
Methods of all the classes
These methods described below are available
in all the classes. The methods that are inherited from OBound
are not documented here (BindToBinder in particular).
Please see the Oracle Objects for OLE C++ online help system
for more information on OBound and its methods.
operator=
and copy constructor
All these classes define the assignment
operator and copy constructor in the class definition but do not
implement them. This prevents the use of the compiler's default
assignment operator (or copy constructor), which would be wrong.
Neither the assignment operation nor construction by copy is
defined for any of these classes. If you inadvertently use assignment
or copy construction on one of these objects, you get a link error.
BindToControl
This method associates the object with
a particular user interface widget.
Usage: oresult BindToControl(CWnd *wnd,
int itemid)
wnd points to the window containing the control
itemid
is the control ID (IDC_WHATEVER) identifying the control.
BindToControl
can only be called once per object. It dynamically subclasses
the control window (calls CWnd::SubclassDlgItem) so that
the OBound subclass code receives user interface messages.
The individual classes
OBoundEdit
This is the class you will most often
use. It displays the database value in a text Edit control.
It has the additional method:
SetProperty
This method sets whether the control
is read-only or read-write.
Usage: oresult SetProperty(bool mode=OBOUND_READWRITE);
mode a flag indicating whether the control is read-only or read-write. It can be either:
OBOUND_READWRITE
OBOUND_READONLY
OBoundStatic
This class allows you to bind a static
text item. Objects of this class are always read-only. It has
no additional methods.
OBoundCheckBox
This class allows you to display and edit database values as a checkbox. It is of greatest value for a database field that only has two possible values, such as TRUE and FALSE.
The SetProperty method enables you to specify what value will be considered "on" and what value will be considered "off". When the user checks the checkbox, the field data is set to the "on" value. When the user unchecks the checkbox, the field data is set to the "off" value.
If the field value is neither "off" nor "on", then the checkbox behaves as follows:
If the checkbox is tristate, it is placed into the "grayed-out" state.
If the checkbox is not tristate, it
is off.
SetProperty
This method specifies whether the control
is read-only or read-write, and what the "on" and "off"
values are for the checkbox.
Usage: oresult SetProperty(const OValue
onvalue, const OValue offvalue, bool mode=OBOUND_READWRITE)
onvalue the value corresponding to the checkbox being checked
offvalue the value corresponding to the checkbox being unchecked
mode a flag indicating whether the control is read-only or read-write. It can be either:
OBOUND_READWRITE
OBOUND_READONLY
OBoundGroupButton
A single radio button cannot represent
a database field value. A group of radio buttons can represent
a value. Each radio button corresponds to a single possible value,
and the single radio button that is on indicates the actual database
field value. Choosing a different radio button changes the field
value.
To use an OBoundGroupButton you
create regular user interface widgets. You then call BindToControl
on the OBoundGroupButton instance for each radio button
in the group. You should call SetProperty for each radio
button to set the value of the radio button.
If the current field value does not
correspond to the value of any of the radio buttons in the group,
none of the radio buttons are selected.
SetProperty
This method specifies whether the control
is read-only or read-write, and what value the radio button represents.
Usage: oresult SetProperty(int itemid,
const OValue value, bool mode=OBOUND_READWRITE)
itemid identifies which radio button you are setting the value for
value the value that this radio button represents
mode a flag indicating whether the control is read-only or read-write. It can be either:
OBOUND_READWRITE
OBOUND_READONLY
OBoundSlider
A slider is a combination of a scroll
bar and a static text display. It is suitable for displaying
and editing numeric data that has a known range. The data is
displayed and edited using the scroll bar. It is also displayed
using the static text display.
The OBoundSlider has a slightly
different BindToControl method. It needs to bind two controls
simultaneously: the scroll bar and the static text.
BindToControl
This method associates the object with
a scroll bar and static text item.
Usage: oresult BindToControl(CWnd *wnd,
int scrollid, int staticid)
wnd points to the window containing the control
scrollid is the control ID (IDC_WHATEVER) identifying the scroll bar
staticid
is the control ID (IDC_STATIC3) identifying the static text item
Except for the extra id argument, which
allows the OBoundSlider to connect to a second user-interface
widget, this method works the same as the BindToControl
described above under "Methods of all the classes".
SetProperty
This method specifies whether the control
is read-only or read-write and sets the range of the scroll bar.
Usage: oresult SetProperty(const OValue
minvalue, const OValue maxvalue, bool mode=OBOUND_READWRITE)
minvalue the minimum value of the scroll bar
maxvalue the maximum value of the scroll bar
mode a flag indicating whether the control is read-only or read-write. It can be either:
OBOUND_READWRITE
OBOUND_READONLY
B. Oracle Objects for OLE C++
Class Library Workbook
INTRODUCTION
This Workbook provides examples
of C++ code that use the Oracle Objects for OLE C++ Class Library.
The Oracle Objects for OLE
C++ online help system
provides general information
on Oracle Objects for OLE as well as detailed descriptions of
all the C++ classes and their methods.
Some of the examples discussed in
this Workbook are so simple that they are represented only with
code fragments. Others are sufficiently complex that the complete
code has been provided in separate files. The notes for those
examples indicate the names of the files containing the code.
The sample files are found in subdirectories of the samples
directory, which is a subdirectory of the workbook directory.
The samples are installed by default or if you choose to install
sample code in a custom install. A few of the examples
provide complete projects.
The examples presented here begin
with simple, fairly common uses of the library and progress to
more complex, less common uses. Clever and dedicated developers
(with time on their hands) may learn how to use the Class Library
simply by working through the examples. However, using the library
will be easier if you read and understand relevant sections of
the Oracle Objects for OLE
C++ online help system.
This Class Library is intended for
use against an Oracle database-a relational database that uses
the SQL language as its primary interface. The SQL used in these
examples is as simple as possible, and brief explanations are
provided of what the SQL statement does in each case. But to
exploit the full power of this Library-and of any relational database-you
need a good grasp of SQL.
Many of the examples use the standard
demonstration tables that are shipped with Oracle databases. Most
of the examples entail a database named "ExampleDB",
a user named "scott", and a password for that user of
"tiger". To use the example code for your own projects,
you need to change the database name to refer to your database
and the user information to refer to some valid user on your database.
(Alternatively, you can create a database alias called ExampleDB
that refers to a database with the scott/tiger account.)
To use the Class Library properly,
your application must call the OStartup and OShutdown
methods. These routines perform necessary initialization and
cleanup for the Class Library (for example, they initialize and
uninitialize OLE). In the examples that contain only fragments
of code, we assume that these routines are called outside the
fragments.
Example 1: Just get some data
This very minimal example demonstrates
the simplest and most common use of the library: to fetch some
simple data from the database. In this example look at a table
named "emp", which has several columns. The only column
we care about for this example is one called "sal",
which represents the salary paid to an employee. The database
contains a record for each current employee.
Our task is to find determine the total salary paid-the sum of the salaries of all the employees. To do this we have to
1. connect to the database,
2. query the database and retrieve records, and
3. process the records.
We can do this all in a single subroutine,
as follows:
double SumSalary(void)
{
ODatabase datab; // the database object
ODynaset dyn; // dynaset object
double sum=0.0; // sum of all salaries
double cursal; //
salary of the current employee
// connect to the database
datab.Open("ExampleDB",
"scott", "tiger");
// query the database
dyn.Open(datab, "select
sal from emp");
// process all the records
while (!dyn.IsEOF()) // until we've gone past all the records
{
dyn.GetFieldValue("sal", &cursal); // get the current salary
sum += cursal;
dyn.MoveNext(); // move to the next record
}
return(sum);
}
For the sake of simplicity, we have
ignored all error handling in this example. In later examples
we consider how to handle various errors.
Both the database and the dynaset
objects are "opened". This is necessary because an
unopened ODatabase has no connection to a database, and
an unopened ODynaset has no records. Until these objects
are opened they are not very useful. The database connection
is established by opening the ODatabase object by passing
in the name of the database (ExampleDB), the username (scott),
and the user's password (tiger). The ODynaset is opened
on a particular ODatabase. ODynasets are always
the set of records that is the result of an SQL select on the
database. The database is represented by the ODatabase
argument to the ODynaset Open method. The ODynaset
Open method is also given a SQL statement that indicates which
records from the database are to be fetched.
In this case the SQL statement is
"select sal from emp" ("emp" is the name
of the table being queried). A relational database contains many
tables. A query can access one or more of the tables at once.
A query against an Oracle database can, in fact, access tables
across many databases distributed on a network. The emp table
has a number of columns in it; "sal", "ename",
and "hiredate" are three of them. Since we are only
interested in salary, we ask only for the "sal" column.
We could have asked for more columns (the special symbol "*"
in the column list gets all the columns). This would not have
changed our code, but it would have fetched more data from the
database-unnecessarily.
Opening the ODynaset gives
us a set of records that match the data in the database. The
ODynaset contains the notion of which record is "current".
We can use the navigational methods on the ODynaset to
move from record to record. By default, when the ODynaset
is opened, the first record becomes current. We can then
use the ODynaset's MoveNext method to traverse all
the records that were fetched.
We use the IsEOF method to
tell when we've gone through all the records. It returns TRUE
when we've "Move[d]Next" past the last record.
Now that we can navigate through
the records, it's time to fetch the data. The simplest way to
do this is to use the GetFieldValue method of the ODynaset.
This method is overloaded to support different types. The method
converts the data from the database to the type you implicitly
ask for, if possible. Here we ask for a double, because the salary
is stored as a value with fractional dollars. We call GetFieldValue
with the name of the column and pass the address of a double variable
we want set to the value of the salary in the current record.
Then we add up all the salaries.
Sounds simple? Actually, it is, because the server can do most
of the work for us. There's no need for us to download all the
records (in a big company there may be thousands, and that's a
lot of network traffic). Instead, we can let the server do the
sum for us.
We replace the line
dyn.Open(datab, "select
sal from emp");
with
dyn.Open(datab, "select
sum(sal) from emp");
Now the database hands us back a
single record that contains the sum of the salaries. Often (as
in this case), the server is a more powerful computer than our
client workstation. In such cases, it is more efficient to let
the server do the simple calculations, and it decreases the total
number of bytes that have to be transferred over the network.
If we use this statement as it stands,
subsequent references to the returned column need to use the name
"sum(sal)", which is clumsy because of the parentheses.
One more modification to the SQL statement can make it more elegant.
We say:
dyn.Open(datab, "select
sum(sal) sumsal from emp");
and now we can refer to the returned
column as "sumsal". This renaming can become significant
in the case of more complex calculations or when we later change
queries but want to keep the same column names.
Our completed routine becomes:
double SumSalary(void)
{
ODatabase datab; // the database object
ODynaset dyn; // dynaset object
double sum; //
sum of all salaries
// connect to the database
datab.Open("ExampleDB",
"scott", "tiger");
// query the database
dyn.Open(datab, "select
sum(sal) ""sumsal"" from emp");
// get the sum of the salaries
dyn.GetFieldValue("sumsal",
&sum); // get the salary total
return(sum);
}
Notice one more thing. We connected
to the database but we never disconnected. This is correct.
When the routine exits, it destroys the ODynaset and ODatabase
objects. The destruction of the ODatabase object properly
drops the database connection-you don't have to think about it.
Example 2: Execute a SQL statement
Another typical operation with a
database is to execute some SQL statement that is not a query.
For example, you execute SQL statements to create tables, add
users, administer the database, delete a set of records, and so
forth. In this example we create a simple table. This example
also demonstrates some simple error handling.
// routine to create the states table.
// returns 0 on success,
-1 on failure
// There is a bug in this
code! (See below) Don't use this!
int CreateStatesTable(void)
{
ODatabase datab; // the database object
oresult ores; //
indicates whether operation succeeded
// connect to the database
ores = datab.Open("ExampleDB", "scott", "tigers");
if (ores != O_SUCCESS)
{ // couldn't open the database connection
ErrorMessage(datab.GetSession().GetServerErrorText());
return(-1);
}
// create the table
const char *sqls = "create
table states (name char(15), area number, population number)";
ores = datab.ExecuteSQL(sqls);
if (ores != O_SUCCESS)
{ // couldn't create the table
ErrorMessage(datab.GetSession().GetServerErrorText());
return(-1);
}
// everything went just fine
return(0);
}
We connect to the database by opening
the ODatabase object. Most methods in the library return
a result of the type oresult, which indicates whether the method
succeeded or failed. If you determine that the method failed,
you need to call other routines to get the actual error. In this
case we want the routine GetServerErrorText, which is a
method in the OSession class, to get the error text back
from the Oracle database software.
We check the return from the Open
method. If the routine did not execute successfully, we get
an OSession object from the ODatabase object and
use it to get the error text. Notice that since the GetSession
method returns an OSession, it can be used inline. Since
the C++ objects in the Class Library are handles to underlying
implementation objects, they are lightweight. The use of temporary
objects, such as the OSession that is returned by GetSession,
is perfectly reasonable. GetServerErrorText returns a
(const char *), which we hand to some generic error processing
routine, which presumably alerts the user.
To create the table, we first construct
a SQL statement. In this example we are creating a trivial table,
so we can use a static string. In general, code puts together
some complex SQL statement. Our SQL statement creates a table
with the name "states" that has three columns in it:
"name", which is a text column with a width of 15 characters,
"area", which is a number, and "population",
which is another number.
Next we use the ExecuteSQL
method to pass the SQL statement to the database to be executed.
Once again we check the return and get the error text if necessary.
Because the cleanup work is done
for you in the object destructors, you don't have to worry about
the database connection-whether or not the open succeeded. Cleanup
happens automatically.
Now, when we run this example it
fails (we've given the wrong password for the user scott). And
GetServerErrorText returns a 0 rather than an error string
indicating a bad password. Why? Because GetSession is
returning an unopened OSession object. And that's because
our database object can't return an OSession because it
isn't open. But we're getting the error on the Open! How do
we get the correct error?
The answer is first to create an
OSession object explicitly, and then open the database
using that session. Then, if the database open fails, we can
refer to our OSession object. Rewritten, the code looks
like this:
// routine to create the states table.
// returns 0 on success,
-1 on failure
// Corrected version
int CreateStatesTable(void)
{
OSession sess; // database session object
ODatabase datab; // the database object
oresult ores; //
indicates whether operation succeeded
// open the default (unnamed) session
ores = sess.Open();
if (ores != O_SUCCESS)
{
ErrorMessage(sess.GetErrorText());
return(-1);
}
// connect to the database
ores = datab.Open(sess, "ExampleDB", "scott", "tigers");
if (ores != O_SUCCESS)
{ // couldn't open the database connection
ErrorMessage(sess.GetServerErrorText());
return(-1);
}
// create the table
const char *sqls = "create
table states (name char(15), area number, population number) ";
ores = datab.ExecuteSQL(sqls);
if (ores != O_SUCCESS)
{ // couldn't create the table
ErrorMessage(sess.GetServerErrorText());
return(-1);
}
// everything went just fine
return(0);
}
This version is almost the same
as the earlier one, except that we use our explicit OSession
object to get the database errors, and there's code to open the
OSession object. Opening the OSession is very straightforward.
Notice that if the OSession Open fails we get an
error message by calling GetErrorText rather than GetServerErrorText.
GetServerErrorText is appropriate when the error is a
problem with the database. This can't be the case until we're
actually connected to, or trying to connect to, the database.
If the OSession open fails, it is due to some internal
problem such as low memory. So we use GetErrorText instead.
Example 3: A joined query
One of the most important and powerful
features of a relational database is the possibility of querying
data from several tables at once. Instead of formulating and
coordinating several related queries on different tables, the
queries can be combined. Most of this example is about writing
the SQL needed for a joined query. If you are already familiar
with SQL, you can skim through this example, but be sure to read
the very last paragraph.
Consider the emp and dept sample
tables that are installed in the scott account of most Oracle7
sample databases. The emp table contains the employee name (ename
field), the employee's job (job field), the employee's salary
(sal field), and the department number where the employee works
(deptno field). A related table is the dept table. It also contains
the department number (deptno field), as well as the department
name (dname field) and the department location (loc field).
Suppose we want to write an application
that looks at where the different employees work. We want to
query employee names and the location of the departments where
they work. One possible solution (though not a good one) is to
create two separate queries:
// bad solution to employee & department join
ODatabase datab;
datab.Open("ExampleDB",
"scott", "tiger"); // open the database
ODynaset empdyn; // employee query
ODynaset deptdyn; // department query
empdyn.Open(datab, "select ename from emp");
deptdyn.Open(datab, "select
loc from dept");
Now we have one query for employee
names and another for department locations. Not only do we need
to manage the two queries, but we do not know which record in
the department list corresponds to which employee. The two queries
are not sorted: the first department is not necessarily the department
of the first employee.
However, the two tables do share
a field: deptno. This field is the "key" that relates
the two tables. Now we could write:
// still not a good solution to employee & department join
ODatabase datab;
datab.Open("ExampleDB",
"scott", "tiger"); // open the database
ODynaset empdyn; // employee query
ODynaset deptdyn; // department query
empdyn.Open(datab, "select ename, deptno from emp");
deptdyn.Open(datab, "select
loc, deptno from dept");
Now whenever we look at an employee
record, we can read its department number. Then we can scan through
the department records to find the department with that department
number. Then we'll have the location. The trouble with this
solution is that it requires us to scan those department records
pretty often. When we're writing a client-server application,
this is the kind of processing that the server does best. The
server is very good at coordinating data; there's no reason to
download the data to the client workstation for coordination.
What we can do here is tell the
server to match the deptno fields of the two tables. Then we
have:
// the joined query solution
ODatabase datab;
datab.Open("ExampleDB",
"scott", "tiger"); // open the database
ODynaset jdyn; // joined query
jdyn.Open(datab, "select emp.ename, dept.loc from emp, dept \
where emp.deptno
= dept.deptno");
Now we have a dynaset with two fields
in it: ename and loc. These are just the fields we wanted. And
the loc field corresponds to the location of the department where
the employee works. In effect, when the server was preparing
the records for return to our client, it did the coordination,
more efficiently than we could have done it ourselves (because
of all the machinery of a relational database).
A couple of points are noteworthy:
· The first is the syntax of the SQL statement. We specified which fields we wanted in our query. Since the fields are coming from several tables, we fully specified the fields by calling them by names like "table.field". When we specified the tables we were querying, we gave a comma-separated list of all the tables we wanted. Finally, we gave a "join condition", which is just a "where" clause that specifies how we want to coordinate the data from the various tables.
· Another point is that the deptno field is not one of the fields in our dynaset. If we wanted deptno we would have to include it in the list of select fields as: "select emp.ename, emp.deptno...".
· By the way, the backslash ("\") is not part of the SQL statement. It is just the standard C syntax for continuing a literal across multiple lines.
· Finally a most important point: Dynasets formed from joined queries are essentially read-only. The "records" in such a dynaset do not correspond to actual records in any table. Therefore it is not possible to delete, update, or add new records to such a dynaset: there's no table to be changed so it doesn't make sense. Other kinds of queries, such as queries that have computed columns, are also read-only.
Example 4: A connection dialog
You need to specify three things
to open a database object: the database name, the user name, and
the user's password. You can hard code these values into your
code, but that is a bad idea because then you won't be able to
use the same program for another user or against a different database.
It is much better to generalize the connection information.
The obvious way to do this is to
create a dialog that obtains the information. We want a routine
that hands back an open ODatabase object if the connection
was possible and a closed ODatabase to indicate failure.
We want the ability to create the ODatabase with different options.
The sample subdirectory logdlg
contains the files LOGDLG.CPP and LOGDLG.H. These files contain
the code for an implementation of such a login dialog using Visual
C++'s MFC framework. The dependency on the framework is slight,
so you don't have to be very experienced with MFC to understand
what the code is doing.
The important portion of the logdlg
class follows:
class logdlg : public CDialog // subclass of a dialog
{
public:
// Construction
logdlg(CWnd* pParent =
NULL); // standard constructor
// get a database login
ODatabase GetLogin(long
options = ODATABASE_DEFAULT);
private:
OSession m_session; // our handle to the default session
ODatabase m_database;
// our handle to the database object
// more implementation details...
};
An instance of the class is a dialog,
which is what is created here. Then, when the client of the class
wants a valid and open ODatabase object, the method GetLogin
is called. This method runs the dialog, tries to make the database
connection, notifies the user of errors, and finally returns.
You can see from the source of GetLogin
that it does very little. It performs some setup and then calls
the "DoModal" routine to run the dialog. It then returns
an ODatabase object, which may or may not be open. The
real work is done when "OK" is pressed. Here's the
method that gets called then:
void logdlg::OnOK()
{
CString dbname; // database name string
CString user; // user name string
CString password; // password string
// get the strings the user has entered
GetDlgItem(IDC_USERNAME)->GetWindowText(user);
GetDlgItem(IDC_PASSWORD)->GetWindowText(password);
GetDlgItem(IDC_DATABASE)->GetWindowText(dbname);
// try to open the database
if (m_database.Open(m_session, dbname, user, password, m_options) != OSUCCESS)
{ // some error
// get the oracle error number
long oraerr = m_session.ServerErrorNumber();
// get the oracle error message, to display to the user
const char *dberrs = m_session.GetServerErrorText();
ErrorMessage(dberrs); // tell user what went wrong
}
else
{
// we're done - get out of the dialog
CDialog::OnOK();
}
return;
}
This method gets the text from the
login dialog and uses it to try to open the ODatabase object.
When it is opening the ODatabase object it uses the options
that were passed in to GetLogin. If there is an error,
the generic "ErrorMessage" routine is called (which
may just put up a simple message box). Only if the open was successful
is the parent dialog class's routine "OnOK" called (which
closes the dialog).
There are two ways for the dialog
to end: Either the OK is successful, in which case the returned
ODatabase object will be open, or the user chooses cancel,
in which case the returned ODatabase object will be closed.
If the user chooses "OK" to try to connect and the
connection fails, the above routine alerts the user but does not
close the dialog.
Note again that it is reasonable
to pass ODatabase instances around. Here we are using
one as the return value from a subroutine. A caller might have
some code like this:
// connect to the database
ODatabase database;
logdlg dblogin;
database = dblogin.GetLogin();
if (database.IsOpen())
; // success
else
; // user canceled for
some reason
Useful improvements to the dialog
would be to allow the fields to be preset, or to allow database
options to be set on the ODatabase.Open.
Example 5: Updating a dynaset
Sometimes you may need to do more
than just read the contents of a dynaset. You may also want to
modify the records in the dynaset, and therefore the database
itself.
You can update database records
using the Class Library in two ways. The first is to execute
an update SQL statement. The second is to use the dynaset to
edit the records.
Consider the emp table. It contains
employee salary information. Suppose the company has had a good
year and everyone is getting a $1000 salary increase.
// give all employees a raise of amount "raise"
oboolean GiveRaise(int raise)
{
ODatabase db; // our database
logdlg db_login; //
used to connect to database (see example 4)
// open a connection to the database
db = db_login.GetLogin();
if (!db.IsOpen())
return(FALSE); //
indicates that we couldn't give out the raises
// give everybody a raise
char sql[80]; // for constructing the sql statement
sprintf(sql, "update
emp sal = sal + %d", raise);
if (db.ExecuteSQL(sql) != OSUCCESS)
return(FALSE); // we couldn't do it
//
would be a good idea to look at errors to see why
return(TRUE); // we've given the raises
}
Notice that we run a login dialog
in this routine and then, by exiting the routine and destroying
the ODatabase instance, we drop the database connection.
Generally a routine like this should probably pass in the ODatabase
object as a parameter so that many such operations can share a
login.
Updating records in this way is
very efficient because the server does all the work. But in more
complex situations it is not possible to use such a simple update
SQL statement. A more realistic example would be a case in which
the raise for each employee is calculated in some fashion and
then applied to each record. This requires us to go through every
record in the database and update some of them as needed. Here's
a routine to do that:
// Calculate and apply raises to all employees
oboolean GiveRaises(const ODatabase &db)
{
if (!db.IsOpen())
return(FALSE); //
we can't work without an open database
// create a dynaset referring to the employees
ODynaset dyn;
dyn.Open(db, "select ename, sal, hiredate from emp");
if (!dyn.IsOpen())
return(FALSE); // can't open dynaset
double salary, raise;
// go through all the records
while (!dyn.IsEOF())
{ // for every record
raise = CalculateRaise(dyn); // calculate raise somehow
if (raise > 0)
{ // edit this record
dyn.StartEdit();
// begin editing this record
dyn.GetFieldValue("sal", &salary); // get old salary
dyn.SetFieldValue("sal",
salary + raise); // set new salary
dyn.Update(); // finish editing this record
}
dyn.MoveNext(); // go to the next record
}
return(TRUE);
}
There are three steps to editing
a record:
1. First, call the StartEdit
method to tell the dynaset that you're about to edit the record.
2. Second, make calls to change
values in the current record. These could be Dynaset.SetFieldValue
calls or they may be OField.SetValue calls.
3. Finally, call Update to
push the changes into the database.
This method is much more flexible
than a direct update, but it is also slower. StartEdit
must go to the database and attempt to lock the record. It also
checks to see if the data in the database is different from the
data in the dynaset (which would indicate that someone else has
edited the record, invalidating the dynaset). If another dynaset
or program has a lock on the record, StartEdit can either
wait indefinitely or can return a "can't obtain lock"
error. (See the documentation on ODatabase options for
the ODATABASE_NOWAIT option.)
Finally there is a subtle difficulty
when updating (or making any other kind of change) to dynasets
or to the database: Changes made to records in a dynaset are
immediately reflected in that dynaset. Any changes made by other
users, or other dynasets in your program, are not reflected immediately.
If the dynaset has not yet fetched the row that was changed,
it gets the new value when and if it does fetch the row. But
it is difficult to predict which rows have really been fetched
(because of caching). You can guarantee that the dynaset has
the most up-to-date version of the data by performing a Refresh
on the dynaset.
Example 6: Adding and deleting
records with a dynaset
Example 5 covers the editing of
existing records. In many cases you need not only to edit records
but to add new records and to delete existing ones. The ODynaset
method DeleteRecord deletes the current record. To delete
a record, navigate to it and then execute DeleteRecord.
Here is a routine that deletes all
employees with a salary above a certain amount.
oboolean DeleteOverpaid(const ODatabase &db, double maxsal)
{
if (!db.IsOpen())
return(FALSE); // error because database object isn't open
ODynaset dyn;
OField salary;
dyn.Open(db, "select sal from emp");
salary = dyn.GetField("sal");
while (!dyn.IsEOF())
{
if ((double) salary > maxsal)
{ // this employee is overpaid. Delete.
dyn.DeleteRecord(); // deletes the current record
}
dyn.MoveNext(); // go through all the records
}
return(TRUE);
}
Once we delete a record with DeleteRecord,
the current record becomes invalid (it has been deleted). Most
operations on such a record fail. Navigating to another record
gets us to a valid record. We won't be able to navigate back
to the deleted (thus invalid) record.
In this example we get the field
value by using an OField object rather than directly through
the ODynaset object. OField objects are closely
linked to the underlying dynaset: as navigation occurs in the
dynaset, the field value changes to contain the current record
value. An OField instance is more convenient to use than
going through the dynaset, because it can be treated as a variable,
as illustrated by the "(double) salary > maxsal"
statement above.
Adding a new record is a little
more complex. You can use two methods to add records to a dynaset.
The first is AddNewRecord, which adds a blank record.
The second is DuplicateRecord, which adds a blank record
and then fills it with the field values found in the current record
(current at the time DuplicateRecord is called). When
a blank record is created, it is either filled with NULLs or sent
to the database server to have defaults filled in, depending on
whether the ODATABASE_PARTIAL_INSERT option has been turned on.
(See the ODatabase documentation for details.)
After adding the record you may
change some of its values. The AddNewRecord or DuplicateRecord
call allows editing similarly to the way that StartEdit
does. When you have made all the changes you want, call Update
to put your changes into the database.
When you are adding new records,
it is important to be aware of the structure of the table that
you are editing. Some fields may require unique values. Some
fields may not allow NULL values. These requirements must be
fulfilled or the Update call fails.
Here is a routine that adds a new
record to the employee table.
oboolean AddEmployee(ODynaset *empdyn, const char *ename,
double salary, int deptno)
{
if (!empdyn->IsOpen())
return(FALSE); //
can't work with unopened dynaset
// Add the new record
if (empdyn->AddNewRecord() != OSUCCESS)
return(FALSE);
// Set the values of the record
empdyn->SetFieldValue("ename", ename);
empdyn->SetFieldValue("sal", salary);
empdyn->SetFieldValue("deptno",
deptno);
// the empno field is required and must be unique.
// We will find the current
maximum empno and add 1 to it.
ODynaset empnodyn;
empnodyn.Open(empdyn->GetDatabase(),
"select max(empno)+1 newempno from emp");
int empno;
empnodyn.GetFieldValue("newempno",
&empno);
// finish setting the employee record
empdyn->SetFieldValue("empno",
empno);
// put this record in the database
if (empdyn->Update() != OSUCCESS)
return(FALSE);
return(TRUE);
}
The values of most of the fields
were left NULL. We set the values of ename, sal, and deptno based
on the arguments to the routine.
The empno field is used as a unique
key in the employee table, so it must be unique and non-NULL.
An easy way to specify this is to ask the server for the highest
current employee number, and then have the server add 1 to that
number so that we get a new unique value.
In practice this is not a good solution,
for two reasons. First, we are making an extra query to the database
for every record added. It would be faster to figure out a good
employee number at the beginning of the program and then use that
number (incrementing it or whatever). Second, this method guarantees
only that the empno field is unique among current records. If
an employee record was deleted, its employee number field might
get reused. This is generally not desirable. The database has
objects called "sequences" that help with this kind
of problem.
As with editing and updating a dynaset,
there is a difficulty with cross-dynaset consistency. If a dynaset
contains a record, and it has been fetched from the server already,
then when another dynaset deletes that same record there will
be a "ghost" record in the first dynaset. Similarly,
records added to the first dynaset won't show up in other existing
dynasets. (Added records can be seen by refreshing the dynaset.)
Attempting to edit a record that has been deleted from the database
will fail on the StartEdit method.
Example 7: Transaction control
Normally, when you change the database
data with dynaset operations, the database immediately reflects
the changes you have made. When you add records or change field
values, the database is changed when you execute the Update
method. When you delete records, the database is changed when
the DeleteRecord method is executed. This is sufficient
when changing a record is an independent operation.
But sometimes you have an entire
set of changes that must be made together. Either they are all
made or none of them are made. The classic example is an application
that maintains bank account balances. When a transfer is made
from one account to the other, two separate records are edited.
The record that records the balance of one account is debited
and the record of the other account is credited. These two operations
must succeed or fail together. If one takes place without the
other, the bank's books will not balance properly.
The solution is to enclose the set
of changes in a transaction. Before making the first change you
call the BeginTransaction method. Then you make your changes.
If you want to cancel the changes, you call the Rollback
method. If you want to make the changes permanent, you call the
Commit method. Either Rollback or Commit
completes the current transaction. Then, by default, we return
to the normal behavior of changing the database directly on each
Update or DeleteRecord.
Note to experienced Oracle developers:
The normal transactional model of the Class Library is similar
to the autocommit mode in SQL*Plus or Oracle Forms. Executing
BeginTransaction is like turning autocommit off. Executing
Rollback or Commit is a rollback or commit. And
the default behavior of Rollback or Commit is then
to reenter autocommit mode.
The example we consider here uses
DynasetMarks. These are objects that remember a position
in a dynaset. You can get a mark on the current position and
then later use the MoveToMark method to reposition on that
record again.
Here's a bank account example:
// transfer money from credit to debit account
// we have a DynasetMark on the two accounts
void TransferMoney(ODynaset accounts, // dynaset of bank accounts
ODynasetMark credit, // mark on record to be credited
ODynasetMark debit, // mark on record to be debited
double amount // amount to transfer
)
{
// get the dynaset's session
OSession banksess = accounts.GetSession();
// start the transaction
banksess.BeginTransaction();
// make the transfer
double balance; // an
account's balance
// credit one account
accounts.MoveToMark(credit);
accounts.StartEdit();
accounts.GetFieldValue("balance", &balance);
accounts.SetFieldValue("balance", balance + amount);
if (accounts.Update() != OSUCCESS)
{ // couldn't change the first record
banksess.Rollback();
return;
}
// debit the other account
accounts.MoveToMark(debit);
accounts.StartEdit();
accounts.GetFieldValue("balance", &balance);
accounts.SetFieldValue("balance", balance - amount);
if (accounts.Update() != OSUCCESS)
{ // couldn't change this record
banksess.Rollback();
return;
}
// everything is fine - commit the transaction
banksess.Commit();
return;
}
(Here we're checking for errors
only on the Update statement. A real application would
be more careful and would check for errors on all the methods.)
How could the second update fail
and the first succeed? The database connection might have been
lost (maybe because of a hardware failure in a network). Or perhaps
the new account balance violated a database constraint (for example,
there might be a trigger stored in the database that caused the
update to fail because the account balance dropped below zero).
Example 8 shows another way to accomplish
the same task. Rather than using a dynaset, it calls ExecuteSQL
directly to make the database changes. Changes made to the database
using ExecuteSQL (by way of update, insert, and delete
statements) are part of the transaction that is managed by BeginTransaction
and Commit or Rollback.
The Commit and Rollback
methods take an oboolean argument named startnew, which
is FALSE by default. If you set it to TRUE, a new transaction
is started immediately after the Commit or Rollback
(just as if you had called BeginTransaction again).
Oracle developers note:
If you call BeginTransaction immediately after opening
the session and always set startnew to TRUE, you will have
a transactional environment similar to the Oracle environment
to which you are accustomed.) Consider the following code:
// fragment illustrating Oracle7
transaction details
OSession sess;
sess.Open(); // open the default session
sess.BeginTransaction(); // start a transaction
ODatabase db;
db.Open(sess, "ExampleDB", "scott", "tiger");
ODynaset dyn1;
dyn1.Open(db, "select
* from emp order by empno");
// change the first record
dyn1.MoveFirst();
dyn1.StartEdit();
dyn1.SetFieldValue("sal", 7500);
dyn1.Update();
// open another dynaset
ODynaset dyn2;
dyn2.Open(db, "select * from emp order by empno");
dyn2.MoveFirst();
long salary2;
dyn2.GetFieldValue("sal",
&salary2);
// open yet another dynaset
// open a named session
OSession nameds;
nameds.Open(sess.GetClient(), "session2");
ODatabase db2;
db2.Open(nameds, "ExampleDB", "scott", "tiger");
ODynaset dyn3;
dyn3.Open(db2, "select * from emp order by empno");
dyn3.MoveFirst();
long salary3;
dyn3.GetFieldValue("sal",
&salary3);
The second ODynaset was opened
within the same session as the first. The first ODynaset
made a change that has not yet been committed to the database.
The third ODynaset was opened on a different session.
What are the values of salary2 and salary3?
Because dyn2 is in the same session
as dyn1, it sees the same database state. It will see a salary
of 7500. But dyn3 is in a different session, so it will not see
a salary of 7500; rather, it will see the salary that was in the
field before it was changed. This result is independent of whether
dyn3 is a part of the same process or not. Dyn3 could be on another
computer altogether.
Example 8: Parameters
All of the SQL statements we have
used in the previous examples have been entirely literal. So,
when we wanted the employee records of Department 20, we use the
SQL statement: "select * from emp where deptno = 20".
And if we then wanted the records from Department 10, we used
"select * from emp where deptno = 10". This is inefficient
in a number of ways. It would be more efficient if we could say:
"select * from emp where deptno = :deptno", where :deptno
is something that can be set to a value at a later time. This
is precisely what a parameter is: A parameter allows you to introduce
variables into the processing of an SQL statement.
Parameters are attached to an ODatabase
object and are accessed by way of an object called OParameterCollection.
The OParameterCollection object exposes a variable number
of parameters, and you can add or remove parameters from the collection.
(The other collection types-OFieldCollection, OConnectionCollection,
and OSessionCollection-are read-only.) You can create
as many parameters as you want. At any given time, any parameters
that are auto-enabled will be bound to your SQL statement. (For
more information, consult the Oracle
Objects for OLE C++ online help system.
The following code uses a parameter
to select different sets of employee records.
// main routine (processes departments)
ProcessCompany(ODatabase *db)
{
// get a list of the department numbers
ODynaset deptnumbers;
deptnumbers.Open(*db, "select deptno from dept order by deptno");
OField deptnof = deptnumbers.GetField("deptno"); // the deptno column
// set up a "dnumber" parameter on the database
OParameterCollection params = db->GetParameters();
// create parameter with initial value of 0
params.Add("dnumber", 0, OPARAMETER_INVAR, OTYPE_NUMBER);
OParameter dnumber = params.GetParameter("dnumber");
// process every department
deptnumbers.MoveFirst();
while (!deptnumbers.IsEOF())
{
// set the value of the parameter
dnumber.SetValue((int) deptnof);
// process that department
ProcessDepartment(db);
// go to next department
deptnumbers.MoveNext();
}
// all done. We don't need the dnumber parameter to be part of
// the collection anymore so get rid of it
params.Remove("dnumber");
return;
}
void ProcessDepartment(ODatabase *db)
{
// open a dynaset of employees for the current department
ODynaset emps;
emps.Open(*db, "select * from emp where deptno = :dnumber");
// process them all
emps.MoveFirst();
while (!emps.IsEOF())
{
// do something interesting here
emps.MoveNext();
}
return;
}
In ProcessCompany we set up the
parameter that is then used by the subroutine ProcessDepartment.
We can then write the ProcessDepartment routine more generally.
The department number it is processing is not hard-coded into
it anywhere; rather, it depends on the parameter "dnumber"
existing for the database, which is its argument.
The parameter is created by using
the Add method on the ODatabase's parameter collection.
The parameter's value can be changed at any later time. However,
changing the parameter's value does not instantly change the contents
of any dynaset that was created using the parameter. The parameter's
value is used only at the time that the SQL statement is executed,
which is either at the ExecuteSQL call, the ODynaset Open,
or the ODynaset Refresh. Finally, when we are finished
with the parameter we can remove it, as in the example, with the
OParameterCollection::Remove method.
Some details of this example are
noteworthy. The ODatabase is passed into the ProcessCompany
routine by address. It is legal to dereference an ODatabase
address, and that is what we do here. It is also possible (and
inexpensive) to pass the ODatabase by value. An OField
object is used to get the department number out of the deptnumbers
dynaset. An OField always gives you, from the current
record, the value of the field to which it is attached. Therefore,
as the dynaset is navigated, the OField objects returns
different values. Because various cast operators are overloaded
for the OField object, getting the value of the column
is just a matter of casting the OField object.
Here is a more realistic version
of the application from Example 7:
// transfer money from one
account to another
// SQL statement to change a balance in the accounts table
static const char *setbalance =
"update accounts
set balance = balance + :amount where accno = :anum";
void Transfer(ODatabase *bankdb, // the bank database
int debitaccount, // account to debit
int creditaccount, // account to credit
double amount) // amount to transfer
{
// we assume the existence of the parameters "amount" and "anum"
// begin the transaction
if (bankdb->GetSession().BeginTransaction() != OSUCCESS)
return; // couldn't start transaction
// get the parameters
OParameter amount = bankdb->GetParameters().GetParameter("amount");
OParameter anum = bankdb->GetParameters().GetParameter("anum");
// credit the first account
anum.SetValue(creditaccount);
amount.SetValue(amount);
if (bankdb->ExecuteSQL(setbalance) != OSUCCESS)
{
bankdb->GetSession().Rollback();
return;
}
// debit the second account
anum.SetValue(debitaccount);
amount.SetValue(-amount);
if (bankdb->ExecuteSQL(setbalance) != OSUCCESS)
{
bankdb->GetSession().Rollback();
return;
}
// it all worked
bankdb->GetSession().Commit();
return;
}
To use this routine, you open the
bank database and set up the two parameters "amount"
and "anum". Then for each transfer you call the Transfer
routine. Notice that the actual SQL statement that is used is
external to the routine. The SQL statement could actually be
moved into some other code, making this Transfer routine even
more general and maintainable.
Example 9: An advisory
Occasionally you need some piece
of your code to be notified when something happens to a dynaset-either
navigation or some editing. Typically the dynaset is controlled
by one portion of your code and you monitor it with some other
portion of your code (for example, some kind of general access
management package).
You accomplish this monitoring with
an advisory. Specifically, you attach an OAdvise object
to an ODynaset. From then on, whenever something happens
to the dynaset, messages are passed to the OAdvise object.
The advisory can exert some control over the dynaset actions
(for example, it can cancel them), and it can monitor what is
happening.
The OAdvise class that is
part of the Class Library does nothing. To get a useful advisory
object, you must create a subclass of OAdvise, and then
create an instance of that subclass. By overriding a few virtual
functions, you can obtain the functionality you desire.
The samples directory contains
a subdirectory called posadv, which contains the files
POSADV.CPP and POSADV.H. These files implement a subclass of
OAdvise called PosAdvise. The PosAdvise
class keeps track of which record the dynaset is currently on-
the first record being 0, the next 1, and so forth. It cancels
the actions that it sees that would disturb its bookkeeping, such
as a DeleteRecord.
Here's how you use PosAdvise:
// use of PosAdvise class
// open a dynaset
ODynaset dyn1;
dyn1.Open(db, "select
* from emp");
// attach a PosAdvise advisory to the dynaset
PosAdvise dynposition;
dynposition.Open(dyn1);
// move to beginning of dynaset to get dynposition started
dyn1.MoveFirst();
// position the dynaset before we do some processing
PositionDynaset(dyn1);
// get the position
long startpos = dynposition.GetPosition();
// now do some processing
ProcessDynaset(dyn1);
// how many records did we process?
long nrecords = dynposition.GetPosition()
- startpos;
The advisory is attached to the
dynaset by using the OAdvise Open method. It is detached
automatically when it is destroyed, or when Close is called.
Actions taken with the dynaset cause several things to happen:
· Before the action takes place, the ActionRequest methods of all attached advisories are called. The advisories can cancel the action at that point. (The PosAdvise class cancels all actions except MoveFirst, MoveLast, MoveNext, and MovePrev.)
· Next, if the action was allowed by all the advisories, it takes place.
· After the action, the ActionNotify methods of all the attached advisories are called.
· The PosAdvise class uses this notification to recalculate the position within the dynaset.
· Finally,
the PosAdvise class can implement additional methods beyond
the bounds of OAdvise. It implements the GetPosition
method.
Example 10: OBound of a variable
All of our examples thus far have
accessed the data in a dynaset more or less directly, either through
ODynaset methods or using OFields. These are not
always the most convenient access methods. Whenever we navigate
to a new record and are interested in the values of various fields,
we need to fetch the values explicitly. Further, when we want
to edit a record or add records, there is a moderate amount of
bookkeeping to do. It would be convenient to have a "managed
dynaset"-an object that does the more routine work for us.
This is the job of the OBound and OBinder classes.
Users of Visual Basic will note
that the OBinder class plays the role of the data control
(though without any user interface), and subclasses of OBound
play the same role as bound controls.
You use an OBinder object
much as you would use an ODynaset object. It is opened
with database and SQL information, for example. It has methods
that enable callers to add and delete records. In addition to
the work that a dynaset can do, an OBinder object can keep
track of changes that have been made to fields and can update
the database automatically.
OBound
objects hold the value of a single field of a record. When the
dynaset changes, the value of the OBound instance is changed.
And when the value of the OBound instance changes, the
value of the corresponding field in the dynaset changes-with all
the appropriate bookkeeping handled.
In the OMFC and OOWL libraries that
are provided with the Class Library, you can see the use of OBinders
and OBounds. Those libraries contain machinery that creates
normal widgets that are also subclasses of OBound. As
a result, in a GUI program that uses these specially subclassed
widgets, when the user edits the text of a widget and then navigates
to another record, the database editing is done automatically
(see Example 11).
In this example we subclass OBound
so that we have an OValue variable that works with the
OBinder-managed dynaset. The value of the bound OValue
object is automatically set to the current value of the field
with which it is associated. When the value of the OValue
object is changed, the database is updated to reflect the change.
We call this class OBoundVal. The implementation of this
class is stored in the samples directory, in the subdirectory
boundval. Our goal is to be able to write a program like
this:
// silly example of use of OBoundVal
void GiveRaises(int minsalary, int saladd)
{ // give everybody with
salary below minsalary a raise of saladd
// open connection to database
ODatabase odb("ExampleDB", "scott", "tiger");
// construct the OBinder (the managed dynaset)
OBinder block;
// set up an OValue bound to the salary field
OBoundVal salary;
salary.BindToBinder(&block, "sal");
// Note that we are binding the "sal" column before opening the
// query. If the select list for the query does not contain the
// "sal" column,
the OBinder.Open() call will fail.
// get the database data for the managed dynaset
block.Open(odb,"select ename, sal, empno from emp order by empno");
// If we do the binding here and the column we are attempting to
// bind does not exist, the following call will fail:
// salary.BindToBinder(&block,
"sal");
// Note that IsLast() will return TRUE after MoveNext() attempts to
// move past the last record. Therefore the last record does get
// processed in the loop
while (!block.IsLast())
{
// check the salary of the current employee
if ((int) salary < minsalary)
{
// change the salary
salary = (int) salary + saladd;
}
block.MoveNext();
}
return;
}
The most interesting part of this
sample is the line where the salary is changed. It appears that
we're setting the value of a variable. However, we're actually
changing the value of a field in the database. The database gets
updated when we navigate to another record.
The OBound class has three
very important methods:
1. Refresh is called (by OBinder) to give the OBound a new value whenever the field's value changes.
2. SaveChange is called (by OBinder) whenever the OBound should save its value to the database.
3. Changed is called by the
OBound subclass implementation to notify OBound
and OBinder that the value has changed.
Refresh
and SaveChange must be implemented by any subclass of OBound.
If the OBound subclass overrides any of the base class
triggers steps, the overloaded triggers should call the default
triggers in the base class to work correctly. Please see the online
documentation for the OBound class for more details. The
implementation for OBoundVal is shown below.
Refresh
transfers the value from the database to the OBound object.
SaveChange transfers the value from the OBound
object to the database. Changed notifies the OBinder
bookkeeping that a change needs to be saved to the database.
OBound
is generally used as a "mix-in" class, adding functionality
to some other class hierarchy. In this example we add functionality
to the OValue class. OBoundVal multiply inherits
from OBound and OValue.
Here is the Refresh method
for OBoundVal:
oresult OBoundVal::Refresh(const OValue &val)
{
OValue::operator=(val); // use OValue assignment to get new value
return(OSUCCESS);
}
When we are handed a new value,
we use OValue's assignment operator to save that value.
This routine is called by the OBinder class. Here is
the SaveChange method:
oresult OBoundVal::SaveChange(void)
{
return(OBound::SetValue(*this));
}
We use the OBound helper
routine OBound::SetValue to set the value. SaveChange
is called by the OBinder class.
But how do we set the value of an
OBoundVal? If we just allow the use of OValue::SetValue,
our value will get changed but the OBoundVal class won't
know about it. Further, we won't tell the machinery that there
has been a change, so no change will get saved to the database.
Here is the proper implementation for setting the value with
an integer argument:
oresult OBoundVal::SetValue(int val)
{
if (!Changed())
return(OFAILURE); // couldn't start change
OValue::SetValue(val);
return(OSUCCESS);
}
The first thing we do is to call
the Changed() method. (The default argument is TRUE, which
indicates that we are making a change.) This sets a flag indicating
that this OBound needs to have SaveChange called
on it later, and telling the dynaset to attempt a StartEdit.
This may fail, for a variety of reasons: the user may not have
permission to edit this database, the record may be locked, and
so forth. Only if the StartEdit is successful do we set
the value of the OBoundVal, using the parent method OValue::SetValue.
We need to ensure that we override
every OValue method that changes the object's value, because
we need to call Changed() to make the OBinder bookkeeping
aware of any change in the object's value. So we must also override
the Clear and operator= methods. And, for extra
utility, we also provide a number of extra operator= methods
so that the OBoundVal can act like a regular variable as
much as possible.
Note that when we use the operator=,
we are copying the OBoundVal's value, not the object itself.
There are subtle differences. In general, subclasses of OBound
want to copy values of objects rather than the objects themselves.
For this reason the copy constructor and assignment operator
are not implemented for OBound.
There isn't much difference in the
way an OBoundVal is used and the way an OField object
is used. Both return the field's value for the current record.
Both enable you to set the field's value. However, the OBoundVal
copies a value every time the dynaset moves to another record,
but the OField does not. OField gets the value out
of the dynaset only when it is requested. At the same time, If
you use OField for editing, you must manage the StartEdits
and Updates of the dynaset yourself.
OBound becomes
much more useful when the database fields are represented in a
user interface. Then, the code for handling changes can be quite
distributed, so keeping track of whether StartEdit has
been called or not (for example) would be troublesome.
Example 11: A full emp table
editor
This example demonstrates the construction
of a complete application. We built the application using Microsoft's
Visual C++ development environment, including the MFC framework
classes. (Users of other development environments will be able
to understand what was built from this example.) The sample source
is in the EMPEDT subdirectory (emp table editor).
The example uses the connection
dialog from Example 4, which provides a convenient interface for
the user to log into the Oracle database. The main screen is a
single window containing a form that displays and allows editing
of all the fields of the employee table. We built it as an MFC
formview. For editing the records of the table, we use the OMFC
bound control classes (built using the OBinder and OBound
machinery) provided with the Class Library. The example also includes
a feature that generates a unique employee number automatically
when new employee records are added. The amount of code needed
to write this application is surprisingly small.
This example is broken up into several
sections:
Application Layout discusses the basic creation of the application
Form Creation discusses the building of the main screen
Binding discusses the OBinder-OBound hooks to the form
Buttons discusses the implementation of navigation buttons
Using a Trigger discusses the implementation of the new employee number feature
Error Handling
discusses the handling of errors
Application Layout
We generated the original application
with AppWizard. We changed the view class to a form view so that
a user interface could be constructed quickly with the App Studio
resource editor. The application's memory model needs to be set
to large.
We set the project to link against
the oraclm, omfc, ole2, and ole2disp libraries. Although you
don't need to interact with OLE to use the class library, the
class library uses OLE internally, so it needs to be linked.
We use the connection dialog from
Example 4. We added the files LOGDLG.CPP and LOGDLG.H to the
project. Then we copied the dialog resource IDD_LOGIND from the
LOGDLG.RC resource file to the EMPEDT.RC resource file.
We made one simple change to the
application class: we placed the class library initialization
call (OStartup) in CEmpedtApp::InitInstance. We added
a destructor method for the application class (~CEmpedt) and placed
in it a call to OShutdown.
Form Creation
The purpose of this application
is to edit the emp table-one of the standard Oracle demonstration
tables. The emp table has eight fields: ename (employee name),
empno (employee number), job (a keyword describing the employee's
job), mgr (the employee ID of the employee's manager), sal (the
employee's salary), comm (a commission to be paid to the employee,
which is often NULL), hiredate (date that the employee was hired)
and deptno (identification number of the employee's department-used
to reference the dept table).
We built the most straightforward
kind of form to edit this table: one text edit per database field.
We edited the form in App Studio. We gave each edit control
a unique ID. We also placed a static text item next to each edit
control as a label. The IDs of the edit controls, their order,
and what the label says or what relation it has to the control
is completely up to you. Each of the edit controls will be attached
to a database field by code that you write, instead of based on
assumptions that the Class Wizard might make.
Neither variables nor message maps
were created for the edit controls. The edit controls will be
completely managed by the OBoundEdit class.
We added a button called Connect
to the form. The user presses the Connect button to connect to
the Oracle database. We used Class Wizard to create a button-clicked
method for the connect button. That method is named OnConnect.
Binding
The form view class CEmpedtView
controls the operation of the form view. We added a single OBinder
instance as a member variable to the view class. Its name
is m_empblock. (It is actually declared as OBinderEmp.
OBinderEmp is a subclass of OBinder. See Error
Handling below.) That OBinder instance manages the
database connection, the dynaset, and all the bookkeeping needed
for editing the database data. Next we need an OBound
subclass instance for each of the user-interface widgets that
we want bound to a database field. Since edit controls were used
for the user interface, we used OBoundEdit instances.
We added one member variable of type OBoundEdit to CEmpedtView
for each edit control. (The member variable for empno is an instance
of a subclass of OBoundEdit; see Using a Trigger
below).
The edit controls are bound to database
fields by calls to methods in the OBound class and subclasses.
This binding is done after a database connection is established.
For simplicity, we placed all the binding code in the OnConnect
method.
The first thing that the OnConnect
method does is to connect to the database. It does this by calling
the login dialog class:
// get an ODatabase object via the connection dialog
logdlg connd;
// get a database object
ODatabase odb = connd.GetLogin(ODATABASE_PARTIAL_INSERT | ODATABASE_EDIT_NOWAIT);
if (!odb.IsOpen())
{ // didn't get a connection - user must have canceled
return;
}
The login dialog prompts the user
(via a dialog) for database name, user name, and password and
attempts to connect to the Oracle database. When it is successful
it returns an open ODatabase object that refers to the
Oracle database. Unsuccessful connection attempts put up an error
dialog. If the user cancels the login dialog, an unopened ODatabase
object is returned. So OnConnect checks the returned
ODatabase to see whether it is opened. This amounts to
a check whether the user canceled from the login dialog or connected
to the Oracle database successfully.
The database is opened with partial
insert and nowait options. The partial insert option ensures
that any default field values set by the database are properly
reflected in the dynaset when a record is added or edited. The
nowait option ensures that if another user has a lock on a record
that we wish to edit, we don't wait for that lock to be freed.
Instead we get an error.
The OBoundEdit controls need
to be attached two ways. First, they need to be attached to particular
fields that will be available in a particular OBinder.
Second, they need to be attached to a particular user interface
widget. We accomplish the first attachment with a call to OBound::BindToBinder:
m_ename.BindToBinder(&m_empblock,
"ename");
This call binds the m_ename member
variable to the "ename" field available in OBinder.
The ename field becomes available once the binder object (m_empblock)
is opened. It is legal to bind a control to a binder before the
binder's SQL statement has been executed.
We accomplish the second attachment
with a call to BindToControl, which is a method available
for the classes in the OMFC and OOWL libraries:
m_ename.BindToControl(this, IDC_ENAME);
This call binds the m_ename member
variable to the user-interface widget identified by IDC_ENAME
in the window identified by "this". Since OConnect
is a method of the formview class, "this" points
to the form window.
Once all the OBoundEdit controls
are hooked up the OnConnect method, we can open the OBinder
by executing an SQL statement:
m_empblock.Open(odb, "select
* from emp order by empno");
This statement creates a dynaset,
fetches records from the database, and puts the correct database
field values in all of the edit controls.
Buttons
With the login dialog and the edit
controls implemented, we can build and run the application. It
works; it shows the values of the first record. We added more
buttons to the form to implement additional functionality.
We implemented navigation by adding
buttons named First, Prev, Next, and Last. We used Class Wizard
to create a button-clicked method for each. The implementation
for each is merely one line of code. For instance, to do Next:
m_empblock.MoveNext();
Calling OBinder::MoveNext moves
the dynaset to the next record, fetches it from the database (if
necessary), and updates the values of all the edit controls to
display the new record. It actually does more than that. If
any of the edit controls had been used to change the data in a
record, OBinder::MoveNext saves that change to the database
before moving to the next record. All this is accomplished without
writing anything more than "MoveNext".
We added a few more buttons for
record-level edits: Add New, Duplicate, and Delete. These were
also very simple to implement, a single line of code for each.
Again, the OBinder class takes care of necessary bookkeeping.
Using a Trigger
The empno field of the emp table
is required to contain a unique number in each record. Rather
than forcing the user to create the unique number manually every
time a new record is added (with either Add New or Duplicate),
we added code to set the unique number for the user.
Calculating a unique number is not
difficult. One way is to ask the Oracle database the current
maximum value of empno across all records. Adding one to that
result guarantees a unique value, relative to the records currently
in the database. (A better way is to use a sequence. Consult
your Oracle documentation for more information about sequences.).
When should this calculation be
made and what should you do with the result? The calculation
needs to be made every time a new record is added. We could do
the calculation every time the Add New or Duplicate button is
pressed, and then change the value of the empno edit control manually.
Then we would have to call OBound::Changed to make sure
that the OBoundEdit instance knew that the value had changed,
so that the OBinder would update the database properly.
This approach would work, but it is prone to error. For example,
what happens if we add some other method that also adds a record?
Instead, we changed the behavior
of the empno edit control. Whenever a new record is added to
an OBinder dynaset, every OBound instance bound
to that OBinder calls its PreAdd method before the
addition and the PostAdd method after the addition. The
PostAdd method of OBoundEdit does nothing. We subclassed
OBoundEdit, , creating a new class called OBoundEmpnoEdit,
and defined a PostAdd method for it that overrides the
default PostAdd. We declared the empno member variable
as an instance of OBoundEmpnoEdit. OBoundEmpnoEdit
is a subclass of OBoundEdit and is implemented in EMPEDVW.H
and EMPEDVW.CPP.
At runtime, whenever a new record
is added to the dynaset, the OBoundEmpnoEdit::PostAdd method
is called after the record is added. That routine sets the value
of itself, which sets the value of the edit control to the unique
number that we have calculated.
Because the trigger methods are
all virtual, the OBinder manages the OBoundEmpnoEdit
as if it were a normal OBound. But we have overridden
the default OBound behavior to suit the needs of the application.
Although in this particular sample
OBoundEmpnoEdit doesn't need any information from the rest
of the program, in a real working program the operation of the
bound control would depend on whatever else is going on. The
bound object needs a context. Just to show one way you can accomplish
that, the sample code also implements a SetContext method
that hands some useful context to the bound object. That context
would then be available in its methods for use.
Error Handling
The sample application performs
some minimum error handling. When an error occurs, it gives the
user an error message.
The first kind of error that can
occur is that the class library or some component upon which it
depends cannot be loaded or initialized. This error is caught
just before we try to log onto the database. We use these lines
of code:
// get the default session
OSession defsess(0);
if (!defsess.IsOpen())
{ // couldn't get default session? Class library isn't working
AfxMessageBox("Can't start Oracle class library");
return;
}
If the class library can't be initialized,
then the default OSession object won't be able to Open.
The next kind of error that can
occur is that the OBinder object can't be opened. We use
this code:
// check for error
if (ores != OSUCCESS)
{ // we couldn't open the dynaset
const char *msg;
if (defsess.ServerErrorNumber() != 0)
{ // we have a server error - tell the user that
msg = defsess.GetServerErrorText();
}
else
{ // no server error - class library isn't working correctly
msg = "Class library error when opening dynaset.";
}
// give the user a message
AfxMessageBox(msg);
}
where ores is the result of the
OBinder::Open call. The most likely errors here are that
the SQL statement has an error (the table doesn't exist or one
of the field names is bad) or that the user doesn't have permission
to read the table. These are both Oracle errors.
Either of these two cases would
prevent the application from working at all. These errors are
generally bugs in the client program that can be avoided with
correct coding (getting the SQL statement right, for instance).
Once we've opened the OBinder , we will look at records
in the table. Then we have to start considering other types of
errors-interactions with other users or lack of system resources.
The most common error occurs when
we attempt to edit the value of a field. When we try to change
the value of the field, the OBinder instance attempts a
StartEdit on the dynaset. This can fail for a variety
of reasons. Most common are: connection to the server is lost,
another user has a lock on the row, or the data in the row has
been changed by another user. The first two are reported as Oracle
errors. The last is an error specifically generated by the class
library to ensure that database data is not accidentally overwritten.
The difficulty here is that, with
OBoundEdit controls, the data is changed by typing a key
into the edit control. Our program isn't making any method calls
whatsoever. How do we get control to report the error?
The OBinder class has a method
called OnChangedError that can be overridden. It is called
when an error occurs while processing the StartEdit on
the dynaset. The example uses this to inform the user of an error
when a record is starting to be edited. The OBinder class
is subclassed to OBinderEmp, and the member variable is
declared to be of that type. Then when an error occurs as a user
types a value into a record, the OBinderEmp::OnChangedError
routine is called.
Finally, errors can occur during the various operations represented by the buttons on the form: navigating to various records or adding and deleting records. Errors during these operations are all sent to the view HandleError method. HandleError takes care of errors that occur when navigating, when adding or deleting records, or when saving a changed record to the database. Any of the navigation methods, or any method that adds records, saves any changes to the current record before doing its work (this is actually done through PreOperation trigger functions).
HandleError reports
the error to the user by fetching (or calculating) the correct
error message.