Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Beginning Visual C++ 2005 (2006) [eng]-1

.pdf
Скачиваний:
110
Добавлен:
16.08.2013
Размер:
18.66 Mб
Скачать

20

Updating Data Sources

In this chapter, you’ll build on what you learned about accessing a database via ODBC in the previous chapter, and try your hand at updating the Northwind Traders database through the same mechanism.

By the end of this chapter, you will have learned about:

Database transactions

How to update a database using recordset objects

How data is transferred from a recordset to the database in an update operation

How to update an existing row in a table

How to add a new row to a table

Update Operations

When you are just writing code to view information from a database, the only issue is whether you are authorized to access the data. As long as the database has the right kind of access protection, the data in the database is safe. As soon as you start writing code to update a database, it’s quite another kettle of fish. Because you are altering the contents of the database, such modifications could destroy the integrity of the database and make nonsense of the contents of a table, or even make it unusable. You always need to take great care to test your code properly with a test database before letting it loose on the real thing.

A database update typically involves modifying one or more fields in a row in an existing table, modifying an order quantity for instance, or adding a new row — a new order perhaps in the context of the Northwind database. You’ll be developing examples of both of these, but first, consider the implications.

Most of the complications that can arise with database update operations become apparent in the context of multi-user databases. Without proper control of the update process, concurrent access

Chapter 20

by several users provides the potential for two kinds of problems. The first arises if one person is allowed to retrieve a record while an update operation is in progress on the same record. The person just reading the data can potentially end up with the old data prior to the update or even a mixture with some fields containing old data and some new. The second problem arises with concurrent update where one person starts updating a record while another update is already in progress on the same record. With a single record in a table involved in this, there is potential for an update to be lost. Where records from several tables are involved, the data in the database can end up in an inconsistent state. Before you look into how this can be handled, see how basic update operations on a recordset work.

CRecordset Update Operations

You saw in the previous chapter how the RFX_() function calls in the DoFieldExchange() member of the recordset object retrieved data from the selected fields in the table or tables in the database, and transferred it to the data members of the recordset object. The same functions are also used to update fields in a database table, or to add a completely new row.

There are five member functions of the CRecordset class that support update operations:

Edit()

Call this function to start updating an existing record. Throws a CDBException

 

if the table cannot be updated, and throws a CMemoryException if an out of

 

memory condition arises.

AddNew()

Call this function to start adding a completely new record. Throws a

 

CDBException if a new record cannot be appended to the table.

Update()

Call this function to complete updating of an existing record or adding a

 

new one. Throws a CDBException if a single record was not updated, or an

 

error occurred.

Delete()

Delete the current record by creating and executing an SQL DELETE.

 

Throws a CDBException if an error occurs — if the database is read-only for

 

instance. After a Delete()operation, all the data members of the recordset

 

will be set to null values — the equivalent of no value set. You must move to

 

a new record before executing any other operation on the recordset object.

CancelUpdate()

Cancels any outstanding operation to modify an existing record, or to add a

 

new one.

 

 

None of the functions have parameters. The first four functions here can throw exceptions, so you should put the call in a try block and add a catch block if you don’t want your program to end abruptly when an error occurs.

To delete the current record for a recordset object, just call its Delete() member. You must then scroll the recordset to a new position before attempting to use any of the functions above because the values of the data members of the recordset object will be invalid after calling Delete().

980

Updating Data Sources

Figure 20-1 illustrates the basic sequence of events in updating an existing record or adding a new one.

Updating an Existing Record

1.Call Edit() for the recordset object:

-Saves current values from recordset field data members in a buffer.

2.Set field data members to new values.

3.Call Update() for the recordset object:

-Checks for modified fields by comparing with the saved values.

-Creates and executes an SQL INSERT to update the DB for changed fields.

-Discards the buffer containing the old saved values for the fields.

Updating a New Record

1.Call AddNew() for the recordset object:

-Saves current values from recordset field data members in a buffer.

-Sets current values for recordset field data members to PSEUDO_NULL.

2.Set new field data member values.

3.Call Update() for the recordset object:

-Checks for non-NULL fields.

-Creates and executes an SQL INSERT to update DB for non-NULL fields.

-Restores the old saved values from the buffer.

Figure 20-1

When you call AddNew() for a recordset to start adding a new record to a table, the function saves the current values of all the data members of the recordset object that correspond to field values in a buffer, and then sets the data members to PSEUDO_NULL. This is not zero or null as in a pointer. It is a value that indicates the data member has not been set. When you call Update() to complete adding a record, the original values of the data members of the recordset before AddNew() was called are restored. If you want the recordset to contain the values for the new record, you must call the Requery() member of the recordset object. This function returns TRUE (a value of the MFC type BOOL) if the operation was successful. You also call Requery() when you want to obtain a different view of the data where you will retrieve records using a different SQL command or a different filter for the records.

The transfer of data between the recordset data members and the database always uses the DoField Exchange() member of the recordset object, so the RFX_() functions provide a dual capability — writing to the database as well as reading from it.

Checking that Operations are Legal

It is a good idea to confirm that the operation you intend to carry out is legal with your recordset object. It is all too easy to end up with a read-only recordset — just forgetting to reset the read-only attribute on the Northwind.mdb file will do it! If you try to update a table that is read-only, an exception is thrown that is entirely avoidable if you just verify that the operation is possible. Using exceptions to catch errors that aren’t that unexpected is inefficient, and generally frowned upon. It’s better to check beforehand when this is possible, as it is in this case. That way, your exception handling code is truly reserved for exceptional occurrences.

The CanUpdate() member of CRecordset returns TRUE if you can modify records in the table represented by the recordset object. When you want to add a new record, you can call the CanAppend() member of CRecordset beforehand to check. This returns TRUE if adding new records to the table is permitted.

981

Chapter 20

Record Locking

Record locking prevents other users from accessing the locked record while a table row is being updated. The extent to which a record is locked during an update is determined by the locking mode set in the recordset object. There are two locking modes defined in CRecordset, referred to as optimistic mode and pessimistic mode.

CRecordset::optimistic

In optimistic locking mode, the record is only locked while

 

the Update() member function is executing. This mini-

 

mizes the time that the record in inaccessible to other users

 

of the database. If an editing operation may take a long

 

time, pessimistic locking is often not a practical solution

 

because other users may need to access the database. The

 

standard solution is to use optimistic locking and to intro-

 

duce some sort of conflict resolution mechanism.

CRecordset::pessimistic

In pessimistic locking mode, the record is locked as soon as

 

you call Edit(), and it remains locked and therefore inac-

 

cessible to other users until the completion of the call to

 

Update() or until the update operation is aborted. This can

 

obviously severely affect performance when updates are

 

being prepared interactively; however, this mode is essen-

 

tial in many instances to ensure the integrity of the data is

 

maintained.

 

 

The default mode for a recordset object is optimistic, so you have to set it only if you want pessimistic mode. To set this mode, you call the SetLockingMode() member of the recordset object with CRecordset::pessimistic as the argument. Of course, you can also reset it by calling the function with CRecordset::optimistic as the argument.

Transactions

The idea of a transaction in the database context is to enable operations to be safely undone when necessary. A transaction packages a well-defined series of one or more modifications to a database in to a single operation so that at any point prior to the completion of the transaction everything can be reversed (or rolled back) if an error occurs. Clearly, if an update were to fail when it was partially completed, due to a hardware problem, for instance, it could have a disastrous effect on the integrity of the database. A transaction is not just an update to a single table. It can involve very complex operations on a database involving a series of modifications to multiple tables and may take an appreciable time to complete. In these situations, support for transactions is virtually a necessity if the integrity of the database is to be assured.

With transaction based operations, the database system manages the processing of the transaction and records recovery information so that anything that the transaction does to the data can be undone in the event of a problem part way through. By making your database operations based on transactions, you can protect the database against errors that might occur during processing. Typically, transaction processing locks records as necessary along the way and also ensures that any other database users accessing data that has been modified by the transaction will see the changes immediately.

982

Updating Data Sources

Transactions are supported by most large commercial database systems on mainframe computers, but this is not always the case with database systems that run on a PC. In spite of this, the CDatabase class in MFC does support transactions, and as it happens, so does the Microsoft ODBC support for Access databases, so you can try out transaction processing with the Sample Data database if you want.

CDatabase Transaction Operations

Transactions are managed through members of your CDatabase class object that provides the connection to the database. To determine whether transactions are supported for any given connection, you call the CanTransact() member of the CDatabase object. This returns TRUE if transactions are supported. Incidentally, there is also a CanUpdate() member of CDatabase that returns FALSE if the data source is read-only.

There are three member functions of CDatabase involved in transaction processing:

BeginTrans()

Starts a transaction on the database. All subsequent recordset opera-

 

tions are part of the transaction, until either CommitTrans() or

 

Rollback() is called. The function returns TRUE if the transaction

 

start was successful.

CommitTrans()

Commits the transaction so all recordset operations that are part of

 

the transaction are expedited. The function returns FALSE if an error

 

occurs, in which case the state of the data source is undefined.

Rollback()

Rolls back all the recordset operations executed since BeginTrans()

 

was called, and restores the data source to the condition at the time

 

when BeginTrans() was called.

 

 

The sequence of events in a transaction is very simple:

Call BeginTrans() to start the transaction.

Call Edit(), Update(), AddNew(), for your recordset as necessary.

Call CommitTrans() to complete the transaction.

Outside of a transaction, Edit() or AddNew()operations on a recordset are executed when you call Update(). Within a transaction they are not executed until you call CommitTrans() for the CDatabase object. If you need to abort the transaction at any time after calling BeginTrans(), just call

Rollback().

Complications can arise with the effect of CommitTrans() and Rollback()(the position in the recordset you are operating on can be lost for instance, so you may need to take some action in your program to recover the record pointer after completing or aborting a transaction. There are two members of CDatabase to help with this. After a CommitTrans() call you need to call the GetCursorCommitBehavior() member of CDatabase, and after calling Rollback() you need to call GetCursorRollbackBehavior(). Both of these functions return one of three values of type int that indicate what you should do:

983

Chapter 20

SQL_CB_PRESERVE

The recordset’s connection to the data source is unaffected

 

by the commit or rollback operation, so do nothing.

SQL_CB_CLOSE

You need to call Requery() for the recordset object to

 

restore the current position in the recordset.

SQL_CB_DELETE

You must close the recordset by calling the Close() mem-

 

ber of the object and then re-open the recordset if necessary.

 

 

There are further complications with using transactions in practice because the particular drivers you are using can affect when you must open the recordset. With some drivers you must open the recordset before you call BeginTrans(). With others, and the Microsoft Access ODBC drivers are a case in point, Rollback() will not work unless you open your recordset after you call BeginTrans(). You need to understand how the particular drivers you intend to use before attempting to use transactions in your application.

A Simple Update Example

It’s time to get some hands on experience with update operations in action starting with a very basic example. This omits most of what I have discussed so far in this chapter initially, but you will be building on this to apply some of what you have learned. You can create an application to update a database table with minimal effort using the MFC Application wizard that you applied in the previous chapter. You’ll be creating a program to allow updating of certain fields in the Order Details table.

Create a project called DBSimpleUpdate using the MFC Application template. Elect to go for the Database view without file support option with ODBC as the Client type option, as you did in the previous chapter. You are still going to use the Northwind database through ODBC, but this time you should choose dynaset as the recordset type. In a multi-user environment, a dynaset is automatically updated with any changes made to a record while it is accessed by your program. This ensures the data you have in your application is always up to date. For operations to modify an existing record or add new ones, you should choose dynaset as the recordset type.

Because you plan to update the database, you must map the recordset to a single database table. The database classes in MFC do not support updating of recordsets that involve joining two or more tables. Choose the Order Details table for the default recordset, as shown in Figure 20-2.

If you select multiple tables here, updating the recordset is inhibited because the recordset is automatically made read-only. The database classes support only read-only access to joins of multiple tables, not updating.

You can change the view and recordset class and associated file names to match the table you are dealing with, as illustrated by the window shown in Figure 20-3.

Now all you need to do is click the Finish button and then customize the dialog resource to do what you want.

984

Updating Data Sources

Figure 20-2

Figure 20-3

985

Chapter 20

Customizing the Application

The Order Details table contains five columns — Order ID, Product ID, Unit Price, Quantity, and Discount. If you display Class View and look at the members of COrderDetailsSet, you will see the data members corresponding to these. You need a static text control and an edit control for each of these on the dialog corresponding to the recordset. I arranged them as shown in Figure 20-4, but you can arrange them how you like.

Figure 20-4

Assign IDs to the edit controls to match the field name as you did in the previous chapter — the last one is IDC_DISCOUNT, for example. The default style set for an edit control allows keyboard input, but on the assumption you want to limit which recordset fields can be altered, you should set the first three edit controls as read-only, using the styles tab in the Properties window. The value displayed in a read-only control can be set in the program, but a value cannot be entered in the control from the keyboard. You can set all these to read-only in a single step by selecting each of the three controls with the Ctrl key held down and then right-clicking to display the pop-up menu and selecting Properties. Whatever you then set in the Properties window is applied to all three. With the dialog arrangement shown, you will only be able to enter data for Quantity and Discount.

The only other thing you need to do is to associate the edit controls with a corresponding data member of the recordset, and as you saw in the previous chapter, you just add a DDX_ function call to the DoDataExchange() function in the recordset view class, COrderDetailsView, for each data field in the recordset. Here’s how that code looks:

void COrderDetailsView::DoDataExchange(CDataExchange* pDX)

{

CRecordView::DoDataExchange(pDX);

DDX_FieldText(pDX, IDC_ORDERID, m_pSet->m_OrderID, m_pSet); DDX_FieldText(pDX, IDC_PRODUCTID, m_pSet->m_ProductID, m_pSet); DDX_FieldText(pDX, IDC_UNITPRICE, m_pSet->m_UnitPrice, m_pSet); DDX_FieldText(pDX, IDC_QUANTITY, m_pSet->m_Quantity, m_pSet); DDX_FieldText(pDX, IDC_DISCOUNT, m_pSet->m_Discount, m_pSet);

}

986

Updating Data Sources

Having done that, you will have completed the program to update the Order Details table, believe it or not.

Try It Out

Updating a Database

This should compile right off the bat if you have set up the controls correctly and remembered to comment out the #error directive that appears before the definition of the GetDefaultConnect() function in the COrderDetailsSet class. This directive is there to ensure you consider security implications when connecting to the database. When the program executes, you will be able to move through the rows in the table using the toolbar buttons. If you enter data into the edit controls for the Quantity or Discount for an order, it is updated when you move backwards or forwards in the recordset. The application window is shown in Figure 20-5.

Figure 20-5

You can see here I have changed the quantity and discount values for the product with the ID 72 on the order with the ID 10248 to some unlikely values.

How It Works

When you click one of the toolbar buttons to move to another record, the OnMove() handler provided by the default base class, CRecordView, is called. This function writes out any changes that have been entered into the recordset before it moves to a new record in the recordset by calling the Move() member of the CRecordset class that is inherited in COrderDetailsSet. Remember there are two levels of data exchange going on here. The RFX_() functions called in the DoFieldExchange() member of the COrderDetailsSet class transfer data between a row in the recordset from the database

and the data members of the class. The DDX_() functions called in the DoDataExchange() member of COrderDetailsView, transfer data between the edit controls and the data members of

987

Chapter 20

COrderDetailsSet. When you change the value in an edit control, the new data is propagated through to the appropriate data member of the recordset object. When you move to the next recordset by clicking a toolbar button, the new data is written to the database by the DoFieldExchange() function.

This example is fine as far as it goes, but having data written to the database without any evident action on the part of the user is a bit disconcerting. You really should have a bit more control over what’s going on. You can put together an example where the code requires the user to do something before expediting an update operation.

Managing the Update Process

You really want a positive action on the part of the user to enable an update rather than allowing it to happen by default. You could start making all the edit controls read-only, so by default data entry from the keyboard is inhibited for all the controls. You could then add an Edit Order button to the dialog box, which is intended to enable the appropriate edit controls to allow keyboard entry. This is shown in Figure 20-6.

Figure 20-6

Here you will be implementing two notional modes in the program: read-only mode’ when updating is not possible, because the controls will be read-only, and edit mode’ when keyboard entry for selected controls are possible so the recordset can be updated. The idea is that when the user clicks the Edit Order button, the edit controls for fields you want to allow updating on are enabled for keyboard input, and you will enter your edit mode’. Add the button to the dialog box for your DBSimpleUpdate application. You can set the ID for the button as IDC_EDITORDER. You can also add a handler for the button to the COrderDetailsView class by right-clicking the button and selecting Add Event Handler from the pop-up. Shorten the name of the handler function to OnEditorder().

Ideally, you should inhibit the use of the toolbar buttons or the Record menu items to move to another row in the table in update mode because you want a button click by the user to end the update operation, not moving the current position of the recordset.

988