In the last several chapters, you have seen what Oracle Objects is and how to use it from Visual Basic. The same power is available to C++ developers through the Oracle Objects for OLE C++ class library.
Because this class library is based on the Oracle Objects for OLE product, it shares many of the same concepts and techniques. Before reading this chapter, you should read the preceding chapters on Oracle Objects for OLE. Those chapters discuss the
basic ideas of Oracle Objects for OLE and provide details such as software requirements.
This chapter takes a quick overview of the class library and then looks at how to use the class library to build some simple routines. The emphasis is more on how to use the library rather than on details about what each method does. A few code snippets
are provided, and the full source code is provided on the accompanying CD-ROM.
The information documented here is accurate for Oracle Objects for OLE 1.0 (patch level 55). Oracle does periodically release patches and upgrades, so please contact Oracle for information about future releases.
To give you a flavor of the class library, look very quickly at a snippet of code. You will write a function to look at Oracle's standard demonstration schema and extract the maximum salary paid to any employee. What kind of code would you have to write
to do this with existing C++ tools? You would have to declare working areas, open a database connection, create a cursor, parse SQL statements, fetch data, and perform various guidekeeping tasks. Here is the function written in the Oracle Objects for OLE
/* connect to the database */
ODatabase dbref("t:220.127.116.11:ORCL", "scott", "tiger");
/* open a dynaset with the SQL query */
ODynaset dynref(dbref, "select max(sal) from emp");
/* get the data */
dynref.GetFieldValue(1, &maxsal); // get value of column 1
/* return the value */
This code does all those previously mentioned tasks in only five lines of C++ code, three of which are declarations! The Oracle Objects for OLE C++ class library gives you methods that enable you to work with your SQL statements without having to worry
about all of the guidekeeping code that you would have to write if you were working directly with Oracle's call interface, or ODBC. Later in the chapter, you'll go through this example line by line to understand what it is doing.
The Oracle Objects for OLE class library is a C++ class library that provides object-oriented access to data stored in an Oracle relational database. It uses the Oracle Objects automation server DLL to perform most of its tasks.
The class library portion of the Oracle Objects for OLE product has several major components. They are
- The class library
- Sample bound classes for MFC widgets
- Sample bound classes for OWL widgets
- Documentation for the class library
- A workguide
- Source code for the class library
The class library provides some basic classes that correspond to the objects in the Oracle Automation Server. It also provides some extra classes for dealing with bound objects. These bound object classes give functionality in C++ that roughly
corresponds to the functionality of data controls and bound objects in Visual Basic. You'll learn about these very powerful bound object classes toward the end of the chapter.
The class library consists of a library file that you link against and a DLL that you use at run time. Versions of the library file are available for both Microsoft and Borland compilers, as are both debug and non-debug versions of the DLLs (for both
Microsoft and Borland). The Microsoft files have names ending in "m", and the Borland files have names ending in "b".
The sample bound classes are provided in both MFC and OWL versions. These are provided as libraries rather than as DLLs. These libraries enable you to use the user interface widgets of the application frameworks as bound objects in your database
The documentation for the class library consists mostly of a single help file, which is more of a reference than a learning guide. An icon for this document was installed in the Oracle Objects for OLE program group. There are also write files for the
MFC and OWL bound widget libraries.
The class library comes with a workguide, which is the piece of documentation that is intended to teach you how to use the library. This workguide works through several examples in detail. Source code is provided.
A complete set of source code for the library itself is also provided. You need the header files to compile your code, of course, but the source code is very handy when it comes time to debug your program. Source code is also provided, in separate
directories, for the MFC and OWL bound widgets.
The interface of the class library has a great deal in common with the objects that you saw in the preceding chapters. There are session, database, dynaset, field, and parameter objects. The objects in Oracle Objects mostly become classes in the class
library. Many of the methods have similar names and behavior. The class library also implements a number of additional classes.
The class library is implemented using the Oracle Objects for OLE automation DLL. Instead of using the OLE automation interfaces that are so useful for Visual Basic, however, the class library uses custom OLE interfaces that the DLL makes available.
This is important because it means that the performance of the class library will be better than an equivalent program written in Visual Basic.
In fact, you could consider the class library's dependence on Oracle Objects for OLE to be strictly an implementation detail. For all that you can see in the library's interface, it could have been implemented a number of different ways. It isn't until
it comes time to distribute your product that you will need to remember the Oracle Objects automation DLL.
The class library uses OLE. Does that mean that you will need to learn OLE to use it? Absolutely not. In fact, you will not even need to include OLE header files in your source code to use the class library. Like the Oracle Objects for OLE layer, OLE is
used strictly as an implementation device. You will only need to link against the OLE libraries.
Users of Microsoft's Visual C++ development environment know that its Microsoft Framework Classes (MFC) library has database classes in it. Because they are part of MFC, they are nicely integrated into the development environment. The Application Wizard
helps you build an application that supports databases, and the Class Wizard makes it easy for you to create C++ classes that correspond to database tables. For an application that is working with a known, fixed schema, the MFC database classes are very
convenient and powerful. How does the Oracle Objects for OLE class library compare to the MFC database classes?
The two libraries take very different approaches. MFC uses a CRecordset class as a base class that handles the database interaction. You then subclass this class for your particular query, constructing a class that has member variables corresponding to
the columns in your result set. This means two things: first, the set of columns that you are working with is determined at compile time; and second, you create a new class for every different query. The benefit to this approach is that it makes nice tools
like the Class Wizard possible. And you never have to see any SQL.
Oracle Objects for OLE has an ODatabase class that handles the database connection and an ODynaset class that handles the SQL query result set. The SQL statement is handed to the ODynaset instance at run time, rather than compile time. This means that
you can create the SQL statement dynamically in your program. However, because the details of the result set aren't known until run time, it is not possible to have a class with column values as member variables. Instead, you call methods such as
ODynaset::GetFieldValue() to get column values. It is a slightly more complicated approach but one that is more flexible.
Which one should you use? If your application only has a few queries, and they are running against a fixed schema, it will probably be easier to use MFC's classes. On the other hand, if your schema is changing or not known until run time, or if you have
many small queries (would you have wanted to create a new class for the trivial query of our simple example at the beginning of the chapter?), you are probably better off using the Oracle Objects for OLE class library.
The requirements for using the class library are basically the same as the requirements for using the Visual Basic part of Oracle Objects. You need access to an Oracle database, either locally (such as Personal Oracle7) or remotely via SQL*Net. The OO4O
DLL is 16-bit, so if you use SQL*Net you will need a 16-bit version of SQL*Net.
The class library is a 16-bit library, so you can only use it to build 16-bit applications. This means that to use Visual C++, you will need a 1.x version (1.0 to 1.5). For Borland compilers, you will need a 4.x version.
This section looks at the classes in the library. These classes are very similar to the objects in the rest of Oracle Objects for OLE, so in many cases, you will simply be noting differences.
The most important classes are ODatabase, which gives you a database connection, and ODynaset, which allows you to execute SQL statements. OValue, which will hold an arbitrary value, and OParameter, which lets you parameterize your SQL statements, are
Instances of all of the classes (with the exception of OValue, ODynasetMark, and OAdvise) are actually surrogate objects. The class library uses a reference-counted surrogate-master architecture. This means
that copying something like an ODatabase instance is very cheap and that the copy will refer to the same master object. It is important to keep track of which operations affect the surrogate and which affect the master. Most affect the master.
Many of the classes in the class library can be called "basic" both because they are the simple foundations of the library, but also because they correspond very closely to the objects used in the Visual Basic version of Oracle Objects for
OLE. These classes are OClient, OSession, ODatabase, ODynaset, OParameter, OField, and the collection classes OSessionCollection, OConnectionCollection, OFieldCollection, and OParameterCollection. These classes correspond to the automation objects
OraClient, OraSession, and so on. You can consult both Chapter 47 and the product documentation to learn about the corresponding automation objects.
For the most part, you can expect these classes to work the way their similarly named object counterparts work (just change "Ora" to "O").
There are a few general differences:
- The C++ methods return error codes.
- Whereas the automation objects have both properties and methods, the classes only have methods. The object properties have been replaced with C++ methods that get values.
- In some cases, the method names have been changed to be more similar to the names of methods in the MFC database classes rather than Visual Basic names.
- Derived objects are not created by calling a method on their parent. They are created by calling a constructor for the object. The constructor will have an argument for the parent object. For instance, the OLE automation object OraDatabase has a
CreateDynaset method. ODatabase has no CreateDynaset method. Instead, ODynaset has an Open() method that takes an ODatabase as an argument.
Let's quickly go over some of the bigger differences between the objects and the classes. Classes that aren't mentioned have minor differences.
The ConnectSession and CreateNamedSession automation object methods don't exist. The class library implements the same functionality with overloadings of the Open method.
A few of the changes are some simple renamings, for instance EOF() becomes IsEOF() and Transactions() becomes CanTransact(). Settable properties become Set() methods; for instance, setting the SQL property becomes SetSQL().
The way guidemarks are handled is somewhat different. In C++, a guidemark is an instance of another class: ODynasetMark. When you retrieve a guidemark, you are handed an instance of this class. Instead of setting the guidemark property, you call the
MoveToMark() method with an ODynasetMark instance as an argument. Also, you can retrieve a mark from the last modified record, as opposed to the current record, using GetLastModifiedMark().
A number of additional methods exist. IsValidRecord() checks whether the current record is valid. CancelEdit() cancels an edit operation on the current record, which is useful to undo an edit on a record without rolling back the database transaction.
DuplicateRecord() is a handy helper function that copies the column values of the current record to a newly added record.
The biggest addition to ODynaset is that you can directly access field information from the result set, rather than having to obtain a field collection first. You can get field values, set field values, get the number of fields, and so on all through
methods such as ODynaset::GetFieldValue().
The CommitTrans() and Rollback() methods are not supported in C++. Oracle recommends against using them anyway.
In addition to the GetValue() methods, which are overloaded for various types, there are also overloaded cast operators. This means that when you have an OField instance, you can simply assign it to a native C++ type. For instance:
fsalary = someDynaset.GetField("mycolumn"); // get a field
dsalary = fsalary; // works because of overloaded cast
OOracleObject does not correspond to any automation object. It is the base class for most of the rest of the objects in the class library. You will never need to instantiate OOracleObject. The most important thing to notice about OOracleObject is that
it contains the methods for error handling, which are then available to all its subclasses.
OOracleCollection is a subclass of OOracleObject, and like OOracleObject, it is only used as a base class. You will never want to create an instance of OOracleCollection. The only important method of OOracleCollection is OOracleCollection::Count(),
which returns the number of objects in the collection.
The classes OSessionCollection, OConnectionCollection, and OFieldCollection are all collections that the class library will create internally. You can open such a collection, but you cannot create a new collection. You will use these classes when you
have asked the library for information, for instance about all connections. OParameterCollection is a little different in that you can assemble a new collection yourself, using OParameterCollection::Add().
The OParameterCollection object gives you a list of parameters attached to an ODatabase's master object. When the OParameterCollection is destroyed, the parameters in the collection are not destroyed because
they are still attached to the database object. To delete a parameter, you need to explicitly call OParameterCollection::Remove().
In the OLE Automation Server, the guidemark that is returned by OraDynaset is a string. In the C++ class library, this has been replaced by a separate class, ODynasetMark. Instances of ODynasetMark are returned to you by the ODynaset methods GetMark()
and GetLastModifiedMark(). ODynasetMarks can be copied and compared for equality. You can return to a marked record with ODynaset::MoveToMark().
A very typical use for an ODynasetMark is for a drop-down list. You get a set of records from the database with an ODynaset and then place the values of one of the columns in the drop-down list. You can get an ODynasetMark for each record and associate
it with its corresponding line in the drop-down list. Then, when the user chooses the item, you can use the ODynasetMark to set the current record in the ODynaset to the record that corresponds to the chosen item.
Because the columns of your query are run-time bound, it is not possible for either you or the compiler to always determine the type of "column 3." What you need is a variable that can accommodate any data type. The OValue class fills that
need. You can place many different types of data into an OValue and ask for many different types as output. The OValue instance will make an effort to convert the data if needed.
OValue takes a conservative approach to your data. It remembers the original data and type of data that it was set to. Consider the case where you set a VARIANT to a value of 1.3, a value of type double. Then you ask for its value as an int, and receive
the value 1. If you then ask for the value as a double again, you do not want to be told 1.0; you want 1.3. OValue is "driftless." The data does not drift depending on what types of values you ask it for.
An OValue object is a remarkably easy way to do data conversions. It does string-to-number and number-to-string conversions using OLE's VARIANT functionality. All you need to do is to construct an OValue with the
type you have, and then cast it to the type that you want.
Occasionally you will create an ODynaset and keep it open, allowing the user to navigate through the result set and make changes to the data it refers to. The navigation and data changes affect the ODynaset's master object. Perhaps because of the
processing you are doing, you want to know about what's going on with the master dynaset object. You could add some notification code at all the places in your program that edit and navigate using the ODynaset. This scattered code, however, would be a
An alternative is to let the dynaset tell you when something is done to it. That's what OAdvise is about. It is essentially a method for registering callbacks. You create a subclass of OAdvise, overriding methods that pertain to actions you are
interested in. You then hand an instance of your OAdvise subclass to an ODynaset, and you will get callbacks when things happen.
There are a host of reasons why something can go wrong in your database application. You will need to detect and handle any error conditions.
If you look at one of the surrogate objects, say an ODatabase instance, you can ask the question: is this instance connected to a master or not? In the Oracle Objects class library, this is what is meant by whether an object is open or not. An open
object is connected to a master, and a closed object is not. You can't do much with closed objects except to ask them whether they're closed, or to obtain error information.
You can make a closed object yourself, either with the default constructor or by calling the Close() method on an already open object. But the most important way to get a closed object is in case of an error.
If you attempt to create or get an object and an error occurs, the object that is returned to you is closed. Or if you use one of the constructors that does some real work, perhaps the ODatabase constructor that opens a connection to the database, and
some problem occurs, the constructed object will be closed instead of open. Because a constructor can't return an error condition, the only way that you can tell that the constructor failed is that the resulting object is closed.
In either case, the closed object will have error information attached to it. You can query the closed object to find out what went wrongwhy it couldn't be created open.
Oracle Objects has two levels of errors that you need to worry about: standard Oracle errors and Oracle Objects errors.
The standard Oracle errors occur for all the reasons that you might get an error from Oracle. For instance, your SQL statement tries to delete records in a table where you only have read privileges.
The Oracle Objects errors are internal library errors. Perhaps you tried to use an object that is closed, or your program has run out of memory.
The Oracle Objects for OLE C++ class library does not throw exceptions. All error handling is done through return values and error-reporting methods.
Most of the methods return a result of type oresult. A return of oresult can have two values: OSUCCESS or OFAILURE. All this tells you is whether the routine worked or not. If the routine did not work, you want to call the routine
OOracleObject::ErrorNumber() to get an internal OracleObject error number. These numbers are listed in the oracl.h header file, with comments indicating what the errors mean.
To get an Oracle error, call ServerErrorNumber() for the Oracle error number or GetServerErrorText() to get the error message text.
This section goes through the example at the beginning of the chapter in some detail so that you know what it is doing. This will illustrate a lot of the power of the class library.
The first line of the example constructs and opens an ODatabase instance, creating a connection to the database in the process:
ODatabase dbref("t:18.104.22.168:ORCL", "scott", "tiger");
This form of the ODatabase constructor does not take an OSession instance as an argument. Therefore, it uses the default session for this connection, which is what you usually want. If the default session hasn't been created yet, this ODatabase
constructor makes it for you. The three arguments to the constructor are the database connection, the username, and password. These are used to make a connection to an Oracle database instance. As a side effect, it creates a connection automation object to
keep track of the connection. If you had already used this connection elsewhere in your program, that connection would be shared here.
If you wanted to write your code correctly and check for error conditions, you could call ODatabase::IsOpen() here to check if the ODatabase instance was opened. If it is open, then everything worked correctly.
The second line of code constructs an ODynaset:
ODynaset dynref(dbref, "select max(sal) from emp");
The first argument is the database you want to fetch data from. The second argument is the SQL statement. This line of code takes care of all the SQL parsing and executing, as well as running a data cache, so that you can have a client-side scrollable
cursor. It also fetches the first row of data. Again, robust code would check for an error condition by checking whether the ODynaset has been properly opened. In a real program, it is often the case that the SQL statement is created dynamically, rather
than using a static string.
The fourth line declares a C++ double variable that will hold our return value.
The fifth line fetches the desired value out of the result set:
dynref.GetFieldValue(1, &maxsal); // get value of column 1
The first argument is a field index. You are getting the value of the first column, which in this case is the only column. The value is placed into maxsal. It is possible that some data conversion may have to be done, for example, if for some crazy
reason the database schema is representing salaries with character strings. The GetFieldValue() method will do type conversions for you so that, in this case, you end up with a good double value.
At this point, the function returns its value and the listing ends. But is that the end of the function? No, it is not. The C++ run-time engine will call the destructors of the ODatabase and ODynaset instances for you. Those destructors will take care
of doing all the necessary guidekeeping for you: freeing memory caches, dropping database connections, and so forth. Because of the reference-counting mechanisms, the order of the destructors is not important.
This section walks through the steps you need to take to actually build a working program with the class library.
In addition to the code that you will be writing, you need to initialize and uninitialize the class library. Initialization is done with a call to OStartup(), and deinitialization is done with a call to OShutdown(). These should only be called once in
your program. It is easiest if you place calls to these routines in the application initialization and shutdown code. For instance, if you are working with an application framework that has an object for the application, you can make these calls in the
constructor and destructor of that object.
This is for those of you writing applications that call OLE directly. One of the things that OStartup() and OShutdown() do is to call OleInitialize() and OleUninitialize(). You can either call OleInitialize()
yourself or let OStartup() call it for you. If you call OleInitialize() before OStartup(), you will be responsible for calling OleUninitialize(), because OStartup() notes whether it was the first initializer.
To compile your code, you will need to access the header files for the class library. These were placed in the oo40\cpp\include directory when you installed the product. You will always need to include oracl.h. If you are using the OBound and OBinder
classes, you will also need obound.h.
If you are using one of the bound widget libraries, you will need its header file, located in either oo4o\cpp\mfc\include or oo4o\cpp\owl\include.
Two other details require your attention. The class library assumes that you are working with the large memory model. So you will need to compile your calling code with the large memory model. And you may need to increase the stack size of your program.
Your program will need to link against one of the provided export libs. There is a different version for Microsoft and Borland compilers. These files are in the oo4o\cpp\lib directory, oraclm.lib for Microsoft, and oraclb.lib for Borland. You do not
have to worry about linking against OLE libraries.
If you are using the bound widget libraries, you'll need to link against them. There are different versions for MFC (Microsoft) and OWL (Borland).
You will need either the file oraclm.dll or oraclb.dll, which are the Microsoft and Borland versions respectively, in either the debug or non-debug flavor, depending on how you built your application.
The class library's DLL is not installed into your path. You will need to copy the appropriate DLL to somewhere on your path, or change your path variable to point to oo4o\cpp\bin or oo4o\cpp\bin\dbg, before
your program will run.
The complete source code for the class library has been provided to help developers debug their applications. The source code is available in oo4o\cpp\src, and the debug DLLs have been built with symbols properly referring to the source. Stepping into
the code and watching it work (or fail) is straightforward.
An example of a working program has been provided with this guide as the program SeeStock. It is a simple program that looks at stock holdings. The program was written in Visual C++ 1.5, but developers using other environments should have little
difficulty adapting the code.
The program assumes the existence of a user "stock" (with password "ticker"). The example comes with an SQL script named stock.sql that should be run in the "stock" account to set up the database.
This section won't go over the source of the program line by line. We'll just cover the high points here. You can examine the sample source code at your leisure.
The heart of this program is the class StockHolding. Its declaration is
StockHolding(ODatabase odb, const char *stockname);
oresult GetInformation(double *price, long *nshares);
oresult BuyShares(long nbuy);
oresult SellShares(long nsell);
An instance of StockHolding enables you to manipulate information about stock in one company. Having a class like this isolates your application from the details of the database. Although in this case the stock information is all taken out of a single
trivial table, in a real application the stock holdings would probably be in one table and the stock pricing information would be in another table, which would be getting up-to-date price information in real time. All that would change in the test program
is the StockHolding class.
To create an instance of StockHolding, you pass in the name of the stock as well as an ODatabase instance. That ODatabase instance is assumed to be open; the connection to the Oracle database has already been established. The StockHolding instance then
keeps a copy of that ODatabase. As long as the StockHolding object exists, the connection to the database will be kept open. What's really happening is that the StockHolding instance has, as a member variable, a surrogate database object. As long as that
object is open, the master database object will stay open, and so the connection will stay open.
You can imagine that you connect to the database in one part of your program and then create several StockHolding objects, corresponding to the different stocks owned by the user of the program. All of the StockHolding objects use different surrogate
databases (different ODatabase instances), but they all refer to the same master database object. When all the StockHolding objects are destroyed, for instance when your program is shutting down, the connection is finally closed when the reference count on
the master object goes to zero.
When you have independent objects sharing a database connection, you need to think carefully about database transactions. A commit executed by any one object commits for all objects on the same connection. The
same is true for rollbacks. You also need to be careful when one object is writing to a table that another object is reading, because the reading object may see some old records and some new records, leading to inconsistencies.
The query that you need to execute to obtain the stock information is always the same; all that changes is the name of the company. There are two ways you could create an SQL statement that gets the desired data for a particular company. You could
construct the selection statement dynamically, including a WHERE clause that explicitly selected a company:
select * from stockinfo where s_name = 'NullBits'
You can take such an approach with this class library because it works with dynamically generated SQL. Because each StockHolding instance will only work with a particular company, it could generate its SQL SELECT statement in its constructor.
Another approach is to use an SQL statement that has a parameter in it, and change the value of the parameter. In this case, the SELECT statement would be:
select * from stockinfo where s_name = :stockname
In this case, the SQL statement is the same for all instances. This enables you to manage the SQL statement string better. Instead of writing code that generates the SQL string, you can read it out of a resource or file. This means that if the database
schema changes, all you need to do is to change the SQL statement, instead of your code.
The StockHolding class uses the second approach. The SQL statement is implemented as a static string. The StockHolding constructor guarantees the existence of the named parameter in the database object so that later selections can just set the value of
the parameter and then execute the selection. Note that parameters are attached to the database, which means that all of the StockHolding objects share the same parameters because they are sharing the same database. The parameters are never destroyed. They
go away when the database is destroyed.
The StockHolding class has two different methods for changing data in the database: StockHolding::BuyShares() and StockHolding::SellShares(). For demonstration purposes, these two routines have been implemented with different techniques.
The routine StockHolding::BuyShares() updates the database directly with an SQL update statement. A static SQL string with a parameter is used, similar to the SQL string used for selection. Executing an SQL statement directly like this gives you the
most power, if you want to use SQL. Note, however, that if you have any open dynasets that have read from the updated table, they will not immediately see the new data.
The routine StockHolding::SellShares() uses another technique. It creates a dynaset and then uses it to update the database. If your program holds onto the dynaset, either for later reading or for more updating, this method is easier to use. It also
guarantees that you see the changes that you have made in the dynaset that was used to make the change.
The ODynaset object is very useful for interacting with an Oracle database. It reads data. It will let you edit data. It lets you write that edited data back to the database.
Now consider how you would use it to set the text in an edittext control to the value of a column in your query result and make it write any changes back to the database. Whenever your code navigated to a new record, you would get the new column value
and put it into the edit column. You want to notice if any changes are made to the text in the control. And when the user or your programs navigates to the next record, you want to save any changes (if there were any changes) back to the database. It is a
fair amount of tedious work. And that tedious work is pretty much the same for all columns, whether they are displayed as edittext controls or check boxes or exist simply as a variable in your program.
This is the problem that the bound object classes are built to solve. They take care of the tedious work, giving you hooks so that you can intervene when you want to.
The OBound class is a base class that lets you build bound objects. These are objects whose values are bound to the value of a particular column in a query result. As your program or user navigates through the result set, the value of the bound
object automatically changes. Typically there are several bound objects per result set, for instance one per column. The OBound class also keeps track of whether the object's value has changed.
The OBinder class is a dynaset manager. An OBinder instance will manage a single dynaset, keeping track of all the bound objects attached to the dynaset, giving them their new values as dynaset navigation occurs. The OBinder instance also watches all
the attached bound objects for value changes. Before the dynaset is allowed to navigate away from the current record, the OBinder class saves any changes in the current record back to the database.
OBinder is a useful class as it stands, because its default functionality gives the simple behavior described. But you will use a subclass of OBound, overriding at least two of its methods. You need to override OBound::Refresh(), which is the method
that transfers data from the dynaset to the bound object. And you need to override OBound::SaveChange(), which is the method that transfers data from the bound object to the dynaset. Because you implement these data transfer methods, your object can be
anything you like: an edit control, a slider, or simply a variable.
Overriding OBound::Refresh() and OBound::SaveChange() will give you useful functionality, but you have even more power available to you if you are willing to override the trigger methods of OBinder and OBound.
A trigger is a piece of code that is executed when a specific event happens. In the application framework world, you would probably call it an event handler. In the database world, you see the term triggers when talking about procedures stored in
an Oracle database, or code in 4GL environments such as Oracle Forms. For instance, there may be a trigger that is run before inserts are performed on a table. Another trigger may run after records are deleted from a table.
Both OBinder and OBound have triggers that are run both before and after the operations of adding (inserting), deleting, and updating records. Other events supported are navigating, querying, and the special startup and shutdown triggers. All the same
triggers exist for OBinder and OBound. The difference is that the OBinder triggers are appropriate for record-level processing, while the OBound triggers are appropriate for field-level processing.
Libraries of bound user-interface widgets are supplied with the Oracle Objects for OLE C++ class library. There are two separate libraries, one for Microsoft's MFC environment and another for Borland's OWL. These widgets make it incredibly easy to
implement a database application with a simple user interface. This section looks quickly at the MFC library and then examines a sample application built with the MFC widgets.
The OMFC library contains bound classes for several of the most important user interface elements. It provides OBound subclass implementations for the following kinds of widgets:
- Text edit control (OBoundEdit)
- Static text (OBoundStatic)
- Check box (OBoundCheckBox)
- Radio button set (OBoundGroupButton)
- Slider (OBoundSlider)
In all cases, it is helpful to remember that the OBound subclass is not the widget itself, but an object that is managing the widget.
The widget that you will use most often is the text edit control. This control can be used to display the value of a column in the current record and edit the value of the column by editing the text in the control.
The static text is useful for displaying read-only values.
The check box control is useful for displaying a column that is expected to have one of two values. One value is assigned to the checked state, and the other is assigned to the unchecked state.
There is no bound control for controlling a single radio button. Instead, OBoundGroupButton is used to control a set of radio buttons. A group of radio buttons is useful for displaying a column whose values are expected to be one of a small number of
discrete possibilities. Each radio button corresponds to a single value.
The slider is used to directly display and manipulate a numeric value.
There are three setup steps to get an OBinder record block going:
- The OBinder must be set up with a database connection and an SQL SELECT statement. This creates a dynaset that will be used to fetch and edit the data.
- Each OBound instance must be attached to a column in the result set. This is done with the OBound::BindToBinder() routine.
- Each OBound instance must be attached to the user interface widget that it is controlling. This is done with the BindToControl() routine in the bound widget classes.
Once you have performed this setup, there is little else you need to do. When the program runs, the user's changes will be automatically entered into the database. The OBinder class has navigation methods, such as MoveNext(), that you will want to use
for moving through your result set.
The SeeStock application in the previous example was fine as long as you only wanted to display your stock holdings. It would be much more interesting, however, if you could actually edit your stock holdings. Writing all the code to note changes and
then saving them back to the database would be a lot of work with the basic classes, but with the bound control classes, it is easy.
The EdtStock application takes advantage of the bound controls. The application displays information about one stock at a time. It uses static text controls to display the name of the current stock and its price. And it uses a text edit control to
display and edit the number of shares currently owned.
The full Visual C++ 1.5 source for this application is provided on the example disk, in the directory SeeStock, ready to compile and run. Those of you who work with other development environments will still be able to read and understand the source
Like the SeeStock example, this program expects to connect to an account named "scott" (with password "ticker") on a Personal Oracle7 database. The account should be initialized with the stock.sql SQL script. This application will
work with the following query:
select s_name, s_price, s_shares from stockinfo;
This query gives you the records of the result set.
To build the application, you first have to do the usual tasks required for the Oracle Objects for OLE class library. You need to get access to the include files oracl.h, obound.h, and omfc.h. You need to link against the oraclm.lib and omfc.lib
libraries. And the program needs to call OStartup() and OShutdown() at its beginning and end.
The user interface is easy to construct. It is simply a standard MFC dialog-based application. In this case, the single window is a formview. The form is constructed using the standard tools in Visual C++: the AppStudio resource editor and the
ClassWizard. All of the controls are created with the resource editor using the dialog editor. They do not have to be given member variables. The navigation buttons are given methods.
The form view class, which is declared in edtstvw.h, defines a single OBinder instance variable and several variables that are subclasses of OBound:
// part of the CEdtstockView class declaration
class CEdtstockView : public CFormView
Most of the declaration has been removed...
// bind objects
The portion of the class declaration shown here is added to the code that the AppWizard and ClassWizard generated.
The OBinder instance replaces both the ODatabase and ODynaset of the previous example. In fact, an OBinder instance contains both an ODatabase and an ODynaset as members. You have a single OBinder instance because this application is working with a
single result set.
You have a separate OBound instance for each bound control, in this case two OBoundStatics and a single OBoundEdit.
To make the bound controls work, you have to get them connected up with all the other objects they need to communicate with. As you saw earlier, each bound control needs to connect to a column in a query, which is controlled by the OBinder instance, and
with a user interface widget. Here are the two relevant lines of code for the edit text control:
The first line connects the OBound object to an OBinder object, specifying that the column that it is displaying and edited is named s_shares. The second line connects the OBound object to a particular user interface widget, whose id is IDC_NSHARES. The
first argument to OMFCBound.BindToControl() is this, which refers to the CFormView instance. It enables the widgets to get information about their containing window.
Because the binding between user interface widgets and controlling objects is made at run time via method calls, it could be done dynamically. You could create widgets on the fly and bind them to columns in dynamically created SQL statements if you so
desired. This example uses AppStudio for convenience.
When the application starts, it is not connected to a database. The user presses the Connect button to connect to a database. In this example, the connection is hard-coded. In a real program, the user would be given some sort of login dialog. The
program will then connect to the database and display the information for the first record in the query result set.
To display and edit other records from the result set, you need to navigate to those other records. EdtStock only implements single record forward and backward scrolling. This is done with the Next and Previous buttons. The code for the routines that
implement these is very simple. Here is the code for the Next button:
You call OBinder::MoveNext(), and it takes care of the guidekeeping, updating the database if necessary, fetching another row, and updating the widgets with new values. Notice that you did not have to write any code that set the values of the widgets or
read the current values of the widgets and put them back into the database. The OBinder class, with the help of the MFC bound widget classes, takes care of all that for you.
This application gives you a little taste of the power available to you when using the class library's bound object classes. Overriding the various trigger methods of OBinder and OBound is outside the scope of this guide. One thing you can do with a
trigger is to perform an additional calculation. For instance, you might want to display a field showing the total value of the shares of stock, which would be calculated automatically whenever the application moves to a new record. Or you could use
triggers to validate the values of columns before entering them into the database, for instance, making sure that the control which is displaying the number of shares owned contains a valid number.
The Oracle Objects for OLE C++ class library is a powerful tool for C++ programmers who want access to data stored in Oracle databases.
The class library shares many features with the rest of the Oracle Objects for OLE. It is dynamic, meaning that it can work with SQL statements generated at run time. It requires very little guidekeeping by the developer using the product. And it has a
rich set of operations available.
You saw how the basic classes work, and how they differ from their analogs in Oracle Objects for OLE. You also explored the bound objects classes, which is an important extension to the Oracle Objects product.
Applications are easy to build with these class libraries, as shown by some of the examples. If you are building an application with a complex user interface, you may want to consider using the MFC or OWL user interface widget bound objects.