This chapter is about building database applications. The truth is, I cannot tell you everything there is to know about writing database applications in one short chapter. What I can do is point out some issues you are likely to encounter when writing database applications.
Because you already know how to create database forms, you will spend most of this day dealing with lower level aspects of database programming. You'll start with creating and populating a database entirely through code, and then you'll move on to data modules. Toward the end of the day you'll look at creating database reports with the QuickReport components. Finally, the day ends with a discussion on deploying database applications.
Up to this point, you have examined almost exclusively the visual aspects of database programming with Delphi. That side of database programming is fun and easy, but sooner or later you have to do some real database programming. The kinds of programs you have written so far have dealt primarily with simple viewing and editing of data. In this section, you learn how to perform certain database operations through code.
TIP: I will mention only database operations that pertain to the TTable class. You could also use SQL to perform database operations in code, but I won't present that aspect of databases today.
This section is short because reading from an existing database isn't difficult. As an example exercise, you will take the CUSTOMER.DB table and create a comma-delimited text file from the data. You won't write every field in the table to the file, but you'll write most of them.
The first step is to create a TTable object. Next, attach it to a database and to a particular table within the database. Here's how the code looks:
var Table : TTable; begin Table := TTable.Create(Self); Table.DatabaseName := `DBDEMOS'; Table.TableName := `Customer.db'; end;
This code emulates what Delphi does when you set the DatabaseName and TableName properties at design time. The next step is to read each record of the database and write it to a text file. First, I'll show you the basic structure without the actual code to write the fields to the text file. After that, I'll be more specific. The basic structure looks like this:
Table.Active := True; while not Table.Eof do begin { Code here to read some fields from the } { current record and write them to a text file. } Table.Next; end; Table.Free;
This code is straightforward. First, the table is opened by setting the Active property to True (you could also have called the Open method). Next, a while loop reads each record of the table. When a record is written to the text file, the Next method is called to advance the database cursor to the next record. The while loop's condition statement checks the Eof property for the end of the table's data and stops the loop when the end of the table is reached. Finally, the TTable object is freed after the records have been written to the text file.
NOTE: You don't specifically have to delete the TTable object. When you create a TTable object in code, you usually pass the form's Self pointer as the owner of the object--for example,
Table := TTable.Create(Self);
This sets the form as the table's owner. When the form is deleted, VCL will automatically delete the TTable object. For a main form this happens when the application is closed. Although you are not required to delete the TTable object, it's always a good idea to delete the object when you are done with it.
Naturally, you need to extract the information out of each field in order to write it to the text file. To do that, you use the FieldByName method and the AsString property of TField. I addressed this briefly on Day 16, "Delphi Database Architecture," in the section "Accessing Fields." In the CUSTOMER.DB table, the first field you want is the CustNo field. Extracting the value of this field would look like this:
var S : string; begin S := Table.FieldByName(`CustNo').AsString + `,';
Notice that a comma is appended to the end of the string obtained so that this field's data is separated from the next. You will repeat this code for any fields for which you want to obtain data. The entire sequence is shown in Listings 18.1 and 18.2. These listings contain a program called MakeText, which can be found with the book's code. This short program takes the CUSTOMER.DB table and creates a comma-delimited text file called CUSOMTER.TXT. Listing 18.1 shows the main form's unit (MakeTxtU.pas). The form contains just a button and a memo. Look over these listings, and then I'll discuss how the program works.
unit MakeTxtU; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, ÂDialogs, StdCtrls, DbTables; type TForm1 = class(TForm) CreateBtn: TButton; Memo: TMemo; procedure CreateBtnClick(Sender: TObject); private { Private declarations } public { Public declarations }
end;
var Form1: TForm1; implementation {$R *.DFM} procedure TForm1.CreateBtnClick(Sender: TObject); var Table : TTable; S : string; begin { Create the Table and assign a database and Table name. } Table := TTable.Create(Self); Table.DatabaseName := `DBDEMOS'; Table.TableName := `Customer.db'; { Change to the busy cursor. } Screen.Cursor := crHourGlass; { We can use a Memo to show the progress as well as to } { save the file to disk. First clear the memo of any text.} Memo.Lines.Clear; { Open the Table. } Table.Active := True; CreateBtn.Enabled := False; { Loop through the records, writing each one to the memo. } while not Table.Eof do begin { Get the first field and add it to the string S, } { followed by the delimiter. } S := Table.FieldByName(`CustNo').AsString + `,'; { Repeat for all the fields we want. } S := S + Table.FieldByName(`Company').AsString + `,'; S := S + Table.FieldByName(`Addr1').AsString + `,'; S := S + Table.FieldByName(`Addr2').AsString + `,'; S := S + Table.FieldByName(`City').AsString + `,'; S := S + Table.FieldByName(`State').AsString + `,'; S := S + Table.FieldByName(`Zip').AsString + `,'; S := S + Table.FieldByName(`Phone').AsString + `,'; S := S + Table.FieldByName(`FAX').AsString; { Add the string to the Memo. } Memo.Lines.Add(S); { Move to the next record. } Table.Next; end; { Write the file to disk. } Memo.Lines.SaveToFile(`customer.txt'); { Turn the button back on and reset the cursor. } CreateBtn.Enabled := True; Screen.Cursor := crDefault; Table.Free; end; end.
All the action takes place in the CreateBtnClick method. When you click the Create File button, the program extracts data from the database table and puts it into the Memo component. First, the value of the CustNo field is read and put into a string, followed by a comma. After that, each subsequent field's value is appended to the end of the string, again followed by a comma. After all the data has been extracted from a record, the string is added to the memo, using the Add method. When the end of the table is reached, the memo contents are saved to disk.
I used a Memo component in this example for two reasons. First, by displaying the results in a memo, you can see what the program produces. Second, the memo component's Lines property (a TStrings) provides an easy way of saving a text file to disk. Figure 18.1 shows the MakeText program after the file has been written.
FIGURE 18.1. The MakeText program running.
Most of what you read on database operations in Delphi tells you how to create a database with utilities such as Database Desktop. That's fine if your application has pre-built tables. But if your application has to dynamically create tables, that approach doesn't work. You must have a way of creating tables through code. For example, if your application enables users to specify the fields of a table, you won't know what the fields are until the user has supplied them.
Creating a table in code requires these steps:
5. Create the actual table with the CreateTable method.
Let's go over these steps individually so that you know what each step involves.
You already performed steps 1 and 2 on Day 16 when I talked about creating a BDE alias, and you created a TTable object in the preceding section. Here's a recap of those two steps:
var Table : TTable; begin { Create the alias. } CreateDirectory(`c:', nil); Session.AddStandardAlias(`MyDatabase', `c:', `PARADOX'); Session.SaveConfigFile; { Create the table. } Table := TTable.Create(Self); Table.DatabaseName := `MyDatabase'; Table.TableName := `MyTable.db'; end;
This code first creates a BDE alias for a database called MyDatabase in the C: directory. After that, a TTable object is created, and the DatabaseName property is set to the alias created in the first part of the code. Finally, the TableName property is set to the name of the new table you are going to create. At this point the TTable object has been created, but the table itself doesn't exist on disk.
The next step is to create the field definitions for the table's fields. As you might guess, the field definitions describe each field. A field definition contains the field name, its type, its size (if applicable), and whether the field is a required field. The field's type can be one of the TFieldType values. Table 18.1 lists the TFieldType values.
Field type | Description |
ftUnkown | Unknown or undetermined |
ftString | Character or string field |
ftSmallint | 16-bit integer field |
ftInteger | 32-bit integer field |
ftWord | 16-bit unsigned integer field |
ftBoolean | Boolean field |
ftFloat | Floating-point numeric field |
ftCurrency | Money field |
ftBCD | Binary-Coded Decimal field |
ftDate | Date field |
ftTime | Time field |
ftDateTime | Date and time field |
ftBytes | Fixed number of bytes (binary storage) |
ftVarBytes | Variable number of bytes (binary storage) |
ftAutoInc | Auto-incrementing 32-bit integer counter field |
ftBlob | Binary large object field |
ftMemo | Text memo field |
ftGraphic | Bitmap field |
ftFmtMemo | Formatted text memo field |
ftParadoxOle | Paradox OLE field |
ftDBaseOle | dBASE OLE field |
ftTypedBinary Typed binary field
You create a field definition by using the Add method of the TFieldDefs class. The FieldDefs property of TTable is a TFieldDefs object. Putting all this together, then, adding a new string field to a database would look like this:
Table.FieldDefs.Add(`Customer', ftString, 30, False);
This code line adds a string field called Customer with a size of 30 characters. The field is not a required field because the last parameter of the Add method is False. Add as many field definitions as you need, setting the appropriate data type and size for each field.
If your table will be indexed, you also need to add one or more index definitions. Adding index definitions is much the same as adding field definitions. The IndexDefs property of TTable contains the index definitions. To add a new index definition, call the Add method of TIndexDefs:
Table.IndexDefs.Add(`', `CustNo', [ixPrimary]);
The first parameter of the Add method is used to specify the index name. If you are creating a primary index (as in this example), you don't need to specify an index name. The second field is used to specify the field or fields on which to index. If you have more than one field in the index, you separate each field with a semicolon--for example,
Table.IndexDefs.Add(`', `Room;Time', [ixPrimary]);
The last parameter of the Add method is used to specify the index type. This parameter takes a TIndexOptions set. Different index options can be specified in combination. For example, an index might be a primary index that is sorted in descending order. In that case, the call to Add might look like this:
Table.IndexDefs.Add(`', `CustNo', [ixPrimary, ixDescending]);
After you add all the field and index definitions to the table, you create the table. So far you have been setting up the table's layout but haven't actually created the table. Creating the table is the easiest step of all:
Table.CreateTable;
The CreateTable method performs the actual creation of the table on disk. CreateTable takes the contents of the FieldDefs and IndexDefs properties and creates the table structure, based on those contents.
Now that the table is created, you can fill it with data by using any method required for your application. You can fill the table programmatically or allow your users to add or edit the data from a form.
The book's code contains a program called MakeTabl. This program first creates a table and then fills it with data. The program fills the table with data from the CUSTOMER.TXT file created with the MakeText program earlier in the chapter. Listing 18.2 contains the unit for the main form (MakeTblU.pas). Note that the uses list for this program includes the DBGrids, DBTables, and DB units.
unit MakeTblU; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, Grids, DBGrids, DbTables, Db; type TForm2 = class(TForm) DBGrid: TDBGrid;
CreateBtn: TButton;
FillBtn: TButton; procedure CreateBtnClick(Sender: TObject); procedure FillBtnClick(Sender: TObject); private { Private declarations } public { Public declarations } end; var Form2: TForm2; implementation {$R *.DFM} procedure TForm2.CreateBtnClick(Sender: TObject); const Dir : PChar = `c:'; var Table : TTable; begin { Create a BDE alias, but only if it doesn't already exist. } if not Session.IsAlias(`MyDatabase') then begin CreateDirectory(Dir, nil); try Session.AddStandardAlias(`MyDatabase', Dir, `PARADOX'); Session.SaveConfigFile; except MessageDlg(`Error Creating Alias', mtError, [mbOk], 0); Exit; end; end; { Now create the Table. } Screen.Cursor := crHourGlass; Table := TTable.Create(Self); try Table.DatabaseName := `MyDatabase'; Table.TableName := `MyTable.db'; { Add the field definitions for each field. } Table.FieldDefs.Add(`CustNo', ftFloat, 0, True); Table.FieldDefs.Add(`Customer', ftString, 30, False); Table.FieldDefs.Add(`Addr1', ftString, 30, False); Table.FieldDefs.Add(`Addr2', ftString, 30, False);
Table.FieldDefs.Add(`City', ftString, 15, False);
Table.FieldDefs.Add(`State', ftString, 20, False); Table.FieldDefs.Add(`Zip', ftString, 10, False); Table.FieldDefs.Add(`Phone', ftString, 15, False); Table.FieldDefs.Add(`Fax', ftString, 15, False); { Add an index definition for the primary key. } Table.IndexDefs.Add(`', `CustNo', [ixPrimary]); { Everything is set up, so create the Table. } Table.CreateTable; except MessageDlg(`Error Creating Table', mtError, [mbOk], 0); Screen.Cursor := crDefault; Table.Free; Exit; end; { All done, so let the user know. } Table.Free; Screen.Cursor := crDefault; CreateBtn.Enabled := False; FillBtn.Enabled := True; MessageDlg( `Table Created Successfully', mtInformation, [mbOk], 0); end; procedure TForm2.FillBtnClick(Sender: TObject); var Table : TTable; Datasource : TDataSource; Lines : TStringList; S, S2 : string; I, P : Integer; begin { Create a TTable. } Table := TTable.Create(Self); Table.DatabaseName := `MyDatabase'; Table.TableName := `MyTable.db'; { Create a data source and hook it up to the Table. } { Then hook the DBGrid to the datasource. } Datasource := TDataSource.Create(Self); Datasource.DataSet := Table; DBGrid.Datasource := Datasource; { Open the Table and the text file. } Table.Active := True; Lines := TStringList.Create; Lines.LoadFromFile(`customer.txt'); { Put the Table in edit mode. } Table.Edit; { Process the Lines. } for I := 0 to Pred(Lines.Count) do begin { Append a record to the end of the file. } Table.Append; { Parse the string and get the first value. } S := Lines[I]; P := Pos(`,', S); S2 := Copy(S, 1, P - 1); Delete(S, 1, P); { Write the value to the CustNo field. } Table.FieldByName(`CustNo').Value := StrToInt(S2); { Continue for each of the fields. } P := Pos(`,', S); S2 := Copy(S, 1, P - 1); Delete(S, 1, P); Table.FieldByName(`Customer').Value := S2; P := Pos(`,', S); S2 := Copy(S, 1, P - 1); Delete(S, 1, P); Table.FieldByName(`Addr1').Value := S2; P := Pos(`,', S); S2 := Copy(S, 1, P - 1); Delete(S, 1, P); Table.FieldByName(`Addr2').Value := S2; P := Pos(`,', S); S2 := Copy(S, 1, P - 1); Delete(S, 1, P); Table.FieldByName(`City').Value := S2; P := Pos(`,', S); S2 := Copy(S, 1, P - 1); Delete(S, 1, P); Table.FieldByName(`State').Value := S2; P := Pos(`,', S); S2 := Copy(S, 1, P - 1); Delete(S, 1, P); Table.FieldByName(`Zip').Value := S2; P := Pos(`,', S); S2 := Copy(S, 1, P - 1); Delete(S, 1, P); Table.FieldByName(`Phone').Value := S2; P := Pos(`,', S); S2 := Copy(S, 1, P - 1); Delete(S, 1, P); Table.FieldByName(`FAX').Value := S2; { We might get a key violation exception if we try to add a } { record with a duplicate customer number. If that happens, } { we need to inform the user, cancel the edits, put the } { put the Table back in edit mode, and continue processing. } try Table.Post; except on EDBEngineError do begin MessageBox(Handle, `Duplicate Customer Number', `Key Violation', 0); Table.Cancel; Table.Edit; Continue; end; end; end; { All done with the TStringList so it. } Lines.Free; { We won't the Table so that the Table's data shows } { in the DBGrid. VCL will the Table and Datasource } { for us. } {Table.Free; } {Datasource.Free; } end;
end.
As you know by now, setting up database components is easy in Delphi. Still, there is some time involved, even for the simple examples you have been writing.
You have to place a Table or Query component on the form and choose the database name. Then you have to set the table name (for a Table) or SQL property (for a Query). You might have to set other properties, depending on how you are using the database. You might also have several events to handle. Next, you have to place a DataSource component on the form and attach it to the table or query. If your application makes use of a Database component, you have to set that component's properties and events as well. None of this is hard, but it would be nice if you could do it all just once for all your programs. Data modules enable you to do exactly that.
At the base level, data modules are really just specialized forms. To create a data module, open the Object Repository and double-click the Data Module icon. Delphi creates the data module and a corresponding source unit just as it does when you create a new form. You can set the Name property of the data module and save it to disk, again, just like a form.
After you create the data module, you place data access components on it. Then you set all the properties for the data access components as needed. You can even create event handlers for any components on the data module. When you have set everything just the way you want it, save the data module. Every form in your application can then access the data module.
A simple exercise will help you understand data modules better. First, you'll set up the data module, and then you'll put it to work:
FIGURE 18.2. The finished data module.
AnimalsTable.Open; BiolifeTable.Open;
Now let's put the new data module to use. You are going to create two buttons for the application's main form. One button will display a form that shows the Animals table, and the other will display the Biolife table. Here goes:
DBDemos.Animals DBDemos.Biolife
These steps demonstrate that after you create a data module, you can use the components on that data module from anywhere in your program. All you have to do is use the unit for the data module, and then all data-aware components will be capable of detecting the data module. Let's finish this application so that you can try it out:
AnimalsForm.Show;
BiolifeForm.Show;
Now you can run the program. When you click a button, the appropriate form will appear. Figure 18.4 shows the program running.
FIGURE 18.3. The second form completed.
FIGURE 18.4. The program running with both forms displayed.
Data modules make it easy to set up your database components once and then reuse those components over and over. After you create a data module, you can save it to the Object Repository, where it is always available for your use.
A database program is not complete without some way of viewing and printing data, and that is where reports enter the picture. Up to this point, you have been looking at ways to view individual records or multiple records with the DBGrid component. These methods might be perfect for some applications, but sooner or later you will need more control over the viewing of records.
Besides viewing the data onscreen, you will almost certainly need to print the data. A database application that can't print is not very useful. Delphi's QuickReport components enable you to view and print your data with ease.
Before you can create a report, you need an overview of how the QuickReport components work.
The base QuickReport component is the QuickRep component. This component acts as a canvas on which you place the elements that your report will display (I'll discuss the report elements soon).
The QuickRep component has properties that affect the way the report will appear when printed. For example, the Page property is a class containing properties called TopMargin, BottomMargin, LeftMargin, RightMargin, Columns, Orientation, PaperSize, and so on.
The PrinterSettings property is also a class. This property has its own properties, called Copies, Duplex, FirstPage, LastPage, and OutputBin. The ReportTitle property is used to display the print job description in the Windows Print Manager and in the title bar of the QuickReport preview window. The Units property controls whether margins are displayed in millimeters, inches, picas, or other choices. The DataSet property is used to set the dataset from which the report's data will be obtained.
NOTE: The DataSet property must be set and the associated dataset must be active before anything will show up in the report.
Primary QuickRep methods include Preview and Print. The Print method, as its name implies, prints the report. The Preview method displays a modal preview window. The preview window includes buttons for viewing options, first page, last page, previous page, next page, print, print setup, save report, open report, and close preview. Figure 18.5 shows the QuickReport preview window at runtime.
FIGURE 18.5. The QuickReport preview window.
QuickRep events of note include OnPreview and OnNeedData. You can use the OnPreview event, instead of the default preview window, to provide a custom preview window. When using a data source other than a VCL database, you use the OnNeedData event. For example, you can create a report from a string list, an array, or a text file.
A QuickReport is composed of bands. Bands come in many different types. A basic report has at least three types: a title band, a column header band, and a detail band. The title band contains the report title, which is displayed only on the first page of the report. The column header band is used to display the column headers for the fields in the dataset; the column header appears at the top of every page. Some reports, such as a report used to generate mailing labels, do not have a column headers band.
The detail band is the band that does all the work. On the detail band you place any data that you want in the report. You define the contents of the detail band, and QuickReport repeats the detail band for every record in the dataset. In a minute you'll do an exercise that illustrates how the different bands work.
Other commonly used band types include page header, page footer, group header, group footer, and summary bands. The QRBand component defines a QuickReport band. The BandType property is used to specify the band type (title, detail, header, footer, and so on).
The bands automatically arrange themselves on the QuickRep component, based on the band's type. For example, if you place a QRBand on the report and change its BandType to rbPageFooter, the band will be moved below all other bands. Likewise, a page header band will be placed above all other bands.
QuickReport design elements come in three forms. The first form includes components for text labels, images, shapes, headers, footers, and so on. These components are primarily used to display static design elements. For example, the report title is usually set once and then doesn't change. Another example is a graphic used to display a company's logo on the report. The components in this group are very similar to the standard VCL components. The QRLabel component resembles a standard Label component, a QRImage is similar to the VCL Image component, the QRShape component is similar to the regular Shape component, and so on. Use these components to design the static portions of your reports.
The second category of elements contains QuickReport versions of the standard VCL data-aware components. These components are placed on the detail band of a report. Components in this group include the QRDBText, QRDBRichEdit, and QRDBImage components. Data is pulled from the dataset and placed into these components to fill the body of the report.
The third group of QuickReport components includes the QRSysData component and the QRExpr component. The QRSysData component is used to display page numbers, the report date, the report time, the report title, and so on. The QRExpr component is used to display calculated results. The Expression property defines the expression for the calculation. The Expression property has a property editor called the Expression builder that is used to define simple expressions. An expression might be simple, such as multiplying two fields, or complete with formulas such as AVERAGE, COUNT, or SUM.
Certainly the best way to write truly custom reports is by hand. That might sound difficult, but fortunately the QuickReport components make it easy. The best way for me to explain how to create reports by hand is to take you through an exercise. This exercise creates a simple application that displays and prints a report in list form. I won't tell you to perform every single step in this exercise. For example, I won't tell you to save the project or give you filenames to use--you can figure those out for yourself. Also, you don't have to worry about making the report real pretty at this point. You can go back later and tidy up.
The first step is to create the main form of the application. After that's done, you create the basic outline of the report. Here goes:
QuickReport2.Preview;
QuickReport2.Print;
Now you have a blank report. What you need to do next is add a title band, a column header band, and a detail band. For the next few steps you might want to look ahead to Figure 18.6 to see the final result. Follow these steps:
8. Add a final QRDBText component on the detail band. Place it below the Salary component on the column header band and attach it to the Salary field in the table. Your form now looks like Figure 18.6.
FIGURE 18.6. Your QuickReport form.
You are probably wondering what the report will look like on paper. Guess what? You don't have to wait to find out. Just right-click on the QuickReport form and choose Preview from the context menu. The QuickReport preview window is displayed, and you can preview the report.
To print the report, click the Print button. When you are done with the report preview, click the Close button. You can now run the program and try out the Preview Report and Print Report buttons.
NOTE: The report you just created is double-spaced. To change the spacing, reduce the height of the detail band.
Before leaving this discussion of reports, let me show you one other nice feature of QuickReport. Right-click on the QuickReport form and choose Report settings from the context menu. The Report Settings dialog will be displayed, as shown in Figure 18.7. This dialog enables you to set the primary properties of the QuickRep component visually rather than use the Object Inspector.
FIGURE 18.7. The QuickReport Report Settings dialog.
Delphi comes with three built-in QuickReport forms, which can be found on the Forms tab of the Object Repository. The forms are called Quick Report Labels, Quick Report List, and Quick Report Master/Detail. These forms give you a head start on creating reports. You can generate one of the forms from the Object Repository and then modify the form as needed.
As I said on Day 16, the Borland Database Engine (BDE) is a collection of DLLs and drivers that enable your Delphi application to talk to various types of databases. When you ship an application that uses the BDE, you need to make sure that you ship the proper BDE files and that they are properly registered on your users' machines.
The most sensible way to do this is with a Borland-approved installation program. Here at TurboPower Software, we use the Wise Install System from Great Lakes Business Solutions (http://www.glbs.com). Others include InstallShield and its little brother, InstallShield Express. Conveniently, InstallShield Express comes with Delphi Professional and Client/Server, so if you have one of those versions of Delphi, you don't have to rush right out and buy an installation program.
You might be wondering why Borland is involved in dictating how the BDE must be deployed. The reason is simple when you think about it: There are many BDE versions in use. Some folks might be writing and deploying applications that use the BDE from Delphi 1. Others might be using the BDE from C++Builder 1. Still others could be using the BDE that comes with Delphi 4 in their applications.
The important point to realize is that the BDE is backward compatible. Newer BDE versions are guaranteed to work with applications written for older BDE versions. This system will only work, however, as long as everyone plays by the rules. Part of the rules say, "Thou shalt not arbitrarily overwrite existing BDE files." Certified installation programs check the version number of any BDE files they find. If the file being installed is older than the existing file, the installation program leaves the existing file in place.
This ensures that the user will always have the latest BDE files on his or her system. Another service that these certified installation programs provide is to determine exactly which files you need to deploy for your application. You should read DEPLOY.TXT in the Delphi root directory for more details on deploying applications that use the BDE.
There's no question that creating database applications requires a lot of work. The good news is that Delphi makes the job much easier than other development environments. Today you found out something about the nonvisual aspects of database programming. You also found out about data modules and how to use them. You ended the day with a look at QuickReport. QuickReport makes creating reports for your database applications easy. I also explained what is required to deploy a database application.
The Workshop contains quiz questions to help you solidify your understanding of the material covered and exercises to provide you with experience in using what you have learned. You can find answers to the quiz questions in Appendix A, "Answers to the Quiz Questions."
© Copyright, Macmillan Computer Publishing. All rights reserved.