Docstoc

Client-Server Developer's Guide with Delphi 3

Document Sample
Client-Server Developer's Guide with Delphi 3 Powered By Docstoc
					To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Table of Contents | Next Page 5

Overview
Introduction xxvii I Getting Started 1 Is Delphi the Silver Bullet? 3 2 Quick Start 13 3 Building Blocks 47 4 Conventions 61 5 A No-Nonsense Approach to SQL 89 6 Client/Server Database Design 111 7 Client/Server Application Design 177 II Tutorial 8 Your First Real Client/Server Database Application 201 9 First Steps 235 10 First Forms 263

11 Forms, Forms, and More Forms 279 12 Reports 333 13 Finishing Touches 353 14 RENTMAN Postpartum 379 III Reference 15 Delphi on Microsoft SQL Server 395 16 Delphi on Oracle 423 17 Delphi on InterBase 447 18 Delphi on Sybase SQL Server 483 IV Advanced Topics 19 Business Reports 515 20 Business Rules on the Database Server 539 21 Business Rules in Delphi Applications 555 22 Beyond the Two-tiered Model 567 Page 6 23 Concurrency Control 575 24 Advanced SQL 593 25 Delphi Client/Server Performance Tuning 633 26 The Borland Database Engine 659 27 Building Your Own Database Components 687

28 Delphi Internet Applications 733 29 Deploying Your Applications 805 App. A A Roadmap for the Journey to Delphi 837 Index 905 Page 7

Contents
Introduction xxvii I Getting Started

1 Is Delphi the Silver Bullet? 3 Why Delphi? 4 No Limits 6 Scalability 8 What's Ahead 8 Summary 10 2 Quick Start 13 A Few Words First 14 An Overview of the Architecture 17 A Simple Form 19 The Database Form Wizard 20

The New Form Analyzed 25 A Simple Form the Hard Way 27 Building Database Applications 33 The TDatabase Component 34 BDE Aliases 35 Data Modules 37 Creating a Master/Detail Form 41 Summary 46 What's Ahead 46 3 Building Blocks 47 What Are Projects? 48 Delphi Projects 48 The program Keyword 50 The Uses Statement 50 The $R Directive 50 Application.CreateForm 50 Application.Run 51 Delphi Libraries 51 Delphi Units 52 The interface Section 53 The implementation Section 53

The form instance Variable 54 The initialization Section 54 The finalization Section 54 Page 8 Delphi Forms 54 Data Modules 56 Include Files 56 Delphi Components 57 Summary 59 What's Ahead 60 4 Conventions 61 Object Pascal Program Elements 62 Directories 62 Project Names 63 Filenames 63 Unit Names 64 Component Names 64 Type Names 70 Constant Names 71 Variable Names 71

Procedure Names and Function Names 71 Object Pascal Coding Styles 71 If...else Constructs 72 begin...end Pairs 72 Comments 73 Database Server Constructs 73 Servers 74 Databases 76 Data Files 77 Tables 78 Indexes 78 Views 79 Stored Procedures and Functions 79 Packages 80 Rules 80 Defaults 81 Domains 81 Generators and Sequences 81 Cursors 82 Triggers 82

Columns 82 Summary 83 What's Ahead 88 Page 9 5 A No-Nonsense Approach to SQL 89 Quick Start 90 Choosing an SQL Editor 91 SQL Terminators 91 Creating a Database 92 Creating Tables 93 Inserting Data 94 The SELECT Command 95 The Finish Line 108 A Few Additional Comments 108 The CONNECT Command 108 The UPDATE Command 108 The DELETE Command 109 COMMIT and ROLLBACK 109 Summary 110 What's Ahead 110 6 Client/Server Database Design 111

General Approach 112 The Five Processes 112 More on the Five Phases 113 On the Complexities of Client/Server Development 115 The Diverse Nature of Client/Server Applications 116 Database Theory Applied 116 Defining the Purpose of the Application 117 Defining the Functions of the Application 117 Designing the Database Foundation and Application Processes 118 CASE Tools 119 Modeling Business Processes 123 Entity Relationship Modeling 135 Relational Data Modeling 155 Summary 175 What's Ahead 176 7 Client/Server Application Design 177 Designing the Database Foundation and Application Processes 179 A Word About Software Development 179

Select an Application Type 180 Chart the Application's Operational Processes 181 Generate the Application 182 Design Form and Report Hierarchies 182 Page 10 Identify and Acquire Third-Party Support Code 183 Schedule the Development Process 183 Time Lines 183 Build the Application 183 Form Design 184 Report Design 192 Test the Application for Compliance with the Predefined Purpose and Functions 196 Installing the Application for Production Use 197 Summary 198 What's Ahead 198 II Tutorial 8 Your First Real Client/Server Database Application 201 Defining the Purpose of the Application 203 Defining the Functions of the Application 204 Designing the Database Foundation and Application Processes 204

Modeling Relevant Business Processes 205 Modeling Supporting Entity Relationships 210 Constructing Your Logical Data Model 219 Creating Your Database 231 The Finish Line 232 RENTMAN's Other Functions 232 Summary 233 What's Ahead 233 9 First Steps 235 Construct RENTMAN's Database Foundation 236 Determine the Type of Application You're Building 236 Derive Application Objects from Application Processes 237 Design a Form Hierarchy 239 Classes of Delphi Forms 240 Begin Building the Application 241 Create a BDE Alias 241 Start a New Project 243 Construct a Data Module 243 Create the Form Hierarchy 247 The Main Form 256

Summary 262 What's Ahead 262 Page 11 10 First Forms 263 Silverrun-RDM and Delphi Mode 264 Importing Data Dictionary Information from Your Database 264 Customize the Data Module 268 Customizing Your Tables 268 The EMPLOYEE Quick Entry/Edit Form 269 Creating the Form Using the Database Form Wizard 269 Testing the New Form 272 Removing the New Form from the Project 273 Building the New Form Using Delphi's Visual Designer 273 Linking with the Data Module Form 275 Testing the New Form 275 The Work Type Quick Entry/Edit Form 276 Summary 277 What's Ahead 277 11 Forms, Forms, and More Forms 279

The TENANT Form 280 Removing Inherited Components 280 Configuring the DBCtrlGrid 281 Aligning Components 284 Setting the Tab Order 284 RENTMAN and Auto-Incrementing Fields 285 Linking the Form into the Application 289 Testing the TENANT Form 290 The PROPERTY Form 291 Configuring the DBCtrlGrid 291 Linking the Components with the PROPERTY Table 293 Live Data at Design Time 294 Resetting the Tab Order 294 Label Accelerators 295 Testing the PROPERTY Form 295 The LEASE Form 296 Configuring the DBCtrlGrid 296 Linking the Components with the LEASE Table 297 Testing the LEASE Form 299

The WORDER Form 301 The Grid Form 301 Overriding DBNavigator 307 The WORDER Master/Detail Form 308 Page 12 The CALL Form 324 The Grid Form 325 The Edit Form 327 Summary 332 What's Ahead 332 12 Reports 333 Methods of Building Delphi Reports 334 Types of Reports 334 The Work Order Form Report 335 Setting Up the Work Order Report Menu Item 336 The Print Button 336 The Object Browser 338 A Shorter Path? 339 Printing Multiple Rows 340 Print Forms 340 Alternative Methods of Creating Form Reports 341

Testing the Form Report 341 Property-List Columnar Report 342 Linking the Report into the Application 344 The Task-List Report 345 Inside TaskList1Click 348 BeginReport 349 PrintHeader 350 More on TaskList1Click 351 Previewing the Report 352 Summary 352 What's Ahead 352 13 Finishing Touches 353 Adding an Application Bitmap 354 Specifying the Application's Title and Icon 356 Adding Windows Help 357 Help-File Creation Utilities 357 Help File Basics 358 Testing Your Help File 366 Linking the Help File with Your Application 368 Adding Context-Sensitive Help 369

Adding Fly-Over Hints 370 Activating the Status Bar 371 Adding an About Box 373 Adding a Form-Print Button 375 Page 13 Adding Report-Confirmation Dialogs 376 Summary 377 What's Ahead 377 14 RENTMAN Postpartum 379 Testing the App 380 Check the App's Database Constraints 381 Check Server-Side Objects 382 Conduct Multi-User Testing 382 Verify User Access Rights 388 Deploying the App 390 Summary 391 What's Ahead 391 III Reference 15 Delphi on Microsoft SQL Server 395 Starting the Server 396 Getting Connected 396

Setting Up Client Connections 396 Troubleshooting Microsoft SQL Server Connection Problems 397 Setting Up a BDE Alias 399 Special BDE Alias Settings 399 SQL Primer 401 Creating a Database 402 The USE Command 403 Creating Tables 403 Constraints 404 Creating Indexes 405 Inserting Data 406 The UPDATE Command 407 The DELETE Command 407 Transaction Control 407 The SELECT Command 408 Column Aliases 413 Table Aliases 413 Views 414 Stored Procedures 415 Triggers 418

Cursors 419 Summary 421 What's Ahead 421 Page 14 16 Delphi on Oracle 423 Starting the Server 424 Getting Connected 424 Setting Up a BDE Alias 425 Troubleshooting Oracle Connection Problems 427 SQL Primer 429 Creating a Database 429 The CONNECT Command 429 Creating Tables 430 Constraints 432 Creating Indexes 433 Tablespaces 434 Inserting Data 434 The UPDATE Command 435 The DELETE Command 435 Transaction Control 436

The SELECT Command 436 Column Aliases 441 Table Aliases 441 Views 442 Stored Procedures 442 Triggers 444 Cursors 444 Summary 446 What's Ahead 446 17 Delphi on InterBase 447 Starting the Server 448 Getting Connected 448 Setting Up a BDE Alias 449 Troubleshooting InterBase Connection Problems 450 SQL Primer 451 Creating a Database 451 Shadows 452 The CONNECT Command 453 Creating Tables 453 Constraints 457

Creating Indexes 458 Inserting Data 459 The UPDATE Command 460 The DELETE Command 461 Page 15 Translation Control 461 The SELECT Command 462 Column Aliases 467 Table Aliases 467 Views 468 Stored Procedures 469 Exceptions 471 Triggers 472 Functions 472 Cursors 474 InterBase Administration 476 Backing Up and Restoring 476 Managing User Accounts 477 Server Configuration 478 Viewing Lock Statistics 479

Database Validation 480 Database Statistics 481 Database Sweeping 482 Transaction Recovery 482 Summary 482 What's Ahead 482 18 Delphi on Sybase SQL Server 483 Starting the Server 484 Getting Connected 484 Net Library/Open Client Error 422 Under NT 484 Configuring the Client 485 SYBPING 486 Win 3.x Drivers 486 Setting Up a BDE Alias 487 Special BDE Alias Settings 488 Miscellaneous Sybase SQL Server Issues 490 Troubleshooting Sybase Connection Problems 490 SQL Primer 491 Creating a Database 491 The USE Command 492 Creating Tables 493

Constraints 494 Creating Indexes 495 Inserting Data 495 The UPDATE Command 496 Page 16 The DELETE Command 497 Transaction Control 497 The SELECT Command 497 Column Aliases 503 Table Aliases 503 Views 503 Stored Procedures 504 Triggers 510 Cursors 510 Summary 512 What's Ahead 512 IV Advanced Topics 19 Business Reports 515 Types of Reports 516 The Employee Database 516

The Customer List Report 517 The Customer Group Report 520 The Master/Detail Report 521 The Cross-Tab Report 524 Constructing a Report Front-End Program 526 Viewing Your Reports at Runtime 532 Business Charts 533 Charting with QRChart 533 Graphing with DecisionGraph 535 Enhancing Your Reports 537 Summary 538 What's Ahead 538 20 Business Rules on the Database Server 539 Business Rules Further Defined 541 Server-Based Business Rule Implementations 542 Server Implementation Strengths 543 Server Implementation Weaknesses 544 Client Implementation Strengths 545 Client Implementation Weaknesses 545 Middleware Strengths 546 Middleware Weaknesses 547

Implementing Server-Based Business Rules 547 Getting Started 548 Primary Key Constraints 549 Page 17 Foreign Key Constraints 549 Check Constraints 550 Defaults 550 Views 550 Triggers 551 Stored Procedures 551 Summary 553 What's Ahead 553 21 Business Rules in Delphi Applications 555 Types of Business Rules 557 Two Rules About Business Rules 557 Rule Number One 558 Rule Number Two 558 Custom Components 559 TField Properties and Business Rules 560 Importing Dictionary Information 560

Creating Your Own Attribute Sets 561 Defining Business Rules Using TFields 561 OnExit 563 DataSets and Business Rules 563 OnNewRecord 564 BeforeDelete 564 BeforePost 564 Summary 565 What's Ahead 566 22 Beyond the Two-tiered Model 567 Remote Data Broker 568 Constraint Broker 570 Briefcase Computing Support 571 Partial Data Packages 572 Summary 573 What's Ahead 573 23 Concurrency Control 575 Transaction Isolation 576 Choosing an Appropriate Transaction Isolation Level 576 Classic Transaction Isolation Problems 577

Transaction Management with TDatabase 577 Controlling Transactions with SQL 578 Concurrency Control Systems 583 Pessimistic Concurrency Control 583 Optimistic Concurrency Control 585 Page 18 Transaction Log Management 587 Keeping Transaction Logs to a Minimum 588 Breaking Up Large Transactions 589 Summary 591 What's Ahead 591 24 Advanced SQL 593 DDL Versus DML 595 Advanced DDL Syntax 595 Databases 595 Segments and Tablespaces 596 Indexes 597 Generators and Sequences 598 Views 600 Stored Procedures 603

Stored Functions 605 Packages 606 Exceptions 607 Triggers 609 Tables 610 Advanced DML Syntax 610 SELECT 611 INSERT 613 DELETE 613 UPDATE 614 Cursors 614 Optimal SQL 615 SQL Syntax 616 Performance 618 Summary 631 What's Ahead 632 25 Delphi Client/Server Performance Tuning 633 How Fast Is Fast Enough? 634 Determine Performance Variables 634 Build a Test Environment 634 Defining Performance Tuning 635

Application Performance Tuning 636 Keep Server Connections to a Minimum 636 SQL Indications—An Economy of Words 637 Use TFields 643 Use the Data Dictionary 643 Page 19 DBText and Read-Only Fields 644 Multi-Threading Database Apps 644 Server Performance Tuning 650 Server Configuration Tuning 650 Query Tuning 652 Network Performance Tuning 655 Summary 657 What's Ahead 658 26 The Borland Database Engine 659 BDE Versus ODBC 660 The Architecture 661 Shared Services 662 OS Services 662 Buffer Manager 662

Memory Manager 662 Blob Cache 662 Sort Engine 662 Query Engine 662 SQL Generator 663 Restructure 663 Data Translation Service 663 Batch Table Functions 663 In-Memory Tables 663 Linked Cursors 663 SQL Drivers Services 664 System Manager 664 Configuration Manager 664 Language Drivers 664 The BDE API 664 Minimum Functionality 665 Key BDE Structures 665 Building a Native BDE Application 667 Accessing the BDE from Delphi Applications 672 Making Native Calls to Your DBMS 676

Retrieving Platform-Specific Info Using the BDE 677 Expression Indexes 682 Optimizing the BDE 683 Optimizing BDE for SQL Access 684 Summary 684 What's Ahead 685 Page 20 27 Building Your Own Database Components 687 The Four Basics of Component Construction 688 TArrayTable 689 Using the Component in an Application 693 TLiveQuery 695 CreateViewSQL 697 DropViewSQL 697 TableNameFormat 698 The Constructor 700 The Destructor 700 SetQuery 701 CreateTemporaryView 701 DoBeforeOpen 703 DropTemporaryView 703

DoAfterClose 703 TDBNavSearch 707 The ZoomDlg Component 722 Rolling Your Own Property Editor 730 Other ZoomDlg High Points 731 Summary 732 What's Ahead 732 28 Delphi Internet Applications 733 Project One: A Web Browser 735 Bringing the New Form to Life 737 Project Two: A Web Server App 745 Creating the Database 745 Building the App 748 Testing Your Web Server App 752 Debugging Web Server Apps 759 Project Three: An ActiveForm Application 760 Building the App 760 Wiring In the Code 762 Deploying the App 764 Testing the App 765

Project Four: A Thin-Client App 766 Project Five: Building a Shopping Cart 779 Summary 804 What's Ahead 804 Page 21 29 Deploying Your Applications 805 Get Organized 806 Ascertain Client Network Requirements 806 Determine Client Database Driver Requirements 806 Evaluate Client Database Server Requirements 807 Ascertain Database Preparation Requirements 807 Basic Setup 807 Set the Visual Design 808 Select InstallShield Options for Delphi 812 Specify Components and Files 814 Select User Interface Components 817 Make Registry Changes 818 Specify Folders and Icons 819 Run Disk Builder 819 Test Run 820

Create Distribution Media 821 Advanced Setup 821 Getting Started 821 Set the Visual Design 823 Select InstallShield Options for Delphi 826 Specify Components and Files 828 Select User Interface Components 832 Make Registry Changes 833 Specify Folders and Icons 834 Run Disk Builder 834 Test Run 835 Create Distribution Media 835 Installing Database Objects 835 Summary 836 Appendix A A Roadmap for the Journey to Delphi 837 C and C++ 838 Common Misconceptions Regarding Object Pascal and C 839 Misconception 1 840 Misconception 2 840 Misconception 3 841

Misconception 4 849 Misconception 5 849 Page 22 Using DLLs Written in C in Delphi 850 Conclusion 852 PowerBuilder 853 Conceptual Issues 853 Language Issues 855 Conclusion 865 Visual Basic and Access 865 Conceptual Issues 865 Language Issues 881 Conclusion 888 The Xbase Dialects: dBASE, Clipper, FoxPro, and So On 888 Ragged Arrays 889 Indexes 889 Code Blocks 893 Functions 894 Indexed Record Location 901 Non-Indexed Record Location 902

Filters 902 Traversing a Table 902 Writing Text Files 903 Conclusion 904 Summary 904 Index 905 Page 27

Introduction
My goals for this book were threefold: First, provide a thorough treatment of the subject of client/server database development with Delphi; second, provide a road map whereby a client/server developer who uses Delphi in her everyday work can find her way; and, third, provide a complete reference that can be used again and again, long after the book itself has been read. You'll have to be the judge of whether I've accomplished these goals. Who Should Read This Book If you develop or intend to develop client/server database applications with Delphi, this book is for you. You don't need to be an SQL expert to use this book—the book teaches you SQL. You also don't need to be a Delphi expert—the book teaches you Delphi from the perspective of database development. If you're an advanced user, the book provides insight into advanced topics like proper business rules deployment and concurrency control. If you're a beginner, the book covers entry-level topics such as database design and the building blocks that make up Delphi database applications. The book provides all you need to get up to speed, including a tutorial that steps you through building a complete client/server database application. If you fall somewhere between the beginner and advanced user levels, there's plenty here for you as well. The book traverses a number of subjects that users of all levels should find useful. Which Delphi?

Which Delphi you choose depends largely on your needs. Delphi 3 comes in three main flavors: the Desktop version, the Developer version, and the highend Client/Server suite. My personal recommendation is this: If you're serious about client/server development, get the client/server edition. Client/server development isn't the wave of the future, it's the wave of the present. This version of Delphi provides more tools and better support for SQL server-based development than the other versions of the product. It's well worth the investment. Conventions Throughout this book, I use a more-relaxed, less-formal writing style. I stress practicality in this book; rigidity and pretension are expressly avoided. To that end, you'll notice that I interchange many terms that are functionally equivalent in database lingo. I use the terms row and record identically, for example. I also use field and column interchangeably. Sometimes I precede the name of a Delphi component class name using the customary "T" prefix, and sometimes I don't. I might also occasionally slip and pronounce SQL as "sequel" rather than "S-Q-L." I think adherence to these sorts of superficial customs has to be less important than Page 28 the actual content of the text. In writing this book, I have tried to follow the way people speak. When people get away from their Victorian pretensions and use language they're comfortable with, they usually communicate more effectively. And communicate effectively is what I'm trying to do here, even if I do occasionally begin a sentence with a conjunction. Along these same lines, you'll notice that I often use the terms client/server development and client/server database development interchangeably. Though there are certainly types of client/server development that don't involve databases, by far the most popular and certainly the most talked-about type of client/server development is client/server database development. So, whether I specifically include the word "database" or not, keep in mind that everything in this book centers around database development. Organization The book is organized into four major parts—"Getting Started," "Tutorial," "Reference," and "Advanced Topics."

"Getting Started" gives you the basic tools you'll need throughout the rest of the book. An introduction to SQL is provided, a discussion on naming conventions is broached and insights into database and application design are offered. The "Tutorial" section takes you through the complete process of developing a Delphi client/server database application. You'll learn everything you need to know, from start to finish, to create a robust, full-featured database application using Delphi. The "Reference" section covers a variety of topics that you may refer to individually. The major DBMSs that Delphi supports are all covered in detail. "Advanced Topics" ventures into a number of topics that should prove useful as you delve deeper into database and client/server development. Proper business rules application, concurrency control, developing your own database components and troubleshooting database connections are all covered exhaustively. This section also includes a discussion on application deployment. Language Wars? I think it would be disingenuous of me to feign no partiality to Delphi when I use it in my everyday work and have just written a book on it. The fact is, I've waited years for Delphi. I tried tool after tool, looking for something that was sophisticated enough to handle any serious development task, yet intuitive enough for people to use who are by trade database analysts. The analysts I've often had on my team are not usually hard-core coders. When Delphi came on the scene, I wholeheartedly embraced it. Do I think there's a best language or a best tool? Of course I do! Just like I have favorite foods and clothes that I prefer over others, I have a favorite client/server development tool—and that tool is Delphi. Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Contents Introduction About this book Acknowledgments

PART I - Getting Started Chapter 1 - Is Delphi the Silver Bullet? Chapter 2 - Quick Start Chapter 3 - Building Blocks Chapter 4 - Conventions Chapter 5 - A No-Nonsense Approach to SQL Chapter 6 - Client/Server Database Design Chapter 7 - Client/Server Application Design PART II Tutorial Chapter 8 - Your First Real Client/Server Database Application Chapter 9 - First Steps Chapter 10 - First Forms Chapter 11 - Forms, Forms, and More Forms Chapter 12 - Reports Chapter 13 - Finishing Touches Chapter 14 - RENTMAN Postpartum PART III - Reference Chapter 15 - Delphi on Microsoft SQL Server Chapter 16 - Delphi on Oracle Chapter 17 - Delphi on InterBase

Chapter 18 - Delphi on Sybase SQL Server PART IV - Advanced Topics Chapter 19 - Business Reports Chapter 20 - Business Rules on the Database Server Chapter 21 - Business Rules in Delphi Applications Chapter 22 - Beyond the Two-tiered Model Chapter 23 - Concurrency Control Chapter 24 - Advanced SQL Chapter 25 - Delphi Client/Server Performance Tuning Chapter 26 - The Borland Database Engine Chapter 27 - Building Your Own Database Components Chapter 28 - Delphi Internet Applications Chapter 29 - Deploying Your Applications

Appendix A - A Roadmap for the Journey to Delphi

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 1

About this book
CLIENT/SERVER Developer's Guide with Delphi™ 3

Ken Henderson Joe Author

201 West 103rd Street Indianapolis, Indiana 46290 SAMSDeveloper'sGuide Page 2 In the quiet hours of another long night way past bedtime but before the dawn a hand touched mine and then I touched it for we knew that it was as close as we could be.

This book is dedicated to my wife Teresa who is my lover, my soul mate, and my best friend. Page 4 Copyright © 1997 by Sams Publishing FIRST EDITION All rights reserved. No part of this book shall be reproduced, stored in a retrieval system, or transmitted by any means, electronic, mechanical, photocopying, recording, or otherwise, without written permission from the publisher. No patent liability is assumed with respect to the use of the information contained herein. Although every precaution has been taken in the preparation of this book, the publisher and author assume no responsibility for errors or omissions. Neither is any liability assumed for damages resulting from the use of the information contained herein. For information, address Sams Publishing, 201 W. 103rd St., Indianapolis, IN 46290. International Standard Book Number: 0-672-31024-4 Library of Congress Catalog Card Number: 96-71200 2000 99 98 97 4 3 2 1 Interpretation of the printing code: the rightmost double-digit number is the year of the book's printing; the rightmost single-digit, the number of the book's printing. For example, a printing code of 97-1 shows that the first printing of the book occurred in 1997. Composed in AGaramond and MCPdigital by Macmillan Computer Publishing Printed in the United States of America Trademarks All terms mentioned in this book that are known to be trademarks or service marks have been appropriately capitalized. Sams Publishing cannot attest to the accuracy of this information. Use of a term in this book should not be regarded

as affecting the validity of any trademark or service mark. Delphi is a trademark of Borland International, Inc. Acquisitions Editor Christopher Denny Development Editor Richard W. Alvey, Jr. Software Development Specialist Brad Myers Production Editor Heather E. Butler Copy Editor Anne Owen Indexers Bruce Clingaman Christine Nelsen Technical Reviewers Danny Thorpe Todd Miller Roland Couchereau Bill Curtis Editorial Coordinators Mandie Rowell Katie Wise

Technical Edit Coordinator Lynette Quinn Resource Coordinator Deborah Frisby Editorial Assistants Carol Ackerman Andi Richter Rhonda Tinch-Mize Cover Designer Tim Amrhein Book Designer Alyssa Yesh Copy Writer David Reichwein Production Team Supervisors Brad Chinn Charlotte Clapp Production Jeanne Clark Michael Dietsch Lana Dominguez Polly Lavrick President, Sams Publishing Richard K. Swadley

Publishing Manager Director of Editorial Services Managing Editor Director of Marketing Product Marketing Manager Assistant Marketing Managers Page 24

Greg Wiegand Cindy Morrow Brice P. Gosnell Kelli S. Spencer Wendy Gilbride Jennifer Pock Rachel Wolfe

Acknowledgments
Special thanks to my friend and mentor, Neil Coy, who first introduced me to Turbo Pascal several years ago. Neil taught me to love coding for its own sake. Under his tutelage, I had the privilege of learning software craftsmanship from a master craftsman. I'd also like to offer a warm handshake of appreciation to Nancy Cara and Barry McClure, my hand-picked technical reviewers. This book would not be the book it is without their attention to detail and their many long hours pouring over the manuscript. Selflessly and tirelessly, they took time away from their own families to help take this book to the next level. Without their unswerving dedication, their contagious enthusiasm, and their constant encouragement, I don't think I could have completed this book. I'd like to thank the book's technical editor, Danny Thorpe, for his friendship, his patience with my many middle-of-the-night phone calls, and for our many journeys together into the perilous world of Diablo. I'd also like to offer my sincere appreciation for the excellent people at Sams. I'd especially like to thank Chris Denny, the acquisitions editor for this book and the lynchpin of my whole relationship with Sams. Thanks, Chris, for keeping me on track and for keeping a level head when the heat was on. Finally, I want to thank my grandfather, Kenneth E. Routen, who taught me to turn adversity to my favor and who was always in my corner, regardless of the circumstances. When he passed away some ten years ago, I lost my role model and my biggest fan, but his memory lives on in the obstacles I have overcome and the things I am able to accomplish because of the truths he instilled in me.

Page 26 Tell Us What You Think! As a reader, you are the most important critic and commentator of our books. We value your opinion and want to know what we're doing right, what we could do better, what areas you'd like to see us publish in, and any other words of wisdom you're willing to pass our way. You can help us make strong books that meet your needs and give you the computer guidance you require. Do you have access to CompuServe or the World Wide Web? Then check out our CompuServe forum by typing GO SAMS at any prompt. If you prefer the World Wide Web, check out our site at http://www.mcp.com. NOTE If you have a technical question about this book, call the technical support line at 317-581-4669.

As the team leader of the group that created this book, I welcome your comments. You can fax, e-mail, or write me directly to let me know what you did or didn't like about this book—as well as what we can do to make our books stronger. Here's the information:

Fax: Email: Mail:

317-581-4669 programming_mgr@sams.mcp. com Greg Wiegand Comments Department Sams Publishing 201 W. 103rd Street Indianapolis, IN 46290 Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 1

PART I

Getting Started
Page 2 Page 3

CHAPTER 1

Is Delphi the Silver Bullet?
Page 4 Every couple of years or so, someone comes out with a new "silver bullet"—a tool or technology that's supposed to help you develop bug-free software in record time. Client/server DBMSs, CASE technology, and object-oriented programming have each taken a turn in the spotlight as the latest, greatest software sensation. Even now, some are touting Java as the answer to all our problems. Conventional wisdom would tell you that there is no silver bullet—that no single development tool can do it all. The thinking is that if a tool produces applications quickly, it probably generates inefficient executables. If the tool enables you to easily build forms visually, it probably gets in the way of doing low-level coding. A tool that is fluent in client/server probably doesn't "speak"

Paradox at all; so the thinking goes. I won't bother trying to convince you that Delphi is the solution to all our software development problems. I will say, however, that it represents a colossal shift in the direction that visual tools have been headed since they first appeared. Delphi was the first Rapid Application Development (RAD) tool to meld quick application development with an optimizing compiler. To this it adds a sophisticated component foundry, comprehensive Internet support, and a multi-tiered database architecture, providing the most potent mix of software development technology ever seen in the world of Windows development. It might not be the silver bullet, but it is awfully close. For all its amenities, Delphi still won't produce bug-free software without proper design. It won't design applications for you or decide what your users need. It also won't think for you or bring about world peace. It's just a tool; it does what you tell it to do. The purpose of this book is to help you learn to tell Delphi to do what you want—to wield its power to forge robust client/server database applications. The concepts and techniques presented herein will put you well on your way to mastering Delphi client/server development in no time.

Why Delphi?
When you talk to people unfamiliar with Delphi about using it to build client/ server applications, the first question they usually ask is "Why Delphi?" After all, there seems to be a number of good client/server tools out there. PowerBuilder, Visual Basic, and others have made significant inroads into the database development market. Why would anyone change from one of these to Delphi or pick Delphi over one of them? NOTE Although I don't think that better sales necessarily equals better technology, there are those who believe otherwise. For those of you keeping score, Delphi has consistently

Page 5

outsold both Visual Basic and PowerBuilder. For example, in the year of its initial release, Delphi sold more copies than Visual Basic and PowerBuilder combined.

To me, the reasons for this are many. As I've said, Delphi successfully marries visual application development with an optimizing compiler. That's not true of most other RAD tools. The fact that a tool includes a compiler or emits native code doesn't mean that it produces optimized machine code. Both PowerBuilder and Visual Basic began as pseudo-compilers. The executables they produced had to be interpreted in order to execute. With the advent of Delphi, both Microsoft and Powersoft have scrambled to add native compiler technology to their respective products. Clearly, both vendors believe that compiling an application into machine language is a worthwhile endeavor. The problem is that because neither Visual Basic nor PowerScript was designed to be compiled, translating them to native machine code is no easy feat. Optimizing the generated machine code is even more difficult. Delphi's Object Pascal, by contrast, has always been compiled—it was designed to be compiled and optimized. The bottom line is this: Delphi is the only industrial strength solution of the current client/server tools offerings. It is to Visual Basic and PowerBuilder what C compilers were to Clipper back in the heyday of DOS. Thanks to the success of Delphi, many vendors are scrambling to integrate some type of native compiler technology into their client/server development tools. Unfortunately for them, Delphi continues to evolve while they play catch-up. I should point out that Delphi's optimizing compiler is no slouch, either. It's the latest in a long line of successful Pascal compilers from Borland. These compilers have a well-deserved reputation for producing executables that are low on resource usage and high on performance. Delphi's Object Pascal compiler is no exception. To be sure, Delphi's code generator is the same one employed in Borland's C++ product line. You get the speed of C without the headaches. Beyond merely producing machine language, though, developers these days want an application development platform that is comprehensive enough to meet their every need, yet nimble enough to allow them to accomplish simple programming tasks. They want an object framework, but they want a tool that lets them code in assembly language when necessary. They'll normally need to generate EXE files, but they want a tool that can produce DLLs and device drivers, too. They want quick database application development, but they want

something that doesn't force them to carry around a database engine in everything they write. Delphi is all these things and more. It's as though Borland took all the best elements of modern Windows development tools and wove them into a single product. Yes, Delphi provides an exhaustive component class library that alleviates much of the coding required in other tools. However, you can still write procedures in assembly language if you like. You can drop components onto a form to build almost any type of application, but you can still interact directly with the Windows API, hooking messages and communicating with other processes at will. Page 6 You can generate normal Windows EXEs with the press of a key, and you can build DLLs, device drivers, and console applications, too. And even though Delphi is expressly database oriented, you can build any sort of Windows application you'd like with it—anything from an editor to a Windows shell to a screen saver—it's up to you. Even Delphi itself was built with Delphi! Add to all this Delphi's two-way tools and its sophisticated debugger, and you have a software tool that's virtually impossible to beat. To recap, there are countless reasons for choosing Delphi over other tools; among them are the following:
q q q q q

A comprehensive object framework A fast, native-code compiler An integrated debugger that's second to none Lean yet easy-to-use database access mechanisms A development environment based on sophisticated two-way tools

In each of these areas, other tools are completely void of anything similar, or Delphi's technology is simply better. For me, the choice is simple.

No Limits
With all the talk about Delphi's impressive list of features, you might wonder whether there's something missing. You might fear that you'll eventually hit the proverbial wall and find something that you just can't do in Delphi. This isn't likely. Unlike most other RAD tools, Delphi is fully extensible—you

can extend it in so many ways, you'll probably never encounter a task that Delphi isn't up to. Whether it's constructing database apps or building ActiveX controls, Delphi comes through time and again. Here's a brief summary of the options that make Delphi so extensible:
q q q q q q

Direct access to the Windows API. Built-in assembler; inline code support. Creation of custom VCL and ActiveX components. Creation of DLLs and other secondary Windows objects. Support for multi-tiered development. Fully object-oriented: You can inherit from component classes already included or build your own from scratch. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 7 I should point out that not all OOP tools are created alike. Many tools feign object-orientation but are not OOP tools in the strictest sense of the term. Support of an "object" data type does not an OOP tool make. Providing an OOP framework that is too slow to use doesn't count, either. A tool must pass four basic criteria in order to be truly object-oriented:
q

q

q

q

Inheritance—New object types must be able to be synthesized from existing ones by inheriting their attributes and method procedures. Polymorphism—Object methods must be callable without respect for the actual object type in which the method resides. For example, the Show method performs radically different tasks when drawing a button control versus a grid control, but the call is the same. Also, provided they both descend from a common ancestor, calling the ancestor's Show method using an instance of either the grid or button control should properly display the correct control. Encapsulation—Data and program code must be locatable within single entities. That is, an object must be able to store data elements (as a record structure does) and procedure elements (called methods) together. Procedural elements within an object must have automatic access to data elements within the object. Primary Methodology—Here's the big one. The object-orientation of a tool must be the primary method of constructing program code, not an afterthought or add-on. Because it's the primary means of coding, the tool's OOP approach must be efficient enough to use productively, or the tool itself is suspect because its main method of coding is counterproductive.

Delphi meets these criteria in the same way that C++ and other traditional OOP

environments do. It's a little known fact that Borland's Pascal compiler was object-oriented before either Microsoft C or Borland C was. The OOP technology in Delphi is not an afterthought, but the basis for the entire environment. With the addition of visual development tools, Borland has removed much of the tedium frequently associated with OOP development. What distinguishes Delphi from most of the other RAD tools on the market is the fact that it was a native code OOP tool before it was a visual tool. As I've said, Delphi descends from Borland's Turbo Pascal product line. Since the late '80s, Borland's Pascal compilers have been object-oriented and produced machine code. Only in Delphi was this technology taken to the next level—to the realm of Rapid Application Development (RAD) tools. Contrast this with Visual Basic and PowerBuilder. They both began life as interpreter-based visual environments and are just now trying to reform themselves into OOP tools and native code generators. The difference this makes can be huge. One environment was designed from the ground up for flexibility and speed. The others emphasized getting to market first and technological excellence second. Only time will tell whether this gamble pays off. Shoehorning non-OOP interpretive technology into a trim native compiler-based OOP footprint is no mean feat. Page 8

Scalability
One of the most important characteristics of a client/server tool is its scalability—its capability of working with simple as well as sophisticated databases. Sure, the tool works well with dBASE tables, but how is it with Sybase tables? Delphi's multi-tier database architecture makes it more scalable than most of the other tools on the market. Among the features that contribute to this scalability are
q

q

q

q q

Support for both local tables and those that reside on remote database servers Support for heterogeneous queries and access to multiple DBMS platforms from within a single application Platform-independent database access through the Borland Database Engine, allowing applications to be easily moved from one DBMS to another Fast, native BDE drivers for the major client/server platforms Virtualized DataSets, allowing vendors to build their own database drivers independent of the BDE

q q q

Support for ultra-thin, zero-configuration client applications Support for building application servers Complete ODBC support

The upshot of all this is that Delphi is the Swiss army knife of client/server development tools. Whether it's local or SQL server-based tables, Delphi provides the tools you need to get the job done. And chances are, you'll get it done a lot quicker and with fewer headaches.

What's Ahead
Here's a brief rundown of what's ahead in the rest of the book. This should help you decide what parts of the book will be the most useful to you. Chapter 2, "Quick Start," lets you get started building Delphi database apps as quickly as possible. If you're anxious to take Delphi out for a test drive, Chapter 2 gives you the bare essentials you'll need to start immediately. Chapter 3, "Building Blocks," introduces the building blocks used to create Delphi applications. If you're new to Delphi, you'll want to be sure to go through this one. Chapter 4, "Conventions," relates my thoughts regarding naming program elements and database objects and structuring your Object Pascal code. Chapter 5, "A No-Nonsense Approach to SQL," is the layperson's guide to SQL. If you've never used SQL, or just need a refresher, have a look at this chapter. It gives you the essentials without inundating you with needless detail. Page 9 Chapter 6, "Client/Server Database Design," explores relational database design theory and the ways in which it applies to Delphi database development. If you're unfamiliar with relational theory or want to sharpen your skills, be sure to read this chapter. Chapter 7, "Client/Server Application Design," extends the discussion of database design to cover database application design. Chapters 6 and 7 are a companion set and are good reading for anyone interested in developing database applications. Chapter 8, "Your First Real Client/Server Database Application," begins the

"Tutorial" section of this book, which continues through Chapter 14, "RENTMAN Postpartum." The "Tutorial" section takes you through the process of designing a full-featured client/server database application. Chapter 15, "Delphi on Microsoft SQL Server," begins the "Reference" section of the book, which continues through Chapter 18. I cover issues specific to building Delphi applications that utilize Microsoft SQL Server databases. If you're building Delphi client/server applications that interact with Microsoft SQL Server, you'll find a treasure trove of valuable information in this chapter. Chapter 16, "Delphi on Oracle," covers issues specific to building Delphi applications that reference Oracle databases. If you're new to Oracle, you can use this chapter as a brief introduction to the world's most popular RDBMS. If you're an experienced Oracle developer, this chapter will alert you to the common issues developers face when building Delphi apps that communicate with Oracle databases. Chapter 17, "Delphi on InterBase," covers the InterBase RDBMS in detail. Depending on which version of Delphi you bought, you might have received a copy of InterBase when you purchased Delphi. This chapter takes you on a guided tour of InterBase and covers the many nuances associated with using Delphi and InterBase together. Chapter 18, "Delphi on Sybase SQL Server," covers issues specific to building Delphi applications that link up with Sybase SQL Server. If you're building Delphi client/server applications that will access Sybase SQL Server, you'll find this chapter invaluable. Chapter 19, "Business Reports," begins the "Advanced Topics" section of the book. It covers the essentials of building database reports with Delphi. You'll learn to build simple table lists, master/detail reports and cross-tab reports. Chapter 20, "Business Rules on the Database Server," and Chapter 21, "Business Rules in Delphi Applications," take you through several methods of setting up client- and server-side business rules in your Delphi applications. Chapter 22, "Beyond the Two-tiered Model," shows you how to build multitiered Delphi client/server applications. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 10 Chapter 23, "Concurrency Control," explores the subject of concurrency control in Delphi database applications. Although much of this is handled for you automatically by database servers and by the Borland Database Engine, there are still several issues you'll want to be aware of if you intend to deploy large-scale client/server applications. This chapter explores Delphi's methods for dealing with concurrency control and the ways you can tweak it to your advantage. Chapter 24, "Advanced SQL," delves into advanced SQL programming. If Chapter 5 was just enough to whet your appetite, you'll want to feast on this one as well. The chapter's main course consists of database device creation, stored procedure and view construction, and the use of triggers. On the side, we have optimal SQL coding techniques and some thoughts on database design philosophies. Chapter 25, "Delphi Client/Server Performance Tuning," goes through a number of techniques for optimizing Delphi client/server applications. The chapter provides a number of hints for optimizing Delphi database apps as well as the server objects they access. Chapter 26, "The Borland Database Engine," covers the internals of the Borland Database Engine. You'll learn how to make direct BDE API (IDAPI) calls and how to use them to supplement the facilities provided by Delphi's database components. Chapter 27, "Building Your Own Database Components," shows you how to construct your own database components. You'll learn to inherit from existing components as well as to create your own from scratch.

Chapter 28, "Delphi Internet Applications," explores the creation of Internetrelated applications. You'll learn to create NSAPI and ISAPI apps, as well as a Web browser, an ActiveForm OCX control and a thin client shopping cart application. Chapter 29, "Deploying Your Applications," introduces you to the bevy of issues surrounding the deployment of Delphi apps, pointing out the many pitfalls, caveats, and near-death experiences of putting Delphi client/server applications into production. The chapter takes you on a tour of the InstallShield setup program builder that ships with Delphi. Appendix A, "A Roadmap for the Journey to Delphi," helps you migrate from other tools to Delphi. The paths from PowerBuilder, Visual Basic, C++, and the Xbase dialects are covered in detail.

Summary
There are many good reasons to choose Delphi over other development environments. Rather than being a "jack of all trades and master of none," Delphi is the closest thing to a silver bullet Page 11 the Windows development community has ever seen. I could ramble on about the relative strengths and weaknesses of Delphi and its competitors, but hitting a few of the high points may put it most succinctly. In short, Delphi offers
q q q q q q q q q q

A sophisticated object framework True object-orientation Native-code compilation Integrated debugging Database access abstraction A complete foundry for fashioning VCL and ActiveX components Direct access to the Windows API Inline assembly language support Creation of DLLs and other auxiliary Windows executable types Comprehensive two-way tools

Delphi is the consummate client/server development tool. It's in a league of its own when compared with first-generation RAD tools such as PowerBuilder and Visual Basic. It's also more powerful and easier to use than second-

generation tools such as Optima++. Among those with limited exposure to Delphi, there is sometimes a perception that it's the lightweight of the client/server tools game—but that's a bad perception. It's been interesting to watch competing vendors claim that Delphi is shallow in one area or another, all the while working feverishly to copy its features. Microsoft claimed Delphi's basis on native compiler technology was a non-issue while working behind the scenes to add a native compiler (albeit, an inferior one) to Visual Basic 5. Sometimes actions speak louder than words. Page 12 Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 13

CHAPTER 2

Quick Start
Page 14 If you're as impatient as I am, the last thing you want is to wade through five or ten chapters before you even build your first form. This chapter gets you started working with Delphi as quickly as possible. You'll get to jump immediately into building simple forms and connecting with databases. If you're anxious to take Delphi out for a spin, you've come to the right place. Some of the things we'll cover in this chapter include
q q

q

q q

q

How to construct a database form using the Database Form Wizard What the essential properties of the TTable, TDataSource, and TDBEdit components are How to construct database forms using conventional form-design techniques What BDE aliases are and how to define them How to build data modules and how to add them to the Object Repository How to construct master/detail forms

A Few Words First
Throughout this chapter, I'll refer to various elements of the Delphi database

architecture by name when describing them or how they interoperate with other elements. So before we begin, let me define some basic terms in order for you to understand the jargon as we go. Some of these are Delphi terms; some are database terms. Table 2.1 summarizes Delphi's key database terms. Table 2.1. Common Delphi database access terms. Term Table Description A collection of rows (or entities) in a database. For example, you might construct an INVOICE table to store invoice entities or rows. A record or entity in a table. For example, a CUSTOMER table would contain rows of customer data. Each row would contain information for a different customer. A field or attribute that's contained in the rows of a table. For example, your INVOICE table might contain a CustomerNumber column. The CustomerNumber column would be present in every row in the table. The set of DLLs and support files that allows Delphi(and Borland products) to access databases. The Borland Data

Row

Column

Borland Database other Engine Page 15 Term base with Description

Engine (BDE) saves much of the work normally associated building full-featured database applications by providing a high level database API that is consistent across all the DBMS platforms it supports. This developer-friendly interface is surfaced in Delphi's database controls so that you rarely have to work directly with the BDE itself.

Borland's Independent Database Application Programming Interface. It's the interface whereby applications (including Delphi apps) talk to the BDE. Because nearly all necessary IDAPI calls are made for you by Delphi's database components, you'll IDAPI rarely write code that directly references IDAPI. Instead, you'll interact with the methods, properties, and events of Delphi's database components; these will, in turn, make the necessary IDAPI calls. A DLL (or set of DLLs) that allows the BDE to communicate with a particular DBMS platform. The client/server version of Delphi includes drivers to connect with Sybase, Microsoft, BDE Oracle, InterBase, Informix, DB2, Paradox, dBASE, and any 32driver bit ODBC data source. Delphi programs don't communicate directly with BDE drivers. Instead, they utilize BDE aliases, which are themselves based on BDE drivers. A collection of configuration parameters that tells the BDE how to connect to a given database. Aliases are based on BDE database drivers. You create aliases using either the BDE Administrator or Delphi's Database Explorer. Aliases are usually database specific. For example, you might create one alias to BDE Alias reference the Microsoft Access Northwind database and another to reference its Orders database. Both drivers would be based on the Access SQL Links driver because they both reference Access databases. Each would differ in that it would connect to a different Access database. This is what distinguishes BDE aliases from BDE drivers—a continues Page 16 Table 2.1. continued Term Description driver references a particular DBMS platform; an alias references a single database on a given DBMS platform.

High-performance database access drivers that the BDE can use to connect with client/server DBMSs. The client/ server version of Delphi ships with SQL Links drivers for the Sybase, Microsoft, Oracle, InterBase, Informix, and SQL Links drivers DB2 platforms. Because these drivers are included with Delphi, you don't need to use alternative methods such as ODBC to access these DBMS platforms, although you still can if you want to. Database access drivers based on Microsoft's Open Database Connectivity specification. Delphi can use 32bit ODBC drivers to connect with database back ends. ODBC drivers You set up and manage ODBC data sources (which are similar to BDE aliases) via the ODBC Administrator applet in the Windows Control Panel. A nonvisual (invisible at runtime) component that provides database access to your application. Data access Data access controls are located on the Data Access page of the control Delphi Component Palette. TDatabase, TTable, and TDataSource are all data-access controls. The Delphi class that provides access to database tables and table-like query result sets. Because the TTable, TDataset TQuery, and TStoredProc components descend from the TDataset class, you'll often see me refer to them collectively as TDatasets. The Delphi component that provides access to database TTable tables. You use TTable's TableName property to reference the actual table that you want to access in your database. The Delphi component that allows you to construct, TQuery execute, and process your own SQL queries. The Delphi component that allows you to run compiled TStoredProc SQL procedures that reside on a database server (also known as stored procedures). A visual (visible at runtime) component that uses the data access provided by your app's data-access controls to Data-aware allow the user to see and modify data in a database. Datacontrol aware controls reside on the Data Controls page in Delphi's Com Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 17 Term Description ponent Palette. For the most part, you can think of them as "data smart" versions of the controls on the Palette's Standard page. TDBGrid, TDBNavigator, and TDBEdit are examples of data-aware controls. The Delphi component that facilitates linking TDatasets with data-aware controls. Data-aware components reference TDataSource TDataSource components that, in turn, reference TDataset controls. The Delphi class that provides access to fields in a database table. Delphi creates TField descendants such as TStringField and TIntegerField when you use the Fields Editor to add field TField components to a form. TField components that have been added to a form are owned by the form, not by their associated TDataset. Figure 2.1. Delphi implements a simple, yet flexible database-access architecture.

An Overview of the Architecture
Now that you have the basic terms down, let's talk about the architecture as a whole. Delphi applications communicate with local and remote databases using the Borland Database Engine. In the case of local formats such as Paradox and dBASE tables, the BDE makes use of its own local DBMS drivers. With remote formats like Oracle and Sybase, Page 18 Figure 2.2. Database access from the perspective of a Delphi app.

the BDE communicates with back-end database servers using SQL Links and ODBC drivers. Often these drivers make calls to native driver libraries supplied by the DBMS vendor. Figure 2.1 illustrates this relationship. Within your applications, data-aware controls reference TDataSource components. Usually, a given form will make use of only a handful of TDataSource controls, although it may include numerous data-aware components. These components will reference one or more TDataSource controls that will, in turn, reference one or more TDatasets. It's not unusual for a form to include just one TDataset and one TDataSource. Figure 2.2 illustrates how these elements relate to one another. The flexibility inherent in this multilevel architecture makes it quite easy to develop database applications that are not only robust, but also scalable. Thanks to the separation of the back-end BDE drivers from your front-end application components, it's at least theoretically possible to change an application's database back end without even recompiling the app. The architecture's modularity allows individual pieces of it to be replaced without Page 19

having to reengineer it or rebuild applications based on it. Now that I've given you a broad overview of the architecture, let me make a few general statements that may help reinforce the concepts I've just discussed. You might be saying, "This architecture stuff sounds nifty, but how do I use it? How does it apply to me? What does all this really mean?" If this sounds like you, hopefully the following tips will help simplify the Delphi database architecture.
q

q

q

q

q

q

You do not need to use the TDatabase component to access databases. The TDatabase component provides some additional features and controls that you may or may not need, but it's not required to build Delphi database applications. You probably will not access the TSession component unless you're developing multithreaded database applications. A multithreaded application opens multiple execution "pipelines" simultaneously. This means that several operations can occur at the same time. Normal database applications are not multithreaded, so, as a rule, you won't need to concern yourself with the TSession component. Delphi automatically creates a TSession (stored in a global variable named Session) for database apps when they start up. This means that for single-threaded apps, you can just reference the Session variable when you need access to TSession's properties or methods. You do not need the TQuery or TStoredProc components unless you're writing your own SQL or accessing server-based stored procedures. You can open database tables in any of the local or remote formats supported by Delphi using just the TTable component. You'll normally use the TTable component to send and receive data from databases. As mentioned, TTable is the centerpiece of Delphi's database access. You use it to reference database tables and to exchange data with data-aware controls. The components on the Data Controls page are visual, data-aware controls—they allow data to be displayed and allow users to change the data visually. They're "data smart" versions of the controls you often see in Windows applications. You'll use these components to build the user interface of database applications. They interact with data-access controls such as TTable to provide users with database access. TDataset descendants (for example, TTable, TQuery, and TStoredProc) retrieve data from databases, but they cannot supply this data directly to data-aware components (such as TDBEdit); they need TDataSource to function as the conduit between them and your application's data-aware controls. This means that data-aware components such as TDBEdit do

not refer directly to the TDataset that provides their data access. Instead, they reference a TDataSource that in turn references a TDataset. So, to build a simple data-aware form you need three things: a TTable, a TDataSource, and whatever data-aware controls the form requires (TDBEdit, TDBMemo, and so on). Page 20

A Simple Form
Now that you understand the basics of the Delphi database architecture, let's get started building a simple database form. By actually going through the process of building a working database form, you'll begin to see how Delphi's database architecture manifests itself in real applications. There are two basic ways to build a database form in Delphi. You can build it manually, Figure 2.3. Select the type and basis of the new form you're building.

dropping components onto your new form one at a time and linking them to one another, or you can let Delphi's Database Form Wizard build the form for you. Let's begin with the Database Form Wizard. If you haven't already done so, load Delphi and start a new project. The Database Form Wizard The purpose of the Form Wizard is to construct a database form based on your responses to a few simple questions. Click the Database | Form Wizard menu item to start the Database Form Wizard.

Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 21 Figure 2.4. Select the table you want your new form to use.

Figure 2.5. Selecting fields in the Database Form Wizard.

Page 22 The first dialog box of the Delphi Database Form Wizard asks whether you'd like to build a simple or master/detail form and whether to base the new form on TTable components or TQuery components. For now, just take the defaults and click the Next button. Figure 2.3 Figure 2.6. Use this dialog box to select your new

form's orientation.

illustrates the first step of the Database Form Wizard. The wizard then asks what table you'd like to make use of on the form. Begin by selecting the DBDEMOS alias in the Drive or Alias name drop-down list box. This establishes the database server or disk location from which you'll access tables. After you select DBDEMOS, the list of table names shows those tables located in the directory referenced by the DBDEMOS BDE Alias. Double-click the BIOLIFE table near the top of the list. Figure 2.4 shows the Database Form Wizard's second dialog box. Next, you're presented with a list of fields from the BIOLIFE table. You can select them one at a time with the > button, or all at once using the >> button. Click the >> button, then click Next to proceed. Figure 2.5 illustrates the fieldselection process. The next form presented by the Form Wizard asks you to select an orientation for the fields on the new form. You have three choices: Horizontal, Vertical, and Grid. Take the default, Horizontal, by clicking the Next button. Figure 2.6 illustrates this. Page 23 Figure 2.7. You can select whether to create a main form by itself or to include a data module form.

Figure 2.8. Your new form as it appears in the Form

Designer.

Page 24 The next—and final—form presented by the Database Form Wizard asks whether you want to generate the form as your application's main form. An application's main form is the form that's first displayed when the application starts. Note the check box at the top of the form that enables you to select whether to generate a main form. We'll accept the default Figure 2.9. The new form you built appears when you run your application.

for now and allow this new form to become our application's main form. Obviously, as you add forms to an application you might not want each new one to become the app's main form. To avoid this, simply clear the check box at the top of the form. This final dialog box also asks whether you'd like to create a new data module along with your new form. Let's accept the default and leave the Form Only option selected. Figure 2.7 shows the Database Form Wizard's final step. Now that you've answered all the prompts provided by the Database Form Wizard, click the Finish button. You should see your new form loaded into the Delphi Form Designer. The form should have its own TTable and TDataSource components, along with a collection of data-aware visual controls. Figure 2.8 shows what the new form should look like. Now that your new form is complete, let's see how it appears at runtime. Press F9 to execute your project now. Because the new form you built was

designated as your app's main form, you should see it first when the application runs. Figure 2.9 shows what your new form should look like. When the app is onscreen, you can navigate the BIOLIFE table using the DBNavigator control at the top of the form. With this tool you can add, change, and delete rows in the table. Page 25 Click the + button on the DBNavigator to add a record; click the _ button to delete one. Type in a field to begin editing it. Click the Post button (the button with a check mark on it) to save any changes you make. With a minimal amount of effort, you've just created a fully functional BIOLIFE data entry form. When you've finished viewing the new form, close it to return to Delphi. NOTE You can customize forms generated by the Database Form Wizard. They're

Figure 2.10. The TTable component on closer inspection.

regular Delphi forms in every sense of the word. As you work, you might find that creating a form using the Database Form Wizard and then tailoring it to your specific needs makes the most sense. The New Form Analyzed Before we move on, let's inspect the form we just generated to learn how it works. Click on the form's TTable component (it should be near the top of the form) to select it. Now Page 26

press F11 to switch to the Delphi Object Inspector. There are two key properties to note here. First, notice the DatabaseName property. It's set to the DBDEMOS BDE alias that you selected in the Database Form Wizard. It provides the information the BDE needs to locate the table specified by the second key property, TableName. The TableName property lists the Figure 2.11. Your TDataSource component under the microscope.

BIOLIFE table you selected in the Database Form Wizard. Figure 2.10 illustrates what you should see in the Object Inspector. Now that we've examined the TTable component, let's have a closer look at the TDataSource component as well. It should be adjacent to the TTable component on your form. Click it now to select it. As with the TTable component, there are two key TDataSource properties of which you should take special note. First, note the DataSet property—it references the form's TTable component, Table1. This means that as the TDataSource couriers data to and from data-aware controls, it will send and receive this data from Table1's underlying database table TTable component. Second, notice the AutoEdit property. It's set to True (True is the property's default setting). As I mentioned earlier, setting the AutoEdit property to True causes the TDataSource to automatically switch to Edit mode when data is modified in a linked data-aware control. Figure 2.11 shows the two properties in the Delphi Object Inspector. The last of the form's elements that beg further examination are its data-aware controls. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 27 They're the controls responsible for allowing data to be visually entered and modified. Click the topmost TDBEdit control on your form to select it; then select a couple of other TDBEdit components along with it. When you have a few TDBEdits selected, press F11 to bring up the Delphi Object Inspector. TIP When you've selected a single component, you can add other components to your selection by holding down the Shift key while you click them with the left mouse button. Alternatively, you can hold down the Ctrl key and drag a selection box around the components you want to select. When you release the mouse button, the controls within your box will be selected.

Similar to the TTable and TDataSource components, there are two key TDBEdit properties that you should consider. First, click the DataSource property and note its setting. It references the form's TDataSource component. This means that data you enter into the form's visual controls is sent to its underlying table by way of the DataSource1 component. Second, notice the DataField property. It establishes which field in the underlying table a particular data-aware control services. Now that we've inspected the TTable, TDataSource, and TDBEdit components a little more closely, the relationship between them becomes more evident. TTables provide the basic link between a form and tables in a database. TDataSource components function as messengers between these TTable components and their linked controls. Data-aware controls such as TDBEdit

send and receive data from TDataSource components. The bottom line is: Delphi provides access to databases through a flexible multi-tiered architecture. You might be wondering why the TDataSource component is even needed. Consider the following: If data-aware controls such as TDBEdit could refer directly to TDataSet components, what would be involved if you wanted to change all the controls on a form to refer to a different TDataSet? You'd have to select each of them and set its DataSet property accordingly. That said, what's involved with changing the underlying TDataSet when controls are instead linked to a TDataSource? You simply change the TDataSource's DataSet property. You change a single property of a single control. It couldn't be much simpler. You might respond, "But I can select all the controls at once and change their DataSource properties just as easily." To which I would respond, "What about at runtime? How would you change a group of them at runtime?" The answer is: You'd have to change each control individually. You might construct a loop to do the work for you, but you'd still be changing each control separately. When you think about it, the way that Delphi does things Page 28 makes a lot of sense. A Simple Form the Hard Way Now that you've created a basic form the easy way, let's create one manually so that you can see the process in more detail. Follow these steps to begin building a database form Figure 2.12. In the Object Inspector, delete the Caption property of both TPanels.

manually:

1. Reload the form that was originally created when you started your project. It should be named Form1 and should be empty. 2. Next, drop two TPanel components onto the form. Position one near the top of the form and set its Align property to alTop. This causes the TPanel to occupy the topmost area of the form. No matter how large or small the form becomes, the TPanel will always occupy its top region. 3. Position the second TPanel near the bottom of the form and set its Align property to alClient. This will cause the TPanel to occupy all the area not taken up by the first TPanel. No matter how the form is sized, the second TPanel will always cover all the area not occupied by the first TPanel. Page 29 4. Next, let's remove the text that each TPanel displays by default. We don't want either TPanel to display a caption of any type, so we need to reset their Caption properties. Select both TPanel controls; then press F11 to bring up the Object Figure 2.13. Your new form with its TScrollBox in place.

Inspector. Next, delete the contents of the Caption property they share. Figure 2.12 shows the form with its TPanels selected and their Caption properties erased. The next order of business is to drop a TScrollBox control onto the lower TPanel. The reason you need to do this is to allow more controls than will fit on the form to be displayed. If a table has lots of fields, you might need to place more data-aware controls on your form than will physically fit on it. TScrollBox allows you to do this by providing an area that can be scrolled at runtime to display more controls as needed. After you've dropped a TScrollBox control onto the lower TPanel, set its Align property to alClient. This will cause the TScrollBox control to occupy all available space on the bottom TPanel.

Figure 2.13 shows what your new form might look like with the TScrollBox in place. Now that the form's visual foundation has been established, you're ready to begin dropping components onto it. Let's begin with two nonvisual controls: a TTable and a TDataSource. Page 30 You'll find these controls on the Data Access page of the Delphi component palette. Place Figure 2.14. Your new form with its nonvisual components.

one of each near the top of the form. Although their locations on the form are unimportant because they're invisible at runtime, placing them near the top allows you to easily access them no matter how you size the form. Set Table1's DatabaseName property to the DBDEMOS BDE alias and set its TableName property to the BIOLIFE.DB table located in the DBDEMOS alias. Next, set the TDataSource component's DataSet property to reference your Table1 component. This creates the link between the TTable and the dataaware controls the TDataSource will service. TIP

You can double-click the TDataSource component's DataSet property to quickly set it to your form's lone TTable component. Because there's only one TDataSet descendant on the form (your Table1 component), Delphi will automatically supply it to your TDataSource's DataSet property when you double-click the property.

Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 47

CHAPTER 3

Building Blocks
Page 48 Although Client/Server Developer's Guide with Delphi 3 isn't intended to be a beginner's book, I thought it appropriate to touch briefly on the building blocks of Delphi applications. You might be coming from a development environment that differs significantly from Delphi. If so, a brief discussion of Delphi basics will be beneficial. If you're familiar with Delphi concepts and have already begun developing applications, you might want to skip this chapter and go straight to Chapter 4, "Conventions." In this chapter, you learn the pieces that make up the Delphi puzzle. You are introduced to Delphi projects and libraries. You get acquainted with Object Pascal units, forms, data modules, and include files. Last, but not least, you meet the cornerstone of Delphi application development: the component.

What Are Projects?
Depending on the software you're using, the term project can have a number of different meanings. Usually, a project is the top-level container for all the objects in an application. Its purpose is to relate the files that make up an application to one another—to establish dependency relationships between them. A single project usually acts as the repository for all the objects in an application. Of course, this isn't always the case. Sometimes an application is composed of multiple projects. Sometimes a project covers more than just a single application. In most development tools, though, a project produces a single executable file.

Delphi Projects

In Delphi, a project contains a master list of all the files that constitute a single application. Delphi projects are unusual in that, aside from listing the application's source code modules, they also function as program source code. That is, a Delphi project is Object Pascal source code that you can view and modify if necessary. NOTE Although it is possible to modify a Delphi project file manually, you shouldn't need to do so. Because Delphi might change parts of the file, you shouldn't make your own changes to it. A change you make could confuse Delphi, or Delphi might overwrite changes you've made to the file.

Because Delphi projects are stored as source code and reside in the operating system as files, project names are limited only by Pascal's identifier restrictions. Project and unit names must begin with a letter and may include letters, digits, and underscores. No spaces or periods are permitted. Listing 3.1 shows a small Delphi project file. Page 49 NOTE Of course, the maximum filename length that your operating system supports affects the names you can use for files. Some network operating systems, notably Novell NetWare 3.x, don't support long filenames. This is also true of some version control systems. You'll need to take this into account as you name your files.

Listing 3.1. The RENTMAN project file. program RENTMAN; uses Forms, RENTDATA in `RENTDATA.pas' {dmRENTMAN: TDataModule}, ANYFORM in `..\..\CH09\CODE\ANYFORM.pas' {fmAnyForm}, DBFORM in `..\..\CH09\CODE\DBFORM.pas' {fmDatabaseForm}, EDITFORM in `..\..\CH09\CODE\EDITFORM.pas' {fmEditForm}, REMPENT0 in `REMPENT0.pas' {fmREMPENT0}, RSYSMAN0 in `RSYSMAN0.pas' {fmRSYSMAN0}, RWKTENT0 in `RWKTENT0.pas' {fmRWKTENT0}, CGRDFORM in `..\..\CH09\CODE\CGRDFORM.pas' {fmControlGridForm}, RTENCGD0 in `RTENCGD0.pas' {fmRTENCGD0},

RPROCGD0 in `RPROCGD0.pas' {fmRPROCGD0}, RLEACGD0 in `RLEACGD0.pas' {fmRLEACGD0}, MSTRFORM in `MSTRFORM.pas' {fmMasterDetailForm}, GRIDFORM in `GRIDFORM.pas' {fmGridForm}, RWORGRD0 in `RWORGRD0.pas' {fmRWORGRD0}, RWORMDE0 in `RWORMDE0.pas' {fmRWORMDE0}, RCALGRD0 in `RCALGRD0.pas' {fmRCALGRD0}, RCALEDT0 in `RCALEDT0.pas' {fmRCALEDT0}, RPROLST0 in `RPROLST0.pas' {frRPROLST0}; {$R *.RES} begin Application.Initialize; Application.HelpFile := `C:\Data\Word\CSD\CH13\code\Rentman.hlp'; Application.CreateForm(TfmRSYSMAN0, fmRSYSMAN0); Application.CreateForm(TdmRENTMAN, dmRENTMAN); Application.CreateForm(TfmREMPENT0, fmREMPENT0); Application.CreateForm(TfmRWKTENT0, fmRWKTENT0); Application.CreateForm(TfmRTENCGD0, fmRTENCGD0); Application.CreateForm(TfmRPROCGD0, fmRPROCGD0); Application.CreateForm(TfmRLEACGD0, fmRLEACGD0); Application.CreateForm(TfmRWORGRD0, fmRWORGRD0); Application.CreateForm(TfmRWORMDE0, fmRWORMDE0); Application.CreateForm(TfmRCALGRD0, fmRCALGRD0); Application.CreateForm(TfmRCALEDT0, fmRCALEDT0); Application.CreateForm(TfrRPROLST0, frRPROLST0); Application.Run; end. Page 50 The program Keyword Note the use of the program keyword in Listing 3.1. It tells the compiler that this file is to become its own executable. In a dynamic link library or unit, the program keyword is replaced with library or unit, respectively. The Uses Statement The Uses statement lists the Object Pascal units that Delphi links to build the executable. Units are the molecules of Delphi applications. Any unit whose source code is more recent than its compiled code is automatically recompiled when you compile or run the project file. Some environments refer to this as

a make. The Forms unit in Listing 3.1 is part of Delphi's own Visual Component Library and defines the characteristics of Delphi forms. The other units listed correspond to the forms that have been added to the project. Each line lists the name of the unit, the operating system file in which it resides, and the name of the form that the unit contains. Each form name reflects the value of its Name property in the Delphi Object Inspector. With the advent of long filename support in Delphi, the name of the unit and the name of the file in which it resides are identical, minus, of course, the file's extension. If, during compilation, the Delphi compiler can't find a unit using its long name, it will attempt to find it using its truncated, or short, name. The $R Directive The $R compiler directive instructs the compiler to include the specified Windows resource with the project. The asterisk tells the compiler that the resource file has the same base name as the project. When you build a project, Delphi creates a resource file for the project itself and for each of its forms. If these resource files are not present when the application is linked, Delphi issues a File not found xxx.RES message. Application.CreateForm The Application.CreateForm statements load the project's forms into memory. Usually, every form in a project is listed here. Using the Options | Project menu selection in the Delphi Integrated Development Environment (IDE), you can control whether a form is created automatically for you. Each form is stored in its form instance variable (for example, fmRSYSMAN0), which is defined in the interface section of the form's unit. Because the project file uses the units that define the form instance variables, it can "see" these variables and pass each of them to the Application.CreateForm routine. Application.CreateForm then loads the specified form into memory and returns a pointer to it in the instance variable. The order in which the Application.CreateForm statements are listed is significant in that it determines the order in which the forms are created. The first form created by Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 46 Now that you've viewed the SQL for the master table, let's have a look at the SQL generated for the detail table. Click the rightmost TQuery component, then double-click its SQL property in the Object Inspector. You should see the following SQL: Select ORDERS."OrderNo", Figure 2.28. Your master/detail form as it appears at runtime.

ORDERS."SaleDate", ORDERS."ShipDate", ORDERS."EmpNo", ORDERS."ShipToContact", ORDERS."ShipToAddr1", ORDERS."ShipToAddr2", ORDERS."ShipToCity", ORDERS."ShipToState", ORDERS."ShipToZip", ORDERS."ShipToCountry", ORDERS."ShipToPhone",

ORDERS."ShipVIA", ORDERS."PO", ORDERS."Terms", ORDERS."PaymentMethod", ORDERS."ItemsTotal", ORDERS."TaxRate", ORDERS."Freight", ORDERS."AmountPaid" From "ORDERS.DB" As ORDERS Where "ORDERS"."CustNo" =:"CustNo" The most notable thing about this SQL is its Where clause. It restricts the rows returned from the ORDERS table to just those whose CustNo column matches a variable named :CustNo. Where is :CustNo defined? Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 41 also avoid having to explicitly open the TTable at runtime in your app. Basically, it's less work and more flexible. Delete the Table1.Open line from the form's OnCreate event and save the form. Next, load Figure 2.23. The completed Add To Repository dialog box.

your data module into the Delphi Form Designer and select its TTable component. Now, press F11 to bring up the Object Inspector and double-click the Table1's Active property to open it. After you've done this, save your project. You can run your app now, if you'd like, to see that it still works as expected. When you've finished, close it to return to Delphi. Now that the data module is completed, let's save it to the Object Repository. You'll often find that storing a form in the Object Repository—whether it's a data module or just a "regular" form—makes a lot of sense because you can later inherit from it to create new forms or include it as is in other projects.

Right-click your data module (not one of its components) and select Add to Repository from the Context menu. Type dmBIOLIFE for the title and something resourceful like BIOLIFE data module form for the description. Select Data Modules in the Page drop-down list box and type your name into the Author entry box, then click OK. Figure 2.23 illustrates the Add To Repository dialog box. Page 42 NOTE You also can select an icon to represent your data module in the Object Repository. As you add more data modules to the Repository, you'll probably find that assigning a unique bitmap to each one makes them easier to distinguish from one another. To choose a special icon for your new data module, click the Browse button in the Add To Repository dialog box.

If you haven't already saved your data module form, you'll next be asked whether you want to save the data module to disk before adding it to the Object Repository. Click Yes and enter a pathname for the file. Now that you've added the dmBIOLIFE data module to the Object Repository, you'll be able to use it whenever an application needs access to the BIOLIFE table.

Creating a Master/Detail Form
A master/detail form usually browses at least two tables that are related to one another. It normally displays one table in one portion of the form and the other in a second portion. For example, you might browse the ORDERS table at the top of a master/detail form and its related LINEITEM table at the bottom. As each row in ORDERS was perused, the associated rows in LINEITEM would be displayed. You see this type of form a lot in OLTP applications. In this section, I'll show you how to build a master/detail form that shows the orders for each customer using the CUSTOMER and ORDERS tables from Delphi's canned demos. Building this form will also acquaint you with the TQuery component. The database forms you've built thus far have all been based on the TTable

component. The master/detail form you're about to build will instead be based on the TQuery component. Begin by starting the Database Form Wizard (choose the Database | Form Wizard menu option). When the Form Wizard starts, click the Create a master/ detail form and the Create a Form using TQuery objects radio buttons; then click Next. In the Database Form Wizard's second dialog box, click on the Drive or Alias drop-down list and select the PDOXDEMO alias you defined earlier, then double-click the CUSTOMER.DB table in the Table Name list. This will select the CUSTOMER table (a Paradox table) in the Delphi demo data directory as your new form's master table. When you double-clicked the CUSTOMER table, the Database Form Wizard automatically moved the next step. Now you're ready to select which fields from the CUSTOMER table to include on the form. Click the >> button to select all of them, then click Next. Page 43 Figure 2.24. Select all but the CustNo field.

The next Database Form Wizard dialog box asks what layout you'd like to use for your master table's fields. Your choices are Horizontal, Vertical, and Grid. Click the Next button to accept the default (Horizontal). Now that you've chosen your master table, you're ready to select the form's detail table. The alias you selected earlier for your master table is selected for you by default—you Figure 2.25. Join your tables using the CustNo column.

Page 44 need only select a table from the list. Double-click the ORDERS table to select it as your form's detail table. After you've selected a detail table, the Database Form Wizard asks which of its fields you'd like to include on the form. Click the >> button to select them all; then click the Figure 2.26. Your finished master/ detail form.

CustNo field in the Ordered Selected Fields list and click the < button to remove it from the list. Because the master table's CustNo field will be displayed in the form's top region, there's no reason to include it in the bottom region as well. TIP You can also select the order in which the fields will appear from this dialog box. You can drag-and-drop fields in the list to move them, or you can use the up- and down-arrow buttons beneath the Ordered Selected Fields list to move a single field one position at a time.

Figure 2.24 shows what the Detail Fields dialog box should look like. Click the Next button to proceed. As with the master table, the Database Form Wizard next prompts for the layout you'd like to use with your detail table's fields. The default is In a grid.

Click Next to accept the default and proceed. Next, you're asked to select the pairs of fields that will join your master and detail tables. Page 45 When databases are designed intuitively, these fields will have the same names in both tables. In this particular case, you'll be joining your master and detail tables using their CustNo columns. Click the CustNo column in both lists, then click Add. Figure 2.25 illustrates how the join definition should appear. When you've finished setting up the join, click Next to proceed. The final dialog box of the Database Form Wizard allows you to choose whether to generate a main form and whether to also generate a data module. Click the Finish button to generate the new form and make it the app's new main form. Figure 2.26 shows your completed form in the Delphi Form Designer. Before we execute your app to view the new form at runtime, let's study it a bit to see how it works. Understanding how the generated form works will help you build your own master/detail forms. Click the new form's leftmost TQuery component, then press F11 to bring up the Object Inspector and double-click its SQL property. You can click the Code Editor button to view the SQL in Delphi's Code Editor. Figure 2.27 illustrates what you might see. This SQL query retrieves all the columns and all the rows in the CUSTOMER. DB table. It names the columns individually, but because you selected all of them in the Database Form Figure 2.27. This SQL generated by the Form Wizard to query the CUSTOMER table.

Wizard, it's actually retrieving all the table's columns. Because the SQL doesn't have a WHERE clause to qualify it, it retrieves all the table's rows as well. Press Ctrl+F4 to close the Code Editor window when you're through viewing the SQL. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 36 property. You should see dbTest in the list. Click it to select it. You've now switched your form to funnel its database access through a single control, your TDatabase component. Figure 2.18 shows your form with its new TDatabase component. 5. Be sure to save the changes you've just made to your form before proceeding.

BDE Aliases
Delphi's Database Explorer has a variety of uses—you'll use it here to create a Borland Database Engine alias. You need a BDE alias in order to access database objects from Delphi. Personally, I've never liked the term alias. To me, the word implies that the name is false—that it's a pseudonym. Implications aside, a BDE alias is analogous to an ODBC data source. It's a collection of parameters that define how to connect to a given database or DBMS. Thus far we've been using the built-in demo alias, DBDEMOS. Delphi includes it by default to support some of its database demo applications. In a moment, I'll show you how to define your own BDE aliases. The Delphi database apps you build will likely require custom alias definitions. Follow these steps to create your own BDE alias: 1. Select the Explore option from Delphi's Database menu to start the Database Explorer.

Figure 2.19. Your new alias definition.

Page 37 2. Click on the Database tab, if it isn't already selected, then click the Databases entry in the treeview below it. 3. Next, right-click the Database entry and select New on the pop-up menu. 4. You're then prompted to select a database driver for your new BDE alias. The alias you'll build will be Paradox based, so select Standard and click OK. 5. Next, you're prompted for the name of the new database alias. Type PDOXDEMO and press Enter. Figure 2.20. Your TDatabase references your new BDE alias.

6. Next, click the Path entry in the right-hand pane and type the name of the directory where Delphi's demo database files are stored (in my case that's C:\Program Files\Borland\Delphi 3\Examples\Data), then press Enter. NOTE

By pointing your new alias to Delphi's demo data directory, you're basically duplicating the DBDEMOS alias. This makes testing the new alias easier, because tables are already present in the directory. In your own apps, though, you'll no doubt specify a different directory.

7. Next, click the Apply button in Database Explorer's toolbar. It's the blue arrow that curves to the right. This saves your new alias definition. Page 38 Figure 2.19 illustrates what the screen should look like when you're done. Now that you've built a new BDE alias, let's change your form to use it. To do so, follow these steps: 1. Close the Database Explorer and return to the Delphi Form Designer. 2. Click your TDatabase component; then press F11 to bring up the Object Inspector. 3. Change Database1's AliasName property to reference your new alias. Figure 2.20 shows what you should see in the Object Inspector. Save your work now and run your app. You should see that it opens the BIOLIFE Paradox table as it did before. When you're finished viewing the app, close it and return to Delphi. Figure 2.21. Your new data module houses dataaccess components.

Data Modules
A data module is a special type of form designed for holding nonvisual controls like database-access controls. It allows you to centrally locate all your

nonvisual data controls for easy access and management. I recommend that you create a data module for every database that you might want to access with Delphi applications. You don't have to put all the tables in a given database in a single data module, but I recommend you do so when possible because this enables you to save the data module to the Delphi Object Repository, where it can be reused in other applications. This helps ensure consistency across your applications and enables you to manage Delphi's database access from a single vantage Page 39 point—the Object Repository. So, select File | New Data Module. You'll be presented with a small form onto which you can then drop nonvisual components. You might want to resize your data module so that it's a bit larger before proceeding. Its default size is quite small. Now that you have a data module, we'll move Form1's nonvisual data access components onto it. If you were building an entire application, you'd place as many of your data access components on the app's data module as possible. Begin by selecting the TTable, TDataSource, and TDatabase components on Form1 and pressing Ctrl+X to cut them to the Windows Clipboard. Next, select your new data module and press Ctrl+V to paste your components from the Clipboard onto your data module. Your data-access components have now been moved to your data module. Figure 2.21 shows what the new data module should look like. Figure 2.22. Your form now references the data module you created.

Page 40 TIP

Depending on where you originally placed your TTable, TDataSource, and TDatabase controls on Form1, they might wind up beyond the right or bottom edge of your data module. If you press Ctrl+V and scrollbars suddenly appear on the sides of the data module, at least one of your controls is out of view. You can resolve this by either resizing the data module so that you can see your controls, or scrolling over to the controls and dragging them to the data module's upper-left corner.

When you've finished this cut-and-paste operation, you need to return to your BIOLIFE form and repair the references to your data-access components that were broken when you moved them. That is, the data-aware controls (for example, TDBEdit) on Form1 referenced the TDataSource that you removed from the form. Now that the TDataSource has been moved, the reference is invalid and Delphi has cleared it. You'll need to repair these broken references before proceeding. The first step in utilizing the components on the data module in Form1 is to include the data module's unit file in Form1's Uses clause. This allows the form to "see" the data module's components. To do this, load your BIOLIFE form into the Form Designer; then select the Use Unit option on the File menu and choose your data module's source code unit (this should be Unit3). Now the objects on the data module are available in Form1 just as if they'd been dropped onto the form itself. Next, select all the data-aware components on your form (including the TDBNavigator), then press F11 to bring up the Object Inspector. When the Object Inspector is up, click on the drop-down list next to the DataSource property and select your data module's TDataSource. You'll notice that the TDataSource component's name is now qualified with the name of your data module. Figure 2.22 shows the form modified to reference your new data module. Because we used source code to open the TTable when the BIOLIFE form was created, we'll have to fix that reference as well. Press F11 to bring up the Object Inspector, then select your form from the drop-down list of components. Next, click on the Events page and double-click the OnCreate event. You should see this code: Table1.Open;

Because there's no longer a Table1 component on this form, this code won't compile. You could change this to DataModule3.Table1.Open; But this would create an inter-unit dependency that you don't need and should avoid when possible. That is, you'd have to be sure that your data module was created before your BIOLIFE form; otherwise, calling Table1's Open method might generate an access violation. The better method is not to open the table with source code at all. It's easier to simply open the table at design time than to do so via a call to the Open method. By opening the table at design time, you get to view your data while working on forms that reference it and you Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 51 Application.CreateForm is the application's main form. If you want to change the order of form creation, use the Project | Options | Application menu selection; don't edit your project source code file. Application.Run Application.Run starts the ball rolling as far as your application is concerned. It enters the loop that runs your application.

Delphi Libraries
Delphi provides a convenient mechanism for building Windows dynamic link libraries (DLLs). Unlike most other Windows programming languages, Delphi actually includes special syntax for building DLLs. You use two keywords to implement Delphi's DLL support: library and export. The library keyword takes the place of the program keyword in the previous example. It tells Delphi to build a DLL rather than an EXE file. The export keyword causes the DLL to actually export the functions so that other executables can call them. Listing 3.2 is the source code to a small library. Listing 3.2. The StrUtil library. library StrUtil; Uses SysUtils; function Pad(InString: String; Len: Integer): String; begin Result:=Format(`%-*s',[Len, InString]); end; function LPad(InString: String; Len: Integer): String; begin Result:=Format(`%*s',[Len, InString]); end; exports Pad index 1, LPad index 2;

begin end. Libraries are very similar in form to programs. Note the use of the library keyword in place of Program at the top of Listing 3.2. The exports statement tells Delphi to export the Pad and LPad routines from the DLL so that other executables can call them. Page 52

Delphi Units
Along with forms, units are the molecular building blocks of Delphi applications. Units house the forms that make up your application's visual appearance. Additionally, they store supplemental code that you write to support application functions. Each form you add to your project comes with its own unit source. This unit source contains a class definition that is a reflection of the visual representation of the form. For each component you add to the form, Delphi's form designer modifies the class definition to reflect the new component. Each time you add an event handler to the form, the event handler code is stored in the unit file. Nearly all the coding you do in Delphi is done in unit files. Listing 3.3 is the source to a simple unit file. Listing 3.3. The source to a Delphi unit. unit RTENCGD0; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, CGRDFORM, Buttons, DBCtrls, StdCtrls, ExtCtrls, Mask, DBNavSch, DBCGrids; type TfmRTENCGD0 = class(TfmControlGridForm) teTenantNo: TDBText; deName: TDBEdit; deEmployer: TDBEdit; deEmpAddress: TDBEdit; deEmpCity: TDBEdit; deEmpState: TDBEdit; deEmpZip: TDBEdit; deHomePhone: TDBEdit; deWorkPhone: TDBEdit; deICEPhone: TDBEdit; deLeaseBeginDate: TDBEdit; deLeaseEndDate: TDBEdit; deMovedInDate: TDBEdit; deMovedOutDate: TDBEdit;

deRentDueDay: TDBEdit; dePetDeposit: TDBEdit; deComments: TDBEdit; Label1: TLabel; dkLawnService: TDBCheckBox; Label2: TLabel; Label3: TLabel; Label4: TLabel; Label5: TLabel; Label6: TLabel; Label7: TLabel; Label8: TLabel; Label9: TLabel; Label10: Tlabel; laName: TLabel; Page 53 Label12: TLabel; Label13: TLabel; Label14: TLabel; Label15: TLabel; Label16: TLabel; Label17: TLabel; private { Private declarations } public { Public declarations } end; var fmRTENCGD0: TfmRTENCGD0; DefaultPetDeposit : Integer; implementation uses rentdata; {$R *.DFM} initialization DefaultPetDeposit:=150; end. The interface Section Note the division of the unit into the interface, implementation, and initialization sections. The interface section contains the header information for the unit. This includes the function and procedure declarations and the

variable, constant, and type definitions you want to be visible to the outside world. For you C programmers out there, this is the equivalent of C's header file. Unlike C, you don't need to store this section in a separate source code file so that other modules can include it. A compiled unit stores its interface information in its header. When another module references this unit in its Uses statement, Delphi looks at the header of the compiled unit, not its source code, to determine the interface it presents. This approach enables you to distribute Delphi units as object code only—without requiring header files of any kind. Furthermore, it does away with the redundancy of recompiling a header file each time a module that includes it is compiled. (Precompiled header files address this issue to an extent, but not all C compilers support them.) This approach is better than that taken by many C compilers and represents the next logical step beyond header files. The implementation Section The implementation section contains the unit's actual programming code. Items you place here are visible only to the unit itself unless you also list them in the interface section. The typical organization of a unit places a function's declaration in the interface section and its code in the implementation section. Page 54 The form instance Variable Note the form instance variable in the var section of the unit's interface: var fmRTENCGD0: TfmRTENCGD0; This defines a variable named fmRTENCGD0 as type TfmRTENCGD0. TfmRTENCGD0 is a descendant of the TForm class created for you by Delphi's visual form designer. fmRTENCGD0 is the variable that the project file initializes when it calls Application.CreateForm. Because this variable is defined in the interface section of the unit, other modules that reference the unit in their Uses clauses, including the project file, can "see" the variable and modify it if they want. The initialization Section When you need to set up code that executes when a unit is first loaded, put it in the unit's initialization section. Anything placed here is executed when the application loads. The order in which a given unit's initialization code executes is determined by the unit's position in the project source file's Uses statement. In Listing 3.3, the initialization section consists of the lines between the word initialization and the end of the unit: initialization DefaultPetDeposit:=150; end. When the application is first loaded, the DefaultPetDeposit variable is initialized to 150. The application can then reference the variable without being concerned with first initializing it.

The finalization Section You can optionally include a finalization section in the Delphi units you construct. Code in the finalization section executes when the application shuts down. This is a good place to close files you've opened and free other resources the unit might have used. This section must appear after the initialization section but before the unit's last end.

Delphi Forms
Delphi forms provide your application's onscreen appearance. Most of the design work you do in Delphi's visual form designer is stored in a form file. When you set a property or move a component, you are modifying settings that are stored in the module's form file. Because Delphi's tools are true two-way tools, it's possible to edit a form as text, save it, and then see the changes you made reflected visually. Listing 3.4 is the text version of a small form. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 55 Listing 3.4. The text representation of a simple form. inherited fmDatabaseForm: TfmDatabaseForm Left = 75 Top = 124 Width = 554 Caption = `fmDatabaseForm' PixelsPerInch = 96 TextHeight = 13 inherited paTop: TPanel Width = 546 end inherited paMiddle: TPanel Width = 546 end inherited paBottom: TPanel Width = 546 object paRight: TPanel Left = 385 Top = 1 Width = 160 Height = 37 Align = alRight BevelOuter = bvNone TabOrder = 0 object bbOK: TBitBtn Left = 4 Top = 8 Width = 75 Height = 25 TabOrder = 0 Kind = bkOK end object bbCancel: TBitBtn

Left = 82 Top = 8 Width = 75 Height = 25 TabOrder = 1 Kind = bkCancel end end object bbPrintForm: TBitBtn Left = 280 Top = 8 Width = 75 Height = 25 Caption = `Print Form' TabOrder = 1 OnClick = bbPrintFormClick Glyph.Data = { 76010000424D7601000000000000760000002800000020000000100000000100 04000000000000010000130B0000130B00000000000000000000000000000000 800000800000008080008000000080008000808000007F7F7F00BFBFBF000000 FF0000FF000000FFFF00FF000000FF00FF00FFFF0000FFFFFF00300000000000 00033FFFFFFFFFFFFFFF0888888888888880777777777777777F088888888888 continues Page 56 Listing 3.4. continued 8880777777777777777F0000000000000000FFFFFFFFFFFFFFFF0F8F8F8F8F8F 8F80777777777777777F08F8F8F8F8F8F9F0777777777777777F0F8F8F8F8F8F 8F807777777777777F7F0000000000000000777777777777777F3330FFFFFFFF 03333337F3FFFF3F7F333330F0000F0F03333337F77773737F333330FFFFFFFF 03333337F3FF3FFF7F333330F00F000003333337F773777773333330FFFF0FF0 33333337F3FF7F3733333330F08F0F0333333337F7737F7333333330FFFF0033 33333337FFFF7733333333300000033333333337777773333333} NumGlyphs = 2 end object DBNavigator1: TDBNavSearch Left = 8 Top = 8 Width = 253 Height = 25 TabOrder = 2 end end end

Data Modules
Delphi supports a special type of form known as a data module. You create a data module by selecting New Data Module from Delphi's File menu. Data modules enable you to group non-visual controls—usually database components—onto a central form. You can then reference these data modules from other forms in the application in order to access their database components. You can also add data modules to Delphi's Object Repository and inherit, copy, or use them, like other Repository members. Note that you can't drop visual controls onto a data module—only non-visual controls are allowed.

Include Files
Like most language tools, Delphi's compiler supports include files. Include files enable you to establish common sections of code you want to share among a number of modules. Because Delphi recompiles include files each time it encounters them in source code, you'll want to restrict their use and utilize units instead whenever possible. A common use of an include file is to list compiler directives common to several units and to cause those units to be automatically recompiled when you alter the contents of the include file. The following segment is an example of such a file from the WinMac32 Windows Macro Engine source: {$B-,F-,G+,I-,K+,N-,P+,Q-,R-,S+,T-,V+,W-,X+} {$IFNDEF DEBUG} {$D-} {$ELSE} {$D+,L+,Y+} {$ENDIF} Page 57 NOTE You can find the complete WinMac32 source code on the CD-ROM included with this book.

The primary purpose of the code is to enable you to easily toggle compiler options related to debugging. Note the {$IFNDEF DEBUG} line. It simply states that if you haven't defined a custom compiler directive named DEBUG, the three options related to debugging—$D, $L, and $Y—should be switched off. By switching off just the $D option, you automatically turn off the other two. Conversely, if you've defined DEBUG, the three options are switched on. If all the modules in an application include this file, you can easily toggle whether they are compiled with debugging related switches enabled by defining or undefining the DEBUG switch. To me, this is simpler than toggling the three options in the Delphi Options | Project dialog box. After the file is defined, you include it in your source code files by using the $I compiler directive, like so: {$I WINMAC32.INC}

NOTE Conditional compiler directives are not carried across units, as you might expect. Each time Delphi's compiler begins compiling a new unit, it resets the current set of conditional directives to those defined in the Project | Options dialog or on the compiler's command line.

Delphi Components
If forms are Delphi's molecular building blocks, components are the atoms that make up those molecules. You combine them to build forms, just as atoms combine to make molecules. Behind the visual representation you see in Delphi's form designer, components are composed of Object Pascal source code. Listing 3.5 lists the source code to a simple component. Listing 3.5. Custom code for a new component—TArrayTable. { ArrayTable Delphi Component Provides an array-like interface to a table. Use the syntax:

continues Page 58 Listing 3.5. continued Records[RecNum].Fields[FieldNum].AsType to access individual field values. Written by Ken Henderson. Copyright (c) 1995 by Ken Henderson. } unit ArrayTable; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, Db, DBTables;

type TArrayTable = class(TTable) private { Private declarations } function GetRecords(RecNum : Longint) : TDataSet; protected { Protected declarations } public { Public declarations } property Records[RecNum : Longint] : TDataSet read GetRecords; published { Published declarations } end; procedure Register; implementation function TArrayTable.GetRecords(RecNum : Longint) : TDataSet; begin First; MoveBy(RecNum); Result:=Self; end; procedure Register; begin RegisterComponents(`Data Access', [TArrayTable]); end; end. This component subclasses Delphi's Table component and adds a single public property to it: Records. Records enables you to treat the table as one large array. For example, the following syntax returns the third field from the fifth record in the table as a string (both the new Records property and TTable's own Fields property are zero based): TArrayTable1.Records[4].Fields[2].AsString; Page 59 As you can see, this added functionality came at a very small price. The process was even simpler with Delphi's New Component option on the Component menu. Using the New Component option, you specify the ancestor class (in this case, TTable), the name of the new class, and the page on which it is to reside. Delphi then generates the necessary source code for you. NOTE

Something that might not be obvious from looking at TArrayTable's source code is that using the Records property physically moves the record pointer. Because of the nature of Delphi's database controls, there's no way around this. Setting a bookmark from within the component wouldn't be of any use because TTable's Fields property always points to the current record. In code that uses this component, you might want to use a bookmark to save your position in the table so that you can return to it after the reference to the Records property.

Note the Register procedure at the end of Listing 3.5. A Delphi component is added to the toolbar palette via the RegisterComponents procedure. Its first parameter denotes the toolbar palette page on which the new component is to reside. The second parameter lists the component to register. Separate multiple component names with commas (within the brackets).

Summary
In this chapter, you learned about the building blocks of which Delphi applications are made. Specifically, you were introduced to the following elements:
q q

q

q q q

Projects—A project is the collection of files that make up a Delphi application. Libraries—A library is a Windows dynamic link library (DLL); it exports routines for other modules to use. Units—In conjunction with forms, units are the molecular building blocks with which Delphi applications are built. Forms—Forms constitute the visual representation of a Delphi application. Include Files—Include files are used to share code snippets among multiple source code modules. Components—Components are the atomic building blocks with which forms and, hence, units are made. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 60

What's Ahead
In the next chapter, I introduce some standard naming and coding conventions. The subject of conventions is no one's favorite; it's about as intriguing as a book on dental flossing. Nevertheless, establishing sound standard guidelines on naming objects and structuring program code can save you many headaches down the road. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 61

CHAPTER 4

Conventions
Page 62 Of the many things you can do to make life easier for yourself when you build client/server apps, few are as beneficial as adhering to standard naming conventions and following consistent coding practices. I felt this important enough to talk about before we actually build any applications because I see it as fundamental to solid application development. In this chapter, I give you my thoughts on the conventions you should follow when naming Delphi client/server application elements. These include both server and client constructs, and I talk about each of them separately. Note that the following suggestions are just that: suggestions. Use whatever works for you. Being consistent is more important than following any particular convention.

Object Pascal Program Elements
Let's begin with Delphi program elements. These elements include components, forms, data modules, units, variables, constants, and so on. In short, anything other than database server constructs are considered Delphi program elements in this discussion. Database server constructs include tables, indexes, views, triggers, stored procedures, rules, defaults, and so on. Let's

tackle Delphi program elements first. NOTE Because of the diverse nature of today's networking environments, I recommend that you stay away from Windows 95/NT long filenames when naming elements. Although it's true that Delphi itself will handle filenames longer than eight characters just fine, you might need to interact with a network operating system (NOS) or host computer that does not. This is fairly likely given that the focus of this book is on client/server application development and your servers will often reside on other computers. I know that it's tempting to utilize this handy advance in PC OS technology, but I recommend you avoid doing so for the immediate future. Someday, most networks and host computers will support long filenames, and there will no longer be any reason to avoid using them.

Directories I start with directories because, in my book, they're at least as important as what they contain. In my case, I keep all data of any type in a first-level subdirectory on my hard disk called DATA. This enables me to back up all the data on my machine just by backing up DATA and its subdirectories. Page 63 Under DATA, I place a subdirectory for each application I use—one for WordPerfect for Windows, one for Delphi, one for Quattro Pro for Windows, and so on. This helps me find a file that's produced by a particular application. This strays from the document-centric philosophy all the vendors seem to be pushing; nevertheless, it works for me. Under each program's subdirectory, I place a subdirectory for each of my projects. For example, the RentalMan project developed in the "Tutorial" section of this book resides in C:\DATA\DELPHI3\RENTMAN. I don't use extensions on my directories. I do use sensible names—names that would make sense to me if, instead of residing on a computer, these directories were files in a file cabinet—because that's what they emulate. All the files for a particular project reside in directories of this type. If a project spans multiple programs, I create an identically named subdirectory under each program's subdirectory.

You might have your own way of doing things, but as I've said, this works for me. NOTE Windows 95 defines a directory that's analogous to the DATA directory I mentioned called My Documents. You might want to store your files under My Documents instead. Also note that Windows 95 introduces the concept of the folder—basically the same thing as a directory. You can create these folders using the Windows 95 Explorer.

Project Names My project names are something sensible that can be squeezed into the 8.3 filenaming convention. Because I never change the extensions of Delphi project files, I'm limited to just eight characters. These always have the same name as the directory in which they reside. Thus, if I move the file elsewhere temporarily or give it to a colleague, I can easily determine its origin. Filenames I strive to name my files sensibly and "expandably." By "expandably," I mean that I regularly designate one to two digits at the end of the filename as sequence digits. These digits enable me to have multiple versions of a file without resorting to renaming the file. That is, this chapter of the book lives in a file named CSD0400. As I make modifications to the file, it becomes CSD0401, CSD0402, and so on. This way I can know what the most recent version is without looking at a time stamp. Furthermore, I can easily move all the files at once or zip them or whatever, using file masks. In a sense, you could equate this scheme to a poor man's source code management system—a manual PVCS. However hackneyed it is, it works. Page 64 Another thing I like to do is prefix my filenames with at least one character identifying the project in which they reside. This gives all the files in a particular project a similar look and makes them easier to manage. For example, the Rental Maintenance Management System that you'll build in the "Tutorial" section of this book uses R as the first character of all its files.

I usually follow this prefix with three to six characters describing the general use of the file. If the file has no purpose more specific than its general use, I use all six characters for the portion of the filename up to the sequence number. If it has both a general and a specific use, I use three characters for the general use and three for the specific use. For example, the Rental Maintenance Management System contains a file whose purpose is to provide an edit dialog box for the CALLS table. This file is named RCALEDT0, for Rental maintenance management system, Call Edit dialog box. You can use a variety of combinations to achieve the desired end. Some people even break the six characters I mentioned into verbs and nouns—the verb describing the action that is to occur and the noun specifying the object of the action. Names and Speech An important aspect of file naming that people often overlook is using names that humans can easily pronounce. Some folks go wild with filenames and make each character in each filename signify something different. Don't do this. These types of mainframe-style names are difficult to remember and more difficult to talk about. You'll often find yourself conversing with a team member about some particular file. Naming the file something you can say easily helps with this. For example, in conversing about RCALEDT0, I might say, "Hey, Joe, you remember that R-Call-Edit file we worked on six months ago?" He'd probably know immediately what I was referring to thanks to the "speech-friendly" naming conventions in use. Unit Names As with filenames, a unit name can be virtually as long as you want it to be, though unit names are only significant through 63 characters. As mentioned previously, I would avoid creating filenames that exceed eight characters in length. The name you give to a unit is used by Delphi to create its corresponding PAS file in the operating system, so using a unit name that's longer than eight characters will create a corresponding file in Windows that is also longer than eight characters. Component Names Because there's no one-to-one relationship between components and filenames, I think you should name components as descriptively as you can bear to type. Name them in a manner that is as spoken-language-like and as sensible to you as possible. Temper your zeal in this with what you are willing to type repetitively. I also recommend the use of mixed case with component names.

Because component names don't allow spaces, mixing the case of the words Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 65 contained therein helps distinguish them from each other. Finally, as you'll see in the next section, I recommend prefixing all Pascal types (include component definitions, which are class types) with a capital T. This is a convention used by Borland throughout Delphi's Visual Component Library (VCL) and it helps make type definitions stand out in code listings. For example, the array table component presented in Chapter 3, "Building Blocks," is named TArrayTable. The T signifies an Object Pascal class type, and ArrayTable describes the compon-ent's function. When setting the Name property of Delphi's built-in components, I use a two- to fourcharacter lowercase mnemonic at the beginning of the name to signify the type of component. For example, a Memo component begins with me; an Edit control begins with ed. The remainder of the name consists of something descriptive, such as edCustomerName or meComments. Table 4.1 summarizes my recommendations for abbreviating Delphi component names. Note that I've included the Form and Data Module classes in this list because you'll want to name them sensibly too. All the dialog components in the list are abbreviated with a two-character mnemonic followed by an underscore (_). This should make them stand out in your code. Note also that, as a rule, all the database controls that correspond to standard components (as, for example, the DBEdit component corresponds to the Edit component) are abbreviated using the letter d followed by the standard component's abbreviation (for example, Edit is abbreviated ed, whereas DBEdit is abbreviated ded). I've done this to make them easier to remember. Table 4.1. Delphi components and suggested abbreviations. Component Animate AutoObject BitBtn BatchMove Bevel BDEProvider BDEResolver Button ColorDialog Calendar ComboBox DdeClientConv Abbreviation an ao bb bm be bp br bt co_ ca cb cc continues

Page 66 Table 4.1. continued Component ClientDataSet DecisionCube DecisionSource DecisionGraph DecisionGrid DecisionPivot DecisionQuery ColorGrid Chart DdeClientItem CheckBox Coolbar ClientSocket ChartFX Database DriveComboBox DBComboBox DBCtrlGrid DBChart DBCheckBox DBEdit DBGrid DBImage DirectoryListBox DBListBox DBLookupComboBox DBLookupCombo DBLookupListBox DBLookupList DataModule DBMemo Page 67 Component DBNavigator DirectoryOutline DataSetTableProducer Abbreviation dna do dp Abbreviation cd cde cds cgp cgr cpi cqu cg ch ci ck co cs cx db dc dcb dcg dch dck ded dgr dim dl dlb dlcb dlco dllb dlli dm dme

DrawGrid DBRadioGroup DataSource DateTimePicker DBText Edit F1Book FilterComboBox FindDialog FileListBox Form FontDialog Gauge GroupBox Graph GraphicsServer HeaderControl HTTPDispatcher Header HotKey HTMLPageProducer HTMLQueryTableProducer HTMLDataSetTableProducer IBEventAlerter ImageList Image Label ListBox

dr drg ds dt dte ed f1 fc fi_ fl fm fo_ ga gb gr gs hc hd he hk hp hq ht ie il im la lb continues

Page 68 Table 4.1. continued Component ListView MaskEdit Memo MainMenu MediaPlayer MemoryDataSet Notebook OpenDialog Abbreviation lv md me mm mp ms nb od_

OleContainer OpenPictureDialog Outline Panel PaintBox PageControl ProgressBar PrintDialog PageProducer PrinterSetupDialog PopupMenu QRBand QRChildBand QRChart QRCompositeReport QRDBCalc QRDBImage QRDetailLink QRDBText QREditor QRExpr QRGroup QRImage Page 69 Component QRLabel QRMemo QueryTableProducer QRPreview QuickReport QRRichText QRSysData QRSubDetail QRShape Query RadioButton RichEdit ReplaceDialog RadioGroup Report RemoteServer

ol op_ ou pa pb pc pr pr_ pp ps_ pu qba qcb qch qcr qdc qdi qdl qdt qed qex qgr qim

Abbreviation qla qme qp qpr qr qrt qsd qsd qsh qu rb re re_ rg rp rs

ScrollBar SaveDialog SpeedButton DdeServerConv SpinEdit Session StringGrid DdeServerItem Splitter StoredProc SavePictureDialog ServerSocket StatusBar SpinButton ScrollBox

sa sa_ sb sc sd se sg si sl sp sp_ ss st su sx continues

Page 70 Table 4.1. continued Component Table TrackBar TabControl Thread Timer TabbedNotebook Toolbar TabSet TreeView UpDown UpdateSQL VtChart VSSpell WebBrowser WebDispatcher WebSession TIP Abbreviation ta tb tc th ti tn to ts tv ud us vc vs wb wd ws

Note that some CASE tools allow you to specify naming conventions for the object names they produce. For example, S-Designor (now called PowerDesignor) allows you to set up naming templates that control the object names it generates. You could use this ability to set up the naming conventions outlined in this chapter as your object name defaults.

Type Names Type names follow much the same conventions as component names. Begin all type definitions with a capital T, and use descriptive words for the rest of the declaration. Limit your names to what you are willing to type repetitively. Use mixed case to make the words in your names more readable. Finally, because all class definitions are types as well, follow these conventions with them. The following is a sample type definition: TRecordMouse = (rmAll, rmNone, rmClicksAndDrags); Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 71 Constant Names When you're typing constants, I recommend the use of all uppercase characters, just as is normally done with #defines in C. This distinguishes constants, which can't change, from variables, which can. For example, the following is a constant declaration from the source to the LiveQuery component mentioned in Chapter 27, "Building Your Own Database Components": DEFAULTCREATEVIEWSQL = `CREATE VIEW %s AS `; Variable Names I use mixed case and complete words for my variable names. Variable names should be descriptive but not so long as to be a pain to type. The following are some examples of good variable names, again from the TLiveQuery source: TemporaryDB : TDatabase; WorkSQL : TStrings; Another technique I've tried on occasion is to prefix each variable name with a single character representing its scope: "l" for local, "g" for global, and "c" for class-specific. This can sometimes help you more easily determine a variable's origin and scope without having to study your application's function, procedure, and class declarations. Procedure Names and Function Names Procedure names and function names should follow variable names fairly

closely. The one exception is that you should make an attempt to distinguish functions that take no parameters from variable names. That is, the following line is ambiguous in that it isn't clear from looking at the code whether SalesTax is a function or a variable name: x:=SalesTax; To disambiguate these two, add the Get prefix to functions that require no parameters. In fact, it's a good idea to prefix most functions with Get, although there are situations in which it doesn't make sense to do so. For example, FileOpen is the name of a built-in Delphi function; prefixing it with Get would be nonsensical. A convention followed by Borland in the VCL source code is the use of Set... for procedures that set a property value and Get... for those that return the value of a property. You could also apply this convention to procedures and functions that are not related to properties.

Object Pascal Coding Styles
Another area in which it pays to standardize is coding styles. You usually find about as many coding styles as people, but if you can at least adhere to a consistent style yourself, you'll find Page 72 your own code easier to read and maintain. I discuss my thoughts on If...else constructs, begin...end blocks, and comment delimiters in the next few sections. As with object names, it's more important to be consistent than to follow any particular coding convention, including mine. If...else Constructs One of the things I like to do in my If...else constructs is delimit the conditional expression with parentheses even though they're not required by default. The reason I do this is twofold. First, I find this style easier to read than those without parentheses. Second, and more important, I can add a second conditional expression without changing the first. Unlike C, Pascal doesn't require parentheses around a single conditional expression, so the following syntax is completely legal: If x=1 then y:=z;

However, two or more expressions in a single If statement require parentheses around each expression: If (x=1) or (x*2=4) then y:=z; If you code an If statement with a single expression as in the first example, adding the second expression requires the placement of parentheses around both the second expression and the first. Add to this that there's no penalty for having parentheses around a single conditional expression, and there's really no reason not to have them. begin...end Pairs Another convention I follow not only with If...else statements but also with other block-oriented constructs is putting the begin of a begin...end block on the same line as the construct itself. In the case of the If statement, this means that the begin is on the same line as the If. Because Pascal is a free-form language, you can actually put the begin anywhere you want—on the same line as the If, on the next line, or down five lines; it makes no difference—the code generated by the compiler is the same. The following is an example of my convention: If (x=1) then begin y:=z; closefile; end; Opinions vary widely on this; some people code If statements in the following way: If x=1 then begin y:=z; closefile; end; Page 73 I dislike this approach because it takes an extra line of screen space. You especially want to avoid this in light of the small default size of Delphi's code window. Furthermore, the appropriate amount of indentation needed by the

begin statement is not immediately clear; should it be indented the same as the If because it's not actual code but a delimiter? Or, should it be indented to the right of the If because the code it contains is dependent on the If? What about the code between the begin...end delimiters? How should it be indented—in relation to the begin or to the If? In my style, the begin is on the same line as the If, so the indentation issue disappears. Furthermore, the end acts as a terminator for the If, which is its actual function anyway. Finally, the code between the two is indented to the right of the If, not the begin, showing its dependency on the If, which, I think, is as it should be. Another approach taken by some developers is to capitalize the begin...end pair. I don't care for this because anything capitalized in a code listing seems to jump off the screen and yell at you. This seems inappropriate for mere delimiters that don't even translate to machine code when the program is compiled. Capitals should be reserved for identifiers that need to get your attention (such as constants), not begin...end pairs that are likely to be all over the place in Delphi applications of any size. Comments Object Pascal supports three different comment styles: (*...*), {...}, and //. For normal commenting—comments that document or explain a section of code—I use {...}. For temporarily commenting out a section of code so that I can see how the program functions without it, I use (*...*). This makes it easy to find sections I've temporarily commented out so that I can uncomment or remove them, as the need arises. For single-line comments or to disable a single line of code, I use //. If you need to nest comments or use one of the delimiters in the text of a comment, you might have to depart from your normal way of doing things. For example, in detailing the syntax of a SendKeys function in its source code, I needed to use the brace delimiters { and } within the comment itself because they are part of the SendKeys syntax. This precluded the use of the braces as comment delimiters because the compiler would have been confused by the ones in the comment's text. Fortunately, I could use the (*...*) pair to delimit the comment.

Database Server Constructs
Over the years, I've learned to name my SQL server constructs consistently to maintain my sanity. Large databases are like large programs; the more sensibly you've named things, the easier you get around. If you name your objects

consistently, they become server road maps—they help you navigate the database. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 74 Not only does using consistent naming conventions aid you in administrating your databases, it also makes queries easier to construct and easier to read. The whole idea of SQL is to provide a query language that is structured, yet spokenlanguage-like and easy to follow. If you name your objects sensibly and consistently, you ensure that your SQL is as easy to ingest as possible. As with Object Pascal program objects, the suggestions provided here are just one way of doing things. It's more important to do what works for you and do it consistently than to follow my conventions to the letter. NOTE Throughout this section I mention my own preference for using mixed case when naming server objects. This goes hand-inhand with my preference for setting up database servers so that they are case sensitive. Of course, if your server is case insensitive (InterBase always is, SQL Server can be installed either way), using mixed case to name database elements is of limited usefulness. Although mixed case serves to make the SQL scripts you write more readable, it may or may not have the desired effect on your server. Your database server may not store or respect the mixed case names you use. For example, even though you use mixed case to name the columns in a table, your server may return them uppercased when you access the table. In cases like this, my recommendation is to uppercase your identifiers and use underscores (_) between the words in them to ensure that they're readable when returned by your server.

Servers It's a little known fact that it's just as important to use a consistent approach when naming resources such as database servers as it is when naming the objects they provide. When you manage a host of similar database servers, naming them consistently can save you lots of confusion. I name database servers using eight character identifiers. I also uppercase them. The first three characters identify the type of server: SQL for Sybase or Microsoft SQL Server, ORA for Oracle Server, SQA for SQL Anywhere, and so forth. The next three characters identify whether the server is a development, test, or production server. They will be either "DEV", "TST", or "PRO". The last two characters consist of a two-digit sequence number that runs from 1 to 99 (zero is reserved, as I'll explain in a moment). They allow me to set up multiple versions of the same server. This convention is best illustrated by example. Suppose you have a Sybase SQL Server that's your one and only development server. You'd name it SQLDEV01. Likewise, the production version of the server would be known as SQLPRO01. When discussing your production server Page 75 with other people, you'd refer to it as Sequel-Pro-One or Sequel-Pro-OhOne—something that's easily spoken and easily distinguishable from other server names. Thus, the server name becomes the beginning of the systematic approach you should take when naming all your database elements. TIP I always reserve sequence zero for special use when naming my servers. I might, for example, create a server named ORADEV00 that's a temporary Oracle development server. Or, I might name the local Sybase server running on my laptop SQLDEV00 because, although it is technically a server, I don't intend for anyone to use it but me. In any event, I've found reserving sequence zero for special use to be handy at times, and you might too.

NOTE

Unless you're a DBA, including a sequence number in server names may seem a bit fastidious. After all, when would you ever have more than one instance of any particular server type, right? Seasoned DBAs know the answer to this question. If you manage a server site of any complexity, you most likely will need multiple development, test, or production database servers at one time or another. You may partition your servers by application, by the departments that use them, or you may set up multiple servers in order to get the best performance out of your applications. Also, you may find that it's safer to create and upgrade separate server machines than to tinker with live production servers, as I'll explain below.

As you manage a given DBMS platform, you'll no doubt need to upgrade the operating system or database server software from time to time. When you do, you'll want to be very careful not to break existing systems. Obviously you wouldn't upgrade a production database server without testing the upgrade first. A common approach is to create a second server machine, install the new database server on it, and then migrate the data from the old server to the new one. This alleviates the possibility of crashing a working server by applying a system update to it. Very often, DBAs upgrade their test or development servers first, and then exchange them with production servers when they're ready to go live. For example, let's say an Oracle DBA wants to update her production server, ORAPRO01, to a new release of the Oracle RDBMS. She might take the following approach: 1. Install the Oracle upgrade on her development server, ORADEV01. 2. Copy existing production data to the upgraded server. Page 76 3. When she's sure that the upgrade causes no problems with existing systems, she could rename the server ORAPRO02, since she already has an ORAPRO01, and switch all production applications to use ORAPRO02 rather than ORAPRO01. On UNIX-based systems, this can be done using automatic host-naming facilities such as DNS. 4. After the new production server is in place, she could rename the original production server, ORAPRO01, to ORADEV01 so that she would again have a development server. 5. Last, she could apply the system update to her new development server (formerly ORAPRO01).

Short of setting up a completely new database server, an approach like this is the safest way to upgrade system software that I know of. And it's made much easier through the server-naming conventions outlined here. (It goes without saying that maintaining good backups at each stage is one's safety net when upgrading any system.) Databases Databases, as covered here, are collections of tables, not the tables themselves. I always name databases in all lowercase. This is significant because some database servers are case sensitive by default. I prefix the first part of the name with up to three characters identifying the system to which the database belongs. If it is used by more than one major application, I give it a more general name. I also usually distinguish master databases from transactional ones using up to four characters after the prefix. A master database is one that contains tables that rarely change and in which you look up names and descriptive-type information. Transactional databases contain data that is much more volatile—it may change daily. Transactional data is usually either keyed in by users or collected from equipment. Sometimes master and transactional databases are created as separate entities, sometimes they're combined into a single database. You can take advantage of their being separate by naming them accordingly. NOTE Some DBMS platforms (for example, InterBase), don't support inter-database references. That is, you can't reference a master table in one database from a transactional table in another. Because of this, be sure you know whether your DBMS platform supports external references when organizing your databases. If your DBMS doesn't support external references, you won't be able to separate transaction databases from master databases as suggested above.

I sometimes include a sequence number as the last digit or two of the database name. This allows for multiple versions of the same database but is rarely used in actual practice. Using Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 89

CHAPTER 5

A No-Nonsense Approach to SQL
Page 90 My goal in this chapter is to remove the mystery surrounding SQL. Despite the fact that it's a relatively simple language, learning SQL can be a daunting prospect, especially to people completely unfamiliar with it. I think this is largely due to the way that SQL is usually presented in books and training materials. Often, every syntax element of every command is presented in one session, along with a good dose of relational database theory, and the reader is left to sort through it all to figure out what's important and what's not. This chapter goes to great lengths to avoid such an approach. It takes you step by step through creating database objects, adding table rows, and querying them. The chapter is pragmatic to the extreme—it attempts to give you the bare essentials of SQL as quickly and succinctly as possible. The idea is to be concise, yet thorough—to cover all the basics of SQL without getting into nonessential topics. I've organized this chapter's SQL tour into two sections. You should be able to work through the first section, "Quick Start," in a single session. The second section ties up a few loose ends by addressing entry-level SQL topics omitted from the "Quick Start" section. Advanced SQL is covered in Chapter 24, "Advanced SQL."

NOTE I don't assume you have any prior knowledge of SQL. If you already have a basic working knowledge of SQL, you might want to skip this chapter and go straight to Chapter 6, "Client/ Server Database Design."

NOTE I do assume that you have an SQL database server of some type and a means of sending SQL commands to it. This chapter uses the Local InterBase server that's included with Delphi Client/Server, though you can use any server that is reasonably compliant with the ANSI '92 SQL specification.

Quick Start
The quickest way to learn to swim in the SQL ocean is to jump in. So without further ado, let's get started. Page 91 NOTE In this chapter, I make a regular point of comparing the two distinct families of SQL syntax to each other. Those two families are the Sybase family and the ANSI family. Sybase SQL Server and its licensed cousin, Microsoft SQL Server, both support an SQL syntax that varies widely from the ANSI standard in many respects. InterBase, Oracle, and most other DBMSs adhere pretty much to the ANSI SQL standard, with each vendor adding its own enhancements. The examples in this chapter use InterBase's syntax, so many of them would require modification to work on SQL Server. Also, be aware that when I mention the Sybase syntax used to do something, the syntax would probably work for Microsoft SQL Server as well. Likewise, when I list InterBase syntax, it's probably a safe assumption that the syntax would work for other ANSI SQLcompliant vendors, including Oracle.

Choosing an SQL Editor One of the first things you need to do is select an SQL editor. An SQL editor is a tool you use for entering and executing SQL commands. All the examples in this chapter use the Windows Interactive SQL (WISQL) utility that comes with the Local InterBase Server. I recommend that you use it in the beginning. If you're a Sybase SQL Server user, you might want to use the included ISQL utility. Oracle users can use SQL*Plus. Otherwise, you might use some thirdparty utility such as Datura's Desktop DBA or Embarcadero's DB Artisan. The tool you use needs to be able to send SQL commands to your server and display any results it returns. SQL Terminators There are two types of SQL terminators: statement terminators and batch terminators. Some SQL dialects require each SQL statement to end with a terminator. InterBase and Oracle, for example, require that statements be terminated with a semicolon (;). Statement terminators are usually requirements of the SQL dialect, not of the tool submitting the SQL to the server. Batch terminators, by contrast, terminate a batch of SQL statements. They cause the batch to be sent to the server for processing. Often, they are tool requirements rather than SQL dialect requirements. For example, Sybase's ISQL utility requires that SQL command batches be terminated with the GO keyword (this can also be changed to something else), even though GO isn't a part of Sybase's Transact-SQL and will result in an error if sent to the server. For the purpose of working through this chapter using InterBase's WISQL, the semicolon terminator is optional. If you run the commands you type into WISQL, they will execute regardless of whether you terminate them with semicolons. Page 92 NOTE

When you compile stored procedures and other constructs on the InterBase platform that include embedded SQL statements, use of a command terminator is not optional, no matter what editor you're using. That is, some SQL commands, including the CREATE PROCEDURE statement, are themselves composed of other SQL statements that you do not want to run when you execute CREATE PROCEDURE in your SQL editor. You want them to run when you execute the procedure itself, not the SQL statement that creates it. To pull this off, you must use the SET TERMINATOR command to temporarily change the editor's default command terminator during the creation of the procedure. This subject is covered in detail in Chapter 24.

Creating a Database You might already have a database in which you can create some temporary tables for the purpose of working through the examples in this chapter. If you don't, creating one is easy enough. In SQL, you create databases using the SQL CREATE DATABASE command. The exact syntax varies from vendor to vendor, but here's the InterBase syntax: CREATE DATABASE "C:\DATA\IB\ORDENT.GDB" USER "SYSDBA" PASSWORD "masterkey"; You could type this syntax into the InterBase ISQL utility to create your database, or you could select the Create Database option on WISQL's File menu to create it by following these steps: 1. Start the Windows Interactive SQL tool (WISQL)—it should be in your Delphi program folder. 2. Select the Create Database option from WISQL's File menu. 3. Type C:\DATA\IB\ORDENT into the Database field of the Create Database dialog, replacing C:\DATA\IB with a valid path. Of course, you might want to create a special directory in advance in which to store your database objects. 4. Type a valid username and password into the appropriate entry boxes. Unless you've changed SYSDBA's password, it should be masterkey. Go ahead and use it for the time being. InterBase passwords are case sensitive, so be careful to enter the password correctly. 5. Click the OK button. InterBase should create the database and connect you to it. In the future, you'll use the Connect to Database option from

the File menu to connect to the database without first creating it. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 88

What's Ahead
In the next chapter, you learn the basics of SQL. I provide a simple, no-frills introduction to SQL for people who aren't yet SQL experts. If you've ever wondered what exactly SQL is and how you can make use of it in your Delphi apps, Chapter 5, "A No-Nonsense Approach to SQL," is for you. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 83 a given column's role in the database obvious. Columns that represent the same entity in a database, such as CustomerNo, should be named identically in all tables. If you abbreviate an element such as Number or Address, abbreviate it consistently in all column names. For example, don't abbreviate Number in the customer number field as CustomerNum, then abbreviate it in the order number column as OrderNo. Abbreviate it consistently everywhere it's used. Examples of good column names include OrderNo, ShippingAddress, SampleWeight, and AverageWellDepth. TIP A real benefit of naming columns descriptively is that you will often be able to use labels that are generated for them by development tools without modifying them. For example, when you drag a field from Delphi's Fields editor, a column label is automatically generated for it on the form. This column label consists of the field's name in the database. If the field was named sensibly, you won't have to change it to make it meaningful to your users. Thus, descriptive names not only make your database schema easier to navigate, they also save you work when developing applications.

Summary
Employing standard conventions when naming your program and database objects can be initially tedious, but it's well worth the effort. Not only will your code be easier to read for others who work on it, it will also be easier for you to read six months after you write it. The same is true for maintaining a consistent coding style. Although you might differ in style from others who look at your work, if you're consistent, they will eventually catch on. Using

good naming conventions and following a consistent coding style takes your mind off trying to remember what something is or where it resides on disk and enables you to focus instead on the more important aspects of database design and application development. I haven't provided naming conventions for every database or program element you might encounter. For example, I don't give you any recommendations on naming Oracle instances or clusters. I'll leave these up to you. The point I'm trying to make here is that it's not as important that you follow my naming conventions as it is that you settle on a set of naming conventions and stick with it. Table 4.2 summarizes the server object-naming conventions I've spelled out in this chapter. Page 84 Table 4.2. Object-naming conventions. Object Convention Case Maximum Length Example in Characters

Server

Database

Three characters to specify the server type, three characters to specify whether the server is a Upper 8 development, test, or production server, and two characters to serve as sequencing digits. Three characters to specify the related application, plus up to Lower 8 five specifying the database's type (for example, transactional, master, and so on).

SQLPRO01

medmast

Data device files

Table

Index

View

Three characters to specify the related medlog00.dat database, three to specify the data file's type (for example, DAT for data, LOG for transaction log, and so on), and two to serve as sequencing digits. Append the extension DAT to the end of device filenames. A single word, spokenlanguage-like identifier. A name that's identical to the base table with the index's order appended to the end. A single word, spokenlanguage-like identifier. You can also append _V to distinguish views from tables.

Lower 8 (plus extension) medlog00.dat

Upper 8

PATIENT

Upper None

PATIENT01

Upper 8

PATIENT or PATIENT_V

Page 85 Maximum Length in Characters

Object

Convention

Case

Example

Stored procedure/ function

Package

Rule

Default

Domain

Constraint/ Exception

A single character that specifies the host mpatlst0 application, three characters that specify the procedure's general purpose, three that identify its specific purpose, and one character that serves as a sequencing digit. A single character that specifies the host mpatupdp application, three characters that specify the package's general purpose and three that identify its specific purpose. A single p (as in "package") is appended to the end. A combination of terms that indicate what type of data the rule either permits or excludes. I prefer permissive rule names rather than restrictive ones. An amalgamation of the associated column and its default value. A multi-word, nounbased identifier TCustomerNo similar to a colmn name with a capital T (as in "type") prefix. A multi-word, messagelike identifier that indicates what raised the exception.

Lower 8

mpatlst0

Lower 8

mpatupdp

Mixed None

NonZero<

Mixed None

StateTexas

Mixed None

TCustomerNo

Upper None

INVALID_ CUSTOMER_NO

Generator/ sequence

The name of the column with which it's associated, with either SEQ (as in Mixed None CustomerNoSEQ "sequence") or GEN (as in "generator") appended to the end.

CustomerNoSEQ

continues Page 86 Table 4.2. continued Object Convention Case Maximum Length in Characters Example

A name corresponding to its associated table, with Cursor either the word UPDATE Upper None or SELECT appended to the end. A name based on its underlying table, with the Trigger database actions that cause Mixed None it to fire appended to the end. A multi-word, noun-based identifier that Column HomePhoneNo fully Mixed None describes the data it contains. Page 87 NOTE

TENANT_ SELECT

TENANT_ ÂInsertUpdate

Home Phone No

As I mentioned earlier in this chapter, remember that some platforms do not properly handle or store mixed case identifiers. On these platforms, you can use all uppercase and separate the elements within identifiers using underscores. For example, HomePhoneNo might become HOME_PHONE_NO on the InterBase platform. Though less aesthetic, the uppercase/underscore combo is a workable alternative to using mixed case identifiers.

Earlier in the chapter, I went through the Object Pascal coding and naming conventions I've adopted. Here's a brief summary of what was discussed:
q

q

q

q

q

q q q

q

q

You should place projects in a sensible directory structure; where you put something is at least as important as what you put there. You should name project files descriptively so that you can easily determine what the project's executable is supposed to do. You should name the project filename and directory identically. At least for now, you should avoid using Windows 95/NT long filenames. Filenames should conform to the 8.3 file-naming convention standard, regardless of their host operating system. Files should also be named such that their names are easy to pronounce and lend themselves to conversation. Although units can have very long names, they should be named the same as their corresponding filenames. This means that they should be limited to eight characters. Good filenames alleviate the need for long unit names. Component class names, as with all Pascal type names, should begin with a T, use mixed case, and describe the component's function. You should name constants in uppercase. You should name variables in mixed case and as spoken-language-like as possible. I personally prefer to surround all Pascal conditional expressions with parentheses, even though it's not always necessary. This makes coding additional conditional expressions easier. I also prefer to place the begin of a begin...end pair on the same line as the statement to which it is related. Object Pascal supports three distinct comment styles. The // style is for single line comments. The {...} and (*...*) sets are for multi-line comments. You should use one set for true comments and the other for commenting code. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 80 For example, I would choose the name RCALLST0 for a procedure that lists the CALLS table in the Rental Maintenance Management System. The R identifies the system that the procedure belongs to, CAL signifies that the procedure works with the CALLS table, LST means that the procedure will list its table, and 0 indicates that this is the first in a possible series of similar procedures. NOTE Both Sybase and Oracle support naming multiple stored procedures using a single base name. In Oracle, you "overload" to designate the actual procedure you want to use. In Sybase, you follow the name of a procedure with a semicolon and the sequence number of its member procedure that you want to use. I recommend you avoid using either of these facilities. They're unwieldy and not worth the confusion they often cause.

Packages Because they're encapsulations of stored procedures and functions, I name packages similarly to stored procedures and functions. I name all objects using lowercase and I use a single initial character to identify the application to which the package belongs. I follow this first character with a three- to sixcharacter section identifying the package's general and specific functions. Finally, I append the letter p to the end of the package's name, in the position normally occupied by the sequence digit of stored procedure and function names.

Rules If your server supports rules, you should name them descriptively and use mixed case. You should also be consistent regarding whether your rule names are restrictive or permissive. Permissive rule names indicate the sets of data the rule permits, not what it doesn't permit. Restrictive rule names indicate the sets of data excluded by the rule, not the sets it allows. Personally, I prefer the permissive convention. For example, I'd name a rule that prevents the entry of zero into a field NonZero. I'd name a rule that forces an entry of M or F into the Sex field MaleFemale. SQL queries rarely refer to rules, so their names are to a large degree insignificant. Also, rules are the types of database objects that won't likely have individual SQL CREATE scripts, so there's no reason to keep their names to eight characters. Page 81 Defaults If your server supports column default objects, you should name them for the field to which they correspond (if they correspond to just one) and for the default value they supply, using mixed case letters. That is, a default that is bound to the Sex field and defaults to F should be named SexFemale. SQL queries rarely refer to defaults directly, so their names are relatively unimportant. Furthermore, defaults are the types of objects that are not likely to have their own SQL CREATE scripts, so there's no reason to limit their names to eight characters. Domains Because domains (user-defined data types) perform a function that's comparable to Object Pascal data types, I like to name them similarly. I name them after the column they will define, prefixed by the letter T as we do with Object Pascal types. For example, a domain that will be used to define the CustomerNo column would be named TCustomerNo. This signifies that the domain defines the CustomerNo column and, within a CREATE TABLE statement, distinguishes the domain from built-in data types. A domain is the type of database object that is not likely to have its own SQL CREATE script, so there's no reason to keep its name to eight characters. Constraints and Exceptions I name constraints and exceptions using a message-type approach that

identifies what the problem is when a constraint is violated or an exception is raised. The idea behind this approach is to notify the user of the problem using the name of the object alone because some tools do not relay server-based messages (particularly messages associated with constraints) reliably. These tools often return names in uppercase, regardless of how they are stored in the database, so I also avoid using mixed case. For example, a foreign key constraint that ensures that a valid customer number is entered might be called INVALID_CUSTOMER_NO. This way, if the constraint is violated and the front-end tool at least relays the name of the violated constraint from the server, the user will have an idea of what the problem is. I take the same approach with exceptions, though hopefully the text of the exception itself, not its name, will be displayed by the front-end tool. Exceptions and constraints are other types of objects that are not likely to have individual SQL CREATE scripts, so there's no reason to limit their names to eight characters. Generators and Sequences As with domains, I name generators and sequences after the column they service. I also append either GEN or SEQ to the end of the names I use. For example, a generator that services the Page 82 CustomerNo column would be named CustomerNoGEN. A sequence that's going to be associated with the OrderNo column would be named OrderNoSEQ. Generators and sequences are the types of objects that are not likely to have their own SQL CREATE scripts, so there's no need to limit their names to eight characters. Cursors I usually name cursors after the tables they access. I also like to distinguish cursors that allow updates from those that don't. I do this by tacking on either the word UPDATE or the word SELECT to the end of the identifier. For example, a cursor declared on the TENANT table that allows updates and deletions might be named TENANT_UPDATE. A cursor referencing the PROPERTY table that does not allow updates would be named PROPERTY_SELECT. Triggers Triggers should be named for their associated tables, followed by the SQL

commands that cause the triggers to fire. A trigger can be activated by an SQL INSERT, UPDATE, or DELETE, so this command list will be some combination of the words Insert, Update, and Delete. For example, a trigger that executes when a row is inserted or updated in the TENANT table might be named TENANTInsertUpdate. One that executes when a record is deleted from the WORDERS table might be named WORDERSDelete. If your server allows you to specify whether a trigger fires before or after one of these events, you can include the Before or After modifier in the name as well. You might, for example, name a trigger PROPERTYBeforeDelete (or PROPERTY_BEFORE_DELETE) that fires before an SQL DELETE statement is executed against the PROPERTY table. Some platforms allow you to specify whether a trigger is a row trigger or a statement trigger. For those platforms, I append either an R or an S to the end of trigger names, like so: PROPERTY_BEFORE_DELETE_S. This tells me at a glance the type of trigger with which I'm dealing and identifies its associated table. Triggers are the types of database objects that won't likely have individual SQL CREATE scripts, so there's no reason to limit their names to eight characters. Columns Columns are the most popular object in SQL queries, so naming them descriptively is important. Column names should be mixed case and spokenlanguage-like. If your server has been set up to be case insensitive, place underscores between the words in your column names. Don't be afraid to be a little wordy with these names; those who have to maintain your SQL in years to come will thank you for it. The names you use should consist of enough identifiers to make Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 93 Creating Tables After a database has been established, you're ready to get started with building database objects. Begin by creating three tables by using the SQL CREATE TABLE statement. Enter the following syntax into your SQL editor and execute it: CREATE TABLE CUSTOMER ( CustomerNumber int NOT NULL, LastName char(30) NOT NULL, FirstName char(30) NOT NULL, StreetAddress char(30) NOT NULL, City char(20) NOT NULL, State char(2) NOT NULL, Zip char(10) NOT NULL ) This builds the CUSTOMER table. Next, build the ORDERS table using similar syntax: CREATE TABLE ORDERS ( OrderNumber int NOT NULL, OrderDate date NOT NULL, CustomerNumber int NOT NULL, ItemNumber int NOT NULL, Amount numeric(9,2) NOT NULL ) Now that the ORDERS table is built, only one table remains. Create the ITEMS table using the

following syntax: CREATE TABLE ITEMS ( ItemNumber int NOT NULL, Description char(30) NOT NULL, Price numeric(9,2) NOT NULL )

NOTE Using the NULL/NOT NULL designation is optional on most servers. Note, however, that they default the designation differently. InterBase, for example, complies with the ANSI SQL standard and defaults columns to NULL if you do not indicate a preference. By default, SQL Server uses NOT NULL if neither is specified. As a rule, it's a good idea to always specify one or the other. InterBase is the one exception to this rule. InterBase will not allow you to explicitly designate a column as NULL, because NULL is the default. If you attempt to use NULL with an InterBase column definition, your SQL statement will fail.

Page 94 Inserting Data The SQL INSERT statement is used to add data to a table one row at a time. Use the following syntax to add data to each of the three tables. First, add three rows to the CUSTOMER table by executing the following syntax in your SQL editor: INSERT INTO CUSTOMER VALUES(1,'Doe','John','123 Sunnylane','Anywhere','OK','73115') INSERT INTO CUSTOMER VALUES(2,'Doe','Jane','123 Sunnylane','Anywhere','OK','73115') INSERT INTO CUSTOMER VALUES(3,'Citizen','John','57 Riverside','Reo','MO','65803') Now, add four to the ORDERS table using this syntax: INSERT INTO ORDERS VALUES(101,'07/07/97',1,1001,123.45)

INSERT INTO ORDERS VALUES(102,'07/08/97',2,1002,678.90) INSERT INTO ORDERS VALUES(103,'07/09/97',3,1003,86753.09) INSERT INTO ORDERS VALUES(104,'07/10/97',1,1002,678.90)

NOTE If you're inserting these rows into Oracle tables, you need to ensure that you use the correct date format. Oracle's default date format varies based on your locale and the setting of the NLS_DATE_FORMAT initialization parameter. On my server, the default is `DD-MMM-YY', so the ORDER table's INSERT statements would read INSERT INTO ORDERS VALUES(101,'07-JUL-97',1,1001,123.45) INSERT INTO ORDERS VALUES(102,'08-JUL-97',2,1002,678.90) INSERT INTO ORDERS VALUES(103,'09-JUL-97',3,1003,86753.09) INSERT INTO ORDERS VALUES(104,'10-JUL-97',1,1002,678.90) INSERT INTO ORDERS VALUES(101,TO_DATE('07/07/97','MM/DD/YY'),1,1001,123.45)

Page 95

INSERT INTO ORDERS VALUES(102, TO_DATE('07/08/97','MM/DD/YY'),2,1002,678.90) INSERT INTO ORDERS VALUES(103, TO_DATE('07/09/97','MM/DD/YY'),3,1003,86753.09) INSERT INTO ORDERS VALUES(104, TO_DATE('07/10/97','MM/DD/YY'),1,1002,678.90)

Finally, add three rows to the ITEMS table with this syntax: INSERT INTO ITEMS VALUES(1001,'WIDGET A',123.45) INSERT INTO ITEMS VALUES(1002,'WIDGET B',678.90) INSERT INTO ITEMS VALUES(1003,'WIDGET C',86753.09) Note that none of the INSERT statements specifies a list of fields, only a list of values. This is because the INSERT command defaults to inserting a value for all the columns in a table in the order in which they appear in the table. You could have specified a field list for each insert using the following syntax: INSERT INTO ITEMS (ItemNumber, Price) VALUES(1001,123.45) Also note that you don't need to follow the order the fields appear in the table when you specify a field list; however, the list of values must match the order you specify. Here's an example of that syntax: INSERT INTO ITEMS (Price, ItemNumber) VALUES(123.45, 1001) The SELECT Command You can quickly check the contents of each table using the SQL SELECT command. Issue a SELECT * FROM tablename, replacing tablename with the name of the table you want to check (for example, CUSTOMER, ORDERS, or ITEMS). At this point, the CUSTOMER and ITEMS tables should have three rows each, and the ORDERS table should have four.

Figure 5.1 shows the CUSTOMER table as it appears when SELECT * FROM CUSTOMER is issued in the WISQL utility. Page 96 Figure 5.1. The WISQL output for CUSTOMER.

Figure 5.2 shows the ORDERS table as it appears when SELECT * FROM ORDERS is issued in the WISQL utility. Figure 5.2. The WISQL output for ORDERS.

Figure 5.3 shows the ITEMS table as it appears when SELECT * FROM ITEMS is issued in the WISQL utility. Page 97 Figure 5.3. The WISQL output for ITEMS.

NOTE

Note that the implementation of the FROM clause varies from platform to platform. It's required on some platforms and optional on others. For example, Oracle and InterBase SELECT statements require a FROM clause, even if you don't select any table columns. Sybase SQL Server, by contrast, requires only that you include a FROM clause when selecting columns from a table. SELECT statements that do not involve table columns (for example, those involving only expressions or system functions) do not require a FROM clause in SQL Server SELECT statements. For example, if you select an SQL Server system function such as getdate(), you don't need a FROM clause, whereas if you select Oracle's SYSDATE system function, you must include one.

The SELECT * syntax causes all the columns in the table to be returned. You could change it to use a comma-delimited field list instead, like so: SELECT CustomerNumber, LastName, State FROM CUSTOMER This syntax qualifies what fields you'd like to see. Figure 5.4 lists the output from this query. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 98 Figure 5.4. A SELECT statement that uses a field list.

Expression Columns A column in the SELECT statement's column list can consist of more than just table columns. It can also consist of expressions containing absolute values and functions. Some SQL dialects support functions that return useful data without even referencing a column. For example, here's the SQL syntax to return the current date and time on a Sybase or Microsoft SQL Server: SELECT getdate() Here's InterBase SQL syntax to return the last name of each customer in the customer table in uppercase (see Figure 5.5): SELECT UPPER(LastName), FirstName FROM CUSTOMER

Figure 5.5. Using the UPPER

function in a SELECT statement.

Page 99 Aggregate Columns Aggregate columns are actually functions that perform some calculation on a set of data. Examples of aggregates are the COUNT, SUM, AVG, MIN, STDDEV, VARIANCE, and MAX functions. Following are some examples of their use. SELECT COUNT(*) FROM CUSTOMER The preceding statement tells you how many customer records are on file. S ELECT MAX(Amount) FROM ORDERS The preceding statement reports the dollar amount of the largest order on file, whereas the following statement returns the total dollar amount of all orders on file: SELECT SUM(Amount) FROM ORDERS The WHERE Clause The SQL WHERE clause is used to qualify the data returned by a SELECT statement. Here are some examples: SELECT * FROM CUSTOMER WHERE State='OK' This example returns only those customers who reside in Oklahoma (see Figure 5.6).

Figure 5.6. Using a WHERE clause in a SELECT statement.

As illustrated by Figure 5.7, the following example returns only those customers whose street address contains the word "Sunny." SELECT LastName, StreetAddress FROM CUSTOMER WHERE StreetAddress LIKE `%Sunny%' Page 100 Figure 5.7. Using a WHERE clause with LIKE in a SELECT statement.

The following example returns only the orders exceeding $500, as shown in Figure 5.8: SELECT OrderNumber, OrderDate, Amount FROM ORDERS WHERE Amount > 500

Figure 5.8. Orders exceeding $500.

The following example returns only those orders occurring between July 8 and July 9, 1997, inclusively. (See Figure 5.9.) SELECT OrderNumber, OrderDate, Amount FROM ORDERS WHERE OrderDate BETWEEN '07/08/97' AND '07/09/97' Page 101 Figure 5.9. Using the WHERE clause with BETWEEN.

Joins The WHERE clause also is used to join one table with another to create a composite result set. Joining tables to one another consists of two changes to the basic SELECT statement syntax: You specify additional tables in the SELECT statement's FROM clause, and you link related fields using the WHERE clause. (See Figure 5.10.) Here's an example: SELECT CUSTOMER.CustomerNumber, ORDERS.Amount FROM CUSTOMER, ORDERS WHERE CUSTOMER.CustomerNumber=ORDERS.CustomerNumber

Figure 5.10. The CUSTOMER table joined with the ORDERS table.

Notice the inclusion of the ORDERS table in the FROM clause. Also notice the use of the equal sign to join the CUSTOMER and ORDERS tables using their

CustomerNumber fields. The table on the left of the equal sign is said to be the outer table, and the one on the right is the Page 102 inner table. They're also commonly referred to as the left and right tables, respectively, indicating their positions in a left (left-to-right) join—the most popular type of join used in relational database management systems. Inner Joins Versus Outer Joins The type of left join just mentioned is formally known as an inner join. An inner join returns only rows if the join condition is met. This is in contrast to an outer join, which returns rows regardless of whether the join condition is met. When the join condition is not met for a given row in an outer join, fields from the inner table are returned as NULL. The syntax for constructing an outer join varies based on the server and the ANSI SQL level it supports. There are two versions of the basic outer join syntax. The Sybase/Microsoft SQL Server syntax looks like this: SELECT CUSTOMER.CustomerNumber, ORDERS.Amount FROM CUSTOMER, ORDERS WHERE CUSTOMER.CustomerNumber*=ORDERS.CustomerNumber Note the use of the asterisk to the left of the equal sign. This signifies a left outer join on the SQL Server platform. Outer joins can also be right outer joins, but left outer joins are by far more popular. The SQL Server syntax for a right outer join is similar to that of the left outer join, with the asterisk located on the right side of the equal sign rather than on the left, like so: SELECT CUSTOMER.CustomerNumber, ORDERS.Amount FROM CUSTOMER, ORDERS WHERE CUSTOMER.CustomerNumber=*ORDERS.CustomerNumber Right outer joins are most useful in finding orphans—keys that exist in the table on the right but not in the one on the left. ANSI Join Syntax The ANSI syntax for a left outer join looks like this (see Figure 5.11):

SELECT CUSTOMER.CustomerNumber, ORDERS.Amount FROM CUSTOMER LEFT OUTER JOIN ORDERS ON CUSTOMER.CustomerNumber=ORDERS.CustomerNumber For right outer joins or left inner joins, you simply replace LEFT with RIGHT, or OUTER with INNER, respectively. NOTE InterBase doesn't support the Sybase convention of constructing outer joins in the WHERE clause; it supports the ANSI syntax only. It does, however, support constructing inner joins using the WHERE clause.

Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 103 Figure 5.11. The CUSTOMER and ORDERS tables joined using an ANSI-SQL outer join.

Subqueries A subquery is a SELECT statement within the WHERE statement of a query that's used to qualify the data returned by the query. (See Figure 5.12.) You generally use a subquery to return a list of items that you then use to qualify the calling query. Here's an example: SELECT * FROM CUSTOMER WHERE CustomerNumber IN (SELECT CustomerNumber FROM ORDERS) Figure 5.12. A subquery within the WHERE clause of another query.

GROUP BY Because SQL is a set-oriented rather than a record-oriented query language, statements that group data are integral to the language, and, in concert with aggregate functions, are the means by which the real work of data retrieval is performed. dBASE programmers find this approach Page 104 unusual because they are accustomed to working with data on a record-by-record basis. Looping through a table in order to generate summary information is the way things are normally done in PC database products—but not in SQL. A single SQL statement can do what 10 or even 50 lines of Xbase code can. This magic is performed by using the SELECT statement's GROUP BY clause in conjunction with SQL's aggregate functions. Here's an example of the use of GROUP BY: SELECT CUSTOMER.CustomerNumber, sum(ORDERS.Amount) TotalOrders FROM CUSTOMER, ORDERS WHERE CUSTOMER.CustomerNumber=ORDERS.CustomerNumber GROUP BY CUSTOMER.CustomerNumber This query returns a list of all customers, along with the total amount of each customer's orders. (See Figure 5.13.)

Figure 5.13. A query that uses the GROUP BY clause.

How do you know which fields to include in the GROUP BY clause? The rule of thumb is to include all the fields from the SELECT statement's column list that are not aggregate functions or absolute values. Take the following SELECT statement as an example:

SELECT CUSTOMER.CustomerNumber, CUSTOMER.LastName, [ccc] CUSTOMER.State, sum(ORDERS.Amount) TotalOrders FROM CUSTOMER, ORDERS WHERE CUSTOMER.CustomerNumber=ORDERS.CustomerNumber If you'd written that statement, you'd need the following GROUP BY clause: GROUP BY CUSTOMER.CustomerNumber, CUSTOMER.LastName, CUSTOMER.State Some servers, including both Oracle and InterBase, enforce this rule; they won't attempt to execute queries that break it. Others, including SQL Server, will attempt to execute the query, Page 105 though the results returned will probably not be what you'd expect. For example, consider the following query: SELECT CUSTOMER.LastName, COUNT(*) NumberWithName FROM CUSTOMER It might appear that this query would list all the last names found in the CUSTOMER table, giving a count for the number of occurrences for each, but that's not the case. As I've said, InterBase won't even execute this query; it's missing its GROUP BY clause. SQL Server, on the other hand, will execute it, but it renders a count of all the records in the table for each row returned. This means that the count returned refers to all the records in the table, not just those with a particular last name. A dead giveaway that a query is flawed in this way is when its aggregate function results are identical in each result row. A query suffering from this malady will run forever against large sets of data. In this example, NumberWithName would list the total number of rows in the table, in each returned result row. Properly written, the preceding query looks like this: SELECT CUSTOMER.LastName, COUNT(*) NumberWithName FROM CUSTOMER GROUP BY CUSTOMER.LastName This query lists the number of customers with each last name from the CUSTOMER table, as illustrated in Figure 5.14.<

Figure 5.14. The results as they should appear when the query is constructed

properly.

HAVING The HAVING clause is used to limit the rows returned by a GROUP BY clause. Its relationship to the GROUP BY clause is similar to the relationship between the WHERE clause and the SELECT itself. It works like a WHERE clause on the rows in the result set rather than on the rows in the query's tables. (See Figure 5.15.) Page 106 Figure 5.15. Using the HAVING clause to limit the rows returned by a GROUP BY clause.

There is almost always a better way of qualifying a query than by using a HAVING clause. In general, HAVING is less efficient than the WHERE clause because it qualifies the result set after it has been organized into groups; WHERE does so beforehand. Here's an example of the use of the HAVING clause: SELECT CUSTOMER.LastName, COUNT(*) NumberWithName FROM CUSTOMER GROUP BY CUSTOMER.LastName HAVING CUSTOMER.LastName<>'Citizen' Properly written, this query should place its selection criteria in its WHERE clause, not its HAVING clause, like so: SELECT CUSTOMER.LastName, COUNT(*) NumberWithName FROM CUSTOMER

WHERE CUSTOMER.LastName<>'Citizen' GROUP BY CUSTOMER.LastName Some servers (such as Sybase SQL Server) recognize HAVING clause misuse and correct it when they optimize a query for execution. Others don't detect it, so you'll have to be careful, especially when querying extremely large tables. On platforms that don't optimize errant HAVING clauses, HAVING clause misplacement usually inhibits index use when resolving queries. ORDER BY You use the ORDER BY clause to order the rows in the result set. (See Figure 5.16.) Here's an example: SELECT LastName, State FROM CUSTOMER ORDER BY State Page 107 Figure 5.16. Using the ORDER BY clause.

Here's another example: SELECT FirstName, LastName FROM CUSTOMER ORDER BY LastName Column Aliases You might have noticed that I use logical column names for aggregate functions such as COUNT() and SUM(). Labels such as these are known as column aliases and serve to make the query and its result set more readable. In ANSI SQL, you place a column alias immediately to the right of its corresponding column in the SELECT statement's field list. For example, in the earlier discussion on the GROUP BY clause, the column alias of the COUNT() aggregate is the NumberWithName label. You can use column aliases for any item in a result set, not just aggregate functions. For example, the following example substitutes the column alias LName for the LastName column in

the result set: SELECT CUSTOMER.LastName LName, COUNT(*) NumberWithName FROM CUSTOMER GROUP BY CUSTOMER.LastName Note, however, that you cannot use aliases in other parts of the query, like the WHERE or GROUP BY clauses. You must use the actual column name or value in those parts of the SELECT statement. SQL Server supports a variation of this syntax that places the column alias to the left of the column, separating it from the column name with an equal sign, as in the following: SELECT CUSTOMER.LastName, NumberWithName=COUNT(*) FROM CUSTOMER GROUP BY CUSTOMER.LastName Oracle and InterBase do not support this syntax. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 108 Table Aliases Rather than always having to specify the full name of a table each time you reference it in a SELECT command, you can define a shorthand moniker for it to use instead. You do this by specifying a table alias for the table in the FROM clause of the SELECT statement. Place the alias to the right of the actual table name, as illustrated here: SELECT C.LastName, COUNT(*) NumberWithName FROM CUSTOMER C GROUP BY C.LastName Notice that the alias can be used in the field list of the SELECT list before it is even syntactically defined. This is possible because references to database objects in a query are resolved before the query is executed. The Finish Line This concludes the "Quick Start" part of the SQL tour. You should now be able to create a database, create tables, and populate those tables with data. You should also be familiar with the basic mechanics of querying tables using the SQL SELECT command.

A Few Additional Comments
The following material ties up a few loose ends and covers other entry-level SQL topics not covered in the "Quick Start" section of this chapter. Chapter 24 continues the discussion on SQL in greater depth.

The CONNECT Command In most SQL dialects, you use the CONNECT command to change the "database context"—to connect to and use a specific database. Both InterBase and Oracle use this syntax. Use the DISCONNECT command to reverse a CONNECT command. With SQL Server, the USE command changes the database context. Unlike CONNECT, USE provides access to only one database at a time. There is also no equivalent to DISCONNECT in SQL Server's SQL dialect. The UPDATE Command You'll eventually want to change the data you've loaded into a table. You use the SQL UPDATE command to do this. It works much like the dBASE REPLACE ALL command. Here's the syntax: UPDATE CUSTOMER SET Zip='90210' WHERE City='Beverly Hills' Page 109 Although the WHERE clause in the preceding query might cause it to change only a single row, depending on the data, you can update all the rows in the table by omitting the WHERE clause: UPDATE CUSTOMER SET State='CA' You can also update a column using columns in its host table, including the column itself, as in UPDATE ORDERS SET Amount=Amount+(Amount*.07) In SQL Server's Transact-SQL, you also can update the values in one table with those from another. Here's an example: UPDATE ORDERS SET Amount=Price

FROM ORDERS, ITEMS WHERE ORDERS.ItemNumber=ITEMS.ItemNumber The DELETE Command You use the DELETE command to delete rows from tables. To delete all the rows in a table, use this syntax: DELETE FROM CUSTOMER Some servers provide a quicker, table-oriented command for deleting all the rows in a table, like the dBASE ZAP command. The SQL Server syntax is TRUNCATE TABLE CUSTOMER The DELETE command can also include a WHERE clause to limit the rows deleted. Here's an example: DELETE FROM CUSTOMER WHERE LastName<>'Doe' COMMIT and ROLLBACK A group of changes to a database is formally known as a transaction. The SQL COMMIT command makes a transaction permanent. Think of it as a database save command. ROLLBACK, on the other hand, throws away the changes a transaction might make to the database; it functions like a database undo command. Both of these commands affect only the changes made since the last COMMIT; you cannot roll back changes you've just committed. On some platforms, including SQL Server, you must expressly start a transaction in order to explicitly commit it or roll it back. InterBase's WISQL utility begins a transaction automatically (by issuing the equivalent of the InterBase SET TRANSACTION command) when it first loads. Page 110 When you exit the utility, it asks whether you'd like to commit your work. You can commit or roll back your work at any time using the Commit Work and Rollback Work options on WISQL's File menu.

Summary
This chapter introduced you to the lingua franca of relational databases: SQL. Though its syntax has been extended in many different directions by DBMS vendors, SQL is basically a very simple language, as I'm sure you've discovered.

What's Ahead
The next chapter discusses the basics of database design. You'll find that the skills you've acquired in this chapter will prove essential in constructing databases and database applications. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 111

CHAPTER 6

Client/Server Database Design
Page 112 The Silverrun tools used in the next three chapters are one of many tool sets available to a Delphi programmer. The selection of Silverrun for these samples was the author's choice and does not reflect an endorsement by Borland. This chapter and the one that follows cover client/server application development from a pragmatist's point of view. They introduce various concepts of database theory and application design and show you how to apply those concepts in your work. These chapters are aimed mainly at those with limited database application development experience. If you're already familiar with basic relational database and application design, you can simply create the database objects listed in this chapter and go straight to Chapter 8, "Your First Real Client/Server Database Application."

General Approach
My approach to database application design may differ from what you've seen in other books. Rather than go into a long discussion of database theory, I'd prefer to be more pragmatic—more practical. I don't think discussing the fine details of E. F. Codd's historic paper or the mathematics behind database normalization would be terribly beneficial or even appropriate in a book such as this one. There are plenty of books out there already that cover those

subjects in depth. Instead, I'll focus my attention on helping you design and build real applications. I'll begin with a solid theoretical foundation, then move on to exploring how to apply theoretical concepts in real applications. One of the most difficult things about writing technical books is the need to balance theory with "how-to" information. If a book leans too much to the howto side of things, it becomes a glorified manual. It attempts to answer the "How?" without addressing the "Why?" On the other hand, if a book is bent too much on abstract concepts, it can fail to be useful from a practical standpoint. People usually buy computer books to learn how to use a particular piece of technology or to perform a given technical task. Books that fail to teach anything of practical value are of little use to most computer practitioners. The ideal place to be, then, is somewhere between the two extremes. That's a goal I've attempted to achieve in this book in general and in this chapter in particular. Although I discuss how to use specific tools in designing databases and the like, I also try to give you a solid theoretical foundation on which to base the practical information you're learning. Hopefully, you'll find both "Why?" and "How?" answered effectively.

The Five Processes
Client/server database application development can be broken into five steps or general processes. These processes outline the normal procedure one follows when building database applications. Think of them as the tenets of client/ server craftsmanship. You should follow them in every application you build. Page 113 The five general processes for building client/server database applications are as follows:
q q

q

q

q

Define the purpose and functions of the application. Design the database foundation and application processes needed to implement those functions. Transform the design into an application by creating the requisite database and program objects. Test the application for compliance with the predefined purpose and functions. Install the application for production use.

To put things a little more succinctly, these five processes can be reduced to

the following five phases:
q q q q q

Analysis Design Construction Testing Deployment

NOTE The construction phase has historically been referred to as the coding phase. However, in this age of visual development, coding doesn't seem to be as applicable as it once was, so I've changed it to "construction." This term works regardless of whether coding, visual development, or application generation is involved in constructing the project.

I'll make a habit in this chapter and elsewhere of referring to the five processes using these easier-to-digest monikers. They are more concise than the formal definitions of the five processes, and they're terms with which most software developers are already intimately familiar. They drive home the point that developing client/server database software is just like developing any other kind of software; there is a methodical approach you usually take to produce the results you want. More on the Five Phases When you build anything—whether it's a software product, a house, a statue, anything—you go through a series of phases or steps that hopefully results in a finished product. The procedure is no different when you build applications, whether they're database applications or not. This series of steps isn't something you need to commit to memory and hope you don't forget. On the contrary, it's completely intuitive and is born of simple logic. Rather than some rigid Page 114 code that you must always dutifully follow, the five general processes I outlined earlier are completely natural; they occur instinctively to those who build things.

As far as the actual work of constructing a database application is concerned, you can focus on just the first three of the five general processes. It's during the first three phases that the application is actually developed. Most of the challenges of building client/server database apps lie in these three phases. Analysis The first application development phase is the analysis phase. Here, you analyze what your application needs to do. You begin with a general statement of purpose, then qualify the statement with specific functions that the app must perform or allow the user to perform in order to serve its intended purpose. These functions are defined in terms of operational, functional, and technical requirements. Operational requirements might state that the app has to perform some process within a given window of time. Functional requirements might stipulate the inclusion of a given entry form or the enforcement of a given business rule. Technical requirements might require that the app runs over a particular type of network operating system or communicates via a given packet protocol. These key functions drive the business process modeling task. This process modeling consists of describing the underlying business processes, resources, and data flows necessary for the application to meet its basic requirements. Design The second phase is the design phase. Here, you turn the analysis you performed in the first phase into a logical design. You translate the business processes modeled during your analysis into logical application and database design elements. This logical design describes what you're building in specific terms. The focus shifts from what the application will do to how it will do it. Here, you determine the application and database components necessary to implement the business process models defined in the analysis phase. One of the ways you do this is through logical data modeling and Entity Relationship (E-R) diagrams. The end result is that you'll develop a separate design for each layer of your client/server application. Construction The third database application development phase is the construction phase. Here, you turn the logical design you developed in the design phase into physical objects. This means that the logical database design you created will be translated into real database objects. Likewise, the application design you produced during the design phase will materialize as forms, code, and other

program objects. The last two of the five phases—testing and deployment—are postdevelopment concerns, so I won't get into them much here. Often, they result in a return to the first three phases, anyway (through new bugs being detected or requests for enhancements from users), so I'll only touch on them for now. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 115

On the Complexities of Client/Server Development
The process of developing client/server database applications is straightforward and intuitive. It's not unlike the approach one takes in building any other type of application—regardless of whether the app accesses databases. This might seem a bit simplistic, but it's from this perspective that I approach the task of developing client/server database applications. The difficulty that arises when applying this basic methodology to client/server application development is that there are at least two distinct pieces of even the simplest application: the server portion and the client portion. Moreover, the objects that end up being created on the database server are foundational to the application. That is, it's advantageous, if not man- datory, that they exist before the application itself is constructed. Also, the app won't work correctly if the database has problems. This duality doubles the work necessary to construct robust applications. Not only must the application be analyzed, designed, and constructed, the database must also go through the same process. In addition to this client- and server-side work, there is also the issue of getting the client and server to communicate with one another. Sometimes this is a task in and of itself. Adding middleware to the mix makes things even more interesting. There's no easy way to completely remove the complexities surrounding client/ server applications. CASE tools help, but applications must still be developed by developers. Client/server apps are complex, multi-layered pieces of software; it's understandable that creating them requires some amount of expertise. Hopefully, the insights shared in this book will help you acquire the skills you need to get the job done.

This chapter and the one that follows take you through the five phases of client/ server appli-cation development from a practical standpoint. In this chapter, I go through the analysis, design, and construction phases as they relate to databases. Chapter 7, "Client/Server Application Design," shows how these phases apply to application development. In addition, Chapter 7 covers database application testing and deployment. The testing phases for databases and applications are covered simultaneously out of necessity. It's difficult to try out a database without an application with which to test it. And, obviously, a database application can't really be tested unless the database objects it requires are in place. The deployment phases for both databases and database applications are covered concurrently out of necessity, as well. Because the database and application are normally a matched set, you'll usually deploy them at the same time. This is why it's important to understand what's involved with deploying them together. After the construction phase is complete, the physical elements of the database and application have been built, so it makes perfect sense to test and deploy them together. For this reason, Chapter 7 covers the deployment of databases and their associated applications in one pass. Page 116 The Diverse Nature of Client/Server Applications Perhaps the best way to approach the multifaceted nature of client/server development is to make a separate pass through the three primary development phases for each layer. That is, you could analyze, design, and construct your application's database first, then repeat the process for the application itself. If middleware is involved, you could follow this same three-step approach with it as well. That's the approach I've taken in this book. Developing in this manner helps ensure that your database objects are present before beginning to construct your application or associated middleware objects, which will save you development time. As I've said, you can't completely isolate a database from its applications, but my normal practice is to create database elements in advance of creating their dependent program and middleware elements. Database Theory Applied Something you'll notice about this chapter and Chapter 7 is that I consider

database design and database application design to be intrinsically linked. This seems obvious enough to me, though there are those who feel the two are completely different disciplines. From the pragmatist's point of view, the process of designing a database is foundational to the process of designing applications that use it. One certainly affects the other. I've never designed a database that wasn't intended to be accessed in some way by an application. You have to remember that the database is just a means to an end; it's your way of servicing a customer's request. Similarly, the client application functions as the conduit between the user and the database server. It, too, is just a tool for servicing your customer. Both the client app and the database server must work together to produce client/server solutions your users find acceptable. In this chapter, I'll take you through the process I use when designing client/ server database applications. I'll base the discussion on a solid understanding of database theory, with the emphasis on the real work of constructing real applications. NOTE This chapter focuses exclusively on client/server database design. You won't see local DBMSs such as Paradox, dBASE, and Access covered here. True client/server database platforms resemble local DBMSs very little. Consequently, the approach one takes to designing client/server versus local databases is quite different. Because this book is about client/server database development, it focuses on client/server DBMSs.

Page 117

Defining the Purpose of the Application
The first step in designing any application—database or non-database—is to define the purpose of the application. What is the application to do? In this chapter, we'll begin designing an application for a fictional company named Allodium Properties. The purpose of the application you'll build will be to help Allodium manage its many rental properties. We'll call the new application RENTMAN. NOTE

The "Tutorial" section of this book picks up where this chapter leaves off and follows RENTMAN through to completion. See Chapter 8 for more information.

An application's statement of purpose should consist of a single sentence that includes the subject, the verb, and the verb's object. The subject is always the application, such as "This system…" or "The RENTMAN System…." The verb describes what the application is supposed to do; for example, "The system will manage…" or "The RENTMAN System will facilitate…." The object denotes the recipient of the application's actions; for example, "The system will manage summer camp registration" or "The RENTMAN System will facilitate rental property management." The statement of purpose needs to be as simple and concise as you can make it. Don't waste time with flowery language or needless details. For example, avoid "for the organization" and "for the client" at the end of the statement because they're implicit. Also, try to avoid compound statements and conjunctions. Reducing the sentence to its simplest possible form helps keep you on course as you further describe what the app is supposed to do. You can expound more on your app's purpose when you later define the functions required to accomplish it. After you've developed a statement of purpose for the application, show it to the potential users of the new application and determine whether they agree. Don't be surprised if they don't understand your brevity, but try to get them to sanction your statement as accurate and complete. Though the statement will not enumerate the specifics of the application, it still needs to be broad enough to encompass the overall purpose of the application. Assure your users that the precise functions of the application will be addressed in a separate step.

Defining the Functions of the Application
After the statement of purpose is in place, it's time to determine the application's prerequisite functions. What does the application need to do to accomplish its statement of purpose? Try to keep this to no more than a handful of major tasks if possible. It's a great idea to develop these items using an outline. These functions should further define the application's statement Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 118 of purpose, not go beyond it. Follow the same three-part format you used with the statement of purpose. The RENTMAN System will facilitate rental property management.
q q q q

It will log and maintain property leases. It will track ongoing property maintenance work. It will generate tenant billing information. It will provide historical information on individual properties.

Make sure you cover all the major functions of the application, but don't overdo it with excessive detail. Also, be sure that the tasks you list don't overlap—don't list a task that's already covered by another item on the list.

Designing the Database Foundation and Application Processes
One of the first things you'll discover about database design is that there's a lot more to it than merely designing physical databases. Business process modeling, entity-relationship diagrams, and logical data modeling all play a part in the database design process. You can think of the steps from business process modeling to database construction as a continuum. Beginning with a 30,000-foot view of the business processes that will compose your app and zooming all the way down to individual database column definitions, you progress from the general to the specific. You progressively refine your understanding of the app's purpose until you're ready to translate that understanding into the app itself. This progression provides a systematic method of transforming the conceptual notions of what it is your app is

supposed to do into real-life program elements. Although it might appear complex, the actual work of designing a database is not as arduous as it might seem. As I've said, it begins with process modeling and ends with designing the physical objects that will make up the database. In this chapter, I'll reduce database design to these three major steps: 1. Document the business processes necessary to accomplish the app's required functions. 2. Diagram the entity relationships necessary to service these processes. 3. Create the logical database design necessary to implement these entity relationships and business processes. See Figure 6.1 for an illustration of the correlation between the five application development processes and these three steps. After you've developed your application's statement of purpose and derived its critical functions, the remaining analysis and design work boils down to just Page 119 these three steps. You begin by modeling business processes that correspond to the functions you defined, then you diagram the entity relationships necessary to support those processes. You finish by translating the E-R diagrams and business processes you developed into a logical model of your data. It's this logical schema that will be used to build your database during the construction phase. Figure 6.1. A model showing the, various elements of the five application development processes.

The progression from process modeling to logical data modeling doesn't need to be done manually. CASE tools are extremely helpful with these sorts of things. As you'll soon see, they can greatly simplify the whole client/server development process.

CASE Tools I should begin the discussion of CASE tools by saying that I don't believe that CASE tools are appropriate for every development project or that they will ever replace adequate planning or software craftsmanship. On the contrary, because they're computer-based, CASE tools have the same weaknesses that any other kind of software has: They do what you tell them to do, not necessarily what you want them to do. I also happen to believe that CASE tools that attempt to automate every aspect of the application-building process get in the way of proficient development. Rather than aid in the process, they hinder it by attempting to automate things that cannot or should not be automated. Like George Jetson and his computerized treadmill, developers and modelers spend more time recovering from this over-automation than they would have spent had they not automated Page 120 things in the first place. James Rumbaugh, data modeling expert and co-author of Unified Method for Object-Oriented Development, puts it this way: "A good [CASE] tool doesn't try to automate everything (as some AI tools tried to do); instead, it should automate the simple things and provide a straightforward way of accomplishing the difficult things, perhaps by some textual escape hatch." As with anything designed to save time, you can reach a point of diminishing returns. When this happens, it's time to get back to basics and focus on automation that is a help rather than a hindrance. The role that CASE software should play in client/server database development is that of a tool in the modeler's toolbox. Tools require skill to use. They also do not think for you or plan how you should approach solving problems. They merely help you more readily perform tasks that you could probably perform through other means, though perhaps not as easily. This is the role for which CASE tools are best suited in client/server application modeling. No matter what tools you use, you still need to have a basic understanding of database design concepts and the workings of your back-end DBMS in order to develop robust applications. Because much of the exploratory work that must be done in order to eventually arrive at a database design amounts to modeling real-world objects, it only makes sense that CASE tools should somehow figure into the mix. Today's CASE tools have come a long way from the pioneers of yesteryear. Without a

doubt, it's now both safer and faster to utilize CASE tools to assist in the design process than to do everything by hand. By permitting us to model objects before they actually exist, CASE tools allow us to perform "what if" analysis on design elements before they're fixed in place. Also, CASE tools assist with the design process itself and can warn of potential problems with the decisions you make. By constructing a model of what you want to do, you give the CASE tool the information it needs to help you do it. Some types of modeling to which CASE tools are particularly well suited are Applications consist of interrelated processes that perform functions essential to the application. Business process modeling involves formally diagramming what those processes are Business process modeling and how they interrelate. There are CASE tools designed specifically for process modeling. Powersoft's S-Designor (now PowerDesigner) Process Analyst is one example; CSA's Silverrun-BPM is another. E-R diagrams are considered indispensable by most data modelers and designers. They provide a method of separating the logical representation of data from its physical implementation. E-R Entity relationship diagrams facilitate thinking of data elements in modeling terms of the real-world objects they represent. There are many CASE tools dedicated to E-R modeling. Logicworks' ERwin is one such tool; Embarcadero's ER/1 is another. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 121 Despite the popularity of E-R modeling, it represents only a subset of the relational data modeling process. ER diagrams are one way of expressing database relationships, but there are many others. There are a number of CASE tools that go beyond simple E-R diagramming and address the larger task of designing entire logical database schemas. S-Designor's Data Architect is one example of such a tool; CSA's SilverrunRDM is another.

Relational data modeling

In this chapter, I'll show you how to use CASE tools to model real-world applications and how to transform these models into database and application objects. As Figure 6.2 illustrates, physical objects evolve from logical data models. Logical data models are in turn born of E-R diagrams, and E-R diagrams are derived from business process models. Business process models, for their part, embody the application's purpose and key functions. The progression from defining an application's purpose and essential functions to physically constructing it represents a gradual evolution in our understanding of what the application is supposed to do. Figure 6.2. CASE tools help transform ideas into physical database and application objects.

Which CASE Tool Is the Best? I don't know that there is a "best" CASE modeling tool. Certainly some tools are more popular than others, but I've seen innovative ideas in tools no one has heard of. When it comes to database CASE tools, I base the tool I use on the size of the application I'm modeling. For small projects I need to crank out in a relatively short amount of time, I prefer Embarcadero's Page 122 ER/1 to Logicworks' ERWin. ER/1 is the new kid on the block, but I find the interface appealing and the program generally easy to use. It is also quite fast—especially at reverse-engineering existing database schemas. Though, like ERWin, ER/1 is an IDEF1X tool, I find I'm more productive with it than with other tools. For high-end projects, I often use a suite of CASE tools from Computer Systems Advisors called Silverrun. Silverrun consists of four primary modules: BPM, a business process modeling tool; ERX, an E-R diagram expert tool; RDM, a relational data modeling tool; and WRM, a repository management tool. You might be wondering why I use Silverrun rather than one of its more popular competitors (for example, S-Designor, ERWin, and so on). There are a number of contributing factors. First, unlike most CASE modeling tools, Silverrun supports a wide variety of client platforms. Want to run Silverrun on Sun Solaris? No problem. Need it on NT? It's already there. So you're a Macintosh modeler? No worries there, either. For enterprise-level development projects, you need modeling tools that not only support oodles of back-end databases, but also support the diversity of computer hardware your modelers might be using. Silverrun's support for such a wide variety of client platforms helps ensure that users won't have to change operating systems just to build data models. Another reason I like Silverrun is its support for Delphi. Silverrun's RDM relational data modeling tool provides a unique Delphi "work mode," wherein it sports special integration with Delphi. Once in Delphi mode, Silverrun-RDM allows you to design Delphi attribute sets that can be saved to the Delphi data dictionary and used in applications. Because most of my client-side work these days is done in Delphi,

explicit Delphi support is a big plus. Silverrun also correctly separates E-R diagramming from basic relational data modeling. Many people confuse the two. Many modelers think that E-R diagrams and relational database models are the same thing. Although it's true that E-R diagrams depict relationships between the entities in a database, they aren't the only means of doing so. E-R diagramming is a particular type of logical data modeling; it's not the only type. The feature I probably like the most about Silverrun is the bang-for-the-buck it delivers. It's priced competitively with tools like S-Designor and ERWin but has features you would normally expect to find only in high-end modeling tools such as ADW or System Engineer. For the money, the Silverrun data modeling suite is a tremendous value. TIP You'll find trial versions of the entire suite of Silverrun modeling tools on the CD-ROM accompanying this book. If you aren't a Silverrun user already, you should install these trial versions so that you can use them to build the models outlined in this chapter and throughout the rest of the book. Note that you can also download the software from the Internet at www.silverrun.com.

Page 123 After you've installed the Silverrun trial version, you'll need to contact Computer Systems Advisors (the makers of Silverrun) at 1800-537-4262 to obtain a key code. This code will temporarily remove the restrictions that normally accompany the trial version of the software. Some of the models constructed in this book are complex enough that they exceed the capabilities of the trial software, so be sure to contact CSA to obtain the code.

Because of their multi-platform heritage, one of the first things you'll notice about the Silverrun tools is their unusual user interface. They don't fully adopt the standard CUA-compliant interface you're probably used to seeing in Windows applications. On the contrary, they employ a uniform interface that is consistent across the platforms on which they run. Whether you use Silverrun on Solaris or on Windows NT, the look and feel of the suite is the same. Although the end result is

application behavior that feels a bit quirky at times, I think Silverrun's ability to run on multiple platforms makes up for this to an extent. Modeling Business Processes Getting back to the subject of modeling itself, after you've defined your application's purpose and functions, you're ready to begin modeling the business processes that will further define them. I'll take you step by step through modeling the business processes for client/server database applications. After the process modeling is complete, you continue with entity relationship diagramming, then proceed to logical data modeling. After your analysis and design phases are complete, you'll implement your database design by physically constructing its objects. At that point, the database foundation is complete and you're ready to move on to developing the application itself. Business process models illustrate the relationships between four basic modeling elements: processes, external entities, stores, and data flows. Additionally, qualifiers, resources, and data structures further define the relationships between these elements. Table 6.1 outlines what each model element is and how it relates to the other elements. Table 6.1. Business process modeling elements. Modeling Element Definition A task or decision to be carried out by an application or organization. Processes are expressed in terms of actions that are accomplished using resources. Examples of processes include hiring new employees, billing, tracking customer complaints, and so on. continues Previous | Table of Contents | Next

Process

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 177

CHAPTER 7

Client/Server Application Design
Page 178 The Silverrun tools used in Chapters 6, 7, and 8 are one of many tool sets available to a Delphi programmer. The selection of Silverrun for these samples was the author's choice and does not reflect an endorsement by Borland. Chapter 6, "Client/Server Database Design," listed the five processes or stages of database application development. To reiterate, the five steps are:
q q

q

q

q

Define the purpose and functions of the application. Design the database foundation and application processes needed to implement those functions. Transform the design into an application by creating the requisite database and program objects. Test the application for compliance with the predefined purpose and functions. Install the application for production use.

Chapter 6 focused on the elements of the five processes that relate to database design. This chapter focuses on their relation to application design. Because database design and application design are so closely related, it's difficult to determine where one ends and the other begins. I tend to think of database

application design in terms of a grid with two columns—one for database design and one for application design—whose rows consist of the five formal processes. Use whatever analogy works best for you; the key thing is to understand that database design and application design are closely linked. Let's now pick up where we left off in Chapter 6 and continue with the five formal processes for designing database applications. NOTE A number of popular methodologies exist for constructing client/server applications. These methodologies go beyond database and application modeling and look at the whole process of defining and producing robust client/server database applications. The Rational-Booch methodology is one example, Sybase's SAFE methodology is another. Although methodologies such as these are outside the scope of this book, they are worth looking into. Client/server application design involves a lot more than simple user-interface issues. It's more complicated even than building databases or linking objects together. The best client/server developers in the business adhere to a standard methodology that addresses the development, testing, and deployment of the application from a holistic standpoint. Out of necessity, this book focuses primarily on database and application modeling. The modeling recipe contained in this book is a blend of many of the best ingredients from popular database and application modeling techniques, with a good dose of personal experience mixed in. As I've said before, I lean more toward the practical than the theoretical. The key is to use what works best for you. If following a particular modeling approach helps you produce better applications faster, by all means do so.

Page 179

Designing the Database Foundation and Application Processes
Now that the database foundation has been laid, it's time to move on to

building the application processes needed to implement your application's key functions. The database acts as a platform on which the rest of the application is built. It's important to finalize it as much as possible before getting too far into the application-building phase. Changes made to the database after the application is built can result in significant portions of the application having to be redesigned or rewritten. A Word About Software Development Before we get down to the business of designing applications, I'd like to give you my thoughts on software development as a whole. This is just an aside, but I think it's very important that you keep what you're doing in perspective as you design applications. There is no "right way" to develop all applications. No single system covers all application software development. Like the master chef who sometimes works from his head and sometimes from a recipe, you get to decide for yourself when to go "by the book" and when to "wing it." The object here is to find an approach that works best for you and that works for most of the development you do. Software development is considered by most people to be a science. Some view it as an artistic science but a science, nonetheless. After all, you earn a B. S., not a B.A., in Computer Science, not Computer Art. Nevertheless, I've always viewed software development as more of an art or a craft than a science. The distinction between art and craft is that a craft is utilitarian in purpose—things that are crafted have a use beyond that of artistic appreciation. Like carpenters, potters, and other craftsmen, software developers must abide by certain physical laws in order to obtain desirable results. The potter, for example, knows to use good clay when making her pots or risk having them fall apart. The carpenter uses a level to ensure the straightness of his work and a square to ensure the accuracy of its angles. But that's where it ends. The potter can make virtually anything she wants and still call it pottery. The carpenter can build whatever fancies him, and the fruit of his labor will still be carpentry. And the process the craftsman uses to craft his work, whether it be with his bare hands, with a hammer and saw, or with tools of his own making, is largely irrelevant. The emphasis is on the object, not the means of creating it. Thus, there's no "right way" to make pottery. There's no exhaustive set of steps that craftsmen always follow when building things. The quality of the creation bears witness to its inherent craftsmanship—so it is with software development.

Page 180 The world of software development today reminds me of the furniture industry of the mid-19th century. For time immemorial, furniture had been constructed one piece at a time, in a shop full of highly skilled craftsmen. Each craftsman built the pieces of furniture he worked on from start to finish. Each piece was hand-tooled and built with such precision that the craftsman proudly inscribed his name on it. It was his personal guarantee that the piece was as good as he could make it. Then came the industrial revolution. Suddenly, craftsmanship was out and glue and staples were in. High quality was out and low price was in. A streamlined, mechanized process was developed for constructing furniture quickly. The process became manufacturing rather than creating; the builders became line workers instead of craftsmen. This type of assembly-line mentality is now pervasive through most industries, not just the furniture business. It has brought with it both blessings and curses. True, most goods can be produced more cheaply now than ever before. But at what cost? Are we really getting the same goods cheaper, or are we getting cheaper goods cheaper? And hasn't the extinction of the skilled craftsman actually made quality goods more, not less, expensive? What does all this have to do with client/server database development? Simple. Remember, it all began with reducing what was in essence a craft to manual labor that anyone, skilled or not, could do. Machines were left to do the real work. With this debasement came the inevitable consequence of lower-quality goods and the virtual elimination of innovation. Software development could certainly suffer the same fate. The school of thought that says there is a "right way" and a "wrong way" to develop applications denies the software craftsman the freedom to leave his own mark on his creation. It stymies creativity and leaves scant room for innovation. Software development should be fun. It should be a creative process. This is why I contend there's no "right way" to build software. Just as there's no "right way" to make pottery or do carpentry, there's no fixed set of steps that will produce a good application every time. Software craftsmanship is not so much a matter of doing things "right" as it is doing the right things. So, bear this in mind as you build client/server database applications. I've purposely avoided arranging this chapter and the preceding one as some sort of series of concrete steps that one always follows to the letter when developing database applications. I offer the application design concepts presented here as

suggestions and nothing more. Use the ones you find useful; omit those you do not. Select an Application Type The first step in designing the processes that make up an application is to decide exactly what type of application you're building. Applications can be organized into two distinct types: transaction processing systems and decision support systems. Generally speaking, decision support systems are utilized by management to achieve a bird's-eye view of some portion of a company's data. Transaction processing systems are responsible for the entry and manipulation of this data. Decision support applications usually need only read-only access to the data. Transaction Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 174 A good place to edit SQL scripts is in Delphi itself. Delphi's development environment includes a very capable programmer's editor and provides several script development niceties, such as SQL syntax highlighting in the code editor and access to databases and the data they contain via the Database Explorer. Figure 6.30 illustrates the LEASE.SQL script loaded into Delphi's code editor. Figure 6.30. You can use Delphi's code editor to edit SQL scripts.

TIP

My favorite Windows text editor is Multi-Edit for Windows. Multi-Edit is simply the best there is. It has everything you need in a powerhouse programmer's editor, including expressionbased searches, text filtering, support for editing huge files, and language/compiler integration that supports all major languages (for example, C, HTML, Pascal, BASIC, and so on). The integration with Delphi is so good that you can actually use it in place of Delphi's built-in editor and Multi-Edit will keep the two tools synchronized. Rather than using Delphi to edit SQL scripts, I often use MultiEdit instead. I've customized Multi-Edit's flexible language support to interface directly with the SQL command-line tools I regularly use (for example, ISQL, WISQL, SQL*Plus, and so on). This allows me to execute SQL scripts and automatically jump to any errors they may contain. Add to this setup MultiEdit's syntax highlighting support, and you have an SQL editor that's hard to beat. You can contact American Cybernetics (makers of Multi-Edit) at 602-968-1945 for more information.

Page 175 NOTE Silverrun-RDM includes special support for Delphi and Delphi's attribute sets. You can take the relational model you just designed and use it to create Delphi attribute sets that can then be used in applications. We'll explore attribute sets in more detail in the "Tutorial" section, which begins with Chapter 8.

Running Your Script Once your script is correctly generated, your physical design is basically complete. At this point, you could run the script you've generated to create your database. Though there's no reason to do so now, seeing your design physically implemented may make the arduous task of creating it in the first place seem more worthwhile. To create the RENTMAN database, open the InterBase WISQL utility and select the Run an ISQL Script option from the File menu, then open your SQL script. Once the script runs, the RENTMAN database has been created.

NOTE Your InterBase server will need to be started before you run the script. If it isn't already running, you can start it by selecting the InterBase Server 4.2 option in your InterBase 4.2 folder (provided, of course, that InterBase has been installed).

Summary
In this chapter, you've discovered how business process modeling, E-R modeling, and relational data model all interrelate. You began the hands-on work of this chapter with nothing but an idea as to what RENTMAN might need to do to be functional. You ended the chapter having constructed three different models representing three different aspects of the app and its database requirements. This soup-to-nuts approach should fully equip you for designing and building your own databases. It's an approach you can take regardless of the front-end tool you're using. Designing the lion's share of the RENTMAN database in this chapter will also save you work when you get to the "Tutorial" section. If you choose to work through the "Tutorial," you can use the database you designed here as the basis for a complete client/server application. Even if you skip the "Tutorial" section, you've still had a thorough hands-on introduction to client/server database design. You can take the techniques you've learned here and apply them to realworld projects. Page 176

What's Ahead
Chapter 7, "Client/Server Application Design," continues the discussion of the five general processes for constructing database applications and elaborates on how they relate to application design. We'll cover the whole gamut of application design issues—everything from selecting a user-interface motif to building data entry forms and decision support forms. Chapter 7 complements the thorough treatment of database design in this chapter with its own in-depth discussion of application design. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 170 to physically implement it. To generate an SQL script you can use to create the objects you've modeled, follow these steps: 1. 2. 3. 4. Select the Schema | Generate DDL menu option. Click the Coded Name radio button in the ensuing dialog. Click the Generate button. When prompted for a filename, supply a name that describes your model's basic function and uses an extension of SQL. In my case, I used LEASE.SQL.

Listing 6.1. lists the contents of the SQL script that should be generated. Because you selected the InterBase DBMS platform early on, the DDL generated by Silverrun is all InterBase SQL. Listing 6.1. LEASE.SQL as generated by Silverrun-RDM. CONNECT "E_R_Diagram_for_Lease_Process" USER " " PASSWORD " ";

CREATE DOMAIN TAddition AS CHARACTER(20) NOT NULL CHECK (VALUE IN (`Deerfield', `Firewheel', `Legacy Hills', Â `Switzerland Estates', `Sherwood', `Rockknoll')); CREATE DOMAIN TAddress AS CHARACTER(30) NOT NULL; CREATE DOMAIN TCity AS CHARACTER(30) NOT NULL CHECK (VALUE IN (`Oklahoma City', `Norman', `Edmond', `Dallas', `Richardson', `Plano')); CREATE DOMAIN TComments AS VARCHAR(80); CREATE DOMAIN TPhone AS CHARACTER(13) NOT NULL; CREATE DOMAIN TPropertyNo AS INTEGER NOT NULL; CREATE DOMAIN TRent AS NUMERIC(5, 2) NOT NULL; CREATE DOMAIN TRooms AS INTEGER NOT NULL CHECK (VALUE IN (0, 1, 2, 3, 4, 5));

CREATE DOMAIN TSchoolDistrict AS CHARACTER(20) NOT NULL CHECK (VALUE IN (`Putnam City', `Oklahoma City', `Richardson', `Edmond', `Garland', Â`Dallas', `Plano')); CREATE DOMAIN TState AS CHARACTER(2) NOT NULL CHECK (VALUE IN (`OK', `TX')); CREATE DOMAIN TTenantNo AS INTEGER NOT NULL; CREATE DOMAIN TYesNo AS CHAR(1) NOT NULL CHECK (VALUE IN (`Y', `N', `T','F')); CREATE DOMAIN TZip AS CHARACTER(10) NOT NULL; Page 171 CREATE TABLE PROPERTY (....Property_Number TPropertyNo NOT NULL, ....Address TAddress NOT NULL, ....City TCity NOT NULL, ....State TState NOT NULL, ....Zip TZip NOT NULL, ....Addition TAddition NOT NULL, ....SchoolDistrict TSchoolDistrict NOT NULL, ....Rent TRent NOT NULL, ....Deposit TRent NOT NULL, ....LivingAreas TRooms NOT NULL, ....BedRooms TRooms NOT NULL, ....BathRooms TRooms NOT NULL, ....GarageType TRooms NOT NULL, ....CentralAir TYesNo NOT NULL, ....CentralHeat TYesNo NOT NULL, ....GasHeat TYesNo NOT NULL, ....Refrigerator TYesNo NOT NULL, ....Range TYesNo NOT NULL, ....DishWasher TYesNo NOT NULL, ....PrivacyFence TYesNo NOT NULL, ....LastLawnDate DATE NOT NULL, ....LastSprayDate DATE NOT NULL, PRIMARY KEY (Property_Number) ); CREATE TABLE TENANT ( Tenant_Number Name Employer EmployerAddress EmployerCity EmployerState EmployerZip HomePhone WorkPhone

TTenantNo NOT NULL, CHARACTER(30) NOT NULL, CHARACTER(30) NOT NULL, TAddress NOT NULL, TCity NOT NULL, TState NOT NULL, TZip NOT NULL, TPhone NOT NULL, TPhone NOT NULL,

ICEPhone TPhone NOT NULL, Comments TComments, PRIMARY KEY (Tenant_Number) ); CREATE TABLE CALL ( Call_Number INTEGER NOT NULL, CallDate DATE NOT NULL CallTime CHARACTER(11), Description CHARACTER(30) NOT NULL, Property_Number TPropertyNo, PRIMARY KEY (Call_Number), CONSTRAINT FK_PROPERTY1 FOREIGN KEY (Property_Number) REFERENCES PROPERTY );

continues Page 172 Listing 6.1. continued CREATE TABLE LEASE ( Lease_Number INTEGER NOT NULL, BeginDate DATE NOT NULL, EndDate DATE NOT NULL, MovedInDate DATE NOT NULL, MovedOutDate DATE NOT NULL, Rent TRent NOT NULL, Deposit TRent NOT NULL, PetDeposit TRent NOT NULL, RentDueDay SMALLINT NOT NULL, LawnService TYesNo NOT NULL, Comments TComments, Property_Number TPropertyNo NOT NULL, Tenant_Number TTenantNo NOT NULL, PRIMARY KEY (Lease_Number), CONSTRAINT FK_PROPERTY2 FOREIGN KEY (Property_Number) REFERENCES PROPERTY, CONSTRAINT FK_TENANT3 FOREIGN KEY (Tenant_Number) REFERENCES TENANT ;

NOTE

You'll notice that I rearranged the columns in the PROPERTY table to sequence them a bit more logically. Because the PROPERTY entity was originally created by the ERX normalization expert, its columns were ordered somewhat randomly. Arranging a table's columns sensibly makes the table easier to manage and saves you work when building applications that access it.

Notice that the script begins by creating the logical domains you defined as InterBase domain objects. It then uses these domain objects to define your database tables. This is precisely what you want. Your business rules are embodied as much as possible in reusable domain objects. You can use these domain objects to build new columns as necessary. For example, if you needed to add another column that contains Yes/No values, you can reuse the TYesNo domain. That's the advantage of embedding your business rules in domains rather than directly in columns. The script does have one problem: It includes a curious CONNECT statement at the top that couldn't possibly connect to an InterBase database. Notice that the name of the model is used as the target database name. You could remedy this by changing the model's name to a valid InterBase database name, but there's a better way. Because the model was converted from the E-R diagram you built earlier, it retained its ERX-related title. Let's change that to reflect that the model is now a full-blown relational data model. Page 173 Select the Schema | Schema Description menu option and change the Local Name entry box to Relational Data Model for Lease Process. Next, change the Coded Name entry to a valid InterBase database name. For example, I'm using C:\DATA \RENTMAN\RENTMAN.GDB because that's the name of the database in which I intend to store the objects created by the DDL script. Figure 6.29 illustrates the Schema Description dialog. Figure 6.29. Use the Schema Description dialog to specify your model's name.

NOTE Be sure to save your model, since you'll reuse it in the "Tutorial" section of this book. I've saved mine as LEASE.RDM.

Now that you've changed the database name RDM will use in the DDL script, let's regenerate the script. Select the Schema | Generate DDL menu option as you did earlier and re-create the LEASE.SQL file (don't forget to generate using coded names). The CONNECT statement in your new file should now look something like this: CONNECT "C:\DATA\RENTMAN\RENTMAN.GDB" USER " " PASSWORD " "; You can edit the script file and supply a valid username and password at your convenience. For example, you could use the SYSDBA username and its default password, like so:

CONNECT "C:\DATA\RENTMAN\RENTMAN.GDB" USER "SYSDBA" PASSWORD "masterkey"; Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 166
q

If your platform supports bit data types (for example, SQL Server), use them to define Boolean columns rather than integer or single-character data types.

Generating Coded Names Because you generated Coded Names during the E-R modeling phase, your tables and columns don't need to be put through this process again. You've since modified the column names that were generated, so you wouldn't want to regenerate Coded Names for them anyway; doing so would destroy the abbreviated names you just set up. On the other hand, the domains you defined earlier have not yet had Coded Names generated for them. Because you'll use the Coded Names of your model elements when you generate the DDL script that is to create them, you'll need to generate Coded Names for your new domains. To do so, follow these steps: 1. Select the Project | Generate Coded Names menu option. 2. Change the Maximum Length and the Nb. of characters used from the beginning of each word in the name to 30. 3. Make sure that the Generation dialog is set to Partial, not Complete. 4. Click the Generate button. CAUTION

Be sure that you don't select the Complete option in the Coded Names Generation dialog. Doing so will overwrite the shortened column names you specified earlier for your model. The Complete option generates new Coded Names for model elements regardless of whether they already have names. The Partial setting, by contrast, generates only new Coded Names for those elements that don't already have names.

Describing Your Design Describing your model elements via comments is a useful and worthwhile habit. In some shops, commenting data models isn't even optional. SilverrunRDM allows you to comment any modeling element you wish. Object comments are included in the SQL that RDM generates for you if your target platform supports them (for example, Oracle). To add a table comment in RDM, follow these steps: 1. Double-click the header portion of a table object to display the Table Description dialog. 2. Click the Table button and select Comment from the drop-down list. You'll then be presented with a dialog in which you can type a comment that describes the table. 3. Type your comment and click OK to finish. Figure 6.26 illustrates this. Page 167 Figure 6.26. You can describe the tables in your model using comments.

To add a comment to a specific table column, follow these steps: 1. Double-click the body of a table object to display the Table Columns dialog. 2. Double-click the column you want to describe in the columns list to

display the Column Description dialog. 3. Click the Column button and select Comment from the drop-down list. You'll then be presented with a dialog in which you can type a comment that describes the column. 4. Type your comment and click OK to finish. (See Figure 6.27.) Describing your model this way helps clarify your understanding of what you're building. In multi-programmer projects, it also helps others understand what you were thinking when you designed a given object. Generating Foreign Keys A final thing that you need to do to complete your model is generate foreign key specifications for it. Foreign keys are the physical implementation of the entity relationships constructed during the E-R modeling phase. Generating foreign keys causes the required columns to be propagated between tables. The way it usually works is that one table inherits the primary key of another. The inherited column or set of columns becomes a foreign key in the inheriting table. Page 168 Figure 6.27. You can describe the tables in your model using comments.

To generate foreign keys in the RDM tool, follow these steps: 1. Select the Schema | Foreign Keys | Generate menu option. 2. Click the Generate button in the ensuing dialog. 3. Accept the default filename for the foreign key report by clicking the Save button. You should then see a number of new columns added to your table objects. Each of these should be prefaced with FK, signifying that they are foreign keys. Figure 6.28 illustrates what you should see.

Your model is now basically complete. You're just about ready to generate the SQL necessary to create the objects defined by your model. NOTE One of the nicer features of the Silverrun-RDM tool is its support for multiple types of relational notation. It supports the popular Information Engineering and Information Engineering+ notation systems, as well as Logical Data Structure notation. Furthermore, it includes it own notation, Datarun, which you may well find preferable to the others. And if you don't like any of the canned notation sets, you can define your own, customizing both the way connectors are displayed and the way that modeling elements are named. To change Silverrun's default notation style, select the Presentation | Notation menu option.

Page 169 Figure 6.28. Your completed logical data model.

Verifying Your Model's Integrity Before you generate SQL to create your database objects, you should verify the integrity of your model. Silverrun-RDM includes a handy facility for doing this for you. Invoke it now by following these steps: 1. Select the Schema | Verify Integrity menu option. 2. When prompted for a filename for the report, accept the default, Verify, by clicking the Save button. 3. Hopefully you'll get a message indicating that there are no errors in your model. You might get warnings, but hopefully there won't be anything seriously wrong with your model. 4. Select the Util. | View a Text File menu option and specify the name of your integrity report (Verify, by default), then click the Open button to

view it. Verifying the integrity of your model before generating SQL can save you from having to drop and re-create database objects because they were defined incorrectly. Silverrun checks a number of aspects of your model and provides a fairly good gauge of whether you're ready to proceed with the SQL generation process. Generating Your DDL SQL statements that create or define objects are known as DDL (Data Definition Language) statements. Now that you've finished your model, you're ready to generate the DDL necessary Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 181 processing systems need to be able to both read and write data. Because of this fundamental difference between the two types of applications, you need to determine what type of application you plan to build before getting started. Chart the Application's Operational Processes Building on the models you designed during the database design stage, chart the flow of each separate application process graphically. Several CASE tools provide specific support for modeling application processes. Many of them, in fact, will allow you to base your application models on the E-R and relational data models you constructed when designing your database objects. Some of them also include direct support for Delphi, include Silverrun-RDM and the PowerDesigner (formerly S-Designor) AppModeler for Delphi tool. Keep in mind that you can model application elements without even using a CASE tool, as well. Regardless of the modeling approach you take, you should seek to accomplish a few basic tasks in the models you build:
q q q

q

q

Identify all forms needed by the application Identify all reports or other output that the app should produce Associate these elements with the data elements in your previous models Identify the flow from one form or report to another and from one process to another Identify any standalone (not embedded in a form or report) support code that the app requires

Figures 7.1 and 7.2 illustrate a couple of different application process models. Figure 7.1. A sample preliminary

application model for RENTMAN.

Page 182 Figure 7.2. Another sample application model.

Generate the Application If you're using a CASE tool and it supports generating application code corresponding to your models, it makes sense to allow the tool to generate at least a portion of your app based on the models you've built. The amount of time this saves you will vary based on the CASE tool you're using. For example, Silverrun's RDM tool will generate Delphi attribute sets that you can then use in your applications. PowerDesigner's AppModeler for Delphi will generate an entire application—including forms, reports, and supporting code—that you can then build upon. Design Form and Report Hierarchies After you've determined what forms and reports are needed by your application, a good first step in building the application itself is to design separate form and report hierarchies in order to take advantage of Delphi's support for visual form inheritance. That is, you can organize your forms and reports into general classes that build upon one another. For example, you could define a top-level form for your application from which all others will descend. If you later need to change an attribute of all the forms in your application, chances are you'll be able to make the change in just one place. The same is true for the report form hierarchy. Because you'll probably end up designing most of your reports using Delphi's QuickReport components, it makes sense to base them on a report form hierarchy as well.

Page 183 Identify and Acquire Third-Party Support Code After you've completed this process, attempt to determine which, if any, support libraries and third-party code you might need. One popular library I make use of in my Delphi applications is the Orpheus Visual Component Library from TurboPower Software. Another is Asynch Professional, from the same vendor. In a large shop, you might need to speak with another team leader or resource manager about utilizing support libraries that have been developed internally by the organization. The time when you decide which processes will have associated forms and which will have associated noninteractive code is a good time to determine what support libraries you might need. Far better to do this now than to delay until you actually need the support library to acquire it, possibly stalling your project until it arrives. Schedule the Development Process The next thing you need to do is schedule the development of the forms and support code your application requires. There are several factors that affect this scheduling. For example, your client may require that certain parts of the application be developed before others. You might be commissioned to develop the application in pieces, deploying each piece separately. The client may also have an idea as to which pieces she'd like to see first. Additionally, some parts of the application may be prerequisites for others. For example, you may need to develop the general ledger module before you can build the accounts receivable portion of the app. You may also find that developing some parts of the application before others aids you in the development effort itself. For example, building an application's main form prior to its other forms will allow these other forms to be tested from the main form's menu. Time Lines The process of scheduling a project along a time line is a black art indeed. It's a complex process that varies from individual to individual. It's further complicated by the prospect of steering committees and team development efforts. Whole books have been written on the subject of scheduling projects along time lines. Because of the innumerable factors that figure into producing an accurate time line, I'll leave the time line estimates to you (after all, that's what they pay you for!). Build the Application

Next up is the actual development of the application according to the schedule you've constructed. The craft of developing source code in a structured manner is outside the scope of this book. Keep in mind that the emphasis on source code design has been lessened a great deal by the emergence of visual tools like Delphi. The emphasis these days is on form design, which is covered in the next section. Page 184 Form Design The approach you take to form design depends heavily on the type of application you're building and on the type of forms you're building for that application. Client/server database apps are usually composed of three basic form types: decision support forms, transaction processing forms, and dataentry forms. Each of these form types differs widely from the others. This disparity makes it all the more important that you know what type each form in your application will be before you begin designing it. As the cliché goes, "If you fail to plan, you plan to fail." The following comments are some guidelines on form design in general. Again, many of these are subjective; you should feel free to use only the ones you find helpful. They're divided into four sections: guidelines that apply to all forms, those that apply to decision support forms, those that apply to transaction processing forms, and those that apply to data-entry forms.
q

Decide on a motif. An important decision you'll want to make early on regarding your application's visual appearance is that of an application motif. The motif you select governs the visual appearance of the forms that make up the application. Three popular motifs are already in widespread use in the world of Windows applications. They are: Corel PerfectOffice, Lotus SmartSuite, and Microsoft Office. If you choose the Corel PerfectOffice motif, your application and the forms in it will resemble Quattro Pro and WordPerfect. If you choose SmartSuite, your application will resemble Lotus 1-2-3 and WordPro. If you choose Microsoft Office, you'll imitate Word and Excel. Figures 7.3, 7.4, and 7.5 show examples of each of the interfaces.

Figure 7.3. The Corel PerfectOffice interface.

Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 185 Figure 7.4. The Microsoft Office interface.

Figure 7.5. The Lotus SmartSuite interface.

The obvious reason for adhering to an already-established user-interface motif is that your users will enjoy instant familiarity with your application and will find it easier to get up to speed with it. Additionally, mimicking a major vendor's visual interface gives your application a seemingly more professional appearance. Page 186
q

Don't add unneeded features. Inundating your client with useless information wastes her time and yours. Pretend you're the user of the application; then determine what features would and would not be

q

q

q

q

q

q

q

useful to you. Too often, language features or nifty interface elements determine what functions make it into an application. Instead, the user's needs should determine what it is that the application does. Construct a separate hierarchy for your forms and reports in order to take advantage of Delphi's visual form inheritance. Create a top-level form for your application from which all others will descend. If you later need to change some facet of all the forms in your application, chances are you'll be able to change just the top form. This is also true for the report form hierarchy. Because you'll probably create most of your reports using Delphi's QuickReport components, you'll want to create a report form hierarchy, as well. Be consistent, both internally and externally. Internal consistency involves being consistent from form to form. Utilizing a form hierarchy should help you with this. External consistency means being consistent with other applications. This is why I suggest you choose a motif early in the process. The fonts, background colors, size of display elements, toolbar placement, and so on should match other mainstream Windows applications. Build SDI, not MDI, forms. There was a time when MDI (multipledocument interface) forms were preferred over SDI (single-document interface) forms. Alas, the pendulum has swung the other direction, and Microsoft now recommends that applications adhere to the SDI convention. This is just a general guideline, and there's certainly something to be said for MDI applications. However, it appears, at least for now, that the trend is toward the SDI interface. Don't display more than one type of information on a single form. For example, don't display both invoices and payment vouchers on the same screen—keep the form as straightforward as possible. Usually, a form should not encompass more than one type of source document at a time. Make use of Windows' common dialog boxes. It's silly to build your own open and save dialogs when Windows already provides them. Delphi envelops the standard Windows dialogs in easy-to-use components that you can simply drop onto a form. Use these where possible to save yourself time and to conserve system resource usage. You can find Delphi's dialog components on the Dialogs page in the component palette. Use a neutral background color (such as clBtnFace, which defaults to gray) for your forms. This affords a more professional look, is easier on the eyes, and displays properly on more types of video cards and monitors than do louder colors. Use ToolBars and SpeedButton groups to show the mode that a multimode application is currently in. A multi-mode application is one that can be in one of many

Page 187 modes at a time. For example, clicking the app's Add button may place it in add mode, whereas clicking its Report button may place it in report mode. You can set up a SpeedButton group by setting the GroupIndex property of a set of SpeedButtons to a non-zero number. You'll also want to set the group's AllowAllUp property to False. SpeedButtons defined this way stay down when clicked until another button in the group is clicked, much like the buttons on old push-button car radios. If your application can have only one of several modes at a time, this is an effective means of allowing the mode to be easily changed and displaying the application's current mode simultaneously. Note that you can set an individual SpeedButton's Down property to True to cause it to appear in the down position. Give the user large mouse targets. Large buttons and easily located radio button groups make applications easier to navigate than a form that's chock-full of microscopic controls. Include a menu option for each button on a form and include menu items for commands not on forms. For those who prefer the use of the keyboard over the mouse, this can be a big time-saver. Also, by providing keyboard shortcuts for the options in your applications, you provide support for voice recognition programs that pull off their magic using keyboard macros. Include menu accelerators for commonly used menu commands. If you want to add an accelerator key to a form without associating it with a menu item, first create a dummy menu item with the desired accelerator key and attach the code you want to execute to the item's OnClick event. Next, make the menu item invisible (at runtime) by setting its Visible property to False in the Object Inspector. When your application runs, the accelerator key will still be active, even though the menu item is hidden. Establish label accelerators for key fields on a form. To do this, first establish a label accelerator key using a Label control's Caption property (use the & character to denote the accelerator key). Next, set the FocusControl property of the Label control to the component that is to receive the input focus when the Label's accelerator is pressed. The location and function of navigation devices should be the same from form to form and between applications. If you locate a DBNavigator control at the bottom of one form and the top of the next, you aren't being consistent within the application and could conceivably confuse your users. It's far better to locate controls that perform the same or similar functions in the same place on each form.

q

q

q

q

q

q

Use a consistent font for button captions. It's important to keep userinterface elements as unobtrusive as possible. If a user has to stop and squint to try to read a label on a button, you've failed in this task. It's much more acceptable to have a few buttons that are larger than they really need to be than to try to economize every inch of screen space. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 188
q

q

q

q

q

In my opinion, sans serif fonts are easier to read than fonts with serifs. Use sans serif fonts like Arial rather than those with serifs, like Times New Roman, when possible. Use fly-over hints. Fly-over hints are the little pop-up labels that display when the mouse pointer pauses over some significant screen element. These are great for letting a user know what it is a given control does (especially a toolbar button) without clicking it. Include online help in your applications. Professional Windows applications include a complete online help database that includes links between related topics. You should also set up your forms to support context-sensitive help. You do this with the HelpContext property of the form and the controls on it. When help is then requested while a given control on a form has focus, the Windows help facility will automatically jump to the relevant topic in your help database. See Chapter 13, "Finishing Touches," to learn more about developing Windows help files. Build an About Box form and include your application's name, its current version number, and the name of your company (if applicable). You might also consider providing a technical support phone number, a copyright notice, if applicable, and information on Windows resource usage. The product name, version number, and copyright notice should be linked into your application using a Windows VERSIONINFO resource. See Chapter 13 for more information about including version information in your Delphi apps. Include a toolbar in your applications. This is done in all three application design motifs discussed earlier and is the convention in modern Windows applications. Delphi includes two special components—TToolBar and TCoolBar—for this purpose. You can also construct toolbars manually by placing TSpeedButton controls on

q

q

q

q

TPanel components. Include a status bar in your applications. You can use the built-in Windows 95 StatusBar component for this. You can find the StatusBar component on the Win95 page of the component palette. Establish a default field (by setting its TabOrder to 0) on forms that include input fields. Use the page and tab controls to condense large numbers of controls into a relatively small screen space. This is now the preferred approach in Windows 95 and has been a standard convention in Borland products for years. Associate a distinctive icon with your application. This is not a form issue but a user-interface issue, nonetheless. The application icon you establish is the one that appears in the Windows Explorer and in the Alt +Tab "cool switch" list, so it's important that your users are able to distinguish it from other applications. Set the icon associated with a Delphi application using the Project | Options | Application menu selection in the Delphi IDE.

Page 189
q

q

q

q

Target your forms for the lowest screen resolution in use by your users. This is probably VGA resolution, so you can safely target VGA's 640 ¥480 resolution in your forms. The best way to do this is to switch the resolution on your own video adapter to VGA. Forms you develop at resolutions too high for VGA to display properly will be clipped to fit it—something you obviously want to avoid. Use consistent font sizes in an app's forms. Build all forms on a system with 96 dpi small fonts, or all on a system with 120 dpi large fonts, but don't mix the two in the same application. Also, forms tend to scale up better than down, so building forms at 96 dpi will give slightly better results on user systems with large fonts installed than deploying large font forms on systems with small fonts installed. If you don't want to use form scaling, set the form's Scaled property to False (it's True by default). If you do want to use form scaling (and you should), set the form's AutoScroll property to False. It's also True by default, but interferes with scaling. Do not rely on online help to explain the basic use of your application. In most cases, its use should be obvious without digging through a manual or reading the online help. Prototype the forms you create as soon as possible for your clients so that you can be sure they share your vision for the application's user interface. I've found that Delphi itself is ideal for this; in many cases, I've designed forms with the client right at my side.

Decision Support Forms Decision support forms are typically used by people who, although possibly not the most computer-literate people in an organization, typically wield more influence than other types of users. They need decision support applications because they have some role in the decision-making process. According to Scott Adams' book, The Dilbert Principle, the level of computer skill these users have acquired is usually inversely proportional to the level of management to which they've risen. Thus, the challenge with decision support forms is to keep them simple yet informative. Here are some suggestions for doing that:
q

q

Occupy most of the display area. Typically, managers like to see things as simplified and spread out as possible. You may also find that the managers at your organization rarely run more than one application at a time under Windows, so maximizing your application's form windows is a wise move. Keep it simple; avoid cluttering a form with a large amount of detail or tabular data. Generally, managers want just the facts, and they want them as palatable and simple as possible. Don't bother with large amounts of transactional data; the manager probably isn't interested in it.

Page 190
q

q

q

Use graphs where possible to visually display the relevance of data to other data. Depending on the manager, graphs can be a very powerful way of summarizing complex data sets. If the manager is comfortable with abandoning raw figures in favor of graphical ones, graphs can give your application a very polished and professional appearance with minimal effort. You should still, however, provide a means of "drilling down" to the underlying raw data, lest the manager attempt to glean exact figures from your charts. A good way of providing this drill-down ability is through Delphi's DecisionCube components. Remove list components and other data-entry niceties if the application reads but does not write data. If the decision support application is to be used merely to view data, data-entry support devices (such as DBListBox or DBLookupComboBox controls) should be removed to conserve system resources. You can replace these list components with DBText or even TLabel controls to display description-type fields without resorting to a full-blown combo box or list box component. Control access to the data from the database or network server—avoid

q

application-based security. Users, especially those in management, tend to dislike messages telling them that they lack access rights for a particular application function, so avoid these where possible. Do not include functions in the application that the management personnel who use the application cannot access. Avoid grayed-out menu items and disabled buttons because these can confuse new users. If an option is not available to a given user of a decision support application, set its Visible property to False and make it invisible (or remove it altogether), rather than merely disabling it.

Interactive Forms Second only to decision support forms, interactive forms are the form type most often found in applications. They provide the means whereby data is normally entered, edited, and deleted. The typical user of an interactive form is someone who is more computer literate than those at the managerial level in an organization. These persons might be administrative assistants, computer operators, or other clerical personnel. An interactive form, therefore, needs to be as straightforward as possible. Here are some tips for constructing interactive forms:
q

Consider enhancing or replacing Delphi's DBNavigator buttons with standard buttons. Though powerful and easy to use, the DBNavigator control lacks certain basic features like a search facility and the ability to assign accelerator keys or labels to its built-in buttons. Chapter 27, "Building Your Own Database Components," shows you how to create a component template that functionally replaces the DBNavigator control. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 191
q

Group controls for ease of use. Arrange controls in such a way as to make their selection intuitive and logical. Place related controls in close proximity to one another. Align related radio group items and place related buttons next to each other. This will help your users become familiar with your forms more quickly and will help avoid user errors.

TIP A good reference on the subject of Windows interface uniformity is the book It's Time To Clean Your Windows: Designing Guis That Work by Wilbert O. Galitz, John Wiley & Sons (ISBN 0-471-60668-5). It provides a number of insights into properly placing controls so that they're both readable and functional. It also delves into tab control usage, font setup, and many other GUI issues.

q

q

q

q

Use accelerator keys for command buttons and entry fields. Accelerator keys are a must, especially for the keyboard-inclined. Arrange accelerator keys logically, not positionally, giving buttons preference over labels. That is, if you have a field at the top of the screen whose label happens to begin with the letter A and you also have a button at the bottom of the screen that's labeled Add, set the accelerator key of the button, not the field, to Alt+A. It's less likely that the user will want to jump to the field at the top of the screen than that he'll want to trigger the button using a short-cut key. Establish a logical tab order. Set up a tab order that enables the user to jump logically from field to field and from button to button on the form in a left-to-right, top-to-bottom fashion. Use the Kind property of Delphi's TBitBtn control to establish both an OK button and a Cancel button where appropriate. Setting up a button as an OK button automatically sets its Default property to True, making it the form's default button. This means that the user can press Enter to end the editing of the current record and can press Esc to abort it. Consider using a right-click pop-up menu in place of or in addition to command buttons. Some users prefer the use of the right-click pop-up menu made popular by Borland. If your users fit this category, setting up such a menu in your Delphi applications is very easy to do.

Data-Entry Forms Data-entry forms are normally used for "heads-down" data entry. These forms are used primarily for getting data into a database. The emphasis here is on speed rather than on screen aesthetics or other application niceties like fly-over hints and drop-down list boxes. Data-entry

Page 192 forms are usually quite terse and include only the most essential elements. Typically, a user of this type of form is a data-entry operator who will be looking mainly at source documents, not the screen, during the entry of the data. Special emphasis is placed on the keyboard in data-entry forms because mouse use requires visual interaction. Here's a list of tips that should help you build better data-entry forms:
q

q

q

q

q

q

Use a bold monospace font for text boxes. Evenly spaced fonts are easier to read at a glance, although they take up more screen space. When speed is the issue—as it probably would be in data-entry forms—use monospace fonts. Boldfacing the text areas in these boxes also makes them easier to read. Remove unneeded buttons and fields. Remove those controls on the form that aren't needed for quick data entry. For example, if you know that your user will never need to look up an account number, remove the account lookup button; it just takes up screen space. Considered to be convenience features in normal transaction processing forms, these types of elements get in the way of rapid data entry. Relate DataSet components underlying data-aware controls on a form using Delphi's master/detail facility. Don't make the user click a button or do anything else to see the detail related to a given master displayed. Relate the tables using the appropriate property selections so that they are automatically synchronized onscreen. Use accelerators that are easy to hit. Don't require athletic jumps to get to your accelerator keys. Assign accelerator keys based on use, not position on the screen. If two controls would naturally have the same intuitive hotkey, give it to the control that is likely to be used most, not the one that's located first on the form. Assign some other accelerator to the lesser-used control. For those keys that the user hits often, make the keys as simple to access as possible. Set the default button to an Add button, not an OK button, where appropriate. Unlike normal transaction processing forms, make the default button an Add button—one that adds new records—in forms where adding records is the form's primary use. This will make things go much faster when a user has to add several records in succession. Keep forms as small as possible. Unlike other types of forms, this form should be as small as possible because this allows it to be repositioned and reduces eye strain. Users of this type of form will be looking mostly at source documents of some type rather than the screen, so open the form as a normal (rather than maximized) window.

Report Design Report writing is covered in detail in Chapter 19, "Business Reports." Nevertheless, here are some general guidelines regarding report design that you may find helpful: Page 193
q

q

Use Delphi's QuickReport components to design your reports when possible. They're less of a hassle to set up and use than external report writers because they are built into your application. You don't have to shell to a report runtime utility of any type to run a QuickReport-based report. For those reports too complex for the QuickReport components, use a graphical report writer to build your reports if possible. Popular report writers include ReportSmith, R&R SQL Report Writer for

q

q

Windows, and Crystal Reports. Using a graphical report writer has the benefit of allowing your users to base new reports on the reports you build without having to modify application source code. Use stored procedures, if your platform supports them, to do as much of the report's work as possible. This keeps the majority of the action on the database server, which is where it should be in properly designed client/server applications. It also makes testing the data retrieval portion of your report easier because you can do it outside the report writer. Furthermore, it separates the data retrieval function from the data display function, so you can more easily switch report writers or retrieve a report's data directly into a Delphi application. If you use stored procedures to drive your reports, log calls to them on your database server. You can do this via your server's auditing services (if supported) or by setting up your procedures to call a log procedure. This procedure would update a report log table on your server when the calling procedure begins and ends. Logging procedure calls will give you useful information on the overall performance of your reports, and it has the added benefit of letting you know what reports users are running the most and when they are running them. The following code is a sample Transact-SQL (Sybase and Microsoft's SQL dialect) for creating such a procedure: CREATE PROCEDURE reportlog @StartEnd char(6),@ReportName char(30) as insert into REPORTLOG select @StartEnd, getdate(), suser_name(), @ReportName When this procedure is in place, you would use the following SQL syntax to call it: /* Log the start of the report procedure */ exec reportlog `STARTED','MAINTENANCE REPORT BY WORK TYPE' /* Do the work of the procedure */ ... /* Log the end of the report procedure */ exec reportlog `ENDED','MAINTENANCE REPORT BY WORK TYPE' . Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 194 TIP Some DBMS platforms include built-in support for monitoring stored procedure execution. Sybase SQL Server, for example, allows you to audit stored procedure calls, giving you basically the same information as the reportlog procedure listed above.

q

Include the report's internal name, the current date and time, and the username of the user running the report (if available) in the page heading of your reports. Including the date and time helps distinguish multiple versions of the same report from each other and gives an idea of the age of the report if it is viewed at some later date. Including the report's internal name helps you track down the "source" to the report if you need to work on it in the future. Having the username in the report header can give you a user contact with whom to discuss the problem further. Figure 7.6 shows a properly designed report page heading.

Figure 7.6. A properly designed report.

q

Include any criteria used to qualify the data displayed on a report in its page header. If the user supplied dates or other criteria in your front-end

application, put them in the report's page header. This is imperative because data might be missing from the report due to the criteria specified in the front-end. Without the proper page heading, this could confuse the user. This is particularly likely when there is a significant amount of time between the running of the report and a subsequent review of it. Figure 7.6 illustrates the inclusion of a report's criteria in its heading. Page 195
q

q

q

q

Use proportional fonts for headers and non-proportional ones for data. Proportional fonts give your reports a more polished look and take full advantage of the high-end printers most everyone uses these days. Furthermore, they distinguish reports generated by a modern PC-based system from those generated on older legacy systems. Unfortunately, proportional fonts have the disadvantage of making it difficult to align columnar data. That is, because the numeral 1 may be narrower than, say, the numeral 5 when printed, proportional fonts are less than ideal for columnar data that must be aligned. Fixed-pitch fonts should be used instead. Typically, I use a proportional font like Arial or Times New Roman in the heading of a report and a non-proportional font like Courier New in the body of the report. When you need an underline in a report, use the font underline attribute. In many report writers, you can embed graphical elements, including lines and boxes, in the reports you build. Underlines drawn in this manner waste printer memory and slow down the report because the line is a graphic, not a text or font element. Another common practice dating back to the days of chain printers is the use of the underscore character (_) to underline text. Lines drawn this way waste valuable vertical space on the report because they are actually located on the line beneath the one they are attempting to underline. Getting the length of lines drawn this way to correspond with the columnar data above them is also difficult when proportional fonts are used. Use the underline attribute of whatever font you're using rather than either of these methods when you need to underline items in a report. Right-justify numbers that represent numeric quantities. Left-justify numbers that are used as identifiers (such as stock numbers, invoice numbers, and so forth). Use advanced print formatting features, like shading and bold font attributes, to make report elements stand out. There's really no reason not to use the available features of today's printers to give your reports a more polished look. Printers of just a few years ago lacked many of the

q

advanced formatting features that are standard on today's printers. Keep in mind, though, that your client's printer needs to support the features you intend to use. Prototype the reports you create as soon as possible for your clients so that you can both be sure that you have the same understanding of what each report is supposed to look like and do. As with Delphi forms, you may even be able to use your report writer itself to construct prototypes of reports with the user at your side. This helps avoid confusion and redoing work later.

Page 196

Test the Application for Compliance with the Predefined Purpose and Functions
The subject of application testing and quality assurance is a book unto itself. Although covering it in depth is outside the scope of this book, here are a few suggestions regarding testing the applications you develop:
q

q

q

q

q

Establish a formal procedure for reporting and responding to anomalies that are discovered in your application. A common practice is to develop a form or, even better, a small application to be used for this purpose. Include such essential information as a number for each report, the steps needed to reproduce it, the type of report (bug, suggestion, or information request), and the response of the developer(s) to the report. Set up a mechanism for easily distributing bug-fixes and updates to your clients as the need arises. One way I've done this is to build a small application that runs in the user's Windows Startup folder and checks the time stamp of the executable on the local hard disk against the one on a network or dial-up server. If the local copy is out of date, the app replaces it with the new version. Include and display a version number in all your applications. Increment the minor version number whenever you send a new release, even a bugfix. As mentioned previously, you should use a Windows VERSIONINFO resource (covered in Chapter 13) to do this. Ask yourself whether the application fulfills the basic purpose you defined for it. For example, you might ask yourself, "Does the application effectively facilitate rental property management?" Compare the application with the operational, functional, and technical requirements you originally defined for it. Make sure that the features that ended up being included in the app match the key functions you originally established for it.

q

If possible, set up a machine that is identical to those the application will run on and install and test the application using it. Be especially careful to reproduce the processor chip, available memory and disk resources, and the video adapter in use at your client sites. Attempt also to duplicate the desktop and network operating systems in use. If your user is a Windows NT user, test your app under NT. If the user is a Windows 95 user, make sure you test it under Windows 95. It's also a good idea to run the appli-cations that your clients use alongside your application to be sure that they're com-patible. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 197
q

q

q

q

q

q

Check your database constraints to be sure that they do what they're supposed to. Try to insert invalid data through your application into the database to ensure that it's rejected. Try to get passed both the lower and the upper boundaries of constraints that are range oriented. Attempt to delete rows with dependent keys. See whether the database will allow you to add duplicate values for columns that are not supposed to allow them. Try to omit fields that you know are required. Conduct concurrency testing. If the application is supposed to support 20 simultaneous users, test it with 30 or 40. Have the testers attempt to update the same row concurrently and have one user attempt to delete a row another user is editing. Test your database's locking facilities to make sure that updates by multiple, simultaneous users aren't lost and that they occur in a timely fashion. If the application is database server or network based, verify that the access rights you've defined control access as they're supposed to. Ensure that password retry lock-outs work as expected. Make sure that users have the rights they need to get to the parts of the application you intend for them to use. Attempt to violate the access rights for each level you've defined. Attempt to delete or update rows using logins that should not have the access privileges to do so; this will help ensure that your access restrictions work as expected. Have co-workers and people not affiliated with the project test the application before clients see it. If the application is to go into wide distribution, establish a small beta test group of key users to test the application before it's actually released. Conduct usability testing with users of different skill levels. Your application may work correctly, but it may be too difficult to use. Allowing users of different skill sets to try the application will help you

q

determine this. Remember that the user has the final word on whether the application meets his or her needs.

Installing the Application for Production Use
Though application deployment is covered in detail in Chapter 29, "Deploying Your Applications," here are a few suggestions to get you started:
q

q

As mentioned previously, set up a test machine that emulates the target user environment as closely as possible and install your application there before installing it anywhere else. Seek to impact the user's machine as little as possible. Avoid modifying anymore configuration parameters than absolutely necessary.

Page 198
q

q

Size up your client's site to determine whether additional hardware or software needs to be installed before your application can be installed. For example, if you intend to communicate with a client/server DBMS over TCP/IP, you'll need TCP/IP protocol support on your clients' machines. You might need to contact your network administrator for assistance with this. Create an installation program using Delphi's InstallShield Express tool to handle installing your application and its supporting files onto client machines. Chapter 29 discusses InstallShield at length.

Summary
Hopefully, you've gleaned enough from Chapters 6 and 7 to begin developing real database applications. You've probably begun to realize that client/server application development is complex, but it's not insurmountable. It may require more work than other types of apps you've built, but it is still software development. As you begin building client/server applications, remember that emphasis must be placed on the practical, not just the theoretical. Sometimes the rules must be bent in order to meet a client's needs. And remember that there's no "right way" to develop applications—no fixed set of steps that produces a good app every time. Software development is a craft, not an exact science.

What's Ahead
The next chapter begins the book's "Tutorial" section, in which you get to build a full-blown client/server database application. The design process is covered first, followed by the development and optimization of the application itself. Page 199

PART II

Tutorial
Page 200 Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 201

CHAPTER 8

Your First Real Client/Server Database Application
Page 202 The Silverrun tools used in Chapters 6, 7, and 8 are one of many tool sets available to a Delphi programmer. The selection of Silverrun for these samples was the author's choice and does not reflect an endorsement by Borland. This chapter begins the second major section of this book, the "Tutorial" section. In this section, you'll build a moderately complex client/server database application known as the Rental Management System, or RENTMAN for short. You'll build on the foundation laid in Chapters 6, "Client/Server Database Design," and 7, "Client/Server Application Design," to construct a complete client/server database application from start to finish. You'll be introduced to some of the most important aspects of developing robust client/ server database applications under Windows. As in the rest of the book, I stress the hands-on part of software development—what actually works—in the concepts presented here. This is especially true of the "Tutorial" section of the book, whose whole purpose is to lead you by the hand through the labyrinth that is Windows client/server application development. By taking you through the process of designing and building a real application, my purpose isn't to produce an application you could immediately begin using.

Time and space constraints don't permit me to explore the details of the RENTMAN application as I might like to. On the contrary, the general idea here is to give you a thorough introduction to the many ingredients that go into palatable client/server applications. Beyond a superficial introduction, though, the "Tutorial" section seeks to show how these ingredients are combined to produce real applications in the real world. Basically, I attempt to give the information I think is most crucial, without getting bogged down in details. The specific goals of the "Tutorial" section are to
q

q q q

Demonstrate how the database and application design concepts presented earlier in the book can be practically applied. Explore the many facets of actual client/server application development. Include sufficient tips and caveats to help you avoid common mistakes. Show how to optimize the application for client/server DBMSs.

As pointed out in Chapter 6, the five formal processes for building a database application are to
q q

q

q

q

Define the purpose and functions of the application. Design the database foundation and application processes needed to implement those functions. Transform the design into an application by creating the requisite database and program objects. Test the application for compliance with the predefined purpose and functions. Install the application for production use.

Page 203 In simple terms, the five formal processes correspond to these five phases of software development in general:
q q q q q

Analysis Design Construction Testing Deployment

This chapter covers the analysis phase in depth and also touches on the design and construction phases. You'll get further into the design and construction of the RENTMAN application in the remainder of the chapters in the "Tutorial"

section. You'll do much of the work in this chapter without ever setting foot in Delphi. Properly developing any type of complex application requires thorough planning before construction begins. Thanks to the work completed in Chapter 6, many of the tasks relating to the design of the RENTMAN app have already been completed, particularly those tasks having to do with database design. Rather than rehash that work in detail, I can summarize and then expand on it for our use here. NOTE The preliminary database design that was laid out in Chapter 6 is a prerequisite of the design work you'll perform in this chapter. In order to work through the examples presented here, you'll either need to complete the tasks laid out in Chapter 6 or load the models for Chapter 6 from the CD-ROM accompanying this book. Be sure you have a good grasp of RENTMAN's lease process models before proceeding.

You might recall that Chapter 6 explored the lease receipt and management process for Allodium Properties. In this chapter, you'll build on that process and explore the property maintenance process, as well. As was done in Chapter 6, you'll completely model the elements involved in the business processes you want to embody in the app. You'll begin by incorporating the elements that the lease and maintenance processes share from the models you built in Chapter 6. In order to do this, you'll need to briefly revisit the lease management process as it was outlined in Chapter 6.

Defining the Purpose of the Application
The first thing you need to do when you begin building a new application is define what the application is supposed to do. As Chapter 6 pointed out, you do this by defining the application's Page 204 statement of purpose. This statement of purpose should consist of a sentence that includes a subject, a verb, and the verb's object. The subject is always the application, the verb describes the work the application will do, and the object

identifies the recipient of this work. The application detailed in this chapter and those that follow has a very simple purpose: rental property management. The system itself won't manage rental properties; people will. It will assist them with this management. So, the RENTMAN application's statement of purpose, restated from Chapter 6, is as follows: The RENTMAN System will facilitate rental property management.

Defining the Functions of the Application
After you have a statement of purpose in hand, you're ready to determine the application's required functions. Required functions are those that the app must have in order to accomplish its statement of purpose. These are essential functions, without which the app would be incomplete. In this case, you need to state exactly what the app must do in order to "facilitate rental property management." You follow the same three-part format you used with the statement of purpose. Chapter 6 states RENTMAN's essential functions this way: The RENTMAN System will facilitate rental property management:
q q q q

It will log and maintain property leases. It will track ongoing property maintenance work. It will generate tenant billing information. It will provide historical information on individual properties.

After you've defined the application's essential functions, it's time to design the database and program objects necessary to accomplish them.

Designing the Database Foundation and Application Processes
Chapter 6 explored the database and program elements necessary to implement the management of property leases. It covered the first of the four essential RENTMAN functions. In this chapter, and throughout the remainder of the "Tutorial" section, I'll explain what's required to support the remaining essential functions, specifically, ongoing property maintenance work, tenant billing, and historical property statistics. Fortunately, these other functions

require many of the same database and program elements the lease process required. I'll examine those elements already defined in Chapter 6 in order to determine the new ones you'll need here. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 205 Modeling Relevant Business Processes The actual work of designing a database begins with process modeling and ends with designing the physical objects that are to compose the database. Generally speaking, database design can be reduced to these three steps: 1. Document the business processes necessary to accomplish the application's required functions. 2. Diagram the entity relationships necessary to service these processes. 3. Create the logical database design necessary to implement these entity relationships and business processes. Let's start with business process documentation in the reexamination of the work from Chapter 6. Business process models help you think through the processes that will make up an application. They assist you in deducing what database and program elements will be necessary in order to implement a given business process in your application. They consist of stores, processes, and external entities linked together with flow objects. Figure 8.1 shows the final version of the lease management business process model from Chapter 6. Figure 8.1. The lease management business process model.

When modeling business processes, you follow these three basic steps: 1. Determine what processes, external entities, flows, and stores are needed. 2. Decide how these elements interrelate. 3. Diagram these elements and relationships in your model. Page 206 You conduct user interviews, ask questions, and enter into a general mode of discovery to obtain the information you need to build your model. In constructing the lease management process model, we asked the following questions:
q q q q q

What external entities are needed to log and maintain property leases? What processes are involved? What resources will these processes require? What data stores will be needed? How does data flow from one element to another?

From these questions, we deduced the following:
q q

q

q

You'll need at least one external entity, that of the prospective tenant. Separate processes will be required to handle lease processing and lease execution. Assuming Allodium Properties wants to track calls from prospective tenants and wants to store tenant, lease, and property information separately, you'll need four data stores. These stores will stockpile calls, tenants, leases, and properties. As far as data flow between these elements, we can assume the following:
q

q

q

q

q

Prospective tenants contact the lease clerk at the property management office to inquire about available properties or enter into lease contracts. The lease clerk logs each call as it is received, regardless of whether it results in a lease. The lease clerk checks available properties before forwarding a lease to the lease manager. When a lease has been verified by the lease clerk, it is forwarded to the lease manager for execution. Tenant information received during the lease is kept on

q

file by the lease clerk. The lease manager keeps a record of executed leases.

Because the lease management process is basically complete, what other elements do we need to implement the remainder of RENTMAN's essential functions? RENTMAN's other required functions were
q q q

Tracking property maintenance work Generating tenant billing information Listing property historical information

Page 207 Let's take the first of these and apply the same set of questions as was applied to the lease management process:
q q q q q

What external entities are needed to track property maintenance work? What processes are involved? What resources will these processes require? What data stores will be needed? How does data flow from one element to another?

Although there is a variety of directions you could follow here, the following deductions seem obvious:
q

q

q

q

q

Your model will need at least one external entity to represent tenants who call with maintenance requests. Separate processes will be needed to handle call processing and maintenance work. A store will be required in which maintenance work can be collected and accessed. You'll need at least three additional data stores: a tenant store, a call store, and a property store. As far as data flow between these elements, you can assume the following:
q

q

q

Tenants contact the rental management office to report problems. Office personnel then open a work order and assign it to a maintenance worker. The worker carries out the work and notifies the office when it's complete.

Just as we did in Chapter 6, let's construct a business process model that includes these elements. Using the skills you gained in Chapter 6, start the Silverrun-BPM tool and construct a model like the one shown in Figure 8.2. TIP You can save yourself a lot of time by basing new models on existing ones. In this case, base your new business process model on the one created in Chapter 6. Do this by re-opening the LEASE.BPM model file in Silverrun-BPM and using the File | Clone menu option to save it under a different name. A good name for the process model in this chapter is MAINT. BPM, but you can use whatever you like. After you've cloned the file, close LEASE.BPM and load your new model. (An alternative to the clone, close, and reload approach is to simply choose the File | Save As command—use whatever suits you.) By beginning a new model using an existing one, you avoid having to re-create all the model elements (for example, stores, processes, and so on) that were in the original.

Page 208 Figure 8.2. A business process model for the property maintenance process.

As you can see, there are two stores in this model that weren't in the lease process model you constructed in Chapter 6. The EMPLOYEE and WORDER stores are new in this model because maintenance work obviously involves maintenance workers. Because Allodium wants to emulate the paper forms it's been using in this new system, the WORDER store is included to house work order information. Neither of these stores were needed in the lease process model, thus their absence from it. Other than the two new stores, the remainder of the model's data stores are

shared in common with the lease process model. So, the answer to the question, "What other elements do we need to implement the remainder of RENTMAN's essential functions?" is this: We need two additional stores, an EMPLOYEE store and a WORDER store, along with the processes and flows that are unique to the maintenance management process. As with the lease process, you'll want to define data structures at this point to save yourself time later. Data structures allow you to define attribute-level information for your stores. Later, this information will be used to construct attributes in your E-R diagrams and tables in your relational data model. The two stores you just added need data structures defined for them. Define two new data structures (Project | Data Structures), then add attributes to each of them (Data Structure | Composition). Add these attributes to the EMPLOYEE data structure: Employee Number Employee Name Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 209 Add these attributes to the WORDER data structure: WOrder Number WOrder Start Date WOrder End Date WOrder Employee Number WOrder Property Number WODetail Line Number WODetail Description Work Type Code Work Type Description Work Type Task Duration WOrder Comments After you've set up your data structures, associate each one with the appropriate store by following these steps: 1. Select the Presentation | Palettes | Data Structures menu option in Silverrun-BPM to display the list of stores currently defined. 2. Drag each data structure from the Data Structures Palette to the appropriate store using the right mouse button. 3. You should see an asterisk appear in the Data Structures Palette to the left of the data structures that have store associations. You can also double-click a store object to ensure that you've correctly linked it with a data structure. Saving Your Model

When your data structures are properly set up, you're ready to save your model and move on to E-R diagramming. Click File | Save to save your new model to disk. A good name for the file, if you haven't already named it, is MAINT. BPM. In order to use the data stores and structures defined in your business process model in the entity relationship diagrams you're about to construct, you'll need to follow a special set of instructions for making your stores visible to the Silverrun-ERX tool. Basically, you'll need to update a repository that both the BPM and ERX tools can access. To do this, follow these steps: 1. Click the Util. | WRM dictionary menu option. This puts the BPM tool into a special mode for working with the Silverrun local repository. After you click the menu option, you should see a dialog titled WRM Dictionary and a new menu on Silverrun-BPM's main menu titled WRM Dictionary. 2. With the dialog still on the screen, click the WRM Dictionary | Update WRM Dictionary menu item. 3. Click Update in the ensuing dialog, then click the Save button to accept the default update report name. This will cause the copy of the repository in memory to be updated with data stores and structures in your model. Accept the default update report filename, then click OK to clear the informational message that follows the update. Page 210 4. Click the WRM Dictionary | Save WRM Dictionary As menu option, then supply the name MAINT.WRM as your new repository filename. This saves the copy of the repository in memory to a disk file you can import in Silverrun-ERX. 5. Click the close button in the frame of the WRM Dictionary dialog. Now that your model and its associated repository are both saved to disk, you're ready to move on to constructing an E-R diagram for the maintenance management process. Close the Silverrun-BPM tool and start the ERX tool. Modeling Supporting Entity Relationships As was the case in Chapter 6, the primary benefit of using Silverrun's ERX tool to design your database is that it can normalize entity relationships. ERX includes a nice facility for detecting and fixing entity relationships that are not third normal form compliant. You've helped it along a bit by naming your

attributes so that non-compliant relationships are easy to spot, but the facility is, nonetheless, unusual among E-R tools. You won't find a facility that actually normalizes your design in very many database CASE tools. You could actually skip the E-R diagramming process and move straight to logical data modeling. Silverrun's RDM tool will import data stores from the BPM tool just as the ERX tool does. Again, the primary advantage to using ERX here is that it helps safeguard the relational integrity of your model. Use whatever process works best for you. Some people find the E-R diagramming process redundant and move directly from process modeling to logical data modeling. Importing Your Business Process Model Data Stores In order to translate into entities the data stores you defined in your business process model, you'll need to import them into Silverrun's ERX tool. To import your data stores, follow these steps: 1. Select the Util. | WRM Dictionary menu option. This places ERX in a special mode for working with the Silverrun local repository. 2. Click the WRM Dictionary | Open WRM Dictionary menu option, and select the MAINT.WRM repository file you created in the BPM tool. 3. Select the WRM Dictionary | Update a Model menu option, then click Update in the ensuing dialog and accept the default name for the update report file. When asked whether to link the model to the same project as the repository, click Yes. This adds the data stores and structures you defined in your business process model to the current model. You'll then be able to use them to construct entities in your E-R diagram. Click OK to clear the informational message that appears when the update completes. 4. Click the close button in the frame of the WRM Dictionary dialog. Page 211 Now that your stores are available in your model, you're ready to drag them onto the modeling surface. This will have the effect of translating them into entities that can participate in your E-R diagram. To translate your stores into model entities, follow these steps: 1. Select the Presentation | Palettes | Component: SR Store menu option. This displays a palette of stores that were imported indirectly from your business process model.

2. With the exception of the LEASE and PROPERTY stores, drag each data store from the palette to the modeling surface using the right mouse button. You'll omit the LEASE and PROPERTY stores because they don't exist in the maintenance management process model. Each of them is a leftover from the lease process model. 3. You'll notice that each entity is created in the center of the model, regardless of where you attempt to drop it, causing the entities to stack up. After all stores have been translated onto the model, drag each entity so that it doesn't overlap other entities. 4. Click the close button in the frame of the Component: SR Store palette to close it. Figure 8.3 shows what your E-R diagram might look like thus far. Figure 8.3. Your E-R diagram in its initial stages.

Normalizing Your Model Now that your initial set of entities is in place, you're ready to normalize your E-R model. To allow ERX to normalize your data model, follow these steps: 1. Click the Expert | Expert Mode menu option. This will put ERX in a special mode wherein it allows you to perform advanced functions like invoking the normalization facility. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 212 2. Click the Expert | Normalize Model menu option. This starts the normalization facility. When asked whether to analyze the names currently in use in the model, click Yes. 3. After the model's element names have been analyzed, click the OK button in the Analyze Attribute Names dialog. 4. You'll then be presented with a dialog warning you that normalization can alter your model significantly. Click the Yes button to tell ERX to proceed with the normalization process. At this point, your model is normalized as far as ERX can tell. The relationships established by the normalization expert are initially stacked on top of one another in your model. Drag them so that they don't overlap. Figure 8.4 shows what your model might look like so far. Figure 8.4. Your E-R diagram after the normalization expert has been executed.

Note the inclusion of two new entities in your model, the WODetail and Work Type entities. In order to be consistent with the rest of the model, double-click the upper portion of each of the new entities and change their names to uppercase. In keeping with the naming conventions outlined in Chapter 4,

"Conventions," remove the space from the Work Type entity when you change it to uppercase. Without a doubt, normalizing your model using ERX's normalization facility has saved you time. However, there are a couple of subtle problems with the model. First, note the relationship between the WORDER and WORKTYPE entities. WORKTYPE is actually related to work order line items; several different types of work can be performed on a single work order. Page 213 In this case, the guess made by the normalization expert is incorrect. To remedy the situation, follow these steps: 1. Click the relationship object between the WORDER and WORKTYPE entities and press the Delete key. This will remove the relationship between the two entities. 2. Click a relationship tool in the ERX tool palette (preferably the tool that allows diagonal lines), then click once on the WODETAIL entity and once on the WORKTYPE entity. This establishes a relationship between the two of them. You'll notice that the minimum and maximum cardinality of their relationship is left blank by default. You'll fix that later when you verify the model's connectivities. Another problem with the model is that the TENANT entity is orphaned. It doesn't participate in any relationships with other entities. This problem isn't the result of a bad assumption by ERX's normalization facility. Instead, it's the result of gaps in the original BPM data structure definitions you built earlier. There's no direct reference to the TENANT data store by any of the other stores in the maintenance process model. In the lease process model, the TENANT store references the LEASE store, and the LEASE store references the PROPERTY store. In the maintenance process model, the PROPERTY and LEASE stores are either defined without supporting data structures or missing altogether. Consequently, the TENANT store is on its own. Because you'll later combine the relational data models for the lease and maintenance processes, this isn't as significant as it might seem. In fact, you can remedy the situation for the moment by simply removing the TENANT entity from the model. Click on it with the left mouse button and press the Delete key to remove it. Figure 8.5 shows what your model might look like so far.

Figure 8.5. Your model as it might appear after it's been cleaned up a bit.

Page 214 Verifying Connectivities After you've cleaned up your model somewhat, you're ready to verify that the normalization expert's assumptions regarding entity connections are correct. When the expert normalized your model, it inferred relationships between your entities based on a number of factors. Before you proceed, you need to ensure that those assumptions were correct. To check the accuracy of your current entity relationships, follow these steps: 1. Select the Expert | Verify Connectivities menu option. This starts a connectivity expert that will ask you a set of questions regarding the entity relationships in your diagram. 2. Click the all connectivities radio button in the Verify Connectivities dialog, and make sure the for selected relationships checkbox is not checked, then click Verify. 3. You'll then be asked several questions in succession regarding the relationships between your entities. Silverrun-ERX will use your answers to these questions to adjust the connectivities in your model. Respond to each question using the answers provided in Table 8.1. Table 8.1. Answers to the questions posed by the ERX connectivity expert. Question In general, is it necessary for a WORDER to have an EMPLOYEE to exist? In general, can one WORDER have many EMPLOYEEs? In general, is it necessary for an EMPLOYEE to have a WORDER to exist? Answer Yes No No

In general, can one EMPLOYEE have many WORDERs? In general, is it necessary for a WORDER to have a WODETAIL to exist? In general, can one WORDER have many WODETAILs? In general, is it necessary for a WODETAIL to have a WORDER to exist? In general, can one WODETAIL have many WORDERs? In general, is it necessary for a CALL to have a WORDER to exist? In general, can one CALL have many WORDERs? Page 215 Question In general, is it necessary for a WORDER to have a CALL to exist? In general, can one WORDER have many CALLs? In general, is it necessary for a WORKTYPE to have a WODETAIL to exist? In general, can one WORKTYPE have many WODETAILs? In general, is it necessary for a WODETAIL to have a WORKTYPE to exist? In general, can one WODETAIL have many WORKTYPEs?

Yes No Yes Yes No No No

Answer No Yes No Yes Yes No

After you've finished answering the questions it poses to you, ERX's connectivity expert adjusts the relationships between your entities to reflect the cardinality your answers dictate. Notice that the cardinality between the WORDER and WODETAIL entities is no longer blank. Figure 8.6 shows what your model should look like thus far. Figure 8.6. Your model as it appears after entity connectivity has been

adjusted.

Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 235

CHAPTER 9

First Steps
Page 236 Now that your database objects have been created, you're ready to proceed with designing and developing the RENTMAN application. Chapter 7, "Client/ Server Application Design," shares a number of thoughts on designing database applications. You might want to review it before proceeding. Let's begin by reiterating the five phases of application development. We'll then pick up where we left off in Chapter 8, "Your First Real Client/Server Database Application." These phases are
q q q q q

Analysis Design Construction Testing Deployment

In Chapter 8, you completed the analysis, design, and construction phases for RENTMAN's database. In this chapter, you'll complete the analysis and design phases for the RENTMAN app itself and then embark on the construction phase. You'll begin the process of building application objects on top of the database foundation you constructed in Chapter 8.

Construct RENTMAN's Database Foundation
At this point, RENTMAN's database should already be designed and created. If it isn't, return to Chapter 8 and construct the RENTMAN database before proceeding. An application's database acts as a platform on which the rest of the app is built. It's important to finalize it before getting too far into the construction phase. Changes made to the database design after the application has been built can result in having to redo significant portions of the app.

Determine the Type of Application You're Building
Remember from Chapter 7, the first step in building a database application is to determine what type of application you need to build. There are two basic choices: online transaction processing (OLTP) systems and decision support systems (DSS). DSS apps are utilized by management to obtain a bird's-eye view of the company's data. OLTP systems are responsible for the entry and manipulation of this data. DSS applications can get by just fine with read-only access, but transaction processing systems need to be able to both read and write data. This fundamental difference between these two application types is the chief reason you need to know what type of app you're building before you get started. RENTMAN is a transaction processing application. Users will need to be able to add, change, and delete data, so the system doesn't neatly fit into the criteria you established for DSS apps. On the other hand, some of the information rendered by the application will be used by Page 237 Allodium's management in the decision-making process. Therefore, as with most client/server applications, RENTMAN will be a hybrid app—it will include key elements from OLTP and DSS apps. For your purposes, you'll build RENTMAN as a transaction processing application, since OLTP apps include most DSS elements, as well.

Derive Application Objects from Application Processes
Using the business process models you developed in Chapter 8, examine each business process to determine the application objects (usually forms and reports) necessary to implement it. For example, you can logically deduce that you'll need a means of getting data into each store in your process models. Remember that there is a correlation between the stores in your process models

and the tables in your physical design. Similarly, there is a simple correlation between the tables in your database and the forms in your app. You'll probably need at least one form for each table. This might consist of a simple data entry form wherein users enter new rows into the table, or it might consist of a fullblown maintenance form where users can locate, add, change, and delete table rows. Don't worry about the uncertainty you may experience at this point regarding the forms and reports you're identifying. Your choices here are just a starting point. The main thing you're after is to determine the development scope of your application. You need to know, in broad terms, what will be involved with constructing the app. Specifics of the implementation will probably change as you go—that's to be expected. As you determine each form, report, or other application object that corresponds to your process model elements, it's helpful to annotate your models with this information. Doing so helps you think through your application's fundamental objects. Figure 9.1 illustrates RENTMAN's lease process model annotated with the application objects necessary to implement it. Note that I've already begun naming RENTMAN's application objects according to the naming guidelines presented in Chapter 4, "Conventions." Also note that these names indicate specific types for each form. That is, I've already begun distinguishing control grid forms from regular grid forms and basic entry/update forms from those designed for quick data entry. What's a control grid form? I'll talk more about this later, but control grid forms are those that use Delphi's DBCtrlGrid component to display multiple records on a single form. These forms are contrasted with those that use Delphi's DBGrid component to display multiple rows. The ability to foresee interface details such as the component you'll use when building a particular form is a skill acquired through experience. Developers who've been building client/server applications for a number of years find it easy to visualize implementation details such as these at a very early stage in the app development process. Page 238 Figure 9.1. RENTMAN's lease process model annotated with application objects.

You'll want to identify the requisite application objects in each of your business process models. In the case of the RENTMAN app, there are two basic business processes to be explored: the lease process and the maintenance process. As you examine each process model for possible application equivalencies, classify each object you identify as either a report, a form, or non-interactive support code (non-interactive support code consists of a block of programming code that you can ascertain the app will need but which is not directly related to the app's user interface). Each element you identify should be one of the three (for our purposes, consider graphs as reports). Also, attempt to make a best guess regarding what type of object you might build (for example, a quick-entry form, a detail list report, and so on), and label your objects accordingly. Keep in mind that some application objects are implicit. For example, every app needs some sort of primary form or main screen. Also, each table in your app will likely need a form to add, change, or delete its rows. Table 9.1 provides a preliminary list of forms and reports for RENTMAN. Table 9.1. RENTMAN preliminary form and report listing. Name Purpose transaction fmRSYSMAN0 processing fmRCALEDT0 data-entry fmRCALGRD0 transaction processing Object Type SDI form quick-entry form control grid form Description The system's main menu The call entry/edit form The call list grid form

Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 232 3. When asked whether to save the script's output to a file, answer No. This causes the script's output to return to ISQL, which, in this case, will simplify things. 4. After a small delay, ISQL should display a message indicating that the script executed successfully. You can now exit the ISQL utility. The Finish Line Congratulations! You've just finished building the RENTMAN database. You started with a mere concept of what the RENTMAN application would need to do, and you ran the entire database design gamut to arrive at a fully normalized, fully functional client/server database. RENTMAN's Other Functions As is so often the case with database design, a cursory review of your new RENTMAN database reveals that you won't need separate processes and supporting players to implement RENTMAN's third and fourth major functions. These functions (tenant billing generation and property historical information) can actually be rendered from the database already in place and produced as printed reports by the RENTMAN application. How do we know this? Let's approach it deductively. Assuming that tenant billing generation amounts to simply printing a reminder to pay the rent each month for each tenant, all we need in terms of data is access to the list of tenants, their addresses, the rent due date, and the amount they pay. The TENANT, PROPERTY, and LEASE tables contain all the elements necessary to derive this information.

Tracking property historical information amounts to showing the property's rental periods and the maintenance work that's been done on it. Again, all the supporting players are already in place. As you'll see in Chapter 12, "Reports," RENTMAN can provide historical information for each property through enduser reports. No additional processes or data objects need to be defined in order to support these functions in the RENTMAN application. Of course, there are all sorts of offshoot processes one could derive from those already discussed. For example, in order to support regular property maintenance, the rental company will have to stockpile an inventory of tools and materials with which to do the work. A whole separate process could be diagrammed for managing and disbursing this inventory. Likewise, an entire payment system could be attached to the billing generation you've devised so far. This system would extend the system's capability to print tenant billing reminders to also track payments received, past-due balances, and so on. I won't venture into very many of these ancillary processes. As I've said, you're mainly concerned with RENTMAN's core functions. Page 233

Summary
In this chapter, you designed and implemented an InterBase client/server database for the RENTMAN app. You defined the purpose and essential functions of the application, then designed and created the database objects necessary to implement those functions. You deepened the skills acquired in Chapter 6 by constructing business process models, E-R diagrams, relational data models, and physical database schemas. You should now have all the tools necessary to begin creating your own client/server databases.

What's Ahead
In the next chapter, we'll begin creating the Delphi program objects needed to implement RENTMAN's core functions. The database objects you've constructed in this chapter will serve as a foundation on which you'll build a full-featured client/server database app in the chapters to come. Page 234 Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 228 Listing 8.1. The DDL script generated by Silverrun-RDM for your RENTMAN relational data model. CONNECT "C:\DATA\RENTMAN\RENTMAN.GDB" USER " " PASSWORD " ";

CREATE DOMAIN TAddition AS CHARACTER(20) NOT NULL CHECK (VALUE IN (`Deerfield', `Firewheel', `Legacy Hills', Â'Switzerland Estates', `Sherwood', `Rockknoll')); CREATE DOMAIN TAddress AS CHARACTER(30) NOT NULL; CREATE DOMAIN TCity AS CHARACTER(30) NOT NULL CHECK (VALUE IN (`Oklahoma City', `Norman', `Edmond', `Dallas', Â'Richardson', `Plano')); CREATE DOMAIN TComments AS VARCHAR(80); CREATE DOMAIN TPhone AS CHARACTER(13) NOT NULL; CREATE DOMAIN TPropertyNo AS INTEGER NOT NULL; CREATE DOMAIN TRent AS NUMERIC(5,2) NOT NULL; CREATE DOMAIN TRooms AS INTEGER NOT NULL CHECK (VALUE IN (0, 1, 2, 3, 4, 5)); CREATE DOMAIN TSchoolDistrict AS CHARACTER(20) NOT NULL CHECK (VALUE IN (`Putnam City', `Oklahoma City', `Richardson', Â'Edmond', `Garland', `Dallas', `Plano')); CREATE DOMAIN TState AS CHARACTER(2) NOT NULL CHECK (VALUE IN (`OK', `TX'));

CREATE DOMAIN TTenantNo AS INTEGER NOT NULL; CREATE DOMAIN TYesNo AS CHAR(1) NOT NULL CHECK (VALUE IN (`Y', `N', `T', `F')); CREATE DOMAIN TZip AS CHARACTER(10) NOT NULL;

CREATE TABLE EMPLOYEE ( Employee_Number INTEGER NOT NULL, Name CHARACTER(30) NOT NULL, PRIMARY KEY (Employee_Number) ); CREATE TABLE PROPERTY ( Property_Number Address City State Zip Addition SchoolDistrict Rent Deposit Page 229 LivingAreas TRooms NOT BedRooms TRooms NOT BathRooms TRooms NOT GarageType TRooms NOT CentralAir TYesNo NOT CentralHeat TYesNo NOT GasHeat TYesNo NOT Refrigerator TYesNo NOT Range TYesNo NOT DishWasher TYesNo NOT PrivacyFence TYesNo NOT LastLawnDate DATE, LastSprayDate DATE, PRIMARY KEY (Property_Number) ); CREATE TABLE TENANT ( Tenant_Number NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL, NULL,

TPropertyNo NOT NULL, TAddress NOT NULL, TCity NOT NULL, TState NOT NULL, TZip NOT NULL, TAddition NOT NULL, TSchoolDistrict NOT NULL, TRent NOT NULL, TRent NOT NULL,

TTenantNo NOT NULL,

Name CHARACTER(30) NOT NULL, Employer CHAR(30) NOT NULL, EmployerAddress TAddress NOT NULL, EmployerCity TCity NOT NULL, EmployerState TState NOT NULL, EmployerZip TZip NOT NULL, HomePhone TPhone NOT NULL, WorkPhone TPhone NOT NULL, ICEPhone TPhone NOT NULL, Comments TComments, PRIMARY KEY (Tenant_Number) ); CREATE TABLE WORKTYPE ( Work_Type_Code INTEGER NOT NULL, Description CHARACTER(30) NOT NULL, TaskDuration NUMERIC(5,2) NOT NULL, PRIMARY KEY (Work_Type_Code) ); CREATE TABLE LEASE ( Lease_Number INTEGER NOT NULL, BeginDate DATE NOT NULL, EndDate DATE NOT NULL, MovedInDate DATE, MovedOutDate DATE, Rent TRent NOT NULL, Deposit TRent, PetDeposit TRent, RentDueDay SMALLINT NOT NULL, LawnService TYesNo NOT NULL, Comments TComments, Property_Number TPropertyNo NOT NULL, Tenant_Number TTenantNo NOT NULL, PRIMARY KEY (Lease_Number), CONSTRAINT FK_TENANT1 FOREIGN KEY (Tenant_Number) REFERENCES TENANT, continues Page 230 Listing 8.1. continued CONSTRAINT FK_PROPERTY2

FOREIGN KEY (Property_Number) REFERENCES PROPERTY ); CREATE TABLE WORDER ( WOrder_Number INTEGER NOT NULL, StartDate DATE NOT NULL, EndDate DATE, Comments TComments, Property_Number TPropertyNo NOT NULL, Employee_Number INTEGER NOT NULL, PRIMARY KEY (WOrder_Number), CONSTRAINT FK_PROPERTY3 FOREIGN KEY (Property_Number) REFERENCES PROPERTY, CONSTRAINT FK_EMPLOYEE4 FOREIGN KEY (Employee_Number) REFERENCES EMPLOYEE ); CREATE TABLE CALL ( Call_Number INTEGER NOT NULL, CallDate DATE NOT NULL, CallTime CHARACTER(11) NOT NULL, Description CHARACTER(30) NOT NULL, Property_Number TPropertyNo, WOrder_Number INTEGER, PRIMARY KEY (Call_Number), CONSTRAINT FK_PROPERTY5 FOREIGN KEY (Property_Number) REFERENCES PROPERTY, CONSTRAINT FK_WORDER6 FOREIGN KEY (WOrder_Number) REFERENCES WORDER ); CREATE TABLE WODETAIL ( WODetail_Line_Number INTEGER NOT NULL, Description CHARACTER(30), Work_Type_Code INTEGER NOT NULL, WOrder_Number INTEGER NOT NULL, PRIMARY KEY (WOrder_Number, WODetail_Line_Number), CONSTRAINT FK_WORKTYPE7 FOREIGN KEY (Work_Type_Code) REFERENCES WORKTYPE, CONSTRAINT FK_WORDER8 FOREIGN KEY (WOrder_Number)

REFERENCES WORDER ); Page 231 Note that the script isn't quite ready to run yet because its CONNECT statement doesn't include a valid username and password. You can edit the script file and add a valid username and password to its CONNECT statement by using Delphi's code editor. Start Delphi now and open RENTMAN.SQL in the editor. Change the CONNECT statement to match the following: CONNECT "C:\DATA\RENTMAN\RENTMAN.GDB" USER "SYSDBA" PASSWORD "masterkey"; After you've made this change, save your script file and exit Delphi. Creating Your Database Now that you have a complete SQL DDL script, you're ready to proceed with creating the RENTMAN database itself. To create an InterBase database to house the RENTMAN database objects, follow these steps: 1. Start the InterBase Server if it is not already started. This server will probably run on your local machine, though it certainly doesn't have to. To start a local copy of the server, execute the InterBase Server 4.2 application in your InterBase 4.2 folder. Under Windows NT 4.0 and Windows 95, you can access the InterBase 4.2 folder from the Windows Explorer. Under Windows NT 3.51, locate the InterBase 4.2 group and double-click the InterBase Server 4.2 icon to start the server. 2. Create a directory on the server machine specifically for storing the new database you're about to create. On my machine, that directory is C:\DATA\RENTMAN. 3. Start the InterBase Windows ISQL tool and click its File | Create Database menu option. You should then see the Create Database dialog. 4. Under Database Name, type the full path to the database file you want to create. By default, this should be C:\DATA\RENTMAN\RENTMAN.GDB. The name you choose here must match the Coded Name you typed in the RDM tool's Schema Description dialog. 5. Key a valid username and password into the appropriate entry boxes. You'll probably see the SYSDBA username provided by default. Out of the box, the default password for InterBase's SYSDBA user is masterkey. The password is case sensitive, so be sure you type it correctly. 6. Click the OK button to create your database. After a short delay, ISQL should return from creating your database and automatically connect you to it. Now that your database is created, you're ready to run the SQL script created for you by Silverrun-RDM. To do this, follow these steps: 1. Select ISQL's File | Run an ISQL Script menu option and supply the name of the script file that the RDM tool generated for you. 2. Click the Open button to execute the script.

Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 224 Generating Coded Names Now that all your columns have been defined, you're ready to generate coded names for your model's elements. Coded names are secondary names for model elements that are legal on your selected DBMS platform. Silverrun-RDM removes spaces, truncates long names, and generally cleans up the names used by your model. Note that the original names you selected are kept intact; coded names are stored separately. When you generate the SQL script you'll use to actually construct your database objects, you can specify whether you want to use the full names of your model's elements or their coded names. You can also control whether RDM itself displays coded names in the graphical representation of your model via the Presentation | Displayed Descriptor menu option. To generate coded names for your model, follow these steps: 1. Select the Schema | Generate Coded Names menu option. 2. Click the Specify button, then set Maximum Length and the Nb. of characters used from the beginning of each word in the name setting to 30. The objects you're creating will reside in an InterBase database, so 30 is a good choice. Changing the abbreviation length from 3 to 30 prevents the words in your element names from being abbreviated to three characters each. 3. Leave the dialog box's other settings unchanged, and click OK to return to the Coded Names Generation dialog and click the Generate button. To view your new coded names, double-click any of the tables in your model. To view the coded name for a particular column, double-click it in the Table Columns dialog.

As you did with the lease model tables, after you've generated coded names for your model elements, you'll need to edit the names generated for the columns in the WORDER, WODETAIL, EMPLOYEE, and WORKTYPE tables. Except for each table's primary key, remove the table name from the left of each column's coded name. You originally included these table name prefixes to assist the normalization expert in Silverrun-ERX, but they are no longer needed. For example, reduce WODetail_Description to Description, but leave WODetail_Line_Number unchanged. Doing this gives you object names that are easier to work with in the applications you build over them, as well as ensuring that columns that are most likely to become foreign keys in other tables—that is, primary keys—remain uniquely identifiable across the database. Establishing Table Relationships Since you imported your model from Silverrun-ERX, most of the relationships between your tables are already in place. However, because a property entity was never defined in the E-R diagram for the maintenance process, no relationship has yet been established between the WORDER and PROPERTY tables. Allodium Properties would naturally want to include property level information on the work orders it assigns to maintenance workers. Though this Page 225 information could usually be looked up indirectly by using the CALL table, not all work orders will have associated calls. Based on a given property's history, Allodium's maintenance manager may decide that the property needs its roof replaced. A work order would then be issued without there ever having been a call from a tenant. The resolution here is to connect the two tables relationally. To do so, click the Connector tool in the RDM tool palette, then click the PROPERTY table, followed by the WORDER table. You should see a default relationship instantly established for them. Fortunately, the guess that RDM makes regarding the relationship's cardinality is correct; you won't need to modify it. This is largely due to the order in which we clicked the tables. RDM assumes that the first table clicked is the parent table and the second one is the child table. Figure 8.11 illustrates the new relationship you should see. Figure 8.11. You can easily link tables using RDM's

Connector tool.

Generating Foreign Keys After all the relationships between the tables in your model have been established, you're ready to generate foreign keys. Rather than automatically propagating foreign keys as you link tables to one another, the RDM tool provides a batch facility for generating the foreign keys for all the model's tables in one pass. Generating foreign keys causes key columns in parent tables to be proliferated to their child tables. In the physical design, a foreign key is usually implemented as a column (or columns) in the child table that references the parent table's primary key. Page 226 Follow these steps to generate foreign keys: 1. Select the Schema | Foreign Keys | Generate menu option. 2. Click the Generate button in the ensuing dialog. 3. Click the Save button to accept the default filename for the foreign key report. You should then see a number of new columns added to your table objects. Depending on your chosen notation style, each foreign key will probably be prefaced with FK. Figure 8.12 illustrates what your model should now look like. Figure 8.12. Your model as it appears after foreign keys have been added.

Your model is now basically complete. You're just about ready to generate the

SQL necessary to create the objects defined by your model. Verifying Model Integrity Before you generate the SQL necessary to create your database objects, you should verify the integrity of your model. Silverrun-RDM includes a handy facility for doing this for you. Invoke it now by following these steps: 1. Select the Schema | Verify Integrity menu option. 2. Select Maximum Level of Verification from the ensuing dialog, then click the Verify button. Page 227 3. When prompted for a filename for the report, accept the default, Verify, by clicking the Save button. 4. You should get a message indicating that there are no errors in your model. You might get warnings, but there shouldn't be anything seriously wrong with the model. 5. If you want to view the model integrity report RDM generated, select the Util. | View a Text File menu option and specify the name of your integrity report (Verify, by default), then click the Open button to view it. Generating DDL After your model is complete and its integrity has been verified, you're ready to move on to generating the SQL Data Definition Language (DDL) statements required to implement your design in a database. Silverrun will create a single SQL script you can then execute to create your database objects. In addition to generating CREATE TABLE statements for all the tables in your model, the RDM tool will also include a CONNECT statement in the script for connecting you with the database in which the new objects will reside. RDM gets the database name it will use from the current schema's coded name, so setting this coded name to the name of your database before generating DDL is advantageous. This allows the CONNECT statement that RDM generates to at least include the correct database name. To set your schema's code name, follow these steps: 1. Click the Schema | Schema Description menu option. This will display the Schema Description dialog.

2. In the Coded Name entry box, type the full path name of the InterBase database in which your new objects will reside. This will be C:\DATA \RENTMAN\ RENTMAN.GDB by default. 3. Click OK to exit the dialog. Now that your database name has been set up, you're ready to generate SQL. To create your DDL script, follow these steps: 1. 2. 3. 4. Select the Schema | Generate DDL menu option. Click the Coded Name radio button in the ensuing dialog. Click the Generate button. When prompted for a filename, type *.SQL, and then press Enter. This will list all SQL scripts and will change the default file extension from TXT to SQL. Next, type RENTMAN.SQL and click the Save button. Your script will then be generated.

Both your model and your script are now complete. You can now save your model and exit the Silverrun-RDM tool. Listing 8.1 lists RENTMAN.SQL in its entirety. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 239 Name Purpose transaction fmRWORMDE0 processing transaction fmRWORGRD0 processing fmREMPENT0 fmREMPGRD0 data-entry Object Type Description master-detail The work order masterform detail form The work order list grid form grid form quick-entry The employee quick form entry form The employee list/edit grid form grid form The work type list/edit grid form grid form quick-entry The work type quick form entry form control grid The PROPERTY table form control grid form control grid The TENANT table form control grid form The LEASE table grid grid form form form report The official lease form The work order form form report given to workers The report listing detail report current employee tasks

transaction processing transaction fmRWKTGRD0 processing fmRWKTENT0 data-entry fmRPROCGD0 fmRTENCGD0 fmRLEAGRD0 rpRLEAPRT0 transaction processing transaction processing transaction processing report

rpRWORORD0 report rpRTSKLST0 report

There, no doubt, could be others, but this is a good start. You'll have plenty to do in constructing these objects alone.

Design a Form Hierarchy
Now that you've organized RENTMAN's forms by type, it's time to design a form hierarchy. Because Delphi supports visual form inheritance, making use of a form hierarchy can save you a lot of time designing forms. You can design an ancestor form that all similar forms inherit from. Then, when you need to make a change to a given type of form (say, the quick data-entry forms in your application), you can simply change the ancestor form, and your modification will cascade through to its descendants. Page 240 Classes of Delphi Forms The first step in designing a form hierarchy is to classify your forms by function. Because you've already identified each form's type, this is easily done. Delphi database forms can be divided into four general functional classes. The classes are
q

q

q

q

Entry/edit—Simple forms that represent a single record from a single table Grid—Forms that show several rows from a single table in a manner similar to a spreadsheet (each row occupies a single screen row) Control grid—Forms that show several rows from a single table (each row can occupy multiple screen rows) Master-detail—Forms that show a row or rows from two or more related tables

If you extract the forms and object types listed in Table 9.1, you'll see that RENTMAN's forms can be broken down as in Table 9.2. Table 9.2. The RENTMAN System's forms organized into form classes. Form Class fmRCALEDT0 Edit/entry fmRCALGRD0 Grid MasterfmRWORMDE0 detail

fmRWORGRD0 fmREMPENT0 fmREMPGRD0 fmRWKTGRD0 fmRWKTENT0 fmRPROCGD0 fmRLEAGRD0 fmRTENCGD0

Grid Edit/entry Grid Grid Edit/entry Control grid Grid Control grid

Given this breakdown, it's obvious that the RENTMAN System has representatives from all four functional groups. It, therefore, makes sense to include ancestor forms for all four groups in RENTMAN's form hierarchy. Figure 9.2 illustrates a simple form hierarchy. You'll create each of these ancestor forms first, then save them to Delphi's Object Repository so that you can inherit from them when you build new forms. Each of the form classes in RENTMAN's form class hierarchy are named for the type of form they typify. All forms, even non-database forms, will descend from AnyForm. It's the top-level form from which the rest of RENTMAN will descend. This helps ensure that all RENTMAN Page 241 forms have a consistent look and feel. Likewise, DatabaseForm ensures that all database- oriented forms behave uniformly. This is also true of the other form classes; each serves as an archetype for the forms that will make up RENTMAN. Figure 9.2. A form hierarchy for RENTMAN.

Begin Building the Application

At long last, you're finally ready to begin actually building the application. You'll take the design you've developed thus far and put it into action. As I've said before, there's no right way to build an application. You might want to deviate from some of the design or development decisions made here and in the following chapters. Feel free to do so; few of them are key to the application working properly, and most are a matter of preference. What I'm attempting to do in this and the ensuing chapters is introduce you to a wide variety of real client/server development issues. You may find alternate paths through these issues that work better for you. Create a BDE Alias The first thing you need to do when creating a Delphi database app is create a BDE alias that references your app's database. Of course, if your app accesses multiple databases, you'll need multiple aliases, as well. For the RENTMAN app, you'll define an alias that provides access to the InterBase database you created in Chapter 8. After the alias is created, you'll use it whenever Delphi needs a reference to your app's physical database. Page 242 Delphi's Database Explorer has a variety of uses; you'll use it here to create a Borland Database Engine alias. To create a BDE alias using Database Explorer, follow these steps: 1. Start Delphi and select Explore from the Database menu. 2. Click on the Database Explorer's Database tab, if it isn't already selected, then click the Databases entry in the list below it. 3. Right-click the Database entry and select New on the ensuing menu. This starts a new BDE alias definition. 4. You're then prompted to select a database driver for your new alias. Select INTRBASE from the list and click OK. 5. Next, you're prompted for the name of the new database alias. Type RENTMAN and press Enter. 6. Click the SERVER NAME entry in the right pane and type the full pathname of the RENTMAN database you created in Chapter 8. By default, this is C:\DATA\RENTMAN\RENTMAN.GDB. You can click the ellipsis (…) button to the right of the entry to select the file using your mouse. 7. Click the USER NAME entry and type in the name of a valid InterBase user (for example, SYSDBA).

8. Change the ENABLE BCD parameter to True. 9. Next, click the Apply button in Database Explorer's toolbar. It's the blue arrow that curves to the right. This saves your new alias definition permanently. Figure 9.3 illustrates what the screen should look like when you're done. Figure 9.3. The Database Explorer as it should look when your new alias is defined.

Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 243 WARNING Be sure to set the ENABLE BCD setting to True in BDE aliases that access InterBase tables with NUMERIC or DECIMAL fields. This will disable an optimization that the BDE normally performs on these types of fields that causes them to be treated as integers. The idea behind the optimization is that floatingpoint types with no digits to the right of the decimal (those with a scale of zero) should be treated as integers in the interest of speed. The problem is, the optimization goes too far and affects all NUMERIC and DECIMAL fields, even those with non-zero scales. The net effect is that data aware controls such as DBEdit won't allow decimals to be entered into these fields because it thinks they're integer fields. Setting ENABLE BCD to True works around this by forcing floating-point types to be treated as Binary Coded Decimals, thus preempting the integer optimization.

Start a New Project Close Database Explorer and return to Delphi and then select File | New Application from Delphi's main menu. Delphi then presents you with a new project and a blank form. Click File | Save All to save your project. Accept the default filename for Unit1, but change the project name to RENTMAN. Save the files to a directory you've set aside for going through this tutorial (for example, C:\DATA\DELPHI\RENTMAN).

Construct a Data Module The next step in building RENTMAN is to create a data module form. A data module is a special type of form designed for holding non-visual controls like database access controls. The data modules presented in this book are database oriented. That is, they store all the tables in a given database. In this case, the data module in the RENTMAN application includes a Table and DataSource component for every table in the RENTMAN system. I recommend that you create a data module for every database you might want to access from your Delphi applications. Although you don't need to locate all the components referencing a given database in a single data module, I recommend you do so when possible because you can save the data module to the Object Repository where it can be reused in other applications. This helps ensure consistency across your applications and enables you to alter Delphi's access to a given database from a single vantage point. So, select File | New Data Module from Delphi's main menu. You'll be presented with a small form onto which you can then drop non-visual components. Click the Name property in the Object Inspector and change it to dmRENTMAN. At this point, you might want to enlarge your data module form so that you can easily work with the numerous objects you'll need to place on it. To finish setting up your data module, follow these steps: Page 244 1. Click the Data Access page on Delphi's component palette and drop a Database component onto the form. 2. Change the Database component's AliasName property in the Object Inspector to reference the RENTMAN alias you created earlier. 3. Change the Database component's DatabaseName and Name properties to dbRENTMAN. 4. Drop eight Table components and eight DataSource components onto the data module form. TIP

You can quickly drop several copies of the same component by pressing Shift before you click the component in the palette. Then, each time you click on a form in Delphi's visual designer, you get another copy of the component. To exit this repetitive drop mode, click the pointer arrow icon located at the left of the component palette.

5. Set the DatabaseName property of all eight Table components to reference the dbRENTMAN application-specific alias you just defined. TIP You can quickly select several components at once by dragging a selection rectangle over them. You do this by positioning the mouse pointer to the left and above the leftmost component, holding down button one (the left mouse button, by default), then dragging the rectangle that subsequently appears so that it encompasses the controls you want to select. When you release the mouse button, the controls within the rectangle will be selected.

6. Change the Name and TableName properties of the Table components to match the list in Table 9.3. Table 9.3. Component and table names for the RENTMAN System. Name taTENANT taPROPERTY taLEASE taCALL taWORDER Page 245 Name TableName taWODETAIL WODETAIL taEMPLOYEE EMPLOYEE TableName TENANT PROPERTY LEASE CALL WORDER

taWORKTYPE WORKTYPE NOTE A dialog box may appear, and it will prompt you for the password to the dbRENTMAN database. The default user is SYSDBA and the default password is masterkey. Be sure to enter the password in lowercase, then click OK. If your login fails, your InterBase server may not be started. See the section "Creating Your Database" in Chapter 8 for the steps on how to start your server. Note that you can add the InterBase server icon to your startup folder so that it's loaded every time you start Windows.

7. Reselect all eight tables and set their Active properties to True. This has the effect of opening all eight tables and causes them to be automatically opened by any application that uses the data module. 8. Now that the Table components have been set up, it's time to set up the DataSource components. Change the DataSet and Name properties of the DataSource components to match the following list: DataSet taTENANT taPROPERTY taLEASE taCALL taWORDER taWODETAIL taEMPLOYEE taWORKTYPE Name dsTENANT dsPROPERTY dsLEASE dsCALL dsWORDER dsWODETAIL dsEMPLOYEE dsWORKTYPE

Figure 9.4 shows the completed data module. Now that the data module is completed, let's save it to the Object Repository: 1. Select File | Save from Delphi's main menu and save your data module to disk as \DATA\DELPHI\RENTMAN\RENTDATA, substituting the name of your RENTMAN source code directory for \DATA\DELPHI \RENTMAN.

2. Right-click on the data module itself and select Add to Repository. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 246 Figure 9.4. The completed RENTMAN System data module.

3. Type dmRENTMAN for the Title and Data module form for the RENTMAN database for the Description. 4. Select Data Modules in the Page drop-down list box and then click OK. Figure 9.5 shows the Add To Repository dialog box. Figure 9.5. Adding dmRENTMAN to the Object Repository.

Page 247 Because you've now added the dmRENTMAN data module to the Object Repository, you'll be able to use it whenever an application needs access to the RENTMAN database.

Create the Form Hierarchy The next step in developing the RENTMAN app is to create the form hierarchy that was designed earlier. You'll also be able to reuse this form hierarchy in other systems you build with Delphi. To reiterate, the hierarchy defines six forms: a top level form, a database form, an edit form, a grid form, a control grid form, and a master-detail form. Let's begin by creating the top level form. AnyForm Click the File | Close menu option to close your data module, then click the New Form button (or select New/Form from the File menu). You should see a form named Form2 appear in Delphi's visual form designer. You'll customize this form and save it as the AnyForm form class (from the preceding form hierarchy) in the Object Repository. Follow these steps to set up AnyForm: 1. Change the form's Name property to fmAnyForm, then change its Position property to poScreenCenter. 2. Set the form's AutoScroll property to False. 3. Drop three Panel controls onto the form, arranging them vertically. Name them paTop, paMiddle, and paBottom, respectively. 4. Set the Align property of the paTop panel to alTop and delete the value in its Caption property. 5. Resize paTop so that it occupies about 1/3 of the form's vertical space. 6. Set the Align property of paBottom to alBottom and delete its caption. 7. Resize paBottom so that it's about 40 pixels in height. 8. Set the Align property of the paMiddle panel to alClient; this should cause it to fill the empty space on the form between the paTop and paBottom components. 9. Delete the contents of paMiddle's Caption property. In case you're wondering, you're designing this first form with three panels because the forms in this system will generally follow a three-panel design. Some won't need three panels, but most will. You're also setting the Position property of the form to ensure that each form in the system displays consistently. Figure 9.6 shows what the new form should look like. Now that AnyForm is complete, follow these steps to save it to the Object Repository: 1. Click the File | Save menu option and save the form as ANYFORM.PAS in your RENTMAN source directory. Page 248

Figure 9.6. The completed AnyForm class.

2. Right-click the form and select Add to Repository. 3. Enter fmAnyForm for its title and Top-level generic form class for its description. 4. Select Forms from the Page drop-down list and click OK. Figure 9.7 illustrates the completed dialog. Figure 9.7. Adding the AnyForm form class to the Object Repository.

Page 249 DatabaseForm Now that the base form has been created, you can proceed with creating the DatabaseForm class. Follow these steps to create DatabaseForm: 1. Select File | New from the menu and then select Forms. 2. On the Forms page, click the fmAnyForm icon and click the Inherit radio button at the bottom of the dialog. 3. Click OK to create the new form. You should see a form named fmAnyForm1 appear in the Delphi visual form designer. You'll notice that it has inherited all the visual attributes of the original fmAnyForm class. 4. Change the form's Name property to fmDatabaseForm. 5. Click the paBottom panel to select it, then drop a DBNavigator component onto its left side. 6. Reselect the paBottom panel and drop a fourth Panel component onto its right side. 7. Set the new Panel component's Name property to paRight, set its Align property to alRight, and set its BevelOuter property to bvNone. 8. Delete paRight's caption and drag its left border to the right end of paBottom's DBNavigator control.

9. Next, drop two BitBtn components side by side onto paRight. Place the leftmost button as close as possible to the left edge of paRight. Position the rightmost button to the immediate right of the button on the left. 10. Resize paRight so that it's just large enough to contain the two new buttons. 11. Set the Kind property of the leftmost BitBtn to bkOK and the rightmost to bkCancel. 12. Name the OK button bbOK and the Cancel button bbCancel. 13. Double-click the bbOK component and type this code into its OnClick event handler: If (DBNavigator1.DataSource.State in [dsEdit, dsInsert]) then DBNavigator1.DataSource.DataSet.Post; Close; This sets up the OK button so that clicking it posts any changes to the current row in the form's DataSet. It's assumed that the DBNavigator control will reference the form's primary DataSet. 14. Double-click the bbCancel component and type this code into its OnClick event handler: Close; This causes the form to close when you click the Cancel button. Because thecode doesn't act on pending database changes, they are left to the form'sOnClose event code to deal with. Page 250 15. Select fmDatabaseForm itself in the Object Inspector and double-click the form's OnClose event on the Object Inspector's Events page. Type this code into fmDatabaseForm's OnClose event: If (DBNavigator1.DataSource.State in [dsEdit, dsInsert]) then DBNavigator1.DataSource.DataSet.Cancel; This causes the form to discard any changes to the current row when it's closed by any method other than clicking the OK button. That is, if you click the Cancel button, click the window frame close button, or simply shut down the application, any pending changes to the current row will be discarded. After you've finished these steps, you've completed the DatabaseForm class. Click the File | Save menu option and save the form as DBFORM.PAS in your RENTMAN source directory. Figure 9.8 shows the completed form. Figure 9.8. The completed DatabaseForm class.

I had you drop a fourth panel onto the paBottom panel in order to ensure that the form's OK and Cancel buttons will always be in its lower-right corner, no matter how the form is sized. Because the fourth panel is docked at the right side of the dialog and the buttons reside on it, they'll remain docked, too. You're now ready to save the form to the Object Repository. Right-click the form and select Add to Repository. Enter fmDatabaseForm for the title and Generic database form class for the description. Select Forms in the Page drop-down list box and click OK. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 251 EditForm Now that you've completed the DatabaseForm class, you're ready to proceed with the EditForm class. The purpose of the EditForm class is to provide a base for forms that edit a single table one row at a time. It will descend from the DatabaseForm class you just created. Follow these steps to create EditForm: 1. Click File | New and select fmDatabaseForm from the Forms page. 2. Click the Inherit radio button and then click OK. You should see a form named fmDatabaseForm1 in the Delphi visual form designer. 3. Change the new form's Name property to fmEditForm. 4. Resize the paTop panel so that it shrinks paMiddle to about the size of paBottom. The paTop panel is where you'll locate data-aware edit controls such as DBEdit and DBComboBox in descendants of fmEditForm. 5. Click paMiddle to select it, then drop three buttons onto it. Name the first button btAdd, the second btEdit, and the third btDelete. 6. Set the btAdd's Caption property to &Add, btEdit's Caption to &Edit, and btDelete's Caption to &Delete. Note that the ampersand denotes which character functions as the button's accelerator key. 7. Double-click btAdd and type this code into its OnClick event handler: DBNavigator1.BtnClick(nbInsert); 8. Double-click btEdit and type this code into its OnClick event handler: DBNavigator1.BtnClick(nbEdit); 9. Double-click btDelete and type this code into its OnClick event handler:

DBNavigator1.BtnClick(nbDelete); Setting up these buttons to simulate click events in the DBNavigator control causes clicking them to perform the same function as the associated DBNavigator button, even if you later customize the behavior of that button in an fmEditForm descendant. After you've done this, you're finished with the EditForm class. Click the File | Save menu option and save fmEditForm as EDITFORM.PAS in your RENTMAN source directory. Figure 9.9 shows the completed form. Save it to the Object Repository with fmEditForm as its title and Generic database edit form class as its description. Store it on the Forms page. GridForm The next class in the hierarchy is the GridForm class. It's a descendant of the EditForm class that allows the display of multiple rows at a time. Each table row is permitted to occupy just one screen row. To build fmGridForm, follow these steps: Page 252 Figure 9.9. The completed EditForm class.

1. Click File | New and select fmEditForm from the Forms page of the New Items dialog. 2. Click Inherit and then click OK. You should see a form named fmEditForm1 in the Delphi form designer. 3. Change the form's Name property to fmGridForm. 4. Select the paTop Panel component, then drop a DBGrid component (located on Delphi's Data Controls palette page) onto it. 5. Set the DBGrid's Align property to alClient so that it takes up all the empty space on the Panel component. 6. Select the fmGridForm itself in Delphi's Object Inspector, and double-click its OnShow event on the Events page. Type this code into fmGridForm's OnShow

event handler: If (DBGrid1.DataSource=nil) then DBGrid1.DataSource:=DBNavigator1.DataSource; This code causes the DBGrid component you just placed on the form to make use of the DataSource defined for the DBNavigator control if one hasn't already been defined for it. This means you only have to set the DataSource of the DBNavigator control in forms that descend from fmGridForm. After you've completed these steps, you're finished with fmGridForm. Save the form now as GRIDFORM.PAS in your RENTMAN source directory. Figure 9.10 shows what the form should look like. Page 253 Figure 9.10. The completed GridForm class.

Save the form to the Object Repository with fmGridForm as its Title and Generic database grid form class as its Description. Store it on the Forms page. ControlGridForm The next form class to define is the ControlGridForm class. It, too, descends from EditForm. fmControlGridForm will facilitate the display of multiple rows from a single table and allows each row to occupy more than one screen row. The main difference between this form and the GridForm you just built is that this form uses a DBCtrlGrid component instead of a DBGrid component. Follow these steps to create fmControlGridForm: 1. Click the File | New menu option and select fmEditForm from the Forms page of the New Items dialog. Click Inherit and then click OK. You should see a form named fmEditForm1 displayed in the visual form designer. Change the form's Name to fmControlGridForm. 2. Select the paTop Panel component, then drop a DBCtrlGrid component (located

on the Data Controls palette page) onto it. 3. Set the DBCtrlGrid component's Align property to alClient so that it covers paTop's entire surface. 4. Select the fmControlGridForm itself in Delphi's Object Inspector, then doubleclick its OnShow event on the Events page. Type this code into fmControlGridForm's OnShow event handler: If (DBCtrlGrid1.DataSource=nil) then DBCtrlGrid1.DataSource:=DBNavigator1.DataSource; Page 254 This code causes the DBCtrlGrid component you just placed on the form to make use of the DBNavigator control's DataSource if one hasn't already been defined for it. This permits you to get by with only setting the DataSource of the DBNavigator control in forms that descend from fmControlGridForm. After you've completed these steps, you're finished with fmControlGridForm. Save the form now as CGRDFORM.PAS in your RENTMAN source directory. Figure 9.11 shows what the form should look like. Figure 9.11. The completed ControlGridForm class.

Save the form to the Forms page of the Object Repository with fmControlGridForm as its title and Generic database control grid form class as its description. MasterDetailForm The next and final form class in the hierarchy is the MasterDetailForm class. It descends from DatabaseForm and permits the display/editing of two tables that are linked together in a master-detail relationship. It differs from DatabaseForm in that it includes a DBGrid component on its paMiddle Panel component. This grid will be used for displaying the detail table's rows. The form's top panel, paTop, will display the rows (one at a time) from the master table. To construct MasterDetailForm, follow these steps:

1. Click the File | New menu option and select fmDatabaseForm from the Forms page of the New Items dialog. Click Inherit and then click OK. You should see a form named fmDatabaseForm1 displayed in the visual form designer. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 255 2. Change the new form's Name to fmMasterDetailForm. 3. Resize the paTop component so that it takes up about half the area presently occupied by the paMiddle component. 4. Click the paMiddle component to select it and then drop a DBGrid component (from the Data Controls page of the component palette) onto it. 5. Set the DBGrid's Align property to alClient so that it occupies all the available space on the paMiddle panel. After you've finished these steps, fmMasterDetailForm is complete. Save the form now as MSTRFORM.PAS in your RENTMAN source directory. Figure 9.12 shows what the form should look like. Figure 9.12. The completed MasterDetailForm class.

Save the form to the Forms page of the Object Repository with fmMasterDetailForm as its title and Generic database master-detail form class as its description. Future Forms

Now that you've created your base form classes and defined a working form hierarchy, you're ready to begin creating real forms. Nearly all the forms you create in the RENTMAN System will descend from one of the form classes you just defined. Page 256 TIP You can store the data module and form hierarchy forms you just defined in their own Delphi project so that you can later easily load them as a set and work on them together. You'll find this to be especially handy in light of the fact that you can't open one of the repository forms in the visual designer without its ancestor forms either being added to the current project or loaded in advance. To store these forms as a stand-alone project, save your current project, naming it something like FORMREPO for "form repository project." I recommend you save this project to the directory in which the forms themselves reside. In order to avoid including the project's default form, Unit1, in your repository, you can remove it from the project before saving the project to disk.

The Main Form The next step in building the RENTMAN application is to build the application's main form. Because you intend for all of RENTMAN's forms, including its main form, to descend from the fmAnyForm class, you won't be able to use the project's default form, Unit1; so close it now and remove it from the RENTMAN project. Before you begin building RENTMAN's main form, save your project now as RENTMAN.DPR. After the project is saved to disk, follow these steps to build RENTMAN's main form, fmRSYSMAN0: 1. Click File | New and select Forms. Then click the fmAnyForm class, click Inherit, and click OK to create the new form. You should then see a form named fmAnyForm1 in the visual designer. 2. Change the new form's Name to fmRSYSMAN0 and its caption to RENTMAN Rental Management System. 3. Select the Project | Options | Forms menu option and change

RENTMAN's main form to fmRSYSMAN0. Click OK to close the Project Options dialog. 4. Set the main form's WindowState to wsMaximized and its Position property to poDesigned. This causes the RENTMAN application to take up the entire screen when it begins. 5. Shrink both the paTop and paBottom panels so that they're about 30 pixels in height. The top panel will house application speed buttons; the bottom panel will be RENTMAN's status bar. 6. Drop a MainMenu component anywhere on the form and set its Name to mmRentMan. This will be the application's main menu. Now, doubleclick the MainMenu component to start Delphi's menu designer tool. Page 257 Follow these steps to construct RENTMAN's main menu: 1. Add a File menu item by setting the Caption property of the default menu item to &File. The ampersand defines F as the menu item's hotkey. 2. Set up the File menu's first item to have a caption of &Log a Call and a Shortcut key of F2. You'll add each of the menu items for the File menu in the same manner. Table 9.4 summarizes the contents of the File menu. Table 9.4. Items for RENTMAN's File menu. Caption &Log a Call &Print Setup E&xit Shortcut F2 None None

3. Add a Tables menu (set its caption to &Tables) at the same level as the File menu (the root level) and add the items in Table 9.5 to it. Table 9.5. Items for RENTMAN's Tables menu. Caption &Calls &Property Shortcut F3 F4

&Tenants &Leases &Work Orders &Employees &Employees

F5 F6 F7 F8 F9

4. Add a Reports menu item at the root level with the File and Tables menus (set its caption to &Reports). Add the items in Table 9.6 to it. Table 9.6. Items for RENTMAN's Reports menu. Caption &Work Order &Lease &Task List Page 258 5. Add a Help menu at the same level as the File, Table, and Report menus by setting the caption of the empty item on the right of the Reports menu to &Help. 6. Add a Contents item to the Help menu, setting its caption to &Contents. 7. Finish the Help menu by adding an About item with a caption of &About. Figure 9.13 shows the completed MainMenu. Figure 9.13. The completed RENTMAN MainMenu component. Shortcut None None None

You've now finished the RENTMAN app's main menu, so close the Delphi

menu designer. Speed Buttons Now you'll set up speed buttons to access the system's more popular functions. Follow these steps to set up your speed buttons: 1. Click the paTop panel to select it and then drop six SpeedButton components onto it, side by side. 2. Select all the SpeedButton controls at once and right-click to bring up the menu. 3. Click Align... and select Space equally in the Horizontal pane of the dialog and Tops in the Vertical pane and then click OK. 4. Name the SpeedButton components sbLogCall, sbProperty, sbTenants, sbLeases, sbWorkOrders, and sbPrintSetup, respectively. 5. You can set their glyphs to whatever you want; Delphi comes with several that you might find useful. They're located in the ...images \buttons subdirectory under your Delphi program directory. Table 9.7 shows my suggestions for the five buttons. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 259 Table 9.7. Suggested SpeedButton glyph settings. Button Glyph Phonerng. sbLogCall bmp Doorshut. sbProperty bmp sbTenants Picture.bmp sbLeases Report.bmp sbWorkOrders Tools.bmp sbPrintSetup Printer.bmp After you've added these buttons, follow these steps to add a Help SpeedButton to the right of the paTop panel: 1. Click the paTop component to select it and then drop a Panel component onto the right side of the paTop panel. 2. Clear the new panel's caption and set its BevelOuter property to bvNone. 3. Set the panel's Align property to alRight and its Name property to paRight. 4. Size the panel to be a bit larger than the default size of a SpeedButton component. 5. Drop a SpeedButton component onto the paRight component. 6. Set the SpeedButton's Name to sbHelp and set its glyph to Help.bmp (located in the \images\buttons subdirectory under your Delphi program

directory). Because the panel is docked on the right of the form, the Help button will always remain there, as well. Figure 9.14 shows the form with the SpeedButton components in place. NOTE Note that you could use Delphi's CoolBar or ToolBar component (located on the Win32 component palette page) in place of setting up individual SpeedButton components. These components require a newer CTL3D32.DLL, so they may not be usable on all systems. Also, you can't use the bitmap files listed in Table 9.7 with them; they utilize bitmaps in a different manner. I've therefore stuck with an approach that will work regardless of the Win32 platform on which you're running and one that can use the canned bitmaps that ship with Delphi.

The Status Bar Now that you've completed the speedbar, the only thing that remains to do on the RENTMAN's main form is to construct its status bar. Follow these steps to set up your main form's status bar: Page 260 Figure 9.14. The RENTMAN System's speedbar.

1. Select the paBottom panel and then drop a StatusBar component (located on the Win32 component palette page) onto it. 2. Name the StatusBar stRENTMAN and set its Align property to alClient. 3. Double-click the StatusBar component's Panels property and add three panels to the list. 4. Set the Text property of the StatusBar's first panel to Status, the second one to User, and the third one to Version. 5. Set the Width of the Status panel to 275 and the Width of the User and Version panels to 150 and then click OK.

Figure 9.15 shows the completed fmRSYSMAN0 form. Test the Main Form Now let's fire up the application to see what the main form looks like at runtime. Before you do anything, though, save the project so you won't lose your work if there's a problem. You can save your project and all its related files by either clicking the Save Project button or selecting Save All from the File menu. Save the main file itself to a file named RSYSMAN0.PAS. Page 261 Figure 9.15. The RENTMAN app's main form.

NOTE You might have to manually add the DB unit to the Uses statement of units that reference the State property. State is an enumerated type that stores values such as dsInsert, dsEdit, and so on. I've seen an odd quirk with Delphi where this unit is not always automatically added as it should be. If you receive a compiler error message to the effect that one of State's enumerated values is unknown, add the DB unit to the Uses clause of the offending unit.

After you've saved the project, click the Run button or press F9 to run the application and test out the main form. When prompted for a username and password, supply the username and password you used when you created the RENTMAN database (by default, the username is SYSDBA, and the password is masterkey). Figure 9.16 shows what the fmRSYSMAN0 form should look like at runtime. NOTE

Make sure your InterBase server is running before you execute the application. The app won't be able to start unless the server is already running.

Page 262 Figure 9.16. The first run of the RENTMAN application.

Summary
In this chapter, you designed and created a form hierarchy from which all RENTMAN's forms will descend, and you created a data module to service the app's data requirements. You also built the application's main form, fmRSYSMAN0, and placed SpeedButtons and a status bar onto it.

What's Ahead
You've just completed the first stage of the development of the RENTMAN application. In the remaining chapters of the "Tutorial" section, you'll construct the rest of the forms and other objects necessary to bring RENTMAN to life. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 263

CHAPTER 10

First Forms
Page 264 Now that you've completed the system's main form, you're ready to move on to the rest of the application. Before you can begin constructing additional forms, you need to customize RENTMAN's data module, dmRENTMAN. Although you've set up dmRENTMAN's Table and DataSource components, you haven't yet added any TField components to those tables, nor have you linked columns with Delphi attribute sets. Completing your column definitions before constructing forms that use them saves you time and helps ensure that your app behaves consistently across its forms.

Silverrun-RDM and Delphi Mode
One way you can set up column-level constraints and masks is through Silverrun's-RDM tool. RDM supports a special Delphi work mode wherein you can define Delphi attribute sets and associate them with the columns in your relational data model. This approach has the advantage of allowing you to centralize your column definitions. The disadvantage to doing things this way is that you must define attribute sets separately from the rest of the model. That is, you can't simply import the constraints defined in your database, as you can in Delphi's Database Explorer. Given this limitation, I see little real advantage to using Silverrun to define

your Delphi attribute sets. Importing Data Dictionary Information from Your Database By far the most direct and, I think, the most intuitive way of importing attribute set information is to use Database Explorer to import it directly from your database. You can import InterBase databases wholesale into Delphi's data dictionary. Tables and columns you've created will become tables and columns in the data dictionary, and domains will become attribute sets. After a database has been imported into Delphi's data dictionary, you can customize the imported column and attribute set definitions to include Delphispecific settings. For example, you can specify the default component for columns that reference a given attribute set. You can also specify attribute set edit masks, field types, and display masks. Note that you can drag objects directly from Database Explorer onto Delphi forms. Dragging a table or view from the Explorer to a regular form in the visual form designer will cause a Table, a DataSource, and a DBGrid component to be dropped onto the form. Delphi also will automatically link these components together. Dragging a table or view to a data module form creates a Table component only. You can likewise drag a stored procedure from Database Explorer to create a StoredProc component. To import the RENTMAN database into Delphi's data dictionary, follow these steps: Page 265 1. If the RENTMAN application is running, close it now and return to Delphi. 2. Start your InterBase data server if it isn't already started. 3. Press Shift+F12 and double-click your data module, dmRENTMAN, in the list to open it. This will make dmRENTMAN's local database alias, dbRENTMAN, available to the Database Explorer. 4. Click the Database | Explore menu option to start Database Explorer. 5. Click the Dictionary page and then click the Dictionary | New menu option. 6. In the Create a new Dictionary dialog box, set the Dictionary Name to RENTMAN and the Database name to dbRENTMAN. This will name

the dictionary RENTMAN and place it in your dbRENTMAN database (the local application alias, dbRENTMAN, was created when you set up your data module's Database component). 7. Next, set the dialog's Description entry to something clever like RENTMAN Data Dictionary and click OK. Figure 10.1 illustrates the completed dialog. Figure 10.1. Create a new data dictionary for the RENTMAN app.

8. You'll next be asked to supply login information. You should be able to use username SYSDBA and password masterkey by default. 9. Double-click the Dictionary list item and then click the Databases list item. If you're prompted for a password, supply the same one you did above. 10. Select the Dictionary | Import from Database menu option, then select your dbRENTMAN database from the list and click OK. Be sure to select your local Page 266 database alias, dbRENTMAN, rather than the RENTMAN alias you defined in Chapter 9, "First Steps."If you choose the wrong alias, you won't be able to import your data dictionary's attribute sets as easily. 11. You'll then be prompted for a username and password—again, log in as SYSDBA. 12. After a small delay, Database Explorer should return with the dbRENTMAN database imported into your data dictionary. A special table named BDESDD should now exist in the RENTMAN database containing your data dictionary. 13. In the Dictionary page's item list, double-click Dictionary, then doubleclick Databases to ensure that dbRENTMAN has indeed been imported. Double-click dbRENTMAN to view its data dictionary information. Double-click Attribute Sets to see RENTMAN's domains imported as Delphi attribute sets. Figure 10.2 shows the imported data dictionary information.

Figure 10.2. RENTMAN's imported data dictionary.

Customizing Your Data Dictionary Any customizations you make to the objects in the data dictionary should be made before you begin using them in forms. Doing this ensures that attributes you define for your objects are accurately reflected in the forms in which they're used. There are myriad ways you could customize the RENTMAN data dictionary. Time and space constraints don't allow me to exhaustively cover these as I might like to, so I'll take you through customizing a handful of RENTMAN's attribute sets. Follow these steps to customize RENTMAN's data dictionary: Page 267 1. Click the TYESNO attribute set and change its TControlClass specification to TDBCheckBox. This will set the default component type for columns defined using TYESNO to the DBCheckBox component on Delphi's Data Controls component palette page. Figure 10.3 illustrates this. Figure 10.3. You can specify the component that gets created when you drag fields on to a form.

2. Set the default component (the TControlClass property) of the TADDITION, TCITY, and TSCHOOLDISTRICT attribute sets to TDBComboBox. You'll populate these combo boxes with the appropriate values when you build forms that reference these attribute sets. 3. Click the TSTATE attribute set and type >AA into its Edit Mask property. This will cause all values keyed into components that reference it to be uppercased. 4. Click TSTATE's Default property and set it to `TX'. This will cause Texas to be the default value for TFields that are linked to the TSTATE attribute. 5. Click the TPHONE attribute set and type !\(999\)000-0000 into its Edit Mask property. This will cause all values keyed into components that reference it to be formatted as US-style phone numbers. 6. Click Attribute Sets once again in the item list and then click Database Explorer's apply button (the blue arrow that curves to the right) to save the changes you've made to your attribute sets. 7. You're now ready to make use of your data dictionary in the RENTMAN app, so close Database Explorer and return to Delphi. In a moment, you'll import the attributes you defined in your data dictionary into the RENTMAN app itself. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 268

Customize the Data Module
Now that your data dictionary is complete, you're ready to begin importing it into your application. You'll do this by adding TField components to each of the tables on the dmRENTMAN data module. When these field definitions are imported, they'll bring with them the specific attribute settings you made in Database Explorer. Customizing Your Tables Follow these steps to import data dictionary information for each Table component on dmRENTMAN: 1. Double-click the Table component to bring up the Fields Editor. 2. Right-click the Fields Editor and select Add fields. 3. Click OK in the Add Fields dialog to create TField components for all the columns in the underlying database table. 4. Set the EditMask property of each date field to !99/99/00;1;_. This includes the LASTLAWNDATE, LASTSPRAYDATE, BEGINDATE, ENDDATE, MOVEDINDATE, MOVEDOUTDATE, CALLDATE, and STARTDATE fields. 5. Set the EditMask property of each time field to !90:00:00>LL;1;_. This includes the CALLTIME field. 6. Close the Fields Editor by clicking its window frame close button. 7. Repeat this process for all the Table components on the data module. To see the impact of your work when you're finished, right-click the taPROPERTY component and select Fields Editor, then click the STATE field.

Note the column's imported attribute set information in the Object Inspector. Figure 10.4 shows what you should see. TIP You can quickly select consecutive fields in Delphi's Fields Editor by positioning on the first field that you want to select and pressing Shift+Down to select each successive field. You can select non-consecutive fields by pressing Shift+F8 while positioned on the first one, then moving to each additional field you want to select and pressing the spacebar.

Page 269 Figure 10.4. The attribute set information you defined in Database Explorer makes its way into your app.

The EMPLOYEE Quick Entry/Edit Form
The next step you'll take in building the RENTMAN application is to create the EMPLOYEE table's quick entry form, fmREMPENT0. There's an easy way and a hard way to build database forms. Let's try it the easy way—using the Database Form Wizard—first, then give the hard way—building the form manually—a shot. The Database Form Wizard won't make use of our form hierarchy; therefore, it isn't a permanent solution for creating simple forms for the RENTMAN System. We'll explore it anyway so that you can get a feel for the potential of Delphi form wizards. Creating the Form Using the Database Form Wizard Start the Database Form Wizard by choosing Database | Form Wizard from the Delphi menu. The purpose of a form wizard is to construct a form by asking you questions about what it should look like. The Database Form Wizard

begins these questions by asking what type of form to create (simple or master/ detail) and whether to use Table or Query components on the form. For now, just take the defaults and click the Next button. (See Figure 10.5.) The wizard then asks what table you'd like to make use of on the form. Begin by selecting the dbRENTMAN alias in the Drive or Alias name drop-down list box. After you select dbRENTMAN, the list of tables changes to reflect the tables in the RENTMAN database. Double-click the EMPLOYEE table near the top of the list. (See Figure 10.6.) Page 270 Figure 10.5. The opening screen of the Database Form Wizard.

Figure 10.6. Selecting a table in the Database Form Wizard.

Next, you're presented with a list of fields from the EMPLOYEE table. You can select them one at a time with the > button, or all at once by using the >> button. Click the >> button, then click Next to proceed as illustrated in Figure 10.7. Figure 10.7. Selecting fields in the Database Form Wizard.

The next form presented by the wizard asks you to select an orientation for the fields on the new form. You have three choices: Horizontal, Vertical, or Grid. Click the Next button to accept the default, Horizontal. The next and final form presented by the Database Form Wizard asks whether you want to generate the form as your application's main form. A checkbox at the top of the form enables you to select whether to "Generate a main form." Although this defaults to checked (or True), Page 271 uncheck it. Your application already has a main form, fmRSYSMAN0, so this form doesn't need to be one. Finish by clicking the Create button to generate the new form. Figure 10.8 shows the new form. Figure 10.8. The EMPLOYEE entry/edit form as created by the Database Form Wizard.

Note that the form includes its own Table and DataSource components. The other forms you'll create will use the Table and DataSource components on dmRENTMAN. You can change this form to use them as well by following these steps: 1. Delete both the Table and the DataSource component from the form. 2. Locate the form's FormCreate event code and remove this line Table1.Open; so that your form won't try to open the Table component you just

deleted. 3. Select the Use Unit option from Delphi's File menu and then doubleclick RENTDATA in the unit list as illustrated in Figure 10.9. Figure 10.9. Linking units together with the File | Use Unit option.

4. Select the DBNavigator component on the form along with the two DBEdit components. 5. Change the DataSource property of all three components to the dmRENTMAN.dsEMPLOYEE DataSource. (It should be in the dropdown list for the DataSource property.) Now, when you test the form, you'll actually be working with the RENTMAN System's own Table and DataSource components. Page 272 Testing the New Form The easiest way to quickly test the new form is to link it into the RENTMAN application. To do this, follow these steps: 1. Select the fmRSYSMAN0 form, then select Use Unit from the File menu and choose the Unit1 unit from the list. 2. Next, double-click the form's MainMenu component. 3. Select the Tables menu and double-click the Employee menu item. This places you in Delphi's code editor so that you can attach program code that executes when the menu item is clicked. 4. Because your new form still has its default name, Form1, key the following single line of code into the code editor as illustrated in Figure 10.10: Form1.Show; Figure 10.10.

Attaching the new form to the RENTMAN application.

This will cause your new form to be displayed when either the Employees item on the Tables menu is clicked or its accelerator key (F8) is pressed. Now, let's run the application to try out the new form. Press F9 or click the Run button to execute the RENTMAN application. When the application starts, press F8 to display the new form. Figure 10.11 shows what the new form should look like at runtime. After the form is displayed, you can add records, delete, or edit them—the form is fully functional. Click the + button on the DBNavigator to add a record, or click the _ button to delete it. After you've viewed the new form, close both it and the RENTMAN application and return to Delphi. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 273 Figure 10.11. The new form as it appears at runtime.

Removing the New Form from the Project Because the new form doesn't use the form hierarchy that you built, it isn't really suitable for inclusion in the RENTMAN app. Follow these steps to remove it from the RENTMAN project: 1. Click the Remove file from project button on the Delphi button bar or select the Remove from Project option on Delphi's Project menu. 2. Double-click Unit1 in the unit list to remove it from the project. 3. When asked whether to save Unit1, answer No. 4. Edit fmRSYSMAN0 and remove the line of code that you added to display the new unit: Form1.Show; 5. Also remove the line of code containing uses Unit1; from the top of fmRSYSMAN0. Building the New Form Using Delphi's Visual Designer

Now that you've seen the easy way to build a database form in Delphi, let's give the hard way a try. Hard is certainly a relative term, though—neither way is exceptionally difficult. Follow these steps to create fmREMPENT0: 1. Select File | New from Delphi's menu. 2. Select the Forms page and then select fmEditForm from the list of forms. 3. Click the Inherit radio button and then click OK. You should see a new form named fmEditForm1 loaded into the visual form designer. 4. Change the new form's Name property to fmREMPENT0 and its caption to EMPLOYEE Quick Entry/Edit Form. Page 274 5. Select the File | Use Unit menu option and double-click the RENTDATA unit to add RENTMAN's data module to your new form's Uses clause. 6. After you've done this, drop two label components onto the left side of the form and align them vertically, one right above the other. 7. Set the caption of the top label to &EmployeeNo and the caption of the bottom one to &Name. 8. Drop two DBEdits onto the right side of the form, also aligned vertically and aligned horizontally with the two labels. 9. Name the top DBEdit dedEmployeeNo and the bottom one dedName. 10. Size the dedName component so that it's at least twice as wide as the dedEmployeeNo component. It will display/edit the EMPLOYEE table's Name field, so it needs to be fairly large. 11. Set the FocusControl property of the top label to reference the dedEmployeeNo component and the FocusControl property of the bottom one to reference the dedName component. This will allow a user to press a label's accelerator key in order to jump to its associated DBEdit control. Figure 10.12 shows the new form so far. Figure 10.12. The fmREMPENT0 form with its labels and DBEdits in place.

TIP You can quickly drop data controls and labels onto a form by dragging TField components from a DataSet's fields list. Here's how: 1. Bring up the dmRENTMAN data module in the visual form designer and position it so that you can see the target form. 2. Right-click the dmRENTMAN DataSet whose fields you want to use and select Fields Editor from the pop-up menu.

Page 275 3. Drag a field from the Fields Editor to your target form. You should see both a data control and a corresponding label created on the target form. 4. Remember that you can specify which control to create for a given field through the TControlClass property of its associated attribute set.

Linking with the Data Module Form Next, you'll link your new form with RENTMAN's data module so that it can access RENTMAN's database objects. Follow these steps to link your new form with dmRENTMAN: 1. Select Use Unit from the File menu and double-click RENTDATA in the unit list. 2. Select the form's DBNavigator and DBEdit controls and set the DataSource property they share to point to the dmRENTMAN. dsEMPLOYEE DataSource component. 3. Unselect all three controls and set the DataField property of each of the DBEdits to reference its respective field in the EMPLOYEE table.

dedEmployeeNo references the EMPLOYEE_NUMBER column, and dedName references the NAME column. After you've completed these steps, the EMPLOYEE Quick Entry/Edit Form is essentially done. Save it as REMPENT0.PAS in your RENTMAN source directory before you proceed. Testing the New Form After you've saved your new form, you're ready to test it. To take your new form on a trial run, follow these steps: 1. Select the fmRSYSMAN0 form in the visual form designer and doubleclick its mmRENTMAN menu component. 2. Select the Tables menu and double-click the Employee option. 3. Key this line of code into the menu option's click event: fmREMPENT0. Show. 4. This will cause the new form to be displayed when the Employee menu item is selected. 5. Close the menu designer and click Use Unit on the File menu. 6. Double-click the REMPENT0 unit in the list. This will cause the system's main form, RSYSMAN0, to use the unit that defines the fmREMPENT0 form so that your call to its Show method will compile and run properly. 7. Now save the RENTMAN project and run it. Figure 10.13 shows the completed form as it should appear at runtime. Page 276 Figure 10.13. The completed fmREMPENT0 form.

The Work Type Quick Entry/Edit Form
Now that you've successfully created the EMPLOYEE form, you're ready to move on to the Work Type Quick Entry/Edit Form. Rather than lead you by the hand, I'm going to let you complete this form without me. Here are some

general guidelines to help you along the way:
q

q q

q

q

q

q

q

The new form's name is fmRWKTENT0, and its caption is Work Type Quick Entry/Edit Form. It descends from fmEditForm, just like the EMPLOYEE form. Be sure to define accelerator keys for the labels you drop (using the & character in their captions) and be sure to set their FocusControl properties to point to their corresponding DBEdit controls. Remember to name the form's components in accordance with the naming conventions spelled out in Chapter 4, "Conventions." The form's data-aware controls should reference the dmRENTMAN. dsWORKTYPE DataSource component. Remember that you'll need to reference the RENTDATA unit via the File | Use Unit menu option in order to access its database objects. Connect the DBEdit components to their corresponding fields in the WORKTYPE table using the DataField property. After you've completed the form, link it into RENTMAN's main menu using the same steps you took to link the EMPLOYEE form.

Figure 10.14 shows what the completed form should look like at runtime. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 277 Figure 10.14. The completed fmRWKTENT0 form.

Summary
In this chapter, you finished your data module and constructed two of the data entry forms needed by the RENTMAN app. Creating these forms entailed accomplishing a number of smaller tasks. Specifically, you
q q q q q q q

q

Constructed and imported a Delphi data dictionary Customized DataSet components on the data module Created a quick entry/edit form for the EMPLOYEE table Created a new form using the Database Form Wizard Removed the new form from the RENTMAN project Constructed new forms using Delphi's visual form designer Established a link between new forms you created and RENTMAN's data module form Created a quick entry/edit form for the WORKTYPE table

Hopefully, you'll find that these smaller tasks have equipped you with the skills needed to take on RENTMAN's more complex forms, which will be tackled next.

What's Ahead
In the next chapter, you'll customize the two forms you just created a little further and then create several more forms. You'll make some changes to the quick entry forms that make them easier to get around in. You'll use visual form inheritance to save lots of time making these changes. Page 278 Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 279

CHAPTER 11

Forms, Forms, and More Forms
Page 280 In this chapter, you'll build the remainder of the data-entry forms for the RENTMAN system. These forms will allow editing of the TENANT, PROPERTY, CALL, LEASE, and WORDER tables. Building the wide variety of forms needed by the RENTMAN system will give you a thorough introduction to constructing Delphi database forms. The TENANT and PROPERTY forms you'll build are unusual in that they use the DBCtrlGrid component. A DBCtrlGrid enables you to display multiple records from the same table in a gridlike fashion. The distinction between a standard DBGrid and a DBCtrlGrid is that each table row can take up multiple screen rows in a DBCtrlGrid; that's not so with DBGrid. In DBGrid controls, each table row occupies exactly one row in the grid. You'll use the grid control for the CALL and WORDER forms, and you'll use the DBCtrlGrid for the TENANT and PROPERTY forms.

The TENANT Form
The TENANT form is a descendant of the fmControlGridForm class; therefore, select File | New from Delphi's menu and then click the Forms page in the New Items dialog. Select fmControlGridForm, click Inherit, then click OK. You should see a form named fmControlGridForm1 in the visual designer.

Change the form's Name property to fmRTENCGD0 and its caption to Tenant Edit Form. Next, select Use Unit from the File menu and double-click RENTDATA in the list. This will make the RENTMAN system's data module form, dmRENTMAN, available to your new form. NOTE If you've locked the controls of fmControlGridForm or of any of its ancestors, your new form's controls will be locked as well. You might want to unlock them before proceeding because this will make visually adjusting them easier. To do this, make sure the form is selected, then select the Lock Controls option on the Edit menu to remove the check beside it. The controls need to be unlocked only if you locked them previously on this form or on one of its ancestors; they're not locked by default.

The first thing you need to do with this form is resize it. Set its Height property to 400 and its Width property to 565. This will enable you to place the numerous fields of the TENANT table more easily. Removing Inherited Components Note that all three of the buttons you added to fmEditForm are present on this form. Because this form isn't designed to be a quick-entry form, you don't really need them. Nevertheless, you can't delete them because Delphi doesn't let you delete controls in a form that were intro Page 281 duced in one of its ancestors. There are, however, ways to remove them for all practical purposes and reclaim the space they take up on the form. Click the paMiddle panel and set its Enabled and Visible properties to False. Because it's a container for the three buttons you added earlier, this also disables them, which is what you're actually after. Disabling the panel isn't worth much to you in and of itself except that this also disables its dependent components. Follow this by setting the panel's Align property to alNone; this will prevent it from claiming the middle region of the form. Next, set the Align property of the paTop component to alClient. Finish up by right-clicking the paMiddle component and selecting the Send to Back option from the pop-up menu. paMiddle and its buttons should then disappear from the form. You might be wondering why it was necessary to disable the button controls. After all, why would you not want to have their accelerator keys available even if they themselves aren't visible? Whether the controls are visible or not, their accelerator keys are still active until you

disable them. This means that pressing a button's accelerator key would have the same effect as clicking the button. Basically, the button wouldn't be removed at all; it merely would be invisible. It's really just a matter of taste, but I personally don't like to set up "hidden" or unlabeled hot keys. If a given key does something special, it seems to me that it should be obvious from looking at the screen. Configuring the DBCtrlGrid Follow these steps to set up and configure your new form's DBCtrlGrid component: 1. Start your InterBase database server if it isn't already running. 2. Press Shift+F12 to bring up the View Form dialog and double-click the dmRENTMAN data module. Log in to the server if prompted for your username and password. 3. Return to your new form by pressing Shift+F12 again and selecting your new form from the list. 4. Select the form's DBCtrlGrid and DBNavigator components and change their DataSource property to dmRENTMAN.dsTENANT; then unselect the DBNavigator control. 5. There are 11 fields in the TENANT table. Ten of these are editable text fields, and one is a read-only text field. Drop one DBText control and 10 DBEdit controls onto the first of the DBCtrlGrid's three panels. Don't drop anything on the panels covered with gray diagonal lines. These panels are off limits. 6. Set the Height property of each component on the DBCtrlGrid to 19. 7. Drop 11 Label components onto the DBCtrlGrid and position each of them above either a DBText or DBEdit control. 8. Finish setting up the components as specified in Table 11.1 and as shown in Figure 11.1. Page 282 TIP To drop a given control repeatedly without having to return to the component palette after each drop: 1. 2. 3. 4. Press and hold the Shift key. Click the component in the palette (you can then release the Shift key). Click the form once for each copy of the component you want to drop. Click the pointer icon in the component palette to end repetitive drop mode.

Table 11.1 lists the essential properties and property values for each of the components on the DBCtrlGrid. Table 11.1. DBCtrlGrid components and their attributes. Name dteTENANT_NUMBER dedNAME dedHOMEPHONE dedWORKPHONE dedICEPHONE dedEMPLOYER Component DBText DBEdit DBEdit DBEdit DBEdit DBEdit Width 30 121 90 90 90 121 121 121 25 90 530 DataField TENANT_NUMBER NAME HOMEPHONE WORKPHONE ICEPHONE EMPLOYER Label No. Name HomePhone WorkPhone EmergencyPhone Employer Employer EMPLOYERADDRESS Address EMPLOYERCITY EMPLOYERSTATE EMPLOYERZIP COMMENTS City State Zip Comments

dedEMPLOYERADDRESS DBEdit dedEMPLOYERCITY dedEMPLOYERSTATE dedEMPLOYERZIP dedCOMMENTS DBEdit DBEdit DBEdit DBEdit

Figure 11.1 shows the form as it should look so far. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 283 Figure 11.1. Your new form with its DBCtrlGrid and components in place.

NOTE To change the value of a given property in a number of components at once, follow these steps: 1. 2. 3. 4. Click the first component. Press and hold the Shift key. Click the remaining components. Press F11 to jump to the Object Inspector. Only the properties common to all selected controls will appear in the list. If you change one of the property values, you change it for all selected components. 5. You can also right-click any of the selected components to display a menu of actions that apply to all selected controls.

NOTE

When not linked to an open data set, a DBEdit control displays its component name in the edit box portion of the control. That is, unlike the standard Edit component, there is no way to clear the contents of an unlinked or closed DBEdit at design time. The component will clear at runtime, however. This differs from the Edit component, which can be cleared at design time by deleting the contents of its Text property.

Page 284 Aligning Components Be sure to use the Align and Size options on Delphi's right-click menu to quickly and accurately align the controls on your form. You can easily align controls horizontally and vertically and size them to match one another using the pop-up menu in the Form Designer. TIP Always click the control that is to be stationary first when aligning a number of controls at once. This is because the Form Designer expects the "reference" component to have been selected first and aligns the other controls with it.

Setting the Tab Order After you have the components in place, make sure their TabOrder properties are set correctly. You can modify the tab order settings for all the controls at once by using Delphi's Tab Order editor. Invoke the editor by selecting Tab Order from Delphi's Edit menu, then click the arrow buttons to move a given component up or down in the list. You can also drag and drop controls to change their tab order settings. After your tabs are set up, you should save your new form. Save the form as RTENCGD0.PAS (R is for the RENTMAN system, TEN signifies the TENANT table, and CGD stands for dbControlGriD; 0 provides support for multiple versions of the form) before you proceed. TIP

Nearly anything you can do in Delphi with the mouse can also be done with the keyboard. Table 11.2 lists some keys that you might find useful. Note that some of these keys are only active if you are using Delphi's default keyboard layout.

Table 11.2. Some common keyboard shortcuts. Key F11 F12 Ctrl+Enter Alt+Down Tab, Arrows Ctrl +Arrows Page 285 Key Shift+Arrows Shift+F12 Alt+0 F9 F1 F5 Ctrl+Tab Ctrl+Shift +Tab Function Size the current component in the Form Designer. Select a form. Select a window. Run the application. Help for the current component in the Form Designer. Toggle debugger breakpoint at the current line in the Code editor. Jump to the next file tab in the Code editor. Jump to the previous file tab in the Code editor. Function Jump to the Object Inspector. Jump to the code editor or Form Designer; toggle between them. Same as a mouse double-click in the Object Inspector. Drop down a list in the Object Inspector. Move between properties or controls. Move the current component in the Form Designer.

RENTMAN and Auto-Incrementing Fields Now that your form is complete, only one task remains before you're ready to test it. You might have noticed that your new form doesn't allow editing of the TENANT_NUMBER column. This is because Allodium wants the system to

automatically generate tenant numbers as tenant rows are added. The actual work of incrementing the TENANT_NUMBER column could be performed by the app itself, but that would mean valid TENANT rows could only be added by the RENTMAN app. As stated in Chapter 20, "Business Rules on the Database Server," the best place for business rules—including those that specify sequentially numbered fields—is on the server. Business rules placed on the server are available to all applications—not just those written in Delphi or based on the BDE. Because the CASE tool you used to build the RENTMAN database doesn't create the InterBase objects required to set up auto-increment columns, you'll have to build them yourself. You'll need to create two new objects in your RENTMAN database: a generator object and a trigger. An InterBase generator is used to generate sequential numbers that can be stored in table columns. The actual insertion of the value occurs within a trigger or stored procedure. In this case, an insert trigger will be used to supply a value for the TENANT_NUMBER column. This trigger will "fire" every time a row is inserted into the TENANT table. When it executes, the trigger will call the GEN_ID() function to retrieve the generator's next value. Calling GEN_ID() increments the generator's internal value so that a new value is returned the next time the function is called. In addition to the TENANT table, Allodium also wants the other tables in the RENTMAN database to number themselves automatically. This means that you'll need a generator object and an insert trigger for each of them as well. The lone exception to this is the WODETAIL table. It has special requirements I'll discuss in a moment. Page 286 Rather than create each of these new database objects separately, you can create them all at once using an SQL script. You can edit your script in Delphi's Code editor; it includes syntax highlighting support for SQL, making your SQL code easier to read. After you've keyed in your SQL script, run it via InterBase's Windows ISQL utility. The objects it defines will then be created en masse in the RENTMAN database. To set up RENTMAN's generators and triggers, follow these steps: 1. Click the File | New menu option, select Text, then click OK in the New Items dialog. This will open a new file in the Delphi Code editor. 2. Save the file as RSYSTRG0.SQL. Saving the file before you actually

3.

4.

5.

6.

7. 8. 9. 10.

11.

12.

key in the script will turn on the syntax highlighting that's associated with the SQL file extension. Key the SQL script from Listing 11.1 into your new text file, and then save your work. Be sure to change the CONNECT statement to reference RENTMAN's path location on your system. Start the InterBase Windows ISQL utility and select the File | Run an ISQL Script menu option. Specify the name of your newly created script when prompted for a script filename. Click No when asked whether to trap the script's output in a separate file. This will route the output to Windows ISQL itself, allowing you to instantly see results. After a slight delay, a message informing you that the script executed without errors should be displayed. Your generators and triggers have now been created. Close Windows ISQL and return to Delphi. Press Shift+F12 to bring up the View Form dialog and double-click the dmRENTMAN data module. Double-click the taTENANT component to bring up the Fields editor, then click the TENANT_NUMBER column. Change the TENANT_NUMBER column's Required property to False in the Delphi Object Inspector, then close the Fields editor. This will keep your app from requiring a value for the TENANT_NUMBER column because InterBase will be setting its value via the generator objects and triggers you created. Repeat this process for each of the other tables, setting the Required property of each of their primary keys to False. Remember that the primary key of the WODETAIL table consists of two separate columns, the WORDER_NUMBER column and the WODETAIL_LINE_NUMBER column. Set both columns' Required property to False. Click the Object Inspector's Events page, then double-click taTENANT's AfterPost event. Key the following source code into the editor: taTENANT.Refresh; Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 287 This call to the Refresh method is necessary to make RENTMAN aware of the values automatically generated on the server for the TENANT_NUMBER column. 13. Using this same approach, set up an AfterPost event for all the tables on your data module. Be sure to call each table's Refresh method in its AfterPost event code. Listing 11.1 shows the results of following these steps. TIP Delphi can import most types of constraints and business rules directly from your SQL server. This means that if you define a column constraint or default value on the server, you can import this information into the Delphi data dictionary, and from there into your apps. Unfortunately, auto-incrementing columns can't be correctly imported this way. That is, the columns themselves are imported, but your app remains unaware that the server will automatically assign values to them. When you run into a situation where Delphi isn't correctly recognizing values supplied by your server, you can call the relevant data set's Refresh method to re-read the current row from the server. The AfterPost event is a good place to make this call because it takes place after your changes to the row have been applied. Calling Refresh should make the app aware of any changes made to the row by your server.

Listing 11.1. SQL script to create RENTMAN's generators and triggers. CONNECT `C:\DATA\RENTMAN\RENTMAN.GDB' USER `SYSDBA' PASSWORD `masterkey'; CREATE CREATE CREATE CREATE CREATE CREATE CREATE GENERATOR GENERATOR GENERATOR GENERATOR GENERATOR GENERATOR GENERATOR TENANT_NUMBER_GEN; PROPERTY_NUMBER_GEN; LEASE_NUMBER_GEN; CALL_NUMBER_GEN; WORDER_NUMBER_GEN; EMPLOYEE_NUMBER_GEN; WORK_TYPE_CODE_GEN;

COMMIT WORK; SET TERM !! ; CREATE TRIGGER TENANTInsert FOR TENANT BEFORE INSERT POSITION 0 AS BEGIN NEW.TENANT_NUMBER = GEN_ID(TENANT_NUMBER_GEN, 1); END !! CREATE TRIGGER PROPERTYInsert FOR PROPERTY BEFORE INSERT POSITION 0 AS BEGIN NEW.PROPERTY_NUMBER = GEN_ID(PROPERTY_NUMBER_GEN, 1); END !!

continues Page 288 Listing 11.1. continued CREATE TRIGGER LEASEInsert FOR LEASE BEFORE INSERT POSITION 0 AS BEGIN NEW.LEASE_NUMBER = GEN_ID(LEASE_NUMBER_GEN, 1); END !! CREATE TRIGGER CALLInsert FOR CALL BEFORE INSERT POSITION 0 AS BEGIN NEW.CALL_NUMBER = GEN_ID(CALL_NUMBER_GEN, 1); END !! CREATE TRIGGER WORDERInsert FOR WORDER BEFORE INSERT POSITION 0 AS BEGIN NEW.WORDER_NUMBER = GEN_ID(WORDER_NUMBER_GEN, 1); END !! CREATE TRIGGER EMPLOYEEInsert FOR EMPLOYEE

BEFORE INSERT POSITION 0 AS BEGIN NEW.EMPLOYEE_NUMBER = GEN_ID(EMPLOYEE_NUMBER_GEN, 1); END !! CREATE TRIGGER WORKTYPEInsert FOR WORKTYPE BEFORE INSERT POSITION 0 AS BEGIN NEW.WORK_TYPE_CODE = GEN_ID(WORK_TYPE_CODE_GEN, 1); END !! CREATE TRIGGER WODETAILInsert FOR WODETAIL BEFORE INSERT POSITION 0 AS DECLARE VARIABLE NumLines INTEGER; BEGIN SELECT MAX(WODETAIL_LINE_NUMBER)+1 FROM WODETAIL WHERE WODETAIL.WORDER_NUMBER = NEW.WORDER_NUMBER INTO :NumLines; IF (NumLines IS NULL) THEN NEW.WODETAIL_LINE_NUMBER = 1; ELSE NEW.WODETAIL_LINE_NUMBER = NumLines; END !! SET TERM ; !! COMMIT WORK; Page 289 TIP You can set the first value returned by an InterBase generator using the SET GENERATOR command. For example, SET GENERATOR TENANT_NUMBER_GEN TO 1000; SET GENERATOR TENANT_NUMBER_GEN TO 0; would reset the generator to its initial state.

Note the unusual coding required by the WODETAIL table. Why is this? You have to make special provisions for WODETAIL because its elements cannot be merely sequentially numbered. WODETAIL stores work order line items. Each work order requires its own set of sequential line numbers. Obviously, you wouldn't want the first line of Work Order 2 to be line 8 simply because Work Order 1 had seven line items. You'd want the first line item of each work order to be line number 1, the second, line number 2, and so forth. Thus, the insert trigger you've defined queries the WODETAIL table to see what the last line number is for the current work order; the trigger then assigns the next number in sequence to the new row's WODETAIL_LINE_NUMBER column. Notice also the use of the COMMIT WORK statement to save your database work. This is a good practice in InterBase SQL scripts because it closes the implicit transaction started by ISQL and writes its changes permanently to disk. This helps you avoid losing work. After you commit the transaction to disk, ISQL starts another, so you'll want to do this throughout your scripts. Linking the Form into the Application To test the new form, you'll need to hook it into the menu system of your application's main menu. Load the system's main form, fmRSYSMAN0, into the visual Form Designer and double-click the Tenants option on its Tables menu. Key the following code into the Delphi Code editor: fmRTENCGD0.Show; Then press F12 to return to the Form Designer. Page 290 Testing the TENANT Form CAUTION It's a good idea to save a project before running it. This ensures that you don't lose work if the application has a problem. This can be done for you automatically if you click the Autosave options | Editor files checkbox on the Tools | Environment Options | Preferences menu. You can also save your project by clicking Delphi's Save Project SpeedBar button or by selecting Save All from the File menu.

Next, with fmRSYSMAN0 still selected, select the File | Use Unit menu option and select RTENCGD0 from the list. This links the unit that encompasses the fmRTENCGD0 form with RENTMAN's main form so that you can display it from the main form's menu system. Now, save your project and run it. When inside the RENTMAN application, press F5 to display the new form. Click the DBNavigator's row insert button (the + button) and add a row to the table. Remember to observe the constraints in place in the database (for example, the State field can contain only OK and TX as values). Add at least three new rows to the table.

Figure 11.2 shows what the new form should look like. Figure 11.2. The TENANT form as it appears at runtime.

Setting Up the SpeedButton Now that you've hooked the TENANT form into RENTMAN's menu system, it's time to activate its corresponding SpeedButton as well. If the application is still running, close it. Load RENTMAN's main form, fmRSYSMAN0, into the Form Designer and click the SpeedButton component you set up for the TENANT form, sbTenants. Press F11 to switch to the Delphi Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 291 Object Inspector, then click the Events page. Click the drop-down list for the OnClick event and select Tenants1Click from the list. Tenants1Click is the name of the event that Delphi created when you double-clicked the Tenants option on the Tables menu in your main form's menu system. By setting the sbTenants' OnClick event handler to Tenants1Click, you cause the menu item and the SpeedButton to function identically. Now that you've linked it with the Tenants menu item, when you click the sbTenants SpeedButton, you'll see the TENANT form just as if you had clicked the menu option. You can also associate fly-over hints with your SpeedButtons so that their functions are more obvious. We'll cover these in Chapter 13, "Finishing Touches." After you've finished your SpeedButton, you're done with the TENANT form for the time being. Let's move on now to the PROPERTY form.

The PROPERTY Form
Click File | New from Delphi's menu and select the Forms page in the New Items dialog. Click the fmControlGridForm icon again, click Inherit, then click OK. You should see a new descendant form of the fmControlGridForm class in the Form Designer. You'll turn this form into one that edits the PROPERTY table. Begin your customization of the form by renaming it to fmRPROCGD0 and setting its caption to something creative like "Property Edit Form." Save the form to your RENTMAN source directory as RPROCGD0.PAS. Configuring the DBCtrlGrid Of the 22 columns in the PROPERTY table, seven are Boolean columns, and the rest are

either text or numeric fields. Follow these steps to set up the top pane of your DBCtrlGrid: 1. Drop one DBText component onto the top pane of the DBCtrlGrid. 2. Drop 11 DBEdit and three DBComboBox components to the right and below the DBText component on the DBCtrlGrid's top pane. 3. Enter these items into the first DBComboBox control's Items property: Oklahoma City Norman Edmond Dallas Richardson Plano This component will service the PROPERTY table's CITY field. Page 292 4. Enter these items into the second DBComboBox's Items property: Deerfield Firewheel Legacy Hills Switzerland Estates Sherwood Rockknoll This component will interface with the PROPERTY table's ADDITION field. 5. Add these entries to the Items property of the third DBComboBox control: Putnam City Oklahoma City Richardson Edmond Garland Dallas Plano This component will service the PROPERTY table's SCHOOLDISTRICT column. 6. Select all three DBComboBox components and set their Style property to csDropDownList. This will prevent users from being able to type in new values, forcing them instead to select from the values you supplied for each component's

7. 8. 9. 10.

Items property. Drop seven DBCheckBoxes onto the last row of the top pane. Select all seven DBCheckBox components and set their ValueChecked and ValueUnchecked properties to T and F, respectively. Place a single label component adjacent to each of your DBText, DBEdit, and DBComboBox controls on the DBCtrlGrid. Position the DBText component so that it's the top leftmost component on the DBCtrlGrid. Position the other components as shown in Figure 11.3.

Figure 11.3. The first cut of the PROPERTY form.

Page 293 CAUTION Be careful not to drop components onto anything but the topmost pane on the DBCtrlGrid; the other panes are off limits.

The DBText component will display (but not edit) the PROPERTY_NUMBER column. The DBEdit and DBComboBox controls will provide access to the PROPERTY table's other textual columns; the DBCheckBoxes will cover its Boolean fields. Linking the Components with the PROPERTY Table Follow these steps to link your data-aware components with the columns in the PROPERTY table: 1. Link the dmRENTMAN data module with your new form by selecting Use Unit from the File menu and double-clicking RENTDATA in the list of units. 2. Next, set the DataSource property of the DBCtrlGrid component to dmRENTMAN. dsPROPERTY. While you're at it, set the DataSource property of the form's DBNavigator component to dmRENTMAN.dsPROPERTY, as well. 3. Click each component in the DBCtrlGrid and set its DataField property to the appropriate field in the PROPERTY table, as specified in Table 11.3.

TIP You can drag fields from the Delphi Fields editor and drop them directly onto forms. Doing so creates the field's default component and links it with the field. It also creates a label for the control, with a caption corresponding to its source field. This is an easy way to avoid having to manually set up data-aware components.

Table 11.3. The DBCtrlGrid's components and their attributes. Width DataField dteProperty_Number DBText 40 PROPERTY_NUMBER dedAddress DBEdit 121 ADDRESS dcbCity DBComboBox 100 CITY dedState dedZip DBEdit DBEdit 40 70 STATE ZIP Name Component Label No. Address City State Zip continues Page 294 Table 11.3. continued Name dcbAddition dcbSchoolDistrict dedRent dedDeposit dedLivingAreas dedBedRooms dedBathRooms dedGarageType dedLastLawnDate dedLastSprayDate dckCentralAir Component DBComboBox DBComboBox DBEdit DBEdit DBEdit DBEdit DBEdit DBEdit DBEdit DBEdit DBCheckBox Width 115 142 50 50 30 30 30 30 60 60 65 DataField ADDITION SCHOOLDISTRICT RENT DEPOSIT LIVINGAREAS BEDROOMS BATHROOMS GARAGETYPE LASTLAWNDATE LASTSPRAYDATE CENTRALAIR Label Addition SchoolDistrict Rent Deposit Living Bed Bath Garage LastLawn LastSpray CentralAir

dckCentralHeat dckGasHeat dckRefrigerator dckRange dckDishWasher dckPrivacyFence

DBCheckBox DBCheckBox DBCheckBox DBCheckBox DBCheckBox DBCheckBox

75 65 73 53 73 84

CENTRALHEAT GASHEAT REFRIGERATOR RANGE DISHWASHER PRIVACYFENCE

CentralHeat GasHeat Refrigerator Range Dishwasher PrivacyFence

Live Data at Design Time After the components are linked with their corresponding database fields, you should see the first row of data in the DBCtrlGrid while still designing the form. This is because the PROPERTY table is already open (you opened all the tables on dmRENTMAN when you constructed it), and Delphi's Form Designer displays live data when you attach a dataaware control to an open data set. Because they're linked to an open data set, you'll notice that your data-aware components don't display their names as they normally do at design time. Instead, they're blank, indicating that the PROPERTY table itself is empty. Resetting the Tab Order You might find that you need to re-establish your components' tab order after you've arranged them on the DBCtrlGrid. To do this, select Tab Order from the Edit menu and click the arrow buttons to move components up and down in the tab order list (you can also move components up and down in the list by dragging and dropping them). The components' tab order Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 333

CHAPTER 12

Reports
Page 334 In this chapter, we'll continue with the construction phase of the RENTMAN project by beginning work on the system's reports. You'll design three reports and link them into the RENTMAN application. Specifically, these reports include the following:
q q q

A form report to print work orders A columnar report to list the PROPERTY table A columnar report to list tasks by employee

Methods of Building Delphi Reports
There are three basic methods of building reports for Delphi applications. You can build your report as a Delphi form and simply print the form, you can construct a report using Delphi's QuickReport components, or you can write Object Pascal program code that produces a report. You'll use each one of these methods in this chapter. NOTE

You may have noticed that a fourth category is conspicuously absent from the reporting tools list: commercial report writers such as Crystal Reports, R&R, and ReportSmith. That's because it's conspicuously absent from the Delphi packaging. Try as you may, you won't find a copy of ReportSmith with any version of Delphi 3. Borland has committed to providing rich reporting tools within the Delphi IDE itself, and, I can attest, they've pretty much succeeded. I would even go a step further and say that building reports with the newest QuickReports is fairly comparable with ReportSmith in terms of ease of use but produces reports that are an order of magnitude faster and more robust.

Types of Reports
Business reports can be classified into four primary types: forms, labels, columnar reports, and cross-tab reports. A form report prints just one record per page (or one master record and its corresponding detail records). A good example of this type of report is an invoice form report. It prints just one invoice per page. A columnar report is a report in the traditional sense. It lists data in a series of parallel columns. Normally, there are several rows per page. A columnar report may include groupings that arrange and summarize its rows, using a column or columns from the source data. A typical example of a columnar report is one that provides a listing of a company's customers. A mailing-labels report is just that: a report that prints labels for the purpose of mass mailings and the like. You can specify the type of label to print and the number of labels to print across Page 335 each page. Normally, these labels are printed on special printer paper that comes with adhesive mailing labels attached to it. A good example of a mailing-label report is one that prints CUSTOMER table records for use in billing accounts-receivable customers. You'll sample a QuickReports-based mailing label form in Delphi's Object Repository. Click File | New | Forms to access it. Cross-tab reports cross tabulate data in the same way that a spreadsheet does.

That is, in contrast to a columnar report, a cross-tab report not only lists data in columns, but can also arrange data so that row and column values intersect, in a manner similar to a spreadsheet. A good example of a cross-tab report is one that lists sales by state and by quarter. The leftmost column of the report would list each state, and each row would list the sales by quarter for that state. If you wanted to find the second-quarter sales for a given state, you'd find the intersection between the second-quarter column and the appropriate state row. You'll get to experience most of these in this book. The Work Order report you design will be a form report. You'll create this report by simply printing the fmRWOREDT0 form itself. You'll learn to print simple reports of this type using only Delphi forms and standard components. You'll list the PROPERTY table in a columnar report that you'll build using Delphi's QuickReport components. You'll find that most of your reports can be built using these components. In Chapter 19, "Business Reports," you'll build a cross-tab report using Delphi's DecisionCube components. You'll finish this chapter by building a task-list report using only Object Pascal code. This will acquaint you with the old-fashioned way of building reports. You'll use simple looping constructs and terminal-output statements to format and print the report.

The Work Order Form Report
Let's begin with the Work Order form report. You build this report by printing your work order master/detail form, fmRWORMDE0. Begin by loading the RENTMAN System's main form, fmRSYSMAN0, into the Delphi form designer. Drop a PrinterSetupDialog component onto the form and name it ps_RENTMAN. Double-click the Print setup option on RENTMAN's File menu and type the following code: ps_RENTMAN.Execute; Next, return to the visual designer and link the sbPrintSetup SpeedButton with the Print setup menu item's OnClick handler; it should be named Printsetup1Click. This will cause your Print Setup dialog to display when the menu item or SpeedButton is clicked.

Among other things, the Print Setup dialog enables the user to change the printer to send output to. All decent Windows apps that send output to a printer include a printer-setup dialog. Page 336 Setting Up the Work Order Report Menu Item Next, link the Work Order option on the Reports menu with the Work Orders option on the Tables menu. Because your master/detail form will do its own printing, all you want to do when a user selects the Work Order option on the Reports menu is display the work-order selection form, fmRWORGRD0. This form is displayed when the Tables/Work Orders menu option is clicked, so it's appropriate to link the two menu items. The Print Button Now, load the fmRWORMDE0 form into the visual designer and drop a BitBtn component onto its paBottom panel, to the immediate left of the OK button. Change the button's name to bbPrint, its Caption to &Print, and its Glyph to Images\Buttons\print.bmp (Images should be a subdirectory under your main Delphi directory). After you've done this, double-click the button; this should place you in the button's OnClick event handler in Delphi's code editor. Type in the code shown in Listing 12.1. Listing 12.1. Printing the fmRWORMDE0 form. Tag := Longint(WindowState); WindowState:=wsMaximized; for I:=0 to ComponentCount-1 do If (Components[I] is TButton) or (Components[I] is TDBNavigator) or (Components[I] is TSpeedButton) then With Components[I] as TControl do begin Tag:=Longint(Visible); Visible:=False; end; Print; for I:=0 to ComponentCount-1 do If (Components[I] is TButton) or (Components[I] is TDBNavigator) or (Components[I] is TSpeedButton) then

With Components[I] as TControl do Visible:=Boolean(Tag); WindowState:=TWindowState(Tag); Be sure to define I as an integer value in the procedure's header. For example, procedure TfmRWORMDE0.bbPrintClick(Sender: TObject); var I : Integer; will do just fine. The code does three major things. Its first objective is to hide components you wouldn't want on a form report prior to printing the form. For example, you wouldn't want buttons on the printed version of the form; they serve no purpose. The second objective is to print the form Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 332 NOTE Note that whatever code is added to the OnShow event handler will also get executed when the dlcbPROPERTY_ADDRESS component is clicked, due to the fact that we set its OnClick event to point to the FormShow method. That's fine in this case, but it's something you'll want to be aware of when you share code between classes, especially different types of classes.

Testing the CALL Edit Form Save your project now and run the RENTMAN application. Figure 11.16 illustrates what you should see at runtime. Figure 11.16. The completed CALL edit form at runtime.

Summary
In this chapter, you created five forms: the TENANT form, the PROPERTY form, the LEASE form, the WORDER form, and the CALL form. You learned the nuts and bolts of real form construction. Each of these forms has its own

nuances and peculiarities; by having dealt with all of them, you garnered valuable skills that will help you in your own projects.

What's Ahead
Now that you've completed the lion's share of the forms in the RENTMAN system, you're ready to move on to building reports. In the next chapter, you'll create a bevy of end-user reports and link them into the application. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 329 Name dlcbPROPERTY_ADDRESS dedDESCRIPTION dteCity dteState dteZip dteAddition dteTenantName dteHomePhone Figure 11.15. The preliminary CALL edit form. Label Property Description City State Zip Addition Tenant HomePhone Component DBLookupComboBox DBEdit DBText DBText DBText DBText DBText DBText DataField PROPERTY_ADDRESS DESCRIPTION CITY STATE ZIP ADDITION NAME HOMEPHONE

Triggering Query Execution Now that the components are in place, the only thing left is to decide what event will trigger the execution of the quTenantInfo query. As with the work order form, there are two good candidates. The first trigger should be the display of the form itself. If a user edits or enters a call record, you'll need to execute the query the moment the form comes up in order to display the relevant information. The second catalyst should be the selection of an item in the dlcbPROPERTY_ADDRESS DBLookupComboBox. That is, when a user clicks an item in the

dlcbPROPERTY_ADDRESS box, you'll want to execute the query in case the selected property has changed. If the property involved in the call has changed, obviously the tenant and address information is also likely to change. To attach the execution of the query to the display of the form, select the form component (you might have to use the drop-down list in the Object Inspector because the form is obscured), then double-click the form's OnShow event in the Object Inspector. After you're in the Delphi Code editor, key in the following code: with dmRENTMAN.quTenantInfo do begin If Active then Close; ParamByName(`PROPERTY_NUMBER').AsInteger:= Page 330 dmRENTMAN.taCALLPROPERTY_NUMBER.AsInteger; Open; end; The code closes the Query component if it's open, then reopens it. This will have the effect of updating the components you attached to the dsTenantInfo DataSource. Now that you've written a handler for the OnShow event that executes the query, you can make use of it for the OnClick event of the dlcbPROPERTY_ADDRESS component. Press F12 to return to the Form Designer, then click the dlcbPROPERTY_ADDRESS component. Select the Events page in the Object Inspector, then click the drop-down list next to the OnClick event and select FormShow from the list. This will cause the query to execute both when the form is initially displayed and any time that the dlcbPROPERTY_ADDRESS component is clicked. Linking the CALL Edit Form with fmRCALGRD0 and fmRSYSMAN0 Now that the CALL edit form is finished, you need to add it to the CALL grid form's Uses list because it's usually accessed through the CALL grid in the first place. To do this, follow these steps: 1. Reload fmRCALGRD0 into the visual Form Designer. 2. Click the File | Use Unit menu option and double-click RCALEDT0 in the list. After you've linked the CALL edit form with the grid form, link it with the Log a Call menu option in fmRSYSMAN0. Follow these steps to do so: 1. Load fmRSYSMAN0 into the Form Designer.

2. Click the File | Use Unit menu option and add fmRCALEDT0 to its Uses list. 3. Click File | Use Unit a second time and add RENTDATA to RSYSMAN0's Uses list. 4. Double-click the Log a Call menu option and key in the following OnClick event code: dmRENTMAN.taCALL.Insert; fmRCALEDT0.Show; This code first inserts a row, then passes control to the CALL edit form directly. Linking the CALL Form with the WORDER Master/Detail Form The last task to accomplish for the CALL edit form is to link it with the work order master/detail form. This form will allow work orders to be generated directly from the CALL edit form, without having to return to RENTMAN's main menu. With the CALL edit form loaded in the visual designer, click the File | Use Unit menu option and double-click the RWORMDE0 unit to add it to your Uses list. Next, click the paMiddle panel, then drop a Button control just to the right of the Delete button. Change the button's name to btGenerateWorkOrder and its caption to &Generate Work Order. Size it so that it's able to display the entire caption. Page 331 Now, double-click the new button and key the following code into the Delphi Code editor: With dmRENTMAN do begin If (taCALLWORDER_NUMBER.AsInteger=0) then begin taWORDER.Insert; taWORDER.Refresh; //Needed because of server-side generators taCALL.Edit; taCALLWORDER_NUMBER.Value:=taWORDERWORDER_NUMBER.Value; taWORDERPROPERTY_NUMBER.Value:=taCALLPROPERTY_NUMBER.Value; end else begin If not taWORDER.FindKey([taCALLWORDER_NUMBER.Value]) then raise Exception.Create(`Work Order has been deleted'); end; end; fmRWORMDE0.Show; FindKey This code does several important things. First, it distinguishes between the generation of a new

work order and the editing of an existing one by inspecting the WORDER_NUMBER column of the CALL table. Second, if the action to be taken is an edit, the routine uses the FindKey method of the taWORDER component to scroll the data set to the correct work order record. Last, notice that the code checks the result of the FindKey to ensure that it's successful and, if not, raises an exception. Although this should never be possible because of the referential integrity constraint between the CALL and WORDER tables, if it were to occur, an error message would be displayed, and the work order form would be skipped. Control would then return to the CALL form. This is exactly what you'd want in that situation. You might have noticed that you have a try...except block around the code that assigned taCALL's WORDER_NUMBER column a value. The reason for this is that when the Generate Work Order button is clicked, you don't know what state taCALL might be in. Certain data set states are incompatible with assigning the data set a value. For example, if taCALL has posted its changes and has returned to dsBrowse state, an attempt to assign a value to one of its columns will raise an exception. Normally, the way to manage this is to check the value of the State property against a set of flags, dsInactive...dsCurVal. Unfortunately for us, those flags live in a unit that's missing from our Uses statement. Of course, we could've just added the missing unit, but I thought coding an exception would be more fun. Dynamically Setting a Button's Caption A nice touch you could add to the work order generation facility would be to dynamically set the caption of the btGenerateWorkOrder button based on whether clicking it will produce a new work order or edit an existing one. The best place to do this is in the CALL edit form's OnShow event. Select the form, then double-click its OnShow event in the Object Inspector. After you're in the Delphi Code editor, add this code to the bottom of the procedure already in place: If (dmRENTMAN.taCALLWORDER_NUMBER.AsInteger=0) then btGenerateWorkOrder.Caption:='&Generate Work Order' else btGenerateWorkOrder.Caption:='Edit &Work Order'; Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 326 Configuring the Columns Displayed by DBGrid Follow these steps to configure your DBGrid control's columns: 1. Switch back to the fmRCALGRD0 form and click the DBGrid component. 2. Double-click its Columns property and click Add All Fields. 3. Next, remove the PROPERTY_NUMBER column and drag the PROPERTY_ADDRESS column up in the list prior to the DESCRIPTION field. 4. Set the CALL_NUMBER field to Read-Only, then close the Columns editor. Turning a DBGrid into a Row Selector After your DBGrid's columns are configured, the next order of business is to set it up so that it lets you pick rows for editing with another form. The best way to do this is to set the DBGrid's Read-Only property to True. This prevents any changes to the data, which is exactly what you want. In addition to preventing direct edits, DBGrid also needs to support intuitive row selection. It should allow, for example, a row to be double-clicked to be selected, since that's a common convention in Windows apps. To rig it to do so, set its OnDblClick event to the btEdit component's btEditClick event. Later, you'll associate code with DBNavigator that will allow double-clicking the DBGrid to actually load the selected row into another form. Maximizing the Form

Set the form's WindowState to wsMaximized, then maximize it. Resize the paTop component so that the paMiddle doesn't grow inordinately large, then restore the form to its original size. Overriding DBNavigator Because this form is really just a gateway to another form, the only real work of setting this one up is establishing the means of calling the other form. In this case, we'll override the way that DBNavigator works by default and have it call the second form. Double-click the DBNavigator component and add the following code to its OnClick event handler: With DBNavigator1 do case Button of nbInsert : begin DataSource.DataSet.Insert; // fmRCALEDT0.Show; end; nbEdit : begin DataSource.DataSet.Edit; // fmRCALEDT0.Show; end; end; Page 327 Because btEdit calls DBNavigator's BtnClick event, and DBGrid calls btEdit's click code, the net effect of overriding DBNavigator's click handler is that all three of the form's input facilities—the buttons, the DBNavigator, and the DBGrid—provide a means of getting to the second form. Note that the actual calls to the second form's Show method are commented out for the time being. That's because you haven't built the form yet. You'll construct this form in a moment. Linking the Form into the Application Before you get to the second form, let's take the first one on a test run. Load the application's main form, fmRSYSMAN0, into the Form Designer and doubleclick the Calls option on its Tables menu. Type this source code into the Code editor:

fmRCALGRD0.Show; After you've set up the File | Calls menu option, assign its OnClick method, Calls1Click, to the sbCalls SpeedButton so that the two will function identically. After the menu and SpeedButton are set up, add the RCALGRD0 unit to the Uses clause of RENTMAN's RSYSMAN0 unit. You can then press F9 to run the application. After you've viewed your new form, close the app and return to Delphi. Figure 11.14 illustrates what your new form should look like. Figure 11.14. The completed CALL grid form.

The Edit Form Now that the grid form is complete, you're ready to move on to the more exhilarating of the two CALL forms: the edit form. The CALL edit form will allow individual call rows to be entered and edited one at a time. For tables with lots of columns or those that data-entry people work with constantly, the single-row-per-form approach is the best way to go. Grids are fine for navigating a table, but for high-speed data entry, building a custom form that's specially designed to meet the task is the best solution. Page 328 In this case, you're recording calls from tenants and prospective tenants. The person answering the phone wants the computer to be as little of a factor as possible. He or she wants to simply be able to type in calls quickly as they're received. That's why you have one form for quickly perusing the whole CALL table and another for entering or updating its rows. Create a new descendant of the fmEditForm generic form class in the Form Designer. Name it fmRCALEDT0 and set its caption to something dapper like Call Edit Form. Preventing a Form from Being Resized

To prevent a form from being resized, you can set its BorderStyle property to one of the non-resizeable styles such as bsDialog. Change the CALL edit form's BorderStyle to bsDialog and reduce its BorderIcons property to just biSystemMenu (set biMaximize and biMinimize to False). As with the other forms, you need to provide the new form access to the system's data module, dmRENTMAN. Click the File | Use Unit menu option and double-click the RENTDATA unit in the unit list. After you've established the link with dmRENTMAN, save your new form to disk as RCALEDT0.PAS. Populating the Form with Components Drop three DBEdit controls, one DBLookupComboBox, and seven DBText controls onto the paTop panel of the new form. Drop corresponding Label components for each of the controls. Set the DataSource properties of the DBNavigator control, the topmost DBText control, and all the other editable controls to dmRENTMAN.dsCALL. These components will talk to the CALL table. Set the DataSource of the six remaining DBText controls to dmRENTMAN.dsTenantInfo. These components will display the tenant and address information returned by the quTenantInfo Query component you built earlier. One of the advantages of placing the quTenantInfo component on the dmRENTMAN form is that you can use it from any form that uses the data module, including this one. Be sure to include TLabel components for each of the data-aware controls on the form. As you did with the work order form, enable the Bold attribute of the labels' Font property to help them stand out. Arrange the components so that the DBText component you attached to the dsCALL DataSource is in the upper left of the panel and so the form generally matches Figure 11.15. Set the DataField property of each component according to Table 11.6. Table 11.6. The components of the paTop panel and their key properties. Label Call dteCALL_NUMBER No. dedCallDate Date Name Component DataField DBText DBEdit CALL_NUMBER CALLDATE

dedCallTime

Time

DBEdit

CALLDATE

Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 337 itself using its Print method. This will send a copy of the form as it appears onscreen to the printer. The final objective is to restore the hidden components to their original status. There are several important aspects of the code that I should point out. First, notice the extensive use of the Tag property. Tag is a long integer property that is included in every Delphi component and has no predefined purpose; you can do whatever you'd like with it. This code uses Tag at two different levels: It uses the Form component's Tag property to store/retrieve WindowState, and it uses each component's Tag property to store/retrieve its Visible property. Although you could certainly get by without saving and restoring either of these, you'd be making assumptions about the form that may not be valid. You don't know whether WindowState will always be initially set to wsNormal. You might decide the form looks better maximized. You also don't know that all the form's components will always be initially visible. Under certain circumstances, you might decide to hide some of the components on the form. If you make the assumption that all components are initially visible, printing the form and returning all components to a visible state would then reveal these hidden components. The code listed previously is a much more robust solution. Another important aspect of the code is the use of the Components property. Components is an indexed property of controls that can contain other components, such as TForm. The previous code uses it to look at each component on the form, determines whether it should be on the printed form, then sets its Visible property accordingly. You can use the Components property to quickly set properties for a group of controls at runtime. The third major aspect of the preceding code is the use of runtime type

information (RTTI). The is and with...as constructs use RTTI to perform their magic. Even though Delphi's compiler produces native machine code, you can still determine the specific data type of a class at runtime. This is normally not the case with native-code compilers. Runtime type information has historically been the exclusive province of interpretive products such as Visual Basic. Native-code products don't usually have the capability to check the data type of a particular element because machine language itself contains no such information. Variables are reduced to offsets and sizes when source code is translated to object code. Delphi's RTTI capability gives you the best of both worlds: native code compilation and useful data-type information at runtime. This capability enables you to generically examine the components on the form and set them appropriately. The fourth thing I should point out is the use of typecasting to set each component's Visible property. Not all components have a Visible property (none of the nonvisual components does, for example), so it's not possible to simply say with Components[I] do begin Tag:=Longint(Visible); Visible:=False; end; Because the base component class does not have a Visible property, the preceding code fragment will not set the Visible property of Components[I] at all. Instead, the assignment of the property will "fall out" to the next scoping level, which is that of the form class itself because Page 338 you're within a method of the form. The end result, then, would be that you'd set the form's Visible property, something you definitely don't want to do. The Object Browser What you must do in order for this to work properly is find the ancestor class that defines the Visible property. The first common ancestor of Delphi's visual controls that defines the Visible property is the TControl component class. I know this by examining the Delphi class hierarchy. There are two ways to examine this hierarchy. The first, and easiest, is to use the Object Browser. After you've compiled your project at least once, the Browser option becomes available on Delphi's View menu. Click the View/Browser menu option to

bring up the Object Browser. You can then scroll the class hierarchy in the left pane until you see the class or classes in which you're interested. You can also search for an element of the hierarchy by name. For example, if you scroll the list until you see the TButton component, you'll notice that it's an immediate descendant of TButtonControl and a second-level descendant (grandchild) of TWinControl. TWinControl, in turn, is an immediate descendant of TControl. If you scroll the right pane, you'll find TControl's Visible property. The other way to investigate the heritage of a particular class is to study the VCL source code. You can learn a great deal from exploring the VCL source. It's not as easy as simply popping up the Object Browser, but you get a lot more information than the Browser can provide. You start with the unit that defines the class in which you're interested and work backward from there. You may have to use a text-search utility (such as grep) to locate the unit that defines a particular class, but Borland has named the units fairly well, so this isn't as bad as it sounds. You can look at the type definition for each class to determine its ancestor. This information is contained in the first line of each class definition. For example, the first line of the TButton class definition is TButton = class(TButtonControl) This tells us that TButton is a descendant of TButtonControl. Furthermore, the first line of TButtonControl's definition is TButtonControl = class(TWinControl) This tells us that TButtonControl descends from TWinControl. Both TButton and TButtonControl are defined in the Stdctrls unit. Careful inspection of both of them reveals that neither defines the Visible property, so you'll have to keep going. The TWinControl class is defined in the Controls unit. Its first line looks like this: TWinControl = class(TControl) A check of its class definition reveals that it doesn't define a Visible property, either. If you check its ancestor, TControl, you'll find that it does define the Visible property. That's why we can use the TControl class to typecast any visual component on the form to set its Visible property. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 339 NOTE Note that the typecasts performed by the form print code are known as checked typecasts: They're checked for validity at runtime. If you typecast a class instance using a class type that is not the instance's class type and not the class type of any of its ancestors, an exception will be raised. The unchecked form of typecasting, by contrast, is implemented through the following syntax: With TControl(Components[i]) do With unchecked typecasts, if you miscast a class instance, your application will most likely generate an access violation and go down in flames. Checked class typecasts are just as functional and much safer; use them whenever possible.

Another interesting aspect of the form-printing code is that the list of controls being checked doesn't include the TBitBtn class. Why not? After all, there are certainly TBitBtns present that you wouldn't want to print. You just dropped one, the bbPrint button, onto the form. How does the following code account for the TBitBtn component class? If (Components[I] is TButton) or (Components[I] is TDBNavigator) or (Components[I] is TSpeedButton) then

Once again, the answer lies in the class hierarchy. Remember that the RTTI is operator returns true when you test a particular class instance using its own class type, or that of any of its ancestors. That last clause is the important part. Because TBitBtn is a descendant of TButton, testing for TButton covers TBitBtn as well. Unfortunately, TSpeedButton doesn't descend from TButton, as one might think, so it must be checked separately. A Shorter Path? You might argue that, rather than looping through all the components on the form, it would make more sense to simply hide the handful of components on the form that shouldn't be printed. Likewise, you could maximize the form at design time, alleviating the need to do so at runtime. For example, you could code a small routine like so: DBNavigator1.Visible:=False; bbOK.Visible:=False; bbCancel.Visible:=False; bbPrint.Visible:=False; This code is certainly shorter than the preceding procedure, but the problems with this approach are many. First, adding even one additional button to this form would require updating the preceding code. That's not true of the earlier approach. Second, adding any nonprinting controls (such as buttons) to any of this form's ancestors would cause the same problem. Third, the preceding coding style doesn't allow for the possibility that a given control might be Page 340 invisible to begin with. That is, if the code were to take into consideration that a control might be initially hidden, it would have to include a line for each control to save the control's Visible status prior to changing it. It would also need to reset the Visible property of each control following the print. You'd soon have as many lines as in the first and a far more flexible approach. Finally, maximizing the form may be desirable from a printing standpoint, but it may not be from a design standpoint. For whatever reason, you might not want it to take up the whole screen. If not, you shouldn't be forced to maximize it at design time just to get it to print correctly. The bbPrint button's OnClick code, as was originally designed, can be used in any Delphi application and with any form. It's a solution that you can make use of in your own applications. You could even copy the Print button and its

OnClick code to the fmAnyForm class in the form hierarchy you designed and effectively add a print capability to all the forms in the RENTMAN System. Printing Multiple Rows You can easily switch a form from printing just the current row in a table to printing multiple rows from the table. For example, to print all the work orders on file, you could write With taWORDER do begin First; While not(EOF) do begin fmRWORMDE0.bbPrintClick(Self); Next; end; end; Print Forms Keep in mind that you can design forms whose whole purpose is to be printed. For example, you could inherit from the work-order master/detail form and create a form just for printing work orders. On this form, you could do various things to make the form print more legibly, including using color combinations that print well (some popular screen combinations, such as black text on gray, should be avoided) and making use of borders and other things that enhance the appearance of printed reports. On this form, you could set the Visible property of those components that shouldn't be printed to False at design time; there'd be no need to do it at runtime. Although we won't build any print forms in this chapter, you might find this preferable to printing forms originally designed for display only. NOTE Note that building reports through printed forms is the least efficient way to get printed output. Because the form image is sent to the printer as a large bitmap, your printer will need lots of memory in order to print it. Some printers won't be able to

Page 341

print it at all. Printing a form this way will also take longer than printing the equivalent report via almost any other means. Also, because printing a form is essentially printing its screen image, the quality of print you receive will depend on the relatively lower resolution of your video card versus the relatively high resolution of a printer.

Alternative Methods of Creating Form Reports Form reports can also be designed using either Delphi's QuickReport components or from within commercial report writers. You may find designing complex form reports using one of these alternative methods easier than merely printing Delphi screen forms. For simple form reports like this one that would typically be for internal use only, TForm's Print method is adequate. However, the QuickReport family of components has matured enough that it's viable for simple as well as complex reporting needs. Testing the Form Report You're now ready to test the new report. Save your project and run the application. Click the Reports/Work Order option (or press F7), and you should see the Work Order Selection form. Select a work order, then click Edit. Next, click your Print button, and you should see the form maximize, hide its button controls, print, and then return to normal display. Now exit the application and return to Delphi. NOTE Note that you can use TForm's PrintScale property to control the manner in which a form is rendered by its Print method. The default setting poProportional causes the form to print very similarly to the way it displays. The same number of pixels per inch in use when a form is displayed are used when it's printed. poPrintToFit causes the form to print using the same screen proportions but sized so that it just fits the printed page. poNone turns off any special scaling or sizing. This may result in a printed form differing significantly from its onscreen counterpart because the resolution of today's displays can't match that of most printers.

You'll build the next report using Delphi's QuickReport components. Through this exercise, you'll see that building reports using the QuickReport components is only slightly more difficult than building them using standard screen components. You'll also find that these components are up to the task of producing most types of reports. You will probably also discover that the QuickReport components offer the fastest route to professional business reports in Delphi applications. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 342

Property-List Columnar Report
The next report we'll build will list out the PROPERTY table. We'll use Delphi's new QuickReport Wizard to get started quickly building reports with the QuickReport components. Click the File | New option and select the Business tab inside the New Items dialog. Double-click the QuickReport Wizard to start it. Basically, the wizard will ask you a few questions and build a basic report based on what you tell it. The first thing the wizard asks is for you to select a directory or database alias from which to work. Find the dbRENTMAN alias in the list that we defined in the RENTMAN app and select it. At the bottom of the screen, you're asked to choose a table on which to base the report. Select the PROPERTY table from the list, then click the Next button. After you've selected a table, the wizard next asks you to specify which fields from the table you want to appear on the report. Select the PROPERTY_NUMBER, ADDRESS, CITY, STATE, ZIP, ADDITION, and SCHOOLDISTRICT fields. You can move fields between the Available and Selected lists by double-clicking them or by selecting them first and clicking the appropriate arrow button. Click the Next button when you have all the specified fields selected. The last thing the wizard asks you to do is supply a title for the report. Specify something daring like PROPERTY Listing Report. You can also select an alternate base font for the report, if you want; however, I think Arial is fine for our needs. When you're finished with the title, click the Finish button, and the wizard will generate a basic report that you can change to your liking. The first cut of the report, though plain, is certainly workable. Toggle the Table component's Active property to True, then right-click the report and select Preview to see what it will look like at runtime. Figure 12.1 illustrates this. Rather than laboriously covering every QuickReport feature in painstaking detail, I've polished up the report the

wizard built for us to highlight some of the things you can do with QuickReport reports. Figure 12.2 illustrates the new, improved version. Here are some of the high points of the changes I made:
q

q q

Summary columns—Notice the total property count at the bottom of the report. QuickReport does groups, group totals and sorts, and so on. Object and band colors—I used a custom color to make the detail band stand out a bit. System fields—Notice the date and time in the upper left of the report. They're based on the QuickReport QRSysData component.

Page 343 Figure 12.1. The first rendition of the PROPERTY Listing Report as created by the QuickReport Wizard.

Figure 12.2. The report after a little refinement.

TIP Don't be surprised when the QuickReport QRExpr expression component doesn't support ANSI SQL-style aggregates. For example, don't use COUNT(*) to return the count of rows from a table. And, COUNT(PROPERTY_NUMBER) doesn't work, either. QRExpr

Page 344 expects the word COUNT, all by itself, with no parentheses and certainly no ANSI SQL syntax.

NOTE The source code for all the reports in this chapter can be found on the CD-ROM accompanying this book.

After you've finished customizing the report generated by the wizard, name it frRPROLST0 and save it as RPROLST0.PAS. Linking the Report into the Application In order to fully test the report, you'll need to link it into the RENTMAN application. To do this, you'll need to do two things:
q q

Use the report form's unit in the application's main unit, RSYSMAN0 Add a menu item and support code for the new report to the application's main menu

Begin these steps by reloading fmRSYSMAN0 in the form designer. Click Use Unit on the File menu and double-click RPROLST0 in the list. This will enable you to reference the report form contained in the RPROLST0 unit. Next, double-click the form's MainMenu component and click the blank menu option on the Reports menu. Set its Caption to &Property List and its ShortCut to F11. Now, double-click the item and type the following code into the code editor: frRPROLST0.QuickRep1.Preview; This line of code causes the new report to be displayed onscreen where it may be printed, saved, or canceled. After you've completed these steps, you're ready to test the new report. Save the project and run the application. Figure 12.3 shows the report at runtime. Page 345 Figure 12.3. The PROPERTY Listing Report as it appears at runtime.

The Task-List Report
You'll build the last report in this chapter using nothing but Object Pascal code. This will be a good exercise, if for no other reason than it will give you a greater appreciation for the complexity of report writers such as

Delphi's QuickReport components. If you ever encounter a report with such special needs that you can't build it using the QuickReport components, you can always fall back to hand coding it entirely in Object Pascal. Because Borland's Pascal was originally intended as a general purpose programming language, it can do all the things you expect 3GLs such as C, BASIC, and Pascal to do. Delphi's Object Pascal is powerful enough that you'll be hard-pressed to find a report that you can't build with it. Drop a new Query component onto the dmRENTMAN data module. Name it quTaskList and set its SQL to SELECT E.Name, P.Address, P.City, P.Addition, T.Description, T.TaskDuration FROM EMPLOYEE E, WORDER W, PROPERTY P, WODETAIL D, WORKTYPE T WHERE E.EMPLOYEE_NUMBER=W.EMPLOYEE_NUMBER and W.WORDER_NUMBER=D.WORDER_NUMBER and W.PROPERTY_NUMBER=P.PROPERTY_NUMBER and D.WORK_TYPE_CODE=T.WORK_TYPE_CODE and W.StartDate <= :ListDate and W.EndDate >= :ListDate ORDER BY E.Name, P.Address, P.City, P.Addition, T.Description Page 346 This query will join the EMPLOYEE, WORDER, PROPERTY, WODETAIL, and WORKTYPE tables to produce a comprehensive employee task list for a given date. Notice that the query defines a single parameter, : ListDate, that must be supplied in order for it to work properly. Bring up the property editor for the Query component's Params property and set ListDate's type to Date. Next, set the DatabaseName to dbRentman, then double-click the component and add all its fields as TField components. Now you're ready to set up the actual Object Pascal code that will produce the report. Load the application's main form, fmRSYSMAN0, into the form designer and double-click its MainMenu component to open the Delphi menu designer. Next, click the Reports | Task List menu option and set its shortcut key to F10, then double-click the Task List option and change its OnClick event handler to match Listing 12.2. Listing 12.2. The TaskList1Click event handler. procedure TfmRSYSMAN0.TaskList1Click(Sender: TObject); var LastName : String; CurrentLine : Byte; procedure PrintColumnHeadings; begin Writeln(PrintFile,' `+Pad(`Address',30), ÂPad(`City',20), Pad(`Addition',20), Pad(`Work',30), Pad(`Time (Days)',15));

end; begin inherited; If (MessageDlg(`Print the Task List report?', mtConfirmation,mbYesNoCancel,0)<>mrYes) then Exit; try Cursor:=crHourGlass; With dmRENTMAN, quTaskList do begin ParamByName(`ListDate').AsDate:=StrToDate('04/05/97'); // Supply a valid date here based on your test data // ParamByName(`ListDate').AsDate:=Date; Open; try BeginReport(poPortrait,Caption); try While not eof do begin CurrentLine:=9; PrintHeader(`RTSKLST0',Caption,'Employee Task List', Â ParamByName(`ListDate').AsString); PrintColumnHeadings; While (not eof) and (CurrentLine<>PageLength) do begin Writeln(PrintFile); Writeln(PrintFile,'EMPLOYEE: `+quTaskListName.AsString); Inc(CurrentLine,2); Repeat LastName:=quTaskListName.AsString; Writeln(PrintFile,' `+ Pad(quTaskListAddress.AsString,30), ÂPad(quTaskListCity.AsString,20), Pad(quTaskListAddition.AsString,20), Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 347 Pad(quTaskListDescription.AsString,30), ÂPad(quTaskListTaskDuration.AsString,15)); Next; Inc(CurrentLine); Until (eof) or (CurrentLine=PageLength) or (quTaskListName.AsString <> LastName); end; end; finally EndReport; end; finally quTaskList.Close; end; end; finally Cursor:=crDefault; MessageDlg(`Task List Report Finished',mtInformation,[mbOK],0); end; end; This code is dependent on several other support functions and procedures. Key the code shown in Listing 12.3 into the RSYSMAN0 unit, just prior to the TaskList1Click method procedure. Listing 12.3. Utility functions for the Task List report. function Pad(InStr : String; TotalLen : Integer) : String; begin Result:=InStr; While (Length(Result)<TotalLen) do Result:=Result+' `; end; function LPad(InStr : String; TotalLen : Integer) : String; begin

Result:=InStr; While (Length(Result)<TotalLen) do Result:=' `+Result; end; function Center(InStr : String; TotalLen : Integer) : String; var NumSpace : Integer; Temp : String; begin NumSpace := (TotalLen-Length(InStr)) div 2; Temp:=''; While Length(Temp)<NumSpace do Temp:=Temp+' `; Result:=Temp+InStr+Temp; While Length(Result)<TotalLen do Result:=Result+' `; end; procedure PrintHeader(ReportName,SystemTitle,ReportTitle,Criteria : String); begin If (Printer.PageNumber <> 1) then Write(PrintFile,^L); Writeln(PrintFile,Pad(`Report: `+ReportName,25), continues Page 348 Listing 12.3. continued Center(SystemTitle,LineLength-45), LPad(`Page: `+IntToStr(Printer.PageNumber),25)); Writeln(PrintFile); Writeln(PrintFile,Pad(`Print date: `+DateToStr(Date),25), Center(ReportTitle,LineLength-45), LPad(`Print time: `+TimeToStr(Time),25)); Writeln(PrintFile); Writeln(PrintFile,Pad(`User name: `+UserName,25), Center(`For: `+Criteria,LineLength-45)); Writeln(PrintFile); end; procedure BeginReport(Orientation : TPrinterOrientation; Title :String); begin Printer.Orientation:=Orientation; Printer.Title:=Title; AssignPrn(PrintFile); Rewrite(PrintFile); With Printer.Canvas.Font do begin Name:='Courier New'; Height:=10;

Style:=[]; end; LineLength:=(Printer.PageWidth div Printer.Canvas.TextWidth(`X'))-5; PageLength:=(Printer.PageHeight div Printer.Canvas.TextHeight(`X'))-2; end; procedure EndReport; begin CloseFile(PrintFile); end; The preceding functions and procedures rely on a handful of variables that are global to the RSYSMAN0 unit. Insert a var declaration section just prior to the {$R *.DFM} line in RSYSMAN0 like so: var PrintFile : Text; LineLength : Integer; PageLength : Integer; UserName : String; Finally, this new code requires the use of the Delphi Printers unit. The Printers unit provides an interface to the basic Windows printing interface. Add Printers to the Uses clause of the RSYSMAN0 unit.

Inside TaskList1Click
There are several points about the TaskList1Click procedure that merit discussion. First, notice the use of Object Pascal's standard Writeln procedure to send output to the printer. Page 349 This is facilitated by the AssignPrn procedure. AssignPrn associates a text file with the default Windows print device. By opening and writing to this text file, you send printer output to Windows. There are a couple of schools of thought regarding the proper way to print text in Windows applications. The first maintains that you should use the BeginDoc procedure to start a print job and the TextOut procedure to send output to the printer. This method has many advantages, including complete control over the appearance of the text and the ability to position the text exactly where you want it. The downside to this approach, though, is that it's somewhat complicated to set up and use. If you need that kind of control, I suggest you use a full-blown report writer; they usually provide a full range of facilities for controlling printer output. The second school of thought regarding printing text under Windows espouses the approach taken here. The idea is to keep things as simple as possible; the task of printing sophisticated reports is left to report writers. By using standard output routines to send the printer output, you avail yourself of all the facilities those routines provide without needlessly complicating the process of getting your report to the printer. This simplistic approach keeps the code size small and easy to follow. Begin Report

Let's reexamine each of the component pieces of the preceding code. Let's look first at the BeginReport routine. To reiterate, its text is as follows: procedure BeginReport(Orientation : TPrinterOrientation; Title :String); begin Printer.Orientation:=Orientation; Printer.Title:=Title; AssignPrn(PrintFile); Rewrite(PrintFile); With Printer.Canvas.Font do begin Name:='Courier New'; Height:=10; Style:=[]; end; LineLength:=(Printer.PageWidth div Printer.Canvas.TextWidth(`X'))-5; PageLength:=(Printer.PageHeight div Printer.Canvas.TextHeight(`X'))-2; end; Notice how the orientation of the report is passed into the routine as a variable of type TPrinterOrientation. TPrinterOrientation is defined by the Printers unit and has the possible values of poPortrait and poLandscape. Because page orientation is usually dependent upon the routine calling it, the routine expects it to be supplied when you initiate a print job. The With Printer.Canvas.Font section sets the font that the report will use. For simplicity's sake, the entire report will be printed in a single font. Moreover, that single font will be a non-proportional TrueType font, Courier New. Unless you have a lot of time on your hands or feel you need an extra source of frustration in your life, I recommend you stay away from proportional fonts when "rolling your own" report-printing mechanism. Page 350 Notice the way in which LineLength and PageLength are calculated. The line LineLength:=(Printer.PageWidth div Printer.Canvas.TextWidth(`X'))-5; divides the number of pixels on the printed page (taking into account the current paper size and page orientation) by the width of a single X in the current font. This is another good reason for using a non-proportional font. Because all characters are sized identically in nonproportional fonts, you can make calculations such as this one by using the TextWidth method function. Using a proportional font, however, invalidates this calculation; the process of determining the maximum number of characters per line gets much more complex. Note that the calculation subtracts 5 from the quotient to allow for left and right margins. The Title parameter that is passed into the routine is used to set the print job name used in the Windows print spooler and on network banner pages. If your network print configuration prints a banner page before each print job, you can use TPrinter's Title property to set the text it prints.

The Pad, LPad, and Center functions all perform a similar function. One of the challenges of formatting print output is getting columns containing variable length data sized appropriately. Another major challenge is in getting column headings and the data they represent to be sized evenly. If the heading over a column is longer than its data, the column must be sized to match the heading; if the data is wider, the column heading must be padded. These three functions pad the elements on the report so that they are easier to align. Along with using non-proportional fonts, consistently padding similar elements is the most important aspect of getting reports to print correctly. Print Header I've written the PrinterHeader routine in such a way that it should be modular enough to use in your own programs. Although I don't recommend that you print reports using only programming code, if you run into a situation where you must, you can use much of the code presented in this chapter, including the PrinterHeader routine. It takes ReportName, SystemTitle, ReportTitle, and report criteria for display in the report's page heading as its parameters. This flexibility should enable you to use it with virtually any type of report. The line If (Printer.PageNumber <> 1) then Write(PrintFile,#12); sends a Ctrl+L character to the printer each time the PrintHeader routine is called, with the exception of the first time. Sending ASCII character 12 to virtually any printer causes it to eject a page. You don't want to eject a sheet the first time the routine is called, and you don't want to waste a sheet of paper each time the report is printed. Note the use of the built-in Date and Time functions. In production code, you might want to store both of these when a print job is initiated and print the stored versions on the report Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 351 itself. By printing the function results directly, you leave yourself open to the possibility that each page will print a different time on it; the current system time is re-retrieved with each call to the Time function. Furthermore, if a print job crosses the midnight boundary, you might have a similar problem with the date printed on each page. I've left the calls to the functions in place for simplicity's sake. Note the use of calculations based on the LineLength variable in the calls to the Center function for the system title, the report title, and the report criteria fields. This is done to allow the orientation of the printed page to be changed without invalidating the code used to print the page header. If the page orientation is switched to poPortrait, the page header will change dynamically along with it. More on TaskList1Click The line ParamByName(`ListDate').AsDate:=Date; supplies the current system date to the quTaskList query as its ListDate parameter. You can uncomment it to enable the Task List report to print the task assignments for the current day (if you uncomment this line, comment out the ListDate assignment that precedes it). After the ListDate parameter is assigned, quTaskList's Open method is called to initiate the query and return a result set. Notice the use of try...finally blocks to prepare the code for exceptions. On

inspecting the code, you'll find that there are three separate try...finally blocks. In the event there's a problem within the procedure, the first one ensures that the query is closed at the end of the routine. The second ensures that the text file the routine uses to send print output to Windows is closed when the routine terminates, regardless of whether the routine terminates normally or because of an error. The third one ensures that the cursor pointer is reset to its normal state when the print job ends. The Repeat...Until (quTaskListName.AsString<>LastName) section handles the report's grouping. The idea within the report is to print a separate task list for each employee. Through the use of an ORDER BY statement in the SQL that drives the report, and due to the logic contained in the Repeat...Until loop, the report is able to do just that: Each employee's task list is printed separately, one after the other. A final thing I should mention about the report code: Notice that the mouse pointer is changed to an hourglass prior to the beginning of the report and restored after it finishes. It's important to let your users know that something's happening while the report runs. As with any task, the more feedback you can provide the user, the better. Another idea you might consider is the use of Delphi's TProgressBar component to show the report's completion status. You could update the bar as you step through the quTaskList query result set. Little touches such as this give your applications a more polished look and keep the user from becoming impatient during times when it might appear that the app isn't doing anything. Page 352 Previewing the Report Another nice feature you could implement is the ability to preview the report before printing it. This can easily be done by using Assign rather than AssignPrn to output your report. What you'll then get when previewing a report is a text file that contains the output that would have gone to the printer had you printed the report instead. You can then use the LoadFromFile method of the TMemo component to display this file in a scrollable window. If you then want to send the previewed report to the printer, you can simply use AssignPrn to initiate a print job, and write the TMemo's Lines property to the printer. Save your project, then press F9 to compile and run the application. After RENTMAN is loaded, pressing F10 prints the employee Task List report.

Summary
You've now constructed reports using just about every method available to you in Delphi. You learned to construct reports by printing forms, by using QuickReport components, and through Object Pascal code. Generally, you'll find that Delphi's QuickReport components are the easiest way to go. Whatever your needs, you should find yourself well-equipped to construct sophisticated reports for the database apps you build with Delphi.

What's Ahead
In Chapter 13, "Finishing Touches," you'll buff up the RENTMAN app a bit and apply a few final touches. The chapter takes you through polishing your app to make it look a little more refined and run more smoothly. You'll learn how to set up a background bitmap, an application icon, Windows help, a status bar, and lots of other niceties. The result will be a Delphi client/server database application that's full-featured and professional-looking. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 353

CHAPTER 13

Finishing Touches
Page 354 Now that you've completed the lion's share of RENTMAN's construction, you're ready to polish the app—to give it that "professional touch." RENTMAN is still a bit rough around the edges—it lacks a few things that would no doubt make it more aesthetic and more palatable for the user. The post-developmental review phase of the development process is often slighted in terms of the importance placed on it. Make no mistake about it, postdevelopmental review is a key step in the application construction phase. It's critical in that it lets you step back from the application and evaluate it as a user. It helps bring to light usability problems and other anomalies that your users might find objectionable before they actually see the app. Programmers are some of the most demanding users you'll find. Chances are, if your application can survive a review by your peers, it will likely pass muster with your users as well. This review phase is also important in that it gives you a chance to polish your work. After some of the more mundane parts of the application's construction phase have been completed, you can do a few things that are more fun and that enhance the application. This enables you to take even more pride in your work, and, I think you'll find, makes the software development process even more enjoyable.

The enhancements you'll make in this chapter to the RENTMAN System are
q q q q q q q q

Add an application logo Change the application's title and icon Add Windows help (including context-sensitive help) Add fly-over hints Activate the status bar Add an About box Add a form Print button (and other system-wide enhancements) Add report front-end dialog boxes

After these changes are complete, you'll finish by comparing the completed application with the original specification that was developed in earlier chapters. You'll test the application for compliance with the original statement of purpose and for stability and usability in general.

Adding an Application Bitmap
There isn't much to adding a bitmap to a Delphi application. Some developers prefer to set up separate splash forms whose whole purpose is to display the bitmap. In the case of RENTMAN, you simply add it to the application's main form, fmRSYSMAN0. Before you can place it onto the form, though, you'll need to procure the bitmap. You can use one of the many canned bitmaps that ship with Delphi, Windows, or some other package. Alternately, you can create your own using Delphi's Image Editor tool. You can access the Image Page 355 Editor tool from Delphi's Tools menu or from your Delphi program folder. Figure 13.1 shows some of the author's world-famous line art created using the Image Editor tool. Figure 13.1. You can use Delphi's Image Editor tool to create spectacular bitmaps like this one.

Reload the RENTMAN project (if you've not already done so) and load the fmRSYSMAN0 form into the visual designer. Click the paMiddle Panel component, then drop an Image component onto it. Name the component imSplash and set its Alignment property to alClient. Next, double-click its Picture property and click the Load button in the ensuing dialog. From the Load picture dialog, select a bitmap and click Open. After you return to the Picture editor dialog, click OK. If your bitmap will be displayed on a form that might be resized (such as this one), you'll want to ensure that it's large enough to accommodate the form's maximum size. If you're placing the bitmap on a Panel component, as we are here, you can simply maximize the form, then view the panel's Height and Width properties in the Object Inspector. You can then either create or find a bitmap with at least these dimensions. Another approach would be to use a smaller bitmap and stretch it to fit its host area. I don't recommend this, though. Usually, this will have the effect of distorting a bitmap to the point of making it look silly. To cause a bitmap to stretch to fit its area, set its Stretch property to True. Note that setting the Image component's Stretch property to True does have the benefit of allowing the bitmap to be shrunk to fit the form when it's reduced in size. You may find this preferable to cropping the bitmap. The important thing is to ensure that the bitmap looks correct when the form is displayed at its maximum size. In the case of fmRSYSMAN0, the form's maximum size is also its default size. Page 356 After you've place the bitmap correctly, save the project and run it. Figure 13.2 shows what the application looks like at runtime with its new bitmap. Figure 13.2. Adding a bitmap to the RENTMAN application helps polish its appearance.

Specifying the Application's Title and Icon
After you've set up RENTMAN's splash bitmap, you're ready to assign its application title and icon. By default, Delphi assigns the same canned icon image to every new application you build. If you develop only a handful of applications, this might be acceptable; over time, however, it can become confusing. An application's icon should give a quick, bird's-eye view of what it is the application does. It should also distinguish the application from other applications. In this section, you'll set up an application icon that does just that. Once again, the first thing you need to do is come up with an image to use. Windows itself comes with a number of icons, as does Delphi. You can also use the Image Editor tool to build your own. Figure 13.3 illustrates more of the author's contribution to the art world. After you have an icon, you're ready to link it into the application. To do this, select the Options item on the Project menu, then click the Application tab. Once on the Application page, type RENTMAN System into the Title box, click the Load Icon button, and specify your new icon (see Figure 13.4). The title you specified will be used on the Windows task bar to identify your application. The icon you included will be used in a variety of places. Basically, it'll be referenced every time Windows needs an icon to represent your application. This icon will also carry through to your users' machines when you run an InstallShield-based setup program to install your software. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 357 Figure 13.3. You can use Delphi's Image Editor tools to build icons for your applications.

Figure 13.4. You use the Project | Options | Application menu option to associate an icon with your applications.

Adding Windows Help
Creating, adding, and using Windows help in applications is a subject that has filled many books. It's a complex topic and a world unto itself. Nevertheless, I think at least a brief excursion into adding Windows help to your applications is in order. All professional Windows applications include help systems. Help-File Creation Utilities Before we get started, I should mention that there are a number of good utilities for creating Windows help files. Among these, two stand out: ForeHelp and RoboHelp. I'd encourage you to invest in one of these if you intend to get serious about building Windows help files. They will save you hours of painstaking work. Since I can't count on

your having either of these, I'll show you how to create Windows application help the old-fashioned way. Page 358 Help File Basics In order to build Windows help files, it's necessary to understand what the components of Windows help files are. You have three basic files that are a part of every help file: the help-project file (.HPJ), the help-contents file (. CNT), and the rich-text file (.RTF) containing the help text itself. The help-project file and the help-contents file are both text files. The help-text file is a rich-text-format file that can be created with a number of word processors, including Microsoft Word. You merge these three files (and possibly others, as well) into your help file by compiling them with the Windows help compiler. NOTE The WordPad accessory that accompanies Windows can also read and write rich-text-format files, but it does not support all the formatting capabilities you'll need to build Windows help files. It doesn't support either footnotes or hidden text, so it's not usable as a Windows help text editor. Because Delphi's TRichEdit component uses the same internal Windows control as WordPad, it suffers from these same limitations.

Each of the three files contains special instructions that control the make-up and behavior of the generated help file. Prior to Windows 95, you had to edit the help-project and help-contents files manually if you didn't already have a third-party Windows help utility. Starting with Windows 95, Microsoft began providing a much nicer facility for working with help files. The utility is called Microsoft Help Workshop and it's included with Delphi. Before you create the help project or contents file, though, you'll need to compose the help text itself. This file consists of free-form text with a handful of special characters and other text-formatting attributes that define your help topics and the relationships between them. Text is organized by topic, with each topic on a separate page. Within each topic, custom footnote characters are used to denote a topic's title, its topic number, its browser sequence, and its index entries. Special text attributes denote hotspots—links from one topic to another. Table 13.1 summarizes these special characters and attributes. Table 13.1. Special characters and attributes used to create Windows help topics. Symbol/ Attribute # $ K + A Page 359 Meaning Defines the topic ID that is used elsewhere to refer to this topic Defines the topic title Defines an index entry or set of entries Defines this topic's order in its browser sequence Defines an A-link keyword

Symbol/ Attribute ! * > @ Doubleunderline Hidden text

Meaning Defines a macro to execute Defines a build tag for conditional exclusion of selected topics Defines the window type to use Indicates a comment in the topic Indicates a hotspot—a jump to another topic Identifies the topic ID to jump to

Only the first one, the topic ID symbol (also known as the context string) is actually required; the rest are optional. In practice, you'll use the first four—#, $, K, and +—more than the others. Building the Help Text File Now that you know what these symbols mean, let's put them to use. Start a new file in your Windows word processor. This word processor will have to support the RTF format, custom footnotes, double-underlines, and hidden text. The one most often used is Microsoft Word, but you can use any one that meets the preceding qualifications. Let's key three sample topics for the RENTMAN System, plus a contents topic. You'll set up links between these topics and define index entries for each of them. To begin with, key the following text for the Log a call menu item: Log a call Select this option to enter a maintenance call. You'll be asked to specify the Âproperty that the call concerns. Entering a Context String Position the I-beam cursor just prior to the word Log and insert a # custom footnote (press Alt+I,n in Microsoft Word 7.0). This will denote the topic's ID or context string. Type LogCall for the footnote's text. Other topics that establish links to this topic will use its context string to set up the link. You'll also use the topic ID in the Help Workshop; you'll map it to actual help-context numbers that your application can use. Entering a Topic Title Next, reposition the I-beam just before the word Log and insert a $ custom footnote. This will establish the topic's title. Key Log a call for the footnote's text. Windows allows topics to be looked up using their titles, so it's a good idea to establish titles for your help topics. The title you specify for a topic is displayed in the WinHelp History list, the Search dialog box, and the Bookmark menu. Usually, the title you specify will match the text on the topic's first line. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 360 You should format the first line of the topic as you would any other type of title, whether it's a screen title, a paragraph title, or something similar. Use a larger point size and boldface the title text to help make it stand out. Entering Topic Keywords Now that you have a context string and a title for the topic, let's define some keywords for it. Insert a K custom footnote just prior to the word Log and set its text to Log a call; Calls, logging; Calls, entering; Calls, adding. These entries will appear in the help file's index, and they will help your users more easily locate topics of interest. Insert a page break following your topic definition. All topic definitions end with a page break (press Ctrl +Enter in Microsoft Word). Figure 13.5 illustrates the completed topic. Notice that the spaces that Word places after its footnotes have been removed. I've done this because I've seen extraneous spaces confuse the help compiler. They serve no real purpose, so I recommend that you omit them from your help topics as well. Figure 13.5. The completed LogCall topic.

Now that the LogCall topic is defined, you're ready to define one for the Property topic on the Tables menu and the Work Orders topic on the Reports menu. Let's begin with the Property topic. Key the following help text for the Property menu item: Property Select this option to enter or update property records. If you are needing  to log a maintenance call, see the Log calls help topic.

Page 361 Insert footnotes for the topic's context string, title, and keywords. Use the same sort of keywords you used for the LogCall topic. Don't forget to format the topic's first line appropriately. It needs to be consistent with the LogCall topic's first line. Linking Two Topics via a Hotspot After the topic is set up, you're ready to link it with the LogCall topic. Position the mouse to the immediate right (no space) of the Log calls string in the preceding text. Type LogCall, then select LogCall with the mouse and hide its text by setting its Hidden attribute to True (use the Format | Font menu item in Word 7.0). Next, select the Log Calls string and set its double-underline attribute to True (again with Format | Font in Word 7.0). This will transform the Log calls string in the preceding text into a hotspot, causing the familiar green underline to appear under it when displayed by WinHelp. When the user clicks the hotspot, the LogCall topic will be displayed, as would be expected. NOTE Although hotspots are usually identifiable via their green underline attributes, this isn't always the case. A developer can turn off both the special coloring and the underline attribute. To see all the hotspots on a given help screen, press and hold Ctrl+Tab. You should then see all the hotspots displayed in reverse video.

Insert a page break following your topic definition. All topic definitions end with a page break. Figure 13.6 illustrates the completed topic. Figure 13.6. The completed Calls topic.

Page 362 The last of the three topics is the Work Orders topic on the Reports menu. Key the following text for it: Work Order Print This option allows you to select and print a work order. A work order lists  the work to be done on a rental property due to a call from a  tenant. See the Property and Log Call topics for more information. Set up this topic using the three basic footnotes as you've done with the other two topics. After you've done that, you're ready to establish some links between the topics.

Begin by unlinking the Property topic; the hotspots you specify on this topic won't jump to other help topics. On the contrary, they'll display linked topics in pop-up windows. The only difference between setting up a hotspot to jump to another topic and setting one up to display the topic in a pop-up window is that you use a single underline, rather than a double underline, to underscore the hotspot. Position the I-beam to the immediate right (no space) of the word Property. Key in the word Property a second time, then select it with the mouse and hide it as you did in the preceding example. Select the original Property and underline it (Ctrl+U in Microsoft Word). After you've done this, move to the immediate right of the Log Call string in the preceding text. Type LogCall, highlight it, then set its Hidden attribute to True. Finish up by selecting Log Calls with the mouse and underlining it. Figure 13.7 illustrates the finished topic. Figure 13.7. The finished Work Order Print help topic.

After you've completed the Work Order Print topic, you're ready to move on to the Contents topic. Page 363 The Contents Topic Even though you'll later define a separate contents file, you still need a Contents topic in your help file. This topic will consist of nothing but links to other topics. It will consist of a whole page of hotspots that jump to other pages in the help file. The Contents topic is assumed by the help compiler to be the first topic in your help-text file, so move to the top of the file and insert a page break before the LogCall topic. Type the following text for the Contents topic: RENTMAN System Contents Log a call Add or update property Print work orders After you've done this, set up the three basic footnotes for the topic, using the word Contents as their key. When you have the footnotes completed, you're ready to move on to setting up the links with the other topics. These should be full-blown hotspots that actually jump to the other topics. Position the I-beam to the immediate right of the Log a call string in the preceding text and type LogCall.

Select LogCall and hide it, as you did in the previous example. Next, select Log a call and double-underline it. Next, position the I-beam to the immediate right of the Add or update property string in the preceding text and type Property. Follow this by selecting Property and hiding it. Finish up by selecting Add or update property and double-underlining it. Repeat this process for Print work orders, substituting WorkOrderPrint for the hotspot's key. Figure 13.8 illustrates the completed Contents topic. There's one last thing you should do before moving on to the help project and help contents file. You need to set up a browse sequence so that you can move sequentially through the topics in your help file. Inside WinHelp, this is done via the Browse buttons, which you'll enable in the Help Workshop program. Although you can define as many browse sequences as you want per help file, we'll keep things simple and define a single one for the entire file. Position the I-beam to the immediate left of your Contents topic and insert a + custom footnote. Type auto for the footnote's text. Repeat this process for each of the remaining three topics. The auto specification tells the help compiler to number your topics sequentially for browsing. Although you can also specify a fixed sequence number for each topic, using auto is the most flexible way to do things because it enables you to insert additional topics without having to rearrange the browse sequence. After you've finished setting up your browse sequence, you're done with the help-text file. Save it (make sure you save it in Rich Text Format, not in your word processor's native format) to your RENTMAN directory as rentman.rtf. You're now ready to move on to the help-project and help-contents files. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 364 Figure 13.8. The completed Contents dialog box.

Creating Your Help-Contents File Prior to Windows 95, developers had to manually create help-contents files if they didn't have third-party utilities to assist them. This text file and the help project file would then be combined with their corresponding RTF file using Microsoft's DOS-based help compiler. In conjunction with the release of Windows 95, Microsoft released a graphical editor for these two text files called the Microsoft Help Workshop. You can also use Help Workshop to compile and test your help files. Delphi includes this utility; you should find it in your \Program Files\Borland\Delphi 3\Help \Tools directory. Its executable is HCW.EXE. Locate the file on your system and start it. Help Author The first thing you'll need to do is turn on the Help Author option on the File menu. Turning on Help Author affords several benefits. First, it causes additional information about your help system to be displayed (such as the topic number for each help topic) while you're designing it. Second, it enables

you to move back and forth through your help file (regardless of whether you have enabled the Browse buttons) using Ctrl+Shift+Left Arrow and Ctrl+Shift +Right Arrow. You can also move to the beginning and end of your help file using Ctrl+Shift+Home and Ctrl+Shift+End. Next, click the New option on the File menu and double-click Help Contents. Key .\rentman.hlp into the Default filename (and window) entry box. Type RENTMAN System in the Default title box. Next, click the Add Above button and key RENTMAN System Contents Page 365 into the Title box of the ensuing dialog and key Contents into its Topic ID entry box. Click OK to save your new entry. Next, click the Add Below button and key Log a call into the Title box of the Edit Contents Tab Entry dialog. Key LogCall into its Topic ID box, then click OK. Repeat the process for the remaining two topics in your help file. After you've added your last topic to the list, you're ready to save your contents file. Save it to your RENTMAN directory as rentman.cnt. Listing 13.1 shows what the file looks like in textual form. Listing 13.1. The help-context file as generated by Help Workshop. :Base .\rentman.hlp :Title RENTMAN System 1 RENTMAN System Contents=Contents 1 Log a call=LogCall 1 Property=Property 1 Print a work order=WorkOrderPrint Creating Your Help-Project File Now that your help-contents file is created, you're ready to move on to the helpproject file. Once again, the help-project file is actually a text file that Help Workshop manages for you. After you've set it up correctly, you can also use Help Workshop to compile your project. NOTE

Even though both the contents file and the project file are text files that you could edit with an external editor, you shouldn't do this because Help Workshop manages these files for you. Editing them yourself could confuse it. Help Workshop itself warns against doing this, so I suggest you avoid it.

Select New from the File menu and double-click Help Project. The first thing Help Workshop does is prompt you for a filename for the project. Change to your RENTMAN directory and type rentman for the project's name, then click the Save button. Help Workshop will append the magical HPJ extension to the file for you. Adding Your RTF and CNT Files to the Project Click the Options button, specify Contents as the Default topic and RENTMAN System as the Help title, then click the Compression tab and specify Maximum. Next, click the Files tab and specify your rentman.rtf file in the Rich Text Format (RTF) files entry, and your contents file, rentman.cnt, in the Contents file box. After you've done this, save your Options settings by clicking OK to exit the dialog. Page 366 Adding Browse Buttons to the Project Next, click the Windows button, click Add in the ensuing dialog, and type main in the Add New Window Type dialog. Click OK to save your new window type. After you're back in the Window Properties dialog, click the Buttons tab and place a checkmark in the Browse Buttons checkbox. This will enable WinHelp's Browse buttons so that you can navigate your help file using the browse sequence you defined earlier. If you now click the Macros tab of the same dialog, you'll see that Help Workshop has added the BrowseButtons() macro to your help project's global-macros list. This is what actually enables the buttons. Click OK to save your changes. Mapping Context Strings to Help-Context Numbers After returning to the Help Workshop main screen, click the Map button. Next, click Add, key Contents into the Topic ID box and 0 into the Mapped numericvalue box. This will cause elements in the RENTMAN application, for which no help has been set up, to display the help contents when the user requests

context-sensitive help. Click OK to save your mapping. Next, click Add again and, this time, add the LogCall topic ID with a numeric value of 100. It's a good idea to space out your help topics so that you can add new ones in between them if the need arises. Add the Property topic ID with a numeric value of 200 and the WorkOrderPrint ID with a value of 300. These numbers are the ones you'll use inside the RENTMAN application to link program elements with the help system. You'll key these numbers into the HelpContext property of the appropriate components. When you get the mappings keyed in, click OK to save them. After you've defined your topic ID mappings, you're ready to save the help project and compile it. Click the Save and Compile button in the lower-left corner of the screen. As you can see, the help file we've defined compiled smoothly and is now ready to be tested. Testing Your Help File You can easily test your help file without ever leaving Help Workshop. Select the Run WinHelp option on the File menu (or click the question mark on Help Workshop's toolbar). You should see the View Help File dialog. Select Contents in the Mapped Topic IDs drop-down list, then click OK. This dropdown list enables you to simulate an application program passing a help context ID to WinHelp. You could select any of the other available mappings to test them, as well. Click the View Help button to open your newly created help file. You should see the Contents topic from your new file as illustrated in Figure 13.9. Click the Log a call topic on the Contents page. Figure 13.10 shows what you should see. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 379

CHAPTER 14

RENTMAN Postpartum
Page 380 Hopefully, you've completed the RENTMAN app as it was originally laid out in the earlier chapters of this tutorial. At this point, the analysis, design, and construction phases should be mostly behind you. That said, what's next? According to the original five-phase model that was presented, the testing and deployment phases are all that remain of the RENTMAN development process. We'll explore these phases as well as discuss the many post-development concerns that come with client/server applications development. Let's begin the discussion by first talking about these issues in general, then we'll explore how they apply specifically to RENTMAN. Let's start with the testing phase.

Testing the App
There are several ways to approach application testing and the quality assurance process. These subjects are books unto themselves. Nevertheless, we can still come up with a few general guidelines that apply regardless of the approach taken. Let's begin with a few tips that relate to testing the app for suitability:

q

q

Ask yourself whether the application fulfills the statement of purpose as it was originally defined. Beyond merely finding software bugs, complete application testing requires that you look at the big picture—to know what the app is supposed to do and compare that with what it actually does. Check the major features that ended up being included in the application against those you originally charted for it. Narrow the focus a bit from the broad purpose of the application to those key functions it's supposed to perform. Ask yourself, "Does it perform them? Does it do what it's supposed to? How would I rate its capability to perform each key function separately?" In addition to ferreting out glitches in the software, thoroughly testing an app requires you to compare the app with its original specification.

After an app has passed your suitability checks, it's ready to be put through the paces as far as actual testing is concerned. You'll want to conduct both internal and external testing. Internal is you and your development group. External is people who work with you but are not otherwise involved with the project, people in beta test groups, and other interested parties outside the company. Here are some tips for internal testing:
q

q

q q q

Check the app's database constraints to be sure that they function properly. Check server-side objects such as stored procedures and views to ensure that they do what they're supposed to. Conduct multi-user testing. Verify that access rights you've defined properly control access. Test your app on a machine that mimics your users' machines as much as possible.

Page 381 Here are a few more tips for external testing:
q q q q q q

Establish a formal procedure for reporting and responding to bugs. Set up a facility for easily distributing bug-fixes and updates. Have technical people not affiliated with the project test the app. Establish a small beta test group of key users. Conduct usability testing with users of different skill levels. Remember that the user has the final word on whether the application meets his or her needs. Dealing with users isn't a part of the business; it

is the business. I can think of a number of ways that these testing tips—specifically those relating to internal testing—could be applied to the RENTMAN app. For obvious reasons, in-depth coverage of external testing applications is outside the scope of this book. First and foremost, does the app meet its original statement of purpose? Here, from Chapter 8, "Your First Real Client/Server Database Application," is RENTMAN's statement of purpose: The RENTMAN system will facilitate rental property management. Does the system do this? At least on the surface, it appears to. Only Allodium can really say whether it does so sufficiently. Now let's move on to the app's key functions. Again from Chapter 8, here are RENTMAN's key functions:
q q q q

It will log and maintain property leases. It will track ongoing property maintenance work. It will generate tenant billing information. It will provide historical information on individual properties.

Again, at least on the surface, RENTMAN seems to comply with the original design. Only Allodium can say for certain. Now that we're past the major hurdles, how do we go about actually testing the app? Let's take the internal test tips one at a time and see how they might be applied to RENTMAN. Check the App's Database Constraints Rather than simply assuming that the entity and relational integrity constraints you put in place work as they should, you need to test them; you need to try to violate them. For example, with RENTMAN, we could attempt these constraint violations:
q q

Delete tenants that have leases on file in the LEASE table. Specify a value for the PROPERTY table's City column other than Richardson, Dallas, Plano, Norman, Oklahoma City, and Edmond.

Page 382

q

q

q q

Delete a row from the PROPERTY table that has leases that reference it in the LEASE table. Omit the Deposit column when inserting data into the PROPERTY table. Supply an invalid date for the StartDate column of the WORDER table. Add a rental property to the PROPERTY table that has six bedrooms.

I could list a lot more of these, but I think you get the picture. The idea is to test the bounds we've created for the app via constraints on the database. Check Server-Side Objects If you've implemented your app using any stored procedures, views, or other server-side objects, these need to be tested. Even though it's obvious that the client-side of the system must be checked, client/server developers often neglect or minimize the importance of server-side objects. SQL is code just as is Object Pascal or C++. Some examples of some things to try are
q

q

If you've built end-user reports that are driven by stored procedures, run the procedures outside your app or report writer to ensure that they work as you expect. Although you could certainly test them from inside your app, executing them directly allows you to see server messages and other information that may be stripped away by your app or database access software. If you're performing DML operations using stored procedures, attempt data modifications that your database or your stored procedures are supposed to prohibit. For example, if a stored procedure you use to add rows to a given table is supposed to validate the column values passed into it, supply invalid values and see if they're all caught. By placing key coding elements such as DML operations into stored procedures, you assume responsibility for making sure they behave themselves. Particularly with DML procedures, this isn't a responsibility that you want to take too lightly.

Conduct Multi-User Testing There are essentially two major things to look out for when you implement systems with lots of users. The first big issue is concurrency. A large number of users trying to access the same database resources can cause all sorts of problems, most of which have to do with locks and blocks. Users can block one another from accessing resources they need. Processes can block each other

from executing. All kinds of things can happen that are difficult to plan for and beyond what new client/server developers usually expect. The second major challenge with multi-user systems is the resource requirements that users bring with them. A system that performs well with 10 users may fall flat on its face with 20. Each additional user connection carries with it some type of overhead, even if those users aren't actually locking resources or blocking connections. There are physical limits on how much data a server can process, how quickly its network card can send and receive that data, and how much traffic the network can bear. Having lots of users on a system stresses these limits. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 378 Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 374 TIP To view some of Delphi's "Easter eggs," hold down the Alt key and type the following secret codes: DEVELOPERS, TEAM, and QUALITY. You should then see a list of some of the people who worked hard to bring you Delphi.

Return to Delphi and create a new form by inheriting from the About box form in the Object Repository. Name the form fmRABTBOX0 and set its Caption to About RENTMAN. Double-click the form and change its FormCreate event handler to look like the code in Listing 13.3. Listing 13.3. Your About box's FormCreate event handler assigns values to its crucial components. procedure TfmRABTBOX0.FormCreate(Sender: TObject); var VersionBuffer, VersionNoText, ProductNameText, LegalCopyrightText, CompanyText : String; VersionSize : Integer; Dummy : Integer; begin inherited; VersionSize:=GetFileVersionInfoSize(PChar(Application.ExeName),Dummy); If (VersionSize<>0) then begin SetLength(VersionBuffer,VersionSize); SetLength(VersionNoText,VersionSize); SetLength(ProductNameText,VersionSize); SetLength(LegalCopyrightText,VersionSize); SetLength(CompanyText,VersionSize); If (GetFileVersionInfo(PChar(Application.ExeName), Dummy, VersionSize, ÂPChar(VersionBuffer))) and (VerQueryValue(PChar(VersionBuffer),'\StringFileInfo\040904E4\ProductVersion', ÂPointer(VersionNoText), VersionSize)) and (VerQueryValue(PChar(VersionBuffer),'\StringFileInfo\040904E4\ProductName', ÂPointer(ProductNameText), VersionSize)) and (VerQueryValue(PChar(VersionBuffer),'\StringFileInfo\040904E4\LegalCopyright', ÂPointer(LegalCopyrightText), VersionSize)) and (VerQueryValue(PChar(VersionBuffer),'\StringFileInfo\040904E4\CompanyName', ÂPointer(CompanyText), VersionSize)) then begin Version.Caption:=VersionNoText; ProductName.Caption:=ProductNameText;

Copyright.Caption:=LegalCopyrightText; Comments.Caption:=CompanyText; end; end; end; Similar to the preceding code that displayed RENTMAN's version on its status bar, this code uses the VERSIONINFO resource you defined earlier to retrieve application-specific information. This information is then displayed in RENTMAN's About box. Page 375 To link your new About box with RENTMAN's main form, assign the following code to the About option on fmRSYSMAN0's Help menu: fmRABTBOX0.ShowModal; After you've done this, select Use Unit from Delphi's File menu and add the RABTBOX0 unit to the main form's Uses statement. Next, run your application and click the About option. Figure 13.14 shows the new About box. Figure 13.14. Your new About box uses the version information you set up earlier.

Adding a Form-Print Button
The next item on the wish list is a global form-print button. Were it not for Delphi's visual form inheritance, adding this would not be a trivial chore. Thanks to Delphi's support for creating forms based on other forms, this is a 15-minute task. Return to Delphi now and load the fmDatabaseForm generic form class into the form designer. Drop a BitBtn component about one centimeter to the right of its DBNavigator component. Name the component bbPrintForm and set its caption to Print Form. Set the button's Glyph property to the Print.bmp bitmap file in the Images\Buttons subdirectory under your Delphi directory. Next, double-click the button and key the following line of code: Print; After you've done this, load the fmRWORMDE0 form into the visual designer. You'll notice that it now has two print buttons: one labeled Print and the other labeled Print Form. The form doesn't need both buttons, so you'll want to disable one of them. The one labeled Print Form is the Page 376 new kid on the block that you introduced by adding it to the form's ancestor, DatabaseForm. Click the Print Form button on fmRWORMDE0 and set its Visible flag to False. Although you can't delete inherited components, you can make them invisible. Finally, run the RENTMAN application. All your forms now have the ability to print themselves.

Adding Report-Confirmation Dialogs
The next item on the agenda is to set up confirmation dialogs for reports in RENTMAN. Currently, when a report is selected from the menu or its hot key is pressed, the report runs without any type of confirmation. If the user accidentally presses the accelerator key that corresponds to a report-menu item, the report is immediately executed, possibly tying up the machine for a significant amount of time. Although doing things this way made it a little easier to code RENTMAN's reports, professional applications don't behave like this. Any action that could conceivably take a long time to carry out or be potentially destructive should be confirmed. Adding a confirmation dialog to your code is really quite simple. Load the fmRSYSMAN0 form into the visual form designer and click the Task List item on its Reports menu. You should be placed into the code editor inside the item's OnClick eventhandling procedure. Find the beginning of the procedure and type the following If statement: If (MessageDlg(`Print the Task List report?', mtConfirmation, ÂmbYesNoCancel, 0)<> mrYes) then Exit; This line of code simply calls the built-in MessageDlg function and asks whether the report should be printed. The first parameter to MessageDlg provides the prompt that's displayed in the dialog. The second parameter specifies the type of dialog to display—in this case, a confirmation-style dialog. The third parameter specifies the buttons to include in the dialog; here, we've opted to include Yes, No, and Cancel buttons. The final parameter indicates a help-context ID. We'll leave this at zero, for now. You could set it to a help-topic ID if you want. Now, save your application and run it. You can use this same technique to control the printing of any of RENTMAN's reports. Simply place this line of code on the first line of the OnClick event handler for any of the report-menu items and change its prompt to match the report. With this last enhancement, you've completed the items on the wish list that we formulated earlier in the chapter. The changes you've made have smoothed many of RENTMAN's rough edges and will help it to be more intuitive and easier to use. Page 377

Summary
Break out the bottle of champagne—you've just completed your first Delphi client/server database application. Hopefully, you've garnered enough skills from your many travels here that this won't be your last such endeavor. The important thing is to enjoy yourself while you learn Delphi's elegant way of doing things. I think you'll find that Delphi simplifies the process of application development so much that you can concentrate more on what your application is supposed to do—and less on exactly how to do it.

What's Ahead
In the next chapter, the final chapter in Part II, "Tutorial," you'll address a number of post-development concerns for the RENTMAN app. You'll explore what's involved with testing the app and you'll also touch on deploying it. Chapter 14, "RENTMAN Postpartum," delves into a number of the issues you face when you've given birth to a new client/server application. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 371 Application.OnShowHint:=RMShowHintProc; end; I don't recommend you do this; there are other problems presented by redirecting fly-over hints that you'll have to address if you intend for this to be truly functional. For one thing, you'll have to come up with a way to remove a hint from your status bar when it's no longer valid (for instance, if the user has moved off the item). If you leave the default mechanism in place, this is handled for you.

Activating the Status Bar
The next task on the list of user-interface improvements for the RENTMAN application is the activation of the status bar. You'll recall that you added the status bar to the fmRSYSMAN0 form when you first designed it, but it hasn't been used since. There are three elements that the status bar is set up to display: a status message indicating what's going on in the application, the name of the current user, and the current version of the software. These three elements are handy to have on the screen. If a user calls with a problem, you can ask her to tell you which version of the software she's using by reading it from the bottomright corner of the screen. And if you visit a client having a problem with your software, you can look at the application's status bar to determine which username she used to log into the system, in case that's relevant. The first of the three elements is a no-brainer. All you do to display status messages on the status bar is update the bar before and after significant program events. For example, let's update the click-event code for the Log a call item on the File menu. Here's what the code looks like now: procedure TfmRSYSMAN0.Logacall1Click(Sender: TObject); begin inherited; dmRENTMAN.taCALL.Insert; fmRCALEDT0.Show; end; All you need to do to enable a status message to display when this item is selected is surround the code with updates to the status message area of the stRENTMAN component. Earlier, we designated this as the leftmost of the component's three areas, so the code would look like this: procedure TfmRSYSMAN0.Logacall1Click(Sender: TObject); begin inherited; try stRENTMAN.Panels.Items[0].Text:='Add a maintenance call';

dmRENTMAN.taCALL.Insert; fmRCALEDT0.Show; finally stRENTMAN.Panels.Items[0].Text:=''; end; end; Page 372 Adding the current username is equally simple. You can retrieve this information from Windows with a single API call. Double-click the main form's OnShow event in the Object Inspector and type in this code: MaxUserName:=30; SetLength(UserName,MaxUserName); GetUserName(PChar(UserName), MaxUserName); SetLength(UserName,Pred(MaxUserName)); stRENTMAN.Panels.Items[1].Text:='User: `+UserName; Make sure the following lines are in the RSYSMAN0 unit header: UserName : String; MaxUserName : Integer; This routine will call the Windows GetUserName function to retrieve the current username. The username returned by the function will then be displayed on the status bar. The third and final element that needs to be displayed on the status bar is the program's version number. Delphi has direct support for the Windows VERSIONINFO resource type, so you can configure the version number reported by your software from inside Delphi. As Figure 13.13 illustrates, you can specify the various elements of the VERSIONINFO resource from Delphi's Project Options dialog. Figure 13.13. Delphi provides direct support for Windows VERSIONINFO resources.

Now let's add code to the application to extract this information from itself and display it on the status line. Locate fmRSYSMAN0's OnCreate event in the Object Inspector and double-click it. You should see the code you added earlier to display the username on the status bar. Listing 13.2 shows the lines you need to add to what you see in the code editor. Listing 13.2. Code to extract the current application's version information. VersionSize:=GetFileVersionInfoSize(PChar(Application.ExeName),Dummy); If (VersionSize<>0) then begin SetLength(VersionBuffer,VersionSize); SetLength(Version,VersionSize);

Page 373 If (GetFileVersionInfo(PChar(Application.ExeName), Dummy, ÂVersionSize, PChar(VersionBuffer))) and (VerQueryValue(PChar(VersionBuffer),'\StringFileInfo\040904E4\ProductVersion', ÂPointer(Version), VersionSize)) then stRENTMAN.Panels.Items[2].Text:='Version: `+Version; end; Be sure to add the following lines to the FormCreate method's header so that it will compile successfully: var VersionBuffer, Version : String; VersionSize : Integer; Dummy : Integer; The code in Listing 13.2 uses three Windows API calls—GetFileVersionInfoSize, GetFileVersionInfo, and VerQueryValue—to return the application's version information. After this information is retrieved, it's displayed on the status bar. NOTE Note that GetFileVersionInfoSize and GetFileVersionInfo have a peculiar requirement: The PChar data passed into these routines must not reside in a read-only block of memory. Passing in a constant will produce an Access violation (string constants are stored in the read-only code section of the executable). Apparently, these functions modify the input data while locating the indicated resource.

To illustrate how external programs utilize the VERSIONINFO resource in your applications, close down your app and start the Windows Explorer. Locate your RENTMAN directory using the Explorer, right-click RENTMAN.EXE, and select Properties from the pop-up menu. As you can see, besides the General tab present for all files, you have an additional tab named Version. Click it, and you should see the version information you specified in Delphi. The ability to create VERSIONINFO resources from within the IDE is yet another example of Delphi's comprehensive support for the Windows API. No corner of the Windows API can escape the Delphi searchlight.

Adding an About Box
The next task on our wish list is to add an About box to the application. All good Windows apps have About boxes. An About box provides the user with critical information such as the program's author, its current version, technical-support numbers, and so on. Sometimes rogue developers even embed hidden messages or bitmaps (commonly known as "Easter eggs") in their About boxes. The About box in Delphi 1.0, for example, has a hidden picture of Anders Hejlsberg, Delphi's principle architect. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 383 The best way to anticipate these kinds of things is to attempt to construct worstcase scenarios in the testing you do. That is, if you're fairly sure that the user count will never exceed 200 users, try to test the app with 400 connections. Even if you know that a given object will never be accessed by more than one user at a time, try it with several. No one will complain if the application performs above expectations. It's crucial to sound out the capacity limits of the systems you build. Attempt to derive a per user resource metric to assist in capacity planning. All these things add up to one central truth: More users equals more work. Concurrency Control and the Client/Server Model Unlike local databases, you don't usually access SQL server data in a record-byrecord fashion. Some servers don't even lock individual rows. Instead, some larger object, often a page or a segment, is locked to prevent changes one user makes from interfering with those of another. If enough locks occur on a single table, the server may escalate the lock to a table-wide lock, preventing other users from making any changes to the table. The situation is quite different from what it is with local tables. You must be cognizant of the way your DBMS platform implements concurrency control. You should do what you can to minimize lock contention. In particular, you'll want to avoid row-by-row processing at all costs. For example, here's some code to traverse the LEASE table, switching the LawnService column to False in the process:

With taLEASE do While not EOF do begin If taLEASEPetDeposit.AsFloat<>0 then taLEASELawnService.AsBoolean:=False; Next; end; Although this would be standard fare on local tables, it creates problems on database servers. It's also inefficient. Why? First, it creates a lock on the LEASE table every time it sets the LawnService field. This could block out other users momentarily and is a relatively slow way of doing things. Second, if enough rows are locked, it's possible that the server will upgrade the page or row locks on the server to a full-blown table lock, putting all other user changes on hold until the lock is released. This isn't the way you want to do things. You should use SQL to handle mass updates like this. Either a stored procedure or a plain query will do, but the point is this: Avoid processing rows individually. Let the server do that; it's best at it. For example, the following SQL query will accomplish the same thing as the looping code: UPDATE LEASE SET LawnService="F" WHERE PetDeposit<>0 AND LawnService<>'F' It accomplishes the task in one pass and releases the lock(s) as soon as possible. Page 384 Transactions and Database Servers You may recall that a transaction is a group of database changes treated as a single batch. Either all the changes are applied to the database, or none of them are. None of this is magical; it's implemented through transaction/redo logs, which must be taken into account and managed. Sizing the transaction log properly, being careful not to overrun it, committing and rolling back transactions effectively, and so on, are real issues that client/server developers have to deal with. Delphi enables you to perform server-based transaction control via the TDatabase component. To begin a transaction, call TDatabase's

StartTransaction method. To save the transaction to the database, call the Commit method. To cancel it, call Rollback. Calling these methods in a client/ server environment is the same as issuing the equivalent SQL on the server. For example, calling dbRENTMAN's StartTransaction method to start a transaction on the RENTMAN database is the same thing as issuing InterBase's SET TRANSACTION SQL command. Transaction Isolation Levels Another important area of transaction management is the use of transaction isolation levels to avoid lost data and lock conflicts. A transaction isolation level (TIL) affects your transaction's capability to see changes made by other transactions concurrent with it, and their capability to see changes made by your transaction. You make use of transaction isolation levels via the TransIsolation property of the TDatabase component. TransIsolation can have one of three values: tiDirtyRead, tiReadCommitted, and tiRepeatableRead. tiDirtyRead returns any row changes that have been made—by this transaction or by others—even those that have not yet been committed. tiReadCommitted returns only row changes that have been committed. tiRepeatableRead returns rows as they originally appeared when the transaction was initiated. Rows appear unchanged for the duration of the transaction, even if another transaction commits row changes to the underlying tables while the transaction is in progress. The possibility for lock conflicts between two transactions accessing the same database objects is affected by each transaction's isolation level. As a rule, the tiRepeatableRead and tiReadCommitted TILs reduce the possibility of lock conflicts the most. For example, say that you have two transactions, Tran1 and Tran2. If Tran1 is a tiReadCommitted transaction with read/write access, and Tran2 is a tiRepeatableRead transaction with read/write access, the two transactions only conflict when attempting to modify the same row. This reduces lock contention on the server and also provides a degree of security against lost updates. tiReadCommitted is the default transaction isolation level and should be adequate for most of your needs. Page 385 NOTE

Not all server platforms support all three transaction isolation levels. If your server does not support a particular isolation level, the BDE will choose the next most restrictive one that it does support. See Chapter 23, "Concurrency Control," for more information.

TIP Most database servers enable you to monitor the locks they maintain on database objects. For example, you can use the sp_lock stored procedure to view locks on the Sybase and Microsoft platforms, and you can query the V$WAITSTAT table to monitor contention on Oracle. You can monitor locks on the InterBase platform using the Lock Manager Statistics option in the InterBase Server Manager program. Monitoring a server's locking habits can help you tweak your apps so that they minimize lock contention and cooperate with each other as much as possible.

UpdateMode You can use the UpdateMode property of the Table and Query components to affect the type of SQL generated when you update a record. This gives you a certain amount of control over the optimistic concurrency approach taken by your Delphi apps and enables you to balance performance with protection against lost updates. UpdateMode has three possible values: upWhereAll, upWhereChanged, and upWhereKeyOnly. The setting you give this property determines the type of SQL WHERE clause that's used to locate a row for update. When you make a change to a table or live query result set, an SQL UPDATE statement is generated to perform the modification on the server. If UpdateMode is set to upWhereAll, the WHERE clause of this UPDATE statement lists every column in the table or query. If UpdateMode is set to upWhereChanged, only the key fields and fields you've changed are used. If UpdateMode is set to upWhereKeyOnly, the key fields alone are listed in the WHERE clause. The default is upWhereAll, but you may find it too restrictive. In most cases, upWhereChanged is just as safe and can be considerably quicker than

upWhereAll, depending on the number of columns in your table. As a rule, you shouldn't use upWhereKeyOnly without first speaking with your Database Administrator. If you need ultra-quick updates on a table to which you have exclusive access, upWhereKeyOnly may be just the ticket. However, in multiuser environments, upWhereKeyOnly can cause updates by other users to be lost. Again, I recommend you use it only after consulting with your DBA. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 386 You can see the effects of UpdateMode firsthand via Delphi's SQL Monitor tool. SQL Monitor enables you to "look under the hood" of the BDE and see the raw interaction between your app and the database. Among other things, you can see the SQL your app sends to the server. To see how this works, select the SQL Monitor tool from Delphi's Database menu and place a checkmark next to the Always on Top setting on its Options menu. Next, run your application and press F4 to display the fmRPROCGD0 form. Once the form is onscreen, change a value in one of its table rows and click the Post button on the DBNavigator component at the bottom of the form. Now, scroll the top pane of the SQL Monitor window until you see an SQL UPDATE statement and then click it. This is the SQL code your app sent to the server to carry out your table modification. Notice that the UPDATE lists all the fields in the table. Now, close the SQL Monitor and exit your application. Bring up the dmRENTMAN data module in the form designer and change the UpdateMode property of the taPROPERTY component to upWhereChanged. After you've switched UpdateMode, restart the SQL Monitor tool and then run RENTMAN. Make a second change to the PROPERTY table and then scroll the top pane in the SQL Monitor tool so that you can see the UPDATE statement that was generated. Click the UPDATE statement to see it fully displayed in SQL Monitor's lower pane. The number of columns listed in the UPDATE statement's WHERE clause should have dropped substantially. Now, close both your app and the SQL Monitor tool and change taPROPERTY's UpdateMode to upWhereKeyOnly. Next, save your work and then restart SQL Monitor and RENTMAN. Make another change to the PROPERTY table and then view your UPDATE statement in SQL monitor. As you can see, upWhereKeyOnly reduces the columns used to their lowest possible number—to just those needed to

uniquely locate the original record. Be careful with upWhereKeyOnly—the performance boon it affords is at the expense of safety. Because it uses only a table's primary to update the table's rows, changes made by one user could overwrite those of another. Cached Updates One way of potentially minimizing lock contention on your database server is through the use of cached updates. When a DataSet's CachedUpdates property is set to True, changes you make to the DataSet are cached—stored locally—until you save them with ApplyUpdates. Let's experiment with CachedUpdates to see how it works in practice. Load the dmRENTMAN data module into Delphi's visual designer. Click the taTENANT table and set its CachedUpdates property to True. Next, load the fmRSYSMAN0 form and click the Tenant option on its Tables menu. Change the line that reads fmRTENCGD0.Show; to fmRTENCGD0.ShowModal; Page 387 Then add this line after it: dmRENTMAN.taTENANT.ApplyUpdates; This will cause the changes you make in the fmRTENCGD0 form to be stored locally until you return from the form. After you return, they're sent to the server. Obviously, if you can reduce the number of updates you make against the server and its objects, you can reduce the potential for lock conflicts with other users. Changing Queries to Stored Procedures and Views Another way of reducing lock contention and improving concurrency is to replace dynamic SQL queries with stored procedures and views. By changing a query to a stored procedure call, you cause the application to call SQL that's already been compiled; it should execute faster. Also, you trim down the SQL statement that must be sent from the client to the server. Instead of sending a potentially large body of text across the network, you send a tiny call to a stored procedure. In terms of efficiency,

stored procedures are hard to beat. Views enable you to convert simple queries into stored procedures that can be queried like tables. On the InterBase platform, you can define stored procedures that themselves can be queries similarly to tables and views, so the line between stored procedures and views is gray indeed. There are several queries within the RENTMAN application that would make good stored procedures. For example, the SQL in the quWorkDuration Query component could be translated to this stored procedure: SET TERM ^; CREATE PROCEDURE GetWorkDuration (WONum INTEGER) RETURNS (WorkDuration float) AS BEGIN FOR SELECT sum(WORKTYPE.TaskDuration) as WorkDuration FROM WODETAIL, WORKTYPE WHERE WODETAIL.WORK_TYPE_CODE = WORKTYPE. WORK_TYPE_CODE AND WODETAIL.WORK_ORDER_NUMBER=:WONum INTO :WorkDuration DO SUSPEND; END ^ SET TERM ;^ After the stored procedure is defined, you simply replace the Query component with a StoredProc component and modify your program code accordingly. The net result is that you'd have a query that would no doubt run faster and be easier to access from different applications. A good use of views in the RENTMAN application would be to replace the many table-lookup field combinations with straight views. That is, instead of having a WODETAIL table defining a field that looks up information in the WORKTYPE table, you could replace it with a view that performs the join on the server. The view would appear to your application like any other table; however, it would already include the Description and TaskDuration fields from Page 388 the WORKTYPE table, so there'd be no need to access them via lookup fields. This is another example of the different mindset that must be taken when working with database servers rather than local DBMSs such as Paradox and dBASE. TIP

The TLiveQuery component that's profiled in Chapter 27, "Building Your Own Database Components," enables you to easily create serverbased views in a manner similar to setting up a TQuery component. As mentioned previously, using views can speed up your application and help eliminate lookup fields. See Chapter 27 for more information.

Verify User Access Rights A security model needs to be in place before you deploy client/server apps to users. Will system security depend on programming code or reside entirely on the server? Will the rights be command based, object based, or both? You need to be able to ensure that access to sensitive information is restricted, that the system is reasonably safe from intrusion from the outside world, and that your client's data is protected against both intentional and unintentional misdeeds. You can approach security from a number of angles, but the bottom line is that the system must be safe without being cumbersome. Users should be prevented from accessing objects they don't need but should have access to those they do need. This can be a difficult task to take on, especially when you must coordinate your efforts with other people or other systems. When you move from using local tables to a database server, the dynamics of user rights and permissions change significantly. With multi-user Paradox applications, for example, you normally grant rights at the network or server level. Users are granted permissions on operating system files which correspond roughly to database tables. Some local table formats enable you to store all the tables in a database in a single physical file, but your users must still have adequate rights to the file. The permissions granted to users are usually limited to allowing read or read/write access to whole files. For instance, most local DBMS platforms don't enable you to permit a user to write new rows to a table while preventing him from deleting rows from it. The controls at your disposal are very simplistic indeed. That all changes when developing applications for client/server platforms. Access to individual database tables, views, and procedures must be granted by a Database Administrator-type, who also creates user logins, groups, and so on. A full treatment of the subject of database administration is outside the scope of this book, but I should briefly touch on a few issues before proceeding. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 389 Granting Rights with GRANT GRANT is the SQL command used to grant user permissions. REVOKE is the command used to remove them. Both GRANT and REVOKE require three parameters: the permissions you're granting or revoking, the database object on which you're granting or revoking them, and the user whose rights you're changing. For example, you could use the following SQL command to grant permissions on RENTMAN's WORKTYPE table: GRANT ALL ON WORKTYPE TO PUBLIC This grants all table rights (SELECT, INSERT, UPDATE, DELETE, and REFERENCES) to all users on the WORKTYPE table. Similarly, you could revoke those rights using REVOKE ALL ON WORKTYPE FROM PUBLIC TIP When upsizing from local tables to a client/server platform, it can save time to grant and revoke user rights en masse. You can build SQL scripts that utilize GRANT ALL ON tablename TO PUBLIC to grant user permissions to all your users at once. You can always go back after the fact and refine these access rights as needed.

You can grant more specific permissions by replacing ALL with a list of permissions, like so: GRANT SELECT, INSERT, UPDATE ON EMPLOYEE TO PUBLIC

You can also revoke those same permissions by using a statement like this: REVOKE SELECT, INSERT, UPDATE ON EMPLOYEE FROM PUBLIC Note that you can replace PUBLIC with a list of users for whom to grant or revoke rights. Before you can do that, though, you'll need to create user logins. Unlike most client/server platforms, InterBase doesn't support user groups (other than the predefined PUBLIC group, of course), and, unlike the Sybase and Microsoft platforms, server logins and database users are not separate from one another. Adding a user login gives the user the ability to connect to any database. To set up access for the RENTMAN app, create user logins using the InterBase Server Manager. To do this, start the Server Manager from the InterBase program folder. Next, log in to the server by selecting File | Server Login. After you're logged in to the server, connect to a database by selecting the Database Connect option on the File menu. After you've connected to your database, select the User Security option on the Task menu. The User Security option enables you to add user logins. You should then see the InterBase Security dialog. Click the Add User button to add a user login. Key in a username and password of your choice. Note that, try as you may, you can't enter a username in lowercase Page 390 letters; they're forced to uppercase. Despite this, the password you key in is case sensitive. Not only are you allowed to lowercase characters, but they're also distinguished by the server from their uppercase counterparts. When you've finished adding your user information, click OK to add the login to the server. You can now connect to any InterBase database using that new username and password, though the user will have to be granted permissions for specific objects in order to access them. As mentioned previously, you grant these rights using the GRANT command. For example, to grant your new user rights to the TENANT table, you might enter the command GRANT ALL ON TENANT TO MARY You could repeat this command for each table, view, and stored procedure in the database. Note that you can extend the ability to grant user permissions to another user via the WITH GRANT OPTION clause of the GRANT command. Here's an example:

GRANT EXECUTE ON PROCEDURE LISTPROP TO MARY WITH GRANT OPTION This command gives user MARY the ability to execute the stored procedure herself and to grant other users access rights to it using the GRANT command.

Deploying the App
Application deployment is covered in detail in Chapter 29, "Deploying Your Applications." However, here are a few ideas to get you started:
q

q

q

q

As mentioned previously, set up a test machine that emulates the target user environment and install your application there before installing it anywhere else. Seek to impact the user's machine as little as possible. Avoid modifying any more configuration parameters than absolutely necessary. Size up your client's site to determine whether additional hardware or software needs to be installed before your application can be. For example, if you intend to communicate with a client/server DBMS over TCP/IP, you'll need TCP/IP protocol support on your clients' machines. You may need to contact your network administrator for assistance with this. Create an installation program using Delphi's InstallShield Express tool to handle installing your application and its supporting files onto client machines. Chapter 29 discusses InstallShield in depth.

Page 391

Summary
Client/server apps, thanks to their complex nature and dynamic work environment, are affected by a number of factors that are actually external to the apps themselves. Whereas you might be able to write a local DBMS app that lives in its own world, that's rarely the case with client/server apps. Long after the development process has ended, client/server apps continue to interact with the outside world and require continual support. You aren't "done" when you finish building a client/server application—your work is just beginning.

What's Ahead
Chapter 15 covers issues specific to using Delphi with Microsoft SQL Server databases. Issues such as connecting in the first place, administration hints, query tuning hints, and so on, are covered in depth as they relate to Delphi and MS SQL. Page 392

Page 393

PART III

Reference
Page 394 Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 395

CHAPTER 15

Delphi on Microsoft SQL Server
Page 396 In this chapter, we'll explore the Microsoft SQL Server RDBMS. I'll discuss issues specific to using SQL Server and Delphi together as well as touch on SQL Server's own SQL dialect.

Starting the Server
You use the SQL Services Manager to start and stop SQL Server. You'll find this facility in your SQL Server program group or folder. You can also use the Services applet in the Windows NT Control Panel to start and stop SQL Server. Since SQL Server runs as a Windows NT service, you can start and stop it via the Services option in the Windows NT Server Manager app as well. In addition to managing SQL Server as a Windows NT service, you can also use the Transact-SQL SHUTDOWN command to stop an SQL Server when necessary. SHUTDOWN offers a NOWAIT option to stop a server immediately. Normally, the command waits for running processes to finish before stopping the server.

Getting Connected
Many of the issues that arise when working with or connecting to Microsoft SQL Server apply to Sybase SQL Server as well. Until a couple of years ago, Microsoft licensed Sybase's SQL Server code and simply repackaged a port of it. Beginning with Microsoft SQL Server 6.0, the two product lines diverged, and the two companies parted ways as well. Because Microsoft SQL Server is inexpensive and runs under Windows NT, you may find yourself developing applications where the client and the server are the same machine. SQL Server's low price has had the effect of positioning it as an entry-level solution, and many small companies have adopted it for single-machine applications. If this is your situation, be very careful to make your application as bulletproof as possible. If you manage to crash NT, something that's not easily done, you're not only taking down the client, you're also taking down the server, possibly corrupting its data in the process. This admonition holds true no matter what platform or what SQL Server you're using. If your client and server are on the same machine, be very careful. Despite being dangerous, running your Delphi applications on the same machine as your SQL Server has the benefit of making database connections easier to configure. For example, rather than having to know or use the machine's actual TCP/IP address, you can use the TCP loop-back address, 127.0.0.1, instead. Setting Up Client Connections You configure client-side connections for SQL Server using the SQL Client Configuration Utility. You should be able to locate it in your Microsoft SQL Server program folder. To configure a new client connection, follow these steps: Page 397 1. Start the SQL Client Configuration Utility. 2. Click the Advanced tab. 3. Type the name of your server in the Server entry box. This could actually be any name—the name itself isn't important—but you should stick with the server's actual name, as defined on the server computer. This makes the rest of the connection definition easier. 4. Under Net Library Configuration choose a default network to use. This will probably be either Named Pipes or TCP/IP, although others are

available. 5. If you selected Named Pipes as your connection protocol and used the server's actual computer name in the Server entry box, you're done. Click the Add/Modify button to add your new connection alias; then click the Done button. 6. If you're using a protocol other than Named Pipes, you need to specify additional connection information. The information required varies based on the protocol you're using. For example, when using TCP/IP, you need two things: the IP address of the server computer and the port number on which it's listening. This information is usually separated by commas. Supply the pertinent connection information; then click the Add/Modify button followed by the Done button. You should now have a client-side connection alias you can use from your Delphi apps to access your SQL Server. WARNING Be sure that you use the correct version of the SQL Client Configuration utility when you set up your connection alias. 16bit clients such as Windows 3.x require the 16-bit version of the connection libraries. On 32-bit clients, you need the 32-bit version. Some third-party administrative tools (for example, DB Artisan) are 16-bit apps even though they will run under Win32. These tools require the 16-bit version of the client connection libraries to be installed, even on 32-bit operating systems.

Troubleshooting Microsoft SQL Server Connection Problems If you have problems connecting to a Microsoft SQL Server, try the following: 1. Use Microsoft's ISQL/w utility to connect to your server. If you're able to connect with ISQL/w, but not from your Delphi applications, you probably have a problem with your BDE alias configuration. Return to the BDE Administrator program and examine the parameters you specified (particularly the server name) to ensure that they're correct. 2. If ISQL/w fails to connect, you probably have a protocol problem. If you're using TCP/IP to connect to the server, attempt to ping the host computer using the PING Page 398

3.

4.

5.

6.

7.

utility that accompanies both Windows 95 and Windows NT. Try both the machine's name, as it appears in the HOSTS file, and its IP address. If you're using named pipes to connect to the server, try the net view \ \servername command, where servername is the name of the NT server on which the SQL server is running. If net view succeeds, try netuse \ \servername\IPC$, replacing servername with the name of your server. If both of these commands succeed, try the makepipe/readpipe facility. Run makepipe on the server, then run readpipe /Sservername / Dteststring from a client machine. The makepipe/readpipe facility tests the integrity of the named pipes services. Press Ctrl+C or Ctrl+Break to terminate makepipe. If any of these tests fail, get with your NT system administrator. You might have problems with the named pipes services on the server. If the machine pings appropriately with the IP address, but not with its host name, you may have a problem with your HOSTS file. You can fix the problem temporarily by changing your connection string to use the IP address rather than the host name. You should resolve the HOSTS file problem as soon as possible because this is a more flexible way to connect to remote machines. If you're using IPX to connect with your server, check to see whether the server is visible to your client machine. You can do this by running isql /L from the OS prompt. isql /L calls dbserverenum, which lists all available servers from the Bindery. If your server is not in the list, you probably have a network problem. Consult with your network administrator for further assistance. If you're using TCP/IP and you have no success when trying to ping the host machine using either its host name or its IP address, you probably have a network problem. Be sure to check obvious things such as whether the IP address you have for the server is correct. Try pinging the TCP/IP loop-back address, 127.0.0.1, to see whether your TCP/IP stack is working properly. This address loops back to your machine, enabling you to ping yourself. If it fails, you have a serious problem with your protocol configuration. You may need to consult your network administrator for help with your network connection. If ISQL/w fails to connect, but PING works fine, check to see that the port number you specified in your connection string is the one on which the database server is listening. You can consult with your database administrator to determine what port(s) your host machine is listening on for connections. If everything seems to be configured properly, you might try switching protocols if that's an option for you. Because the server can be configured to listen on multiple protocols simultaneously, you can configure it to listen on a protocol that works until you resolve problems

with ones that don't. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 399 TIP If you're running Windows NT, you'll find the Windows NT Diagnostics program to be an indispensable troubleshooting utility. It's extremely useful for getting a bird's-eye view of your machine. You can find it in the Administrative Tools folder. It's really just a Windows version of the popular MSD (Microsoft Diagnostics) DOS app that was shipped with various versions of DOS and Windows for several years. The executable itself is named WINMSD.EXE and lives in the \WINNT\SYSTEM32 directory.

Setting Up a BDE Alias Once you're able to connect to your database server, you're ready to build a BDE alias so that your Delphi applications can access it. This subject is covered elsewhere in this book, but let's review it here for your convenience and to establish the relationship between configuring your database driver software and setting up a BDE alias. You can create BDE aliases using either the BDE Administrator utility or Delphi's Database Explorer. The following instructions use the BDE Administrator program. 1. Start the BDE Administrator, right-click the Databases tab, and select New from the menu. When the New Database Alias dialog box comes up, select MSSQL in the Alias type drop-down list, and then click OK.

2. Once the new alias is added, type a name for it. 3. After you've named the alias, configure it by setting the parameters on the Definition page. 4. Click the SERVER NAME parameter and set it to the name of your SQL Server. This will be the same name you used in the SQL Client Configuration Utility. 5. Optionally, you can also set the USER NAME parameter to the name of the server user you want to login as by default. The username you specify is displayed as the default whenever Delphi's built-in login dialog box is shown. Finish by clicking the Apply button to save your changes and exiting the Administrator. Special BDE Alias Settings The following are several BDE alias settings that you might find useful when connecting to Microsoft SQL Server. Although you can begin by accepting the defaults for these settings, it's helpful to know what's available to you in the event you encounter problems. Note that some of these are driver-level settings. You access driver-level settings via the Configuration page in the BDE Administrator. Page 400 DRIVER FLAGS Setting and BLOB Handling BLOB reads and writes can sometimes cause a Microsoft SQL Server to crash. If you run into this, try changing your alias's DRIVER FLAGS parameter from zero to one. This may alleviate the problem, but be aware that it may also cause time-out problems during BLOB writes. APPLICATION NAME You can set the APPLICATION NAME parameter to specify the name that appears in SQL Server's sysprocesses table for your process. This helps distinguish your process from other processes on the server. CONNECT TIMEOUT This controls the amount of time the client will continue to attempt to connect when attaching to an SQL Server. This setting defaults to 60 seconds; you

might find that increasing it improves the capability of client workstations (especially those dispersed across a WAN) to connect. HOST NAME You can set the HOST NAME parameter to specify the workstation name that appears in SQL Server's sysprocesses table. This helps distinguish your connection from other connections in listings such as those generated by the sp_who stored procedure. NATIONAL LANG NAME This setting specifies the language that's used when error messages are displayed. If you leave this blank, SQL Server's default language is used. TDS PACKET SIZE Use this setting to configure the size of Tabular Data Stream (TDS) packets. TDS is the high-level packet protocol that SQL Server uses to exchange data with client connections. Although you can specify packet sizes from 0 to 65,535 in the BDE Administrator, you should limit your choices to the range supported by SQL Server: 512 through 32,767 bytes. You may find that larger packet sizes increase system throughput by lowering the number of packets required to transmit large amounts of data (for example, BLOB fields). The default packet size is 4,096, and you should probably keep it to between 4,096 and 8,192 for the best performance. You can use SQL Server's sp_configure stored procedure to determine the current maximum packet size supported by your server. Execute sp_configure `network packet size' to list the current network packet size. Adjust this parameter before you change its corresponding setting in the BDE Administrator. If TDS PACKET SIZE in the BDE Administrator program and SQL Server's network packet size don't agree with one another, you might experience the following errors: Page 401
q q q

Error: unknown user name or password Server error—4002 Login failed Server error—20014 Login incorrect

NOTE You'll need TDS version 5 or later in order to change the default packet size.

DATABASE NAME Use this option to specify the name of the SQL database to which you want to connect. When people build SQL Server aliases, the normal approach is to set up a separate one for each database they might want to access. If you take this approach, you'll use the DATABASE NAME setting to specify which database you want to access for each alias. Although you could leave DATABASE NAME blank in order to select the user's default database on the server, you shouldn't do this—this ambiguity will likely cause confusion. It will also force you to query the syslogins table on the server to find out which database a given alias references. BLOB EDIT LOGGING You can disable the logging of changes to BLOB fields by switching this parameter to False. Setting this option to False minimizes BLOB storage requirements and improves performance. This option causes BLOBs to be transmitted using SQL Server's bulk copy facility, so you'll need to set select into/bulk copy on in the target database if you plan to use it. You can enable select into/bulk copy using the sp_dboption stored procedure. MAX QUERY TIME The parameter controls the amount of time that SQL Links will wait for an asynchronous query to complete before canceling it. The default query mode is now synchronous for the Microsoft and Sybase SQL Links drivers. Use the TIMEOUT parameter on the MSSQL driver page to specify a synchronous query time-out. Enable asynchronous query execution by incrementing the MSSQL DRIVER FLAG by 2,048. By default, SQL Links will give an asynchronous query five minutes (300 seconds) to finish.

SQL Primer
Although the name of the product is SQL Server, you'll be serving up most of the SQL used with it. SQL Server is a database server, not an SQL server; you just happen to communicate with it using SQL. This section introduces you to

SQL Server's Transact-SQL dialect and Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 402 touches on a few of the features that distinguish it from the SQL implementations of other vendors. I assume that your server is running and that you know how to connect to it using SQL Server's ISQL utility. Creating a Database You may already have a database in which you can create some temporary tables for the purpose of working through the examples in this chapter. If not, creating one is easy enough. In SQL, you create databases using the CREATE DATABASE command. The exact syntax varies from vendor to vendor; here's the basic SQL Server syntax: CREATE DATABASE dbname ON datadevice=size LOG ON logdevice=size Before you can create a database, though, you need logical devices on which you can store it. SQL Server's logical devices allow you to avoid having to refer to physical disk locations in the databases you build. They provide a layer between your disk drives and databases. You use the DISK INIT command to create SQL Server devices. Here's some sample syntax: DISK INIT name='saltrn00', physname='c:\mssql\data\saltrn00.dat', vdevno=5, size=2048

The name parameter specifies the logical handle that you'll use elsewhere to refer to the device. The physname parameter details the physical location and name of the device file. Specify only the drive letter if you're creating the device on a Windows NT raw partition. vdevno is a logical virtual device number. Its only significance is that it must be unique among the devices defined on the server. The range of acceptable values is 1 to 255; 0 is reserved for the master device. size is the number of 2K pages the device is to occupy. For example, if you wanted to create a device that's 4M in size (4,096K), you'd specify a size of 2,048. Data devices and log devices are created using the same syntax. For example, here's a command to create an SQL Server log device: DISK INIT name='sallog00', physname='c:\mssql\data\sallog00.dat', vdevno=6, size=512 Page 403 TIP Building devices using DISK INIT is much faster on NTFS than it is on the FAT file system. On FAT, new device files must be initialized with zeros immediately after creation. This can take a while with large devices. By contrast, device creations on NTFS come back immediately; the initialization step isn't necessary.

Once the devices that are to contain it exist, you're ready to create the database itself. Here's a CREATE DATABASE command that uses the devices we just defined: CREATE DATABASE sales ON saldat00=4 LOG ON sallog00=1 If you don't already have a database where you can store objects for working

through this section, go ahead and create this database now. The USE Command The SQL Server USE command changes the active database context. Simply put, this means that it switches the current database. The command syntax is USE dbname Note that you can also select a database from the DB combo box in SQL Server's ISQL utility to change your current database. If you've created the database in ISQL, click the drop-down list and select <Refresh> to update the list of available databases. When you click the list a second time, you should see your new database. Creating Tables After you've created your database and issued the USE command to make it your active database, you're ready to begin building database objects. Virtually any relational database concept can be demonstrated with a set of three tables. For the purpose of working through this chapter, begin by creating the following three tables. Tables are created using the SQL CREATE TABLE statement. Enter the following command in ISQL to create the CUSTOMER table: CREATE TABLE CUSTOMER ( CustomerNumber LastName FirstName StreetAddress City State Zip ) Page 404 Next, build the SALE table using this command: CREATE TABLE SALE ( SaleNumber int NOT NULL, SaleDate datetime NULL,

int char(30) char(30) char(30) char(20) char(2) char(10)

NOT NULL, NULL, NULL, NULL, NULL, NULL, NULL

CustomerNumber int NOT NULL, ItemNumber int NOT NULL, Amount money ) Now that the SALE table is built, only one table remains. Create the ITEM table using this command: CREATE TABLE ITEM ( ItemNumber int NOT NULL, Description char(30) NULL, Price money NULL ) Adding Columns You use the SQL ALTER TABLE command to add columns to an existing table. Microsoft SQL Server is one of the few servers that doesn't support modifying or dropping table columns. The following syntax shows how to add a column to a table: ALTER TABLE CONTACT ADD PhoneNumber char(10) NULL Note that you can't add a NOT NULL column to a table that already has rows because it would necessarily have to allow NULLs immediately after being added to the table. Constraints A constraint is the mechanism by which you limit, or constrain, the type of data a column may store. A constraint can also be used to define a default value for a column. Constraints can be defined when a table is first created using the CREATE TABLE command, or afterward using the ALTER TABLE command. Here's an example of a primary key constraint: ALTER TABLE CUSTOMER ADD PRIMARY KEY (CustomerNumber) This syntax adds a primary key constraint to the CUSTOMER table, defining its CustomerNumber field as the table's primary key. This causes a unique index to be

created over the table using the CustomerNumber column as the key. Note that you cannot define a column that accepts NULL values as a table's primary key. A foreign key constraint defines a column in one table whose values must exist in a second, or foreign, table. A foreign key doesn't uniquely identify rows as a primary key does. On the contrary, its key columns must be a primary or unique key in the table that it references. Adding Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 405 a foreign key constraint to a table causes SQL Server to automatically build a secondary index over its key columns. The following is an example of the syntax: ALTER TABLE SALE ADD FOREIGN KEY (CustomerNumber) REFERENCES CUSTOMER This constraint defines the CustomerNumber field in the SALE table as a foreign key that references the same column in the CUSTOMER table. This means that customer numbers entered into the SALE table must first exist in the CUSTOMER table. It also means that customer numbers that are being used in the SALE table cannot be deleted from the CUSTOMER table. This capability to enforce the relationship between the two tables by merely defining it is called declarative referential integrity. The term simply means that the integrity of the relationship between the tables is ensured by defining (or declaring) it, not by user program code. A third type of constraint is one that checks a column against a list of predefined values. Here's an example of such a constraint: ALTER TABLE CUSTOMER ADD CONSTRAINT INVALID_STATE CHECK (State in (`OK','AR','MO')) Note the negative vantage point of the naming convention used for the constraint. This is done so front-end tools that report the constraint name will give a somewhat meaningful message to the user. By using a simple message as the name of the constraint, you allow for the possibility that the message could give the user a hint as to what the problem is when the constraint is violated. Additionally, this might save you the effort of having to replace the Delphi exception generated due to the constraint with your own.

Testing Constraints You should test every constraint that you place on a database. You do this by attempting to add values to the database that the constraint is supposed to disallow. For example, to test the preceding INVALID_STATE constraint, enter this command in ISQL: INSERT INTO CUSTOMER (CustomerNumber, State) VALUES (123,'CA') Because the constraint limits states entered to `OK', `AR', and `MO', it should reject your attempted row insertion with an error. If a constraint you've defined fails to function as expected, verify that you successfully added it in the first place and that it's checking the data in the way you intended. Creating Indexes You create indexes in SQL Server SQL using the CREATE INDEX command. Here's the basic syntax: CREATE INDEX SALE02 ON SALE (SaleDate) Page 406 SALE02 is the name of the new index, SALE is the name of the table on which to build the index, and SaleDate is the index key. Note that SQL Server index names must be unique across the database in which they reside. You can create an index that prohibits duplicates by using the CREATE UNIQUE INDEX variation of the command, as in the following: CREATE UNIQUE INDEX SALE01 ON SALE (SaleNumber) Inserting Data The SQL INSERT statement is used to add data to an SQL Server table. You can add data one row at a time using INSERT's VALUES clause, or you can insert several rows at once by selecting them from another table. Use the following syntax to add data to each of the three tables. First, add three rows to the CUSTOMER table by executing the following commands separately in ISQL:

INSERT INTO CUSTOMER (CustomerNumber, LastName, ÂFirstName, StreetAddress, City, State, Zip) VALUES(1,'Doe','John','123 Sunnylane','Anywhere', Â'MO','73115') INSERT INTO CUSTOMER (CustomerNumber, LastName, ÂFirstName, StreetAddress, City, State, Zip) VALUES(2,'Doe','Jane','123 Sunnylane','Anywhere', Â'MO','73115') INSERT INTO CUSTOMER (CustomerNumber, LastName, ÂFirstName, StreetAddress, City, State, Zip) VALUES(3,'Philgates','Buck','57 Riverside','Reo', Â'AR','65803') Now, add three rows to the ITEM table with these commands: INSERT INTO ITEM (ItemNumber, Description, Price) VALUES(1001,'Zoso LP',13.45) INSERT INTO ITEM (ItemNumber, Description, Price) VALUES(1002,'White LP',67.90) INSERT INTO ITEM (ItemNumber, Description, Price) VALUES(1003,'Bad Co. LP',11.45) Finally, add four to the SALE table using these statements: INSERT INTO SALE (SaleNumber, SaleDate, ÂItemNumber, Amount) VALUES(101,'10/18/90',1,1001,13.45) INSERT INTO SALE (SaleNumber, SaleDate, ÂItemNumber, Amount) VALUES(102,'02/27/92',2,1002,67.90) INSERT INTO SALE (SaleNumber, SaleDate, ÂItemNumber, Amount) VALUES(103,'05/20/95',3,1003,11.45) INSERT INTO SALE (SaleNumber, SaleDate, ÂItemNumber, Amount) VALUES(104,'11/27/97',1,1002,67.90) Page 407 Note that you don't have to include all the columns or follow the order in which they appear in the table when you specify a column list, but the list of values you specify must match the content and order of the column list. Here's an example: CustomerNumber,

CustomerNumber,

CustomerNumber,

CustomerNumber,

INSERT INTO ITEM (Price, ItemNumber) VALUES(13.45, 1001) The UPDATE Command You use the SQL UPDATE command to change data that's already in a table. UPDATE can include a WHERE clause to qualify which rows to update. Here's the syntax: UPDATE CUSTOMER SET Zip='65803' WHERE City='SpringField' Though an UPDATE's WHERE clause might cause it to change only a single row, depending on the data, you can update all the rows in the table by omitting the WHERE clause: UPDATE CUSTOMER SET State='MO' You can also update a column using other columns in its host table. You can even use the column itself. Let's say you wanted to increase the price of each item in the ITEM table by seven percent. You could issue this UPDATE command to get the job done: UPDATE ITEM SET Price=Price+(Price*.07) The DELETE Command You use the DELETE command to delete rows from tables. To delete all the rows in a table, use this syntax: DELETE FROM CUSTOMER The DELETE command can also include a WHERE clause to limit the rows deleted. Here's an example: DELETE FROM CUSTOMER WHERE LastName<>'Doe' Transaction Control

A group of changes to a database is formally known as a transaction. You initiate userdefined transaction batches using the BEGIN TRANSACTION command. You use the COMMIT command to save the changes made during a transaction permanently; you use ROLLBACK to discard them. Both of these commands affect only the changes made since the last COMMIT; you cannot ROLLBACK changes that have already been committed. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 423

CHAPTER 16

Delphi on Oracle
Page 424 In this chapter, we'll explore the Oracle DBMS and how Delphi applications interact with it. We'll examine Oracle's flavor of SQL as well as issues specific to using Oracle and Delphi together. You'll learn how to perform basic database administration tasks and performance tuning on Oracle servers as well. Now that the tour guidelines are nailed down, let's begin with starting the server.

Starting the Server
The method used to start Oracle varies widely from platform to platform. I'll show you how to start it on Windows 95 and Windows NT. See your server documentation if you're running Oracle on a different OS platform. Normally, Oracle installs itself to start automatically each time you restart Windows. If you change this, you can start it manually via the Oracle Instance Manager utility or, under Windows NT, via the Control Panel. Start the OracleStartORCL (where ORCL is the name of your database instance) to start Oracle under Windows NT.

Getting Connected
To connect to an Oracle database server, you need to install and configure Oracle's SQL Net connection software. To install the software, you simply run Oracle's ORAINST general purpose installation

program. To configure the software, define Database Aliases—a collection of settings containing all the information necessary to connect to a database server. These are not unlike Delphi's own BDE aliases; it's just that they work at the level of the Oracle driver rather than that of all database drivers. To configure an Oracle database alias using the Windows 95/NT SQL Net configuration program, start the SQL Net Easy Configuration program, select Add Database Alias, and then click OK. The next dialog box asks you to choose a database alias. Because you're adding one, type the new name you'd like to use and click OK. You're next asked to choose a network protocol. Select the one that you want to use to connect to your server (such as TCP/IP) and click OK. After you select a network protocol, you're asked to supply additional information about your server. If you selected TCP/IP as your network protocol, you're asked to enter the server's hostname as it appears in your HOSTS file. You can also enter its IP address. If you're connecting to a Personal Oracle server on the same machine, you can enter the TCP/IP loop-back address, 127.0.0.1 (you can also use localhost on most systems), as your server's IP address. If Page 425 you selected SPX as your connection means, you are prompted for an SPX service name instead. Enter the requested information and click OK. The final dialog box displays the connection information for the database alias you're about to create. Click OK to add the database alias. When you return to the SQL Net configuration program's main menu, select Exit, and then click OK to close the application. After the database alias is configured, you should test it by using it to connect to your database server. To do this, use your new alias to connect via Oracle's SQL*Plus utility like so: sqlplus USERNAME/PASSWORD@ALIASNAME @<file>.<ext> where USERNAME and PASSWORD are the username and password you want to use, and ALIASNAME is the name of the database alias you just created. Note that you can also use Oracle's TNSPING program to test a server connection. It uses the same syntax as the familiar UNIX PING command. NOTE

When connecting to a Personal Oracle server running on the same machine, I've found the TCP/IP protocol to be the least problematic. Using TCP/IP enables you to use the TCP/IP loop-back address, 127.0.0.1, as the server's IP address. TCP/IP is included with Windows 95 and Windows NT; you need only to install it to have everything you need to connect to Oracle.

Setting Up a BDE Alias When you're able to connect to your database server, you're ready to build a BDE alias so that your Delphi applications can access it. This subject is covered elsewhere in this book, but let's review it here for your convenience and to establish the relationship between configuring your database driver software and setting up a BDE alias. You can create BDE aliases using either the BDE Administrator utility or Delphi's Database Explorer. The following instructions use the BDE Administrator program. TIP In addition to clicking its icon in the Delphi folder, you can start the BDE Administrator from the Windows Control Panel.

Page 426 1. Start the BDE Administrator, and then click the Configuration | Drivers | Native | Oracle option on the Configuration page tab. 2. Click the VENDOR INIT drop-down box in the Definition grid and select the Oracle client DLL that corresponds to the version of Oracle you're accessing. For example, if you are connecting to an Oracle 7.2 server, you would select ORA72.DLL. Note that you can also specify other driver-wide settings here. Settings you specify here become the default settings for all new aliases you create that reference the driver. 3. Right-click the Databases tab and select New from the pop-up menu. When the New Database Alias dialog box comes up, select ORACLE in the Alias type drop-down list, and then click OK. 4. When the new alias is added, type a name for it. 5. After you've named the alias, configure it by setting the parameters on the Definition page. 6. Click the SERVER NAME entry on the Definition page and set it to the name of the database alias you created in the SQL Net configuration program. 7. Click the NET PROTOCOL entry and set it to the network protocol you chose in the SQL Net configuration program. 8. You can also set the USER NAME parameter to the name of the database server user with which

you want to connect. The username you specify is displayed as the default whenever Delphi's built-in login dialog box is shown. Finish by clicking the Apply button (or pressing Ctrl+A) to save your changes, then exit the Administrator. TIP You can locate the database alias definitions you've created using the BDE Administrator program in the Windows Registry. The key to look for is the following: HKEY_LOCAL_MACHINE\SOFTWARE\BORLAND\DATABASE ENGINE\SETTINGS You could use this key, for example, to create alias definitions without need of the BDE Administrator program or Delphi's Database Explorer.

Although you probably won't need to worry about these initially, here are a few other BDE Administrator settings that you might need to adjust at some point. ENABLE INTEGERS ENABLE INTEGERS controls whether the BDE treats NUMERIC fields with no digits to the right of the decimal as integer fields. The default behavior is to treat them as NUMERIC fields regardless of their scale. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 421

Summary
In this chapter, you learned how to start the SQL Server, how to connect to it, and how to troubleshoot connection problems. You received a thorough introduction to SQL Server's Transact-SQL dialect, and you learned how to perform some basic database administration tasks. Microsoft SQL Server is the type of subject that merits books of its own, but hopefully the crash course presented here will make life easier as you build Delphi applications that interact with SQL Server.

What's Ahead
Similar to this chapter, Chapter 16, "Delphi on Oracle," takes you through the many issues involved with producing Delphi apps that work with Oracle databases. Page 422 Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 417

Âfrom sysindexes where indid = 255 and id = o.id))),0)*2, unused_space_in_KB = isnull(((select sum(reserved) from Âsysindexes where indid in (0, 1, 255) and id = o.id) (select sum(used) from Âsysindexes where indid in (0, 1, 255) and id = o.id)),0)*2, owner= user_name(o.uid) from sysobjects o, sysindexes i where o.name like @mask and o.type like @obtype and o.id*=i.id and i.indid<=1 select @orderby=upper(@orderby) if @orderby = `/N' alter table #sp_dir add constraint PK_dir primary key Âclustered (name,id) else if @orderby = `/R' alter table #sp_dir add constraint PK_dir primary key Âclustered (row_count,name) else if @orderby = `/S' alter table #sp_dir add constraint PK_dir primary key Âclustered (total_size_in_KB,name) else if @orderby = `/D' alter table #sp_dir add constraint PK_dir primary key Âclustered (date_created,name) else if @orderby = `/DS' alter table #sp_dir add constraint PK_dir primary key Âclustered (data_space_in_KB,name) else if @orderby = `/IS' alter table #sp_dir add constraint PK_dir primary key

Âclustered (index_space_in_KB,name) else if @orderby = `/US' alter table #sp_dir add constraint PK_dir primary key Âclustered (unused_space_in_KB,name) if @orderby = `/RL' alter table #sp_dir add constraint PK_dir primary key Âclustered (row_len_in_bytes,name) else if @orderby = `/O' alter table #sp_dir add constraint PK_dir primary key Âclustered (owner,name) alter table #sp_dir add sequencer int identity alter table #sp_dir drop constraint PK_dir alter table #sp_dir add primary key clustered (sequencer) insert into #sp_dir (id,name,type,date_created,row_count,row_len_in_bytes, Âtotal_size_in_KB,data_space_in_KB,index_space_in_KB,unused_space_in_KB,owner) select power(2.0,31)-1,'TOTAL:','NA',getdate(),row_count=sum(row_count), Ârow_len_in_bytes Â=max(row_len_in_bytes),total_size_in_KB=sum(total_size_in_KB),date_space= Âsum(data_space_in_KB), index_space_in_KB=sum(index_space_in_KB),unused_space_in_KB=sum (unused_space_in_KB),'NA' from #sp_dir continues Page 418 Listing 15.1. continued select name,type,date_created, row_count, Ârow_len_in_bytes, total_size_in_KB, Âdata_space_in_KB, index_space_in_KB, unused_space_in_KB, owner from #sp_dir drop table #sp_dir go IF OBJECT_ID(`dbo.sp_dir') IS NOT NULL PRINT `<<< CREATED PROC dbo.sp_dir >>>' ELSE PRINT `<<< FAILED CREATING PROC dbo.sp_dir >>>' go Notice that the USE statement at the top of the file establishes the database in which the procedure will be created. Note also the use of the GO command batch terminator to separate the different sections of the script. NOTE

The complete source to this procedure is included on the CD-ROM accompanying this book.

Running Stored Procedures You can run SQL Server stored procedures using the EXECUTE command. The syntax is EXECUTE procedurename parameters You can abbreviate EXECUTE to EXEC, and you can omit it altogether when the EXEC is the first command in a command batch. For example, you can run the listcustomers procedure in the ISQL utility with this syntax: listcustomers You can pass parameters to a stored procedure positionally or by name, like so: exec listcustomersbystate and exec listcustomersbystate Triggers Not unlike stored procedures, triggers are SQL routines that are activated when data in a given table is inserted, updated, or deleted. You associate a trigger with a specific operation on a table: a row insertion, an update, or a deletion. Here's an example using Transact-SQL: Page 419 CREATE TRIGGER SALEDelete ON CUSTOMER FOR DELETE AS BEGIN DELETE FROM SALE WHERE CustomerNumber=(SELECT CustomerNumber FROM deleted) END This trigger deletes the sales made to a customer from the SALE table when the customer's record is deleted from the CUSTOMER table. This sort of delete is known as a cascading delete: A delete operation on one table cascades through others using a common key. Note the use of the deleted logical table. Whenever a DELETE trigger fires, SQL Server creates a logical table named deleted that contains the row(s) about to be deleted. In this trigger, deleted is queried to determine the CustomerNumber being deleted; this CustomerNumber value is then used to remove rows from the SALE table. When INSERT and UPDATE triggers fire, a similar logical table named inserted is constructed by the server. The @LastNameMask='%',@State='MO' `MO','%'

table doesn't actually exist anywhere except memory, but it's accessible like any other table by the trigger. Cursors Cursors are set-oriented SQL's approach to row-oriented processing. Cursors enable you to work with tables one row at a time. Because the BDE automatically creates and maintains cursors for you, you won't generally create your own cursors using SQL. However, you might find them handy in stored procedures. There are four basic operations you can perform on cursors: You can declare them, open them, fetch from them, and close them. You can also use a cursor to UPDATE and DELETE rows in its base table. A cursor declaration consists of a SELECT statement and, for updatable cursors, a list of updatable columns. Here's the syntax: DECLARE CUSTOMER_SELECT CURSOR FOR SELECT * FROM CUSTOMER Before you can retrieve rows using the cursor, it must be opened. You use the OPEN command to initiate the query that makes up the cursor declaration: OPEN CUSTOMER_SELECT OPEN doesn't actually retrieve any rows back to the client application. You must use FETCH for that. Here's the syntax: FETCH CUSTOMER_SELECT This retrieves a single row from the cursor result set. Each subsequent call to FETCH retrieves the next row in the set. SQL Server supports one-way cursors only; you cannot FETCH backward. If you want to move back up in a set, you must CLOSE and re-OPEN the cursor. Page 420 NOTE The fact that SQL Server doesn't support bidirectional cursors doesn't prevent your Delphi applications from using them anyway. The BDE emulates bidirectional cursoring at the application level regardless of whether your server back-end supports it. This is why you're able to scroll both backward and forward in TDataSets such as TQuery and TTable.

The rows returned by updatable cursors can be updated using special versions of the UPDATE and DELETE commands. A cursor must be declared using the FOR UPDATE OF clause in order to be updatable. Here's an example: DECLARE CUSTOMER_UPDATE CURSOR FOR SELECT * FROM CUSTOMER FOR UPDATE OF LastName

NOTE Be sure to list only those columns in the FOR UPDATE OF clause that you actually intend to update. Declaring more updatable fields than you need wastes server resources.

In order to UPDATE or DELETE the current row of an updatable cursor, you use the WHERE CURRENT OF cursorname syntax to qualify the command, as in UPDATE CUSTOMER SET LastName="Cane" WHERE CURRENT OF CUSTOMER_UPDATE or DELETE FROM CUSTOMER WHERE CURRENT OF CUSTOMER_UPDATE When you finish with a cursor, you use the CLOSE command to close it. Here's the syntax: CLOSE CUSTOMER_UPDATE Closing a cursor doesn't release the system resources it uses. You use the DEALLOCATE cursor syntax for that, as in DEALLOCATE CUSTOMER_UPDATE Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 414 Notice that the alias can be used in the field list of the SELECT list before it is even syntactically defined. This is possible because references to database objects are resolved before a query is executed. Views An SQL view consists of a SELECT statement that you can treat as a table and, in turn, query with SELECT statements. In some cases, you can also issue INSERT, DELETE, and UPDATE statements against the view. The view itself does not actually store any data; it's a logical construct only. Think of a view as a small SQL program that runs each time you query it. It's similar to an SQL Server select procedure, which is discussed in the following section, "Stored Procedures." When you query a view, the query optimizer takes the SELECT used to create the view, blends in the one you are executing against it, and optimizes the two as a single query. SQL views are created using the CREATE VIEW command. Here's an example: CREATE VIEW MOCUSTOMERS AS SELECT * FROM CUSTOMER WHERE State='MO' After the view is created, it can be queried just like a table, as in SELECT * FROM MOCUSTOMERS When you run this query, notice that, even though the SELECT against the view didn't include a WHERE clause, the result set appears as though it did due to the WHERE clause that's built in to the view. The SELECT statement that makes up a view can do almost anything a normal SELECT statement can do. One thing it can't do is contain an ORDER BY clause. This limitation exists not only on SQL Server, but also on the Sybase, InterBase, and Oracle platforms.

When you create an updatable view, you can tell the server to ensure that rows that are updated or added using the view meet the selection criteria imposed by the view. That is, you can ensure that an updated or added record doesn't "go out of scope"—that it doesn't vanish from the view once it's changed or added. You do this by using the WITH CHECK OPTION clause of the CREATE VIEW command. Here's the syntax: CREATE VIEW MOCUSTOMERS AS SELECT * FROM CUSTOMER WHERE State='MO' WITH CHECK OPTION Now, any record updates or inserts that specify a State column containing anything but `MO' will fail. Page 415 Stored Procedures A stored procedure is a compiled SQL program that's stored in a database with other database objects. Stored procedures are created using the CREATE PROCEDURE command. Here's an example of the SQL Server syntax: CREATE PROCEDURE listcustomers AS BEGIN SELECT LastName FROM CUSTOMER END If the procedure receives parameters from the caller, the syntax changes slightly. Here's the SQL Server syntax: CREATE PROCEDURE listcustomersbystate (@State varchar(2), Â@LastNameMask varchar(30)) AS BEGIN SELECT LastName FROM CUSTOMER WHERE State=@State AND LastName LIKE @LastNameMask END Scripts It's a good idea to construct Data Definition Language (DDL) statements, including stored procedures, using SQL script files. You can create these scripts using a text editor; most good SQL editors support saving their contents to disk. Remember that these scripts must include any necessary USE statements and GO command batch terminators. You can execute SQL Server SQL scripts by clicking the Load ISQL Script button in ISQL. Listing 15.1 shows an example of such a script file. The procedure defined in the script provides an object listing similar to the DIR command when executed.

Listing 15.1. An SQL script file containing the sp_dir stored procedure. use master go /* * DROP PROC dbo.sp_dir */ IF OBJECT_ID(`dbo.sp_dir') IS NOT NULL BEGIN DROP PROC dbo.sp_dir PRINT `<<< DROPPED PROC dbo.sp_dir >>>' END go create procedure sp_dir @mask varchar(30) = `%', Â@obtype varchar(2) = `U', @orderby varchar(3)='/N' as /* Stored procedure to list object catalog information Âsimilar to the DOS DIR command. continues Page 416 Listing 15.1. continued Takes three parameters: @mask = pattern of object names to list (supports SQL Âwildcards); defaults to all objects @obtype = type of objects to list (supports SQL Âwildcards); default to user tables @orderby = column on which to sort listing. The following Âparameters are supported: /N = sort by Name /R = sort by number of Rows /S = sort by total object Size /D = sort by Date created /DS = sort by total Size of Data pages /IS = sort by total Size of Index pages /US = sort by total Size of Unused pages /RL = sort by maximum Row Length /O = sort by Owner The default order is by Name. Parameters can be specified positionally, like so: sp_dir `TRA%','U','/S' or by name, like so: sp_dir @mask='TRA%',@obtype='U',@orderby='/S'

All parameters are optional. If no parameters are specified, Âthe following command is executed: sp_dir `%','U','/N' */ CREATE TABLE #sp_dir( id int NOT NULL, name varchar(30) NOT NULL, type char(2) NOT NULL, date_created datetime NOT NULL, row_count int NOT NULL, row_len_in_bytes int NOT NULL, total_size_in_KB int NOT NULL, data_space_in_KB int NOT NULL, index_space_in_KB int NOT NULL, unused_space_in_KB int NOT NULL, owner varchar(30) NOT NULL) insert into #sp_dir select o.id, o.name, o.type, date_created= o.crdate, row_count = isnull(rows,0), row_len_in_bytes= isnull((select sum(length) from syscolumns Âwhere id=o.id and o.type in (`U','S')),0), total_size_in_KB = isnull((select sum(reserved) from Âsysindexes where indid in (0, 1, 255) and id = o.id),0)*2, data_space_in_KB = isnull(((select sum(dpages) from Âsysindexes where indid < 2 and id = o.id)+ (select isnull(sum(used), 0) Âfrom sysindexes where indid = 255 and id = o.id)),0)*2, index_space_in_KB = isnull(((select sum(used) from Âsysindexes where indid in (0, 1, 255) and id = o.id) ((select sum(dpages) from Âsysindexes where indid < 2 and id = o.id)+ (select isnull(sum(used), 0)

Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 427 This setting is similar to the ENABLE BCD setting. ENABLE INTEGERS has precedence when both settings are enabled. Synonyms Oracle enables you to set up alternate object names known as synonyms. You can use a special database driver setting, LIST SYNONYMS, to specify how you want Oracle synonyms to be handled. You specify this special setting using the Params property of the Database component. Table 16.1 lists the setting's possible values. Table 16.1. Possible values for LIST SYNONYMS. Value Translation NONE No synonyms are included. PRIVATE Only private synonyms are included. Include all (public and private) ALL synonyms. To specify one of these values, double-click the Database component's Params property and type the name of the setting followed by its value, like so: LIST SYNONYMS=ALL Public Synonyms When you set LIST SYNONYMS to ALL, Oracle's PUBLIC synonyms show up in

the table list. However, to open a PUBLIC synonym, you must have SELECT privileges on the synonym's underlying base object. If you (or the user running your application) lack the necessary access rights, the synonym will not appear to exist when you attempt to open it. Your Database Administrator (DBA) should be able to grant you the appropriate rights. Included in Oracle's public synonyms is a set of dynamic performance tables. By default, only the user SYS can access them. These synonym names are in the format V$NAME, where NAME is the remainder of the synonym name (such as LOCK, OPEN_CURSOR, and so on). Troubleshooting Oracle Connection Problems If you have problems connecting to Oracle, try the following: 1. Use the Oracle SQL*Plus utility to connect to your server. If you're able to connect with SQL*Plus, but not from your Delphi applications, you probably have a problem with your BDE alias configuration. Return to the BDE Administrator program and examine the parameters you specified (particularly the server name) to ensure that they're correct. Page 428 2. If SQL*Plus fails to connect, you probably have a protocol problem. If you're using TCP/IP to connect to the server, attempt to ping the host computer using the PING utility that accompanies Windows 95 and Windows NT. Try both the machine's name, as it appears in the HOSTS file, and its IP address. 3. If the machine pings appropriately with the IP address, but not with its hostname, you might have a problem with your HOSTS file. You can fix the problem temporarily by changing your connection string in the SQL Net configuration program to use the IP address rather than the hostname. You should resolve the HOSTS file problem as soon as possible because this is a more flexible way to connect to remote machines. 4. If you're using SPX to connect with your server, be sure that the server's SPX service number matches the one you're specifying in the SQL Net configuration program. If this is the case and you still can't connect, or, if you're using some other protocol to connect, consult with your network administrator for further assistance. 5. If you're using TCP/IP and you have no success when trying to ping the host machine using either its hostname or its IP address, you probably have a network problem. Be sure to check obvious things such as whether the IP

address you have for the server is correct. Try pinging the TCP/IP loop-back address, 127.0.0.1, to see whether your TCP/IP stack is working properly. This address loops back to your machine, enabling you to ping yourself. If it fails, you have a serious problem with your protocol configuration. You might need to consult your network administrator for help with your network connection. 6. If SQL*Plus fails to connect, but PING works fine, check to see that the Oracle home directory is on your PATH. If your PATH is correct, make sure that (ORACLE HOME DIRECTORY)\NETWORK\ADMIN \TNSNAMES.ORA file is set up properly. It's a text file, so you can look at it using the TYPE command or in an editor, but don't edit it directly—use the SQL Net configuration program to edit it instead. Here's an excerpt that shows what the file should look like for a local server: SCOTTSDATABASE.world = (DESCRIPTION = (ADDRESS_LIST = (ADDRESS = (COMMUNITY = tcp.world) (PROTOCOL = TCP) (Host = 127.0.0.1) (Port = 1521) ) (ADDRESS = (COMMUNITY = tcp.world) (PROTOCOL = TCP) (Host = 127.0.0.1) (Port = 1526) ) ) (CONNECT_DATA = (SID = ORCL) ) ) Page 429 7. If everything seems to be configured properly, you might try switching protocols, if that's an option for you. Because the server can be configured to listen on multiple protocols simultaneously, you can configure it to listen on a protocol that works until you resolve problems with ones that don't. For example, if you're connecting to a local Oracle server (one that runs on your client machine), you can switch to Oracle's Bequeath protocol. This is a simple loop-back protocol that alleviates the need to have a functional

network.

SQL Primer
This section introduces you to Oracle's rich SQL dialect and covers a few of the features that distinguish it from the SQL implementations of other vendors. I assume that your server is running and that you know how to send it SQL using Oracle's SQL*Plus utility. Creating a Database You may already have a database in which you can create some temporary tables for the purpose of working through the examples in this chapter. If you don't, creating one is easy enough. In SQL, you create databases using the CREATE DATABASE command. The exact syntax varies from vendor to vendor; here's the basic Oracle syntax: CREATE DATABASE databasename LOGFILE filespec DATAFILE filespec; As you might imagine, there are a number of other parameters you can specify. Specifying the name of the database and the locations of the data and log files is a minimum of what's usually specified to create an Oracle database. If brevity is your pleasure, you can abbreviate things even further by omitting all parameters, like so: CREATE DATABASE; This will create a small database using default values for all parameters. The files that make up the database usually have an extension of DBF or DAT, which helps them stand out in directory listings. Also note that you can pre-allocate space for these files or set them up to be extended automatically as the database grows in size. The CONNECT Command You use the Oracle CONNECT command to connect to an already existing database. The command syntax is CONNECT USER/PASSWORD@DBLink

Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 430 Replace DBLink with the name of the server to which you want to connect. If you're connecting to a local server, you can omit the @ symbol and the server name. Here's an example of a CONNECT to a local database: CONNECT SYS/57RIVERSIDE; Use the DISCONNECT command to explicitly break a database connection established with the CONNECT command, as in the following: DISCONNECT; Creating Tables After a database connection has been established, you're ready to begin building database objects. Virtually any relational database concept can be demonstrated with a set of three tables. For the purpose of working through this chapter, begin by creating the following three tables. Tables are created using the SQL CREATE TABLE statement. Enter the following command in SQL*PLUS to create the CUSTOMER table: CREATE TABLE CUSTOMER ( CustomerNumber LastName FirstName StreetAddress City State Zip )

number char(30), char(30), char(30), char(20), char(2), char(10)

NOT NULL,

Next, build the SALE table using this command: CREATE TABLE SALE

( SaleNumber number NOT NULL, SaleDate date, CustomerNumber number NOT NULL, ItemNumber number NOT NULL, Amount number(5,2) ) Now that the SALE table is built, only one table remains. Create the ITEM table using this command: CREATE TABLE ITEM ( ItemNumber Description Price ) Page 431 Tablespaces Oracle supports some interesting options for the CREATE TABLE command. One option allows you to place tables on a specific tablespace. An Oracle tablespace is a storage object that can contain database objects such as tables and indexes. When you create a tablespace, you assign it to a specific disk drive or volume name, like so: CREATE TABLESPACE salestables DATAFILE `C:\ORANT\DATABASE\salestables.dbf' SIZE Â250K After you've created a tablespace, you can place tables or indexes on it using the TABLESPACE clause, like so: CREATE TABLE ITEM ( ItemNumber Description Price ) TABLESPACE salestables

number char(30), number(5,2)

NOT NULL,

number char(30), number(5,2)

NOT NULL,

Because this allows you to control which disk drives tables and indexes end up on, you can use it to ensure that they don't compete with one another for disk throughput. By placing tables and indexes on tablespaces that reference different disk drives (and, ideally, separate disk controllers), you can improve system performance dramatically. AS Subquery

Another interesting variation of the CREATE TABLE command allows you to create one table by querying another. For SQL Server developers, this is the equivalent of the Transact-SQL SELECT...INTO syntax. Rather than requiring column data type information, Oracle creates the new table based on the result set of the query from the other table. The new table receives the rows from the original table that satisfied the query condition. Here's an example: CREATE TABLE LARGE_SALE AS SELECT * FROM SALE WHERE Amount > 50; Notice that no column information is specified. Column-level information for the new table is retrieved from the table being queried. Although this syntax bears some similarities to the CREATE VIEW command, tables created using the AS subquery syntax are different in that they actually contain data. UNRECOVERABLE In order to create a large table quickly, you can disable the logging activity that usually occurs as rows are added to it. You do this by adding the UNRECOVERABLE keyword to your CREATE TABLE command. Here's an example: CREATE TABLE LARGE_SALE UNRECOVERABLE AS SELECT * FROM SALE WHERE Amount > 50; Page 432 PARALLEL Clause Another option that speeds the creation of large tables is the PARALLEL clause. You can use it to spread the work required to create the table over multiple threads. For example, you use this syntax to speed up the creation of the LARGE_SALE table: CREATE TABLE LARGE_SALE UNRECOVERABLE PARALLEL (DEGREE 5) AS SELECT * FROM SALE WHERE Amount > 50; This spreads the work related to the creation of the table over five separate threads, potentially improving performance significantly. Adding and Modifying Table Columns You use the SQL ALTER TABLE command to add and modify columns in an existing table. Not all servers support modifying columns after the fact (Microsoft SQL Server doesn't, for example), but Oracle does. Use the following syntax to add a column:

ALTER TABLE CUSTOMER ADD (PhoneNumber char(10)) Use this syntax to modify it: ALTER TABLE CUSTOMER MODIFY (PhoneNumber char(13)) Note that you can't add a NOT NULL column to a table that already has rows because it would necessarily have to allow NULLs immediately after being added to the table. Constraints A constraint is the mechanism by which you limit, or constrain, the type of data a column may store. A constraint can also be used to define a default value for a column. Constraints can be defined when a table is first created using the CREATE TABLE command, or afterward using the ALTER TABLE command. Here's an example of a primary key constraint: ALTER TABLE CUSTOMER ADD PRIMARY KEY (CustomerNumber) This syntax adds a primary key constraint to the CUSTOMER table, defining its CustomerNumber field as the table's primary key. This causes a unique index to be created over the table using the CustomerNumber column as the key. Note that you cannot define a column that accepts NULL values as a table's primary key. A foreign key constraint defines a column in one table whose values must exist in a second, or foreign, table. A foreign key doesn't uniquely identify rows as a primary key does. On the contrary, its key columns must be a primary or unique key in the table that it references. The following is an example of the syntax: Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 433 ALTER TABLE SALE ADD CONSTRAINT INVALID_CUSTOMER_NUMBER FOREIGN KEY (CustomerNumber) REFERENCES CUSTOMER This constraint defines the CustomerNumber field in the SALE table as a foreign key that references the same column in the CUSTOMER table. This means that customer numbers entered into the SALE table must first exist in the CUSTOMER table. It also means that customer numbers that are being used in the SALE table cannot be deleted from the CUSTOMER table. This capability to enforce the relationship between the two tables by merely defining it is called declarative referential integrity. The term simply means that the integrity of the relationship between the tables is ensured by defining (or declaring) it, not by user program code. A third type of constraint is one that checks a column against a list of predefined values. Here's an example of such a constraint: ALTER TABLE CUSTOMER ADD CONSTRAINT INVALID_STATE CHECK (State in (`OK','AR','MO')) Note the use of the negative naming convention for the constraint. This is done so that front-end tools that report the constraint name will give a meaningful message to the user when the constraint is violated. By using a simple message as the name of the constraint, you allow for the possibility that the message might give the user a hint as to what the problem is. This might also save you the effort of having to replace the Delphi exception generated as a result of the constraint with your own. Creating Indexes You create indexes in Oracle SQL using the CREATE INDEX command. Here's the basic syntax: CREATE INDEX SALE02 ON SALE (SaleDate) SALE02 is the name of the new index, SALE is the name of the table on which to build the index, and SaleDate is the index key. Note that Oracle index names must be unique across the schema in which they reside. Also note that you can't create a new index if one has already been created using the same key columns in the same order. You can create an index that prohibits duplicates by using the CREATE UNIQUE INDEX variation of the command, as in the following: CREATE UNIQUE INDEX SALE01 ON SALE (SaleNumber)

Even though CREATE INDEX supports the inclusion of the DESC keyword, indexes based on descending keys aren't supported. Oracle indexes are always created in ascending order. The ASC and DESC keywords are supported for compatibility with DB2. Page 434 UNRECOVERABLE and PARALLEL The UNRECOVERABLE and PARALLEL clauses that we first discussed with the CREATE TABLE command are also available with CREATE INDEX. You can use these two options to speed the creation of indexes over large tables. NOSORT You can use the NOSORT option to indicate to Oracle that the table's rows are already sorted on the index key. This can save time creating indexes over data that's already in the order you need. Tablespaces As I mentioned earlier, tablespaces aren't just for tables. You can explicitly place indexes on tablespaces as well. By locating a table and its indexes on separate drives, you help avoid disk contention. Inserting Data The SQL INSERT statement is used to add data to an Oracle table. You can add data one row at a time using INSERT's VALUES clause, or you can insert several rows simultaneously by selecting them from another table. Use the following syntax to add data to each of the three tables. First, add three rows to the CUSTOMER table by executing the following commands in SQL*PLUS: INSERT INTO CUSTOMER (CustomerNumber, LastName, FirstName, StreetAddress, City, Â State, Zip) VALUES(1,'Doe','John','123 Sunnylane','Anywhere','MO','73115'); INSERT INTO CUSTOMER (CustomerNumber, LastName, FirstName, StreetAddress, City, Â State, Zip) VALUES(2,'Doe','Jane','123 Sunnylane','Anywhere','MO','73115'); INSERT INTO CUSTOMER (CustomerNumber, LastName, FirstName, StreetAddress, City, Â State, Zip) VALUES(3,'Philgates','Buck','57 Riverside','Reo','AR','65803'); Now, add three rows to the ITEM table with these commands: INSERT INTO ITEM (ItemNumber, Description, Price) VALUES(1001,'Zoso LP',13.45); INSERT INTO ITEM (ItemNumber, Description, Price) VALUES(1002,'White LP',67.90); INSERT INTO ITEM (ItemNumber, Description, Price)

VALUES(1003,'Bad Co. LP',11.45); Finally, add four to the SALE table using these statements: INSERT INTO SALE (SaleNumber, SaleDate, CustomerNumber, ItemNumber, Amount) VALUES(101,'18-OCT-90',1,1001,13.45); Page 435 INSERT INTO SALE (SaleNumber, SaleDate, CustomerNumber, ItemNumber, Amount) VALUES(102,'27-FEB-92',2,1002,67.90); INSERT INTO SALE (SaleNumber, SaleDate, CustomerNumber, ItemNumber, Amount) VALUES(103,'20-MAY-95',3,1003,11.45); INSERT INTO SALE (SaleNumber, SaleDate, CustomerNumber, ItemNumber, Amount) VALUES(104,'27-NOV-97',1,1002,67.90); Note that you don't have to include all the columns or follow the order in which they appear in the table when you specify a column list, but the list of values you specify must match the content and order of the column list. Here's an example: INSERT INTO ITEM (Price, ItemNumber) VALUES(13.45, 1001) The UPDATE Command You use the SQL UPDATE command to change data that's already in a table. UPDATE can include a WHERE clause to qualify which rows to update. Here's the syntax: UPDATE CUSTOMER SET Zip='65803' WHERE City='SpringField' Though an UPDATE's WHERE clause might cause it to change only a single row, depending on the data, you can update all the rows in the table by omitting the WHERE clause: UPDATE CUSTOMER SET State='MO' You can also update a column using other columns in its host table. You can even use the column itself. Let's say you wanted to increase the price of each item in the ITEM table by seven percent. You could issue this UPDATE command to get the job done: UPDATE ITEM SET Price=Price+(Price*.07) The DELETE Command You use the DELETE command to delete rows from tables. To delete all the rows in a table, use this syntax:

DELETE FROM CUSTOMER The DELETE command can also include a WHERE clause to limit the rows deleted. Here's an example: DELETE FROM CUSTOMER WHERE LastName<>'Doe' Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 447

CHAPTER 17

Delphi on InterBase
Page 448 In this chapter, I'll take you on a guided tour of the InterBase RDBMS. I'll discuss InterBase's flavor of SQL as well as issues specific to using InterBase and Delphi together. We'll also talk about how to perform basic database administration on InterBase servers. Now that the tour guidelines are nailed down, let's begin with starting the server.

Starting the Server
The method used to start InterBase varies widely from platform to platform. I'll show you how to start it on Windows 95 and Windows NT. See your server documentation if you're running InterBase on a different OS platform. Normally, InterBase is installed so that it automatically starts each time you start Windows. If you change this, simply select the InterBase Server 4.2 item in the InterBase 4.2 folder to restart the server (double-click the InterBase Server 4.2 icon in the InterBase 4.2 group under Windows NT 3.51). When the server is up, you should see its icon on the Windows 95/NT taskbar (you'll see a minimized Program Manager icon under Windows NT 3.51). You can right-click the taskbar icon to view or change the server's basic properties. For example, you can configure whether the server automatically

starts each time you cycle Windows by selecting the Startup Configuration option from the pop-up menu. You can view the maximum number of connections supported by the server and the number of current connections by selecting Properties from the pop-up menu. You also can shut down the server by selecting the pop-up menu's Shutdown option.

Getting Connected
Unlike the servers mentioned thus far, InterBase doesn't have a separate database connection element that you must define in order to access InterBase servers. In fact, if you're connecting to a local InterBase server, client software (WISQL, the BDE, and so on) can refer directly to the database file you want to access; you don't need a server reference at all. If you're connecting to a remote InterBase server, the required connection information differs based on the protocol you're using. If you're connecting using TCP/IP, your HOSTS file must contain a reference to your server, like so: 100.10.15.12 marketing

A line must also be added to your TCP SERVICES file that identifies the InterBase access protocol: gds_db 3050/tcp

This is done automatically when you install InterBase (you can also add the line later yourself). Once your TCP/IP access is configured, you're ready to connect using WISQL and other Page 449 InterBase tools. Note that these required file entries are unique to TCP/IP implementations—neither NetBEUI nor IPX/SPX has a similar requirement. Setting Up a BDE Alias Once you're able to connect to your database server, you're ready to build a BDE alias so that your Delphi applications can access it. This subject is covered in Chapter 9, "First Steps," but let's review it here for your convenience and to establish the relationship between database drivers and BDE aliases.

You can create BDE aliases using either the BDE Administrator utility or Delphi's Database Explorer. The following instructions use the BDE Administrator program. 1. Start the BDE Administrator, right-click the Databases tab, and select New from the menu. When the New Database Alias dialog box comes up, select INTRBASE in the Alias type drop-down list, then click OK. 2. Once the new alias is added, type a name for it. 3. After you've named the alias, configure it by setting the parameters on the Definition page. 4. Click the SERVER NAME parameter and set it to the name of the server and database file to which you want to connect. By convention, this file usually has an extension of GDB. Your entry should look something like this: MIS:/data/interbase/accounting.gdb where MIS is the name of your server and /data/interbase/accounting. gdb is the full path to the file containing your database. TIP If you're connecting to a local InterBase Server, you don't need to include a server name in the SERVER NAME field. Instead, just specify the full path to your database file, like so: C:\DATA \INTERBASE\ACCOUNT.GDB

5. Optionally, you can also set the USER NAME parameter to the name of the database server user you want to connect with by default. The username you specify is displayed as the default whenever Delphi's built-in login dialog box is shown. As you can see, there are several other parameters you can set on this screen. One of them in particular should always be set for InterBase aliases: ENABLE BCD. ENABLE BCD affects the way the BDE treats certain types of numeric data types. Despite the name, this isn't limited to just Binary-Coded Decimal (BCD) types; it also affects DECIMAL and NUMERIC types, which are floating-point data types. When ENABLE BCD is set to False (the default), columns you've

Page 450 defined as DECIMAL or NUMERIC types will be treated by Delphi as integers. This means that data aware controls will assume that they can't store decimals and will prevent decimal entries. This isn't what you'd want, obviously, so switch ENABLE BCD to True in all InterBase aliases. Finish by clicking the Apply button to save your changes and exit the Administrator. Troubleshooting InterBase Connection Problems If you have problems connecting to an InterBase server from within your Delphi applications, try the following: 1. Use the InterBase Communication Diagnostics utility located in your InterBase 4.2 program folder to attempt to connect to your database. Try the Test button on the DB Connection page first. If you're able to connect using this method, your BDE alias is probably configured incorrectly; return to the BDE Administrator and check the alias you're using thoroughly. If you're unable to connect from the DB Connection page, click either the NetBEUI or Winsock page tabs and try their Test buttons. If either of these work, you may have incorrectly specified. If none of these methods works and your server is running, you can try the following methods to troubleshoot the problem. 2. Use the InterBase WISQL utility to attempt to connect to your server. If you're able to connect to the server but still have problems doing so within Delphi applications, you probably have a problem with your BDE alias configuration. Return to the BDE Administrator and examine the parameters you specified (particularly the server name) to ensure that they're correct. 3. If WISQL fails, you probably have a protocol problem. If you're connecting over TCP/IP, use the PING utility included with Windows 95 or Windows NT to attempt to PING the host computer. Try both the machine's name, as it appears in the HOSTS file, and its IP address. 4. If you're using named pipes to connect to an NT-based server, try the net view \\ servername command, where servername is the name of the NT machine on which the SQL server is running. If net view succeeds, try netuse \\servername\IPC$, replacing servername with the name of your server. If this test fails, you won't be able to see the server machine across the network. This is a problem you should probably refer to your NT system administrator. You might have problems with the named

pipes services on the server. 5. If you're connecting over TCP/IP and the machine pings appropriately with the IP address but not with its host name, you might have a problem with your HOSTS file. You have to resolve the HOSTS file problem in order to connect to the server, because the InterBase client software uses it to find your server. You should have an entry in your HOSTS file for each server you want to access that looks like this: 100.10.15.12 marketing

Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 445 This retrieves a single row from the cursor result set. Each subsequent call to FETCH retrieves the next row in the set. Oracle supports one-way cursors only; you cannot FETCH backward. If you want to move back up in a set, you must CLOSE and re-OPEN the cursor. NOTE The fact that Oracle doesn't support bidirectional cursors doesn't prevent your Delphi applications from using them anyway. The BDE emulates bidirectional cursoring at the application level regardless of whether your server back-end supports it. This is why you're able to scroll both backward and forward in TDataSets such as TQuery and TTable.

The rows returned by updatable cursors can be updated using special versions of the UPDATE and DELETE commands. A cursor must be declared using the FOR UPDATE OF clause in order to be updatable. Here's an example: DECLARE CUSTOMER_UPDATE CURSOR FOR SELECT * FROM CUSTOMER FOR UPDATE OF LastName NOTE Be sure to list only those columns in the FOR UPDATE OF clause that you actually intend to update. Declaring more updatable fields than you need wastes server resources.

In order to update or delete the current row of an updatable cursor, you use the WHERE CURRENT OF cursorname syntax to qualify the command, as in UPDATE CUSTOMER SET LastName="Cane" WHERE CURRENT OF CUSTOMER_UPDATE or DELETE FROM CUSTOMER WHERE CURRENT OF CUSTOMER_UPDATE When you finish with a cursor, you use the CLOSE command to close it. Closing a cursor also releases any system resources in use by it. Here's the syntax: CLOSE CUSTOMER_UPDATE Page 446

Summary
You've just been on a whirlwind tour of the Oracle DBMS. Oracle is the type of subject that merits books of its own, but hopefully the crash course presented here will make life easier as you build Delphi applications that interface with Oracle.

What's Ahead
Chapter 17, "Delphi on InterBase," takes you through the many issues involved with producing Delphi apps that connect to InterBase database servers. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 442 Views An SQL view consists of a SELECT statement that you can treat as a table and, in turn, query with SELECT statements. In some cases, you can also issue INSERT, DELETE, and UPDATE statements against the view. The view itself does not actually store any data; it's a logical construct only. Think of a view as a small SQL program that runs each time you query it. It's similar to an Oracle select procedure, which is discussed in the following section, "Stored Procedures." When you query view, the query optimizer takes the SELECT used to create the view, blends in the one you are executing against it, and optimizes the two as a single query. SQL views are created using the CREATE VIEW command. Here's an example: CREATE VIEW MOCUSTOMERS AS SELECT * FROM CUSTOMER WHERE State='MO' After the view is created, it can be queried just like a table, as in SELECT * FROM MOCUSTOMERS When you run this query, notice that, even though the SELECT against the view didn't include a WHERE clause, the result set appears as though it did due to the WHERE clause that's built into the view. The SELECT statement that makes up a view can do almost anything a normal SELECT statement can do. One thing it can't do is contain an ORDER BY clause. This limitation exists not only on Oracle, but also on the Sybase, Microsoft, and InterBase platforms.

When you create an updatable view, you can tell the server to ensure that rows that are updated or added using the view meet the selection criteria imposed by the view. That is, you can ensure that an updated or added record doesn't "go out of scope"—doesn't vanish from the view after it's changed or added. You do this by using the WITH CHECK OPTION clause of the CREATE VIEW command. Here's the syntax: CREATE VIEW MOCUSTOMERSU AS SELECT * FROM CUSTOMER WHERE State='MO' WITH CHECK OPTION Any updates or inserts to this view that specify a State column that's not equal to `MO' will fail. Stored Procedures A stored procedure is a compiled SQL program, often consisting of many SQL statements, that is stored in a database with other database objects. You create stored procedures using the CREATE PROCEDURE command. Here's an example of the Oracle syntax: Page 443 CREATE OR REPLACE PROCEDURE increaseprices AS BEGIN UPDATE ITEM set Price=Price+(Price*.05); END; If the procedure receives parameters from the caller, the syntax changes slightly. Here's the Oracle syntax: CREATE OR REPLACE PROCEDURE increaseprices (increasepercent number) AS BEGIN UPDATE ITEM set Price=Price+(Price*(increasepercent /100)); END; Scripts It's a good idea to construct Data Definition Language (DDL) statements, including stored

procedures, using SQL script files. You can create these scripts using a text editor; most good SQL editors support saving their contents to disk. Remember that these scripts must include any necessary CONNECT and SET TERM statements. You can execute Oracle SQL scripts by clicking the Run an ISQL Script option on SQL*PLUS's File menu. Listing 16.1 shows an example of such a script file. Listing 16.1. A stored procedure that resides in an SQL script file. CONNECT SCOTT/TIGER SPOOL PRICE PROMPT BUILDING PROCEDURE increaseprices CREATE OR REPLACE PROCEDURE increaseprices (increasepercent number) IS BEGIN UPDATE ITEM set Price=Price+(Price*(increasepercent /100)); END; / SPOOL OFF EXIT; Notice the use of the SPOOL command to redirect output from the script to a file. Also note the use of the PROMPT command to display text on the user's screen. It's a good idea to lace the scripts you write with messages like this in order to keep the user informed. Running Stored Procedures You run Oracle stored procedures using syntax like the following: Increaseprices(5); Page 444 Triggers Not unlike stored procedures, triggers are SQL routines that are activated when data in a given table is inserted, updated, or deleted. You associate a trigger with a specific operation on a table: a row insertion, update, or deletion. Here's an example using Oracle SQL: CREATE TRIGGER SALEDelete BEFORE DELETE ON CUSTOMER

BEGIN DELETE FROM SALE WHERE CustomerNumber=OLD.CustomerNumber; END; This trigger deletes the sales made to a customer from the SALE table when the customer's record is deleted from the CUSTOMER table. This sort of delete is known as a cascading delete: A delete operation on one table cascades through others using a common key. Note the use of the OLD context variable. The OLD context variable references the current column values in a row prior to an UPDATE or DELETE operation. The NEW context variable references the new value of the columns about to be inserted or applied through an UPDATE. Note also the use of the BEFORE keyword. A trigger can be activated before or after an INSERT, UPDATE, or DELETE. Cursors In SQL, you use cursors to process rows individually. Cursors enable you to work with tables one row at a time. Because the BDE automatically creates and maintains cursors for you, you generally won't create your own cursors using SQL. However, you may find them handy in stored procedures. There are four basic operations you can perform on cursors: You can declare them, open them, fetch from them, and close them. You can also use a cursor to UPDATE and DELETE rows in its base table. A cursor declaration consists of a SELECT statement and, for updatable cursors, a list of updatable columns. Here's the syntax: DECLARE CUSTOMER_SELECT CURSOR FOR SELECT * FROM CUSTOMER Before you can retrieve rows using the cursor, it must be opened. You use the OPEN command to initiate the query that makes up the cursor declaration: OPEN CUSTOMER_SELECT OPEN doesn't actually retrieve any rows back to the client application. You must use FETCH for that. Here's the syntax: FETCH CUSTOMER_SELECT

Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 439 Self-Joins Aside from joining to other tables, a table can be joined with itself. This type of join is known as a reflexive join or self-join. Consider the following query: SELECT S.CustomerNumber, S.Amount, (S.Amount / SUM(S2.Amount))*100 Percentage FROM SALE S, SALE S2 WHERE S.CustomerNumber=S2.CustomerNumber GROUP BY S.CustomerNumber, S.Amount The purpose of this query is to list each sale made to a customer, comparing the amount of each sale to the total sales made to the customer. The only way to perform this type of query in a single SELECT statement is through a self-join. The individual statistics for each customer are gathered and grouped as one would expect, then the SALE table is joined back to itself to retrieve the total sales for each customer. The amount of the current sale is then divided by this total and converted to a percentage for placement in the result set. Theta Joins A theta join merges the rows from two tables using comparison operators other than equality operators, usually the not-equal (<>)operator. The following is an example of a theta join; it's a different spin on the query used to demonstrate self-joins: SELECT C.CustomerNumber, S.Amount, Sum(S2.Amount) OTHERS FROM CUSTOMER C, SALE S, SALE S2 WHERE C.CustomerNumber=S.CustomerNumber AND C.CustomerNumber<>S2.CustomerNumber GROUP BY C.CustomerNumber, S.Amount Actually, this query contains two joins. First, it joins the CUSTOMER and SALE tables in order to retrieve the sales made to each customer. Second, it uses a theta join to retrieve a total of all the sales not made to each customer. Because the query is using two different types of joins to link the same table, it uses two separate table

aliases for the SALE table. Cartesian Products A Cartesian product is the product of all the rows in one table multiplied by the rows in another. Such a result set is usually an accident, the result of missing or improper joins between tables. Here's an example: SELECT SALE.SaleNumber, ITEM.ItemNumber FROM SALE, ITEM ORDER BY SaleNumber, ItemNumber Because of its exponential-like nature, a Cartesian product can return a surprisingly large number of rows from relatively small tables. For example, two tables of 100 rows each yield a Cartesian product of 10,000 rows. As a rule, Cartesian products are to be avoided, especially when dealing with large tables. Page 440 Subqueries A subquery is a SELECT statement within the WHERE clause of another SELECT. Generally, you use a subquery to return a list of values that you then test the query against. Here's an example: SELECT * FROM CUSTOMER WHERE CustomerNumber IN (SELECT CustomerNumber FROM SALE) GROUP BY Because SQL is a set-oriented query language, statements that group data are the bread and butter of the language. A single SQL statement can do what 10 or even 50 lines of traditional record-oriented program code can. The SELECT statement's GROUP BY clause and SQL's aggregate functions are responsible for pulling off this magic. Here's an example of how GROUP BY works: SELECT CUSTOMER.CustomerNumber, sum(SALE.Amount) TotalSale FROM CUSTOMER, SALE WHERE CUSTOMER.CustomerNumber=SALE.CustomerNumber GROUP BY CUSTOMER.CustomerNumber This query returns a list of all customers along with the total amount of each customer's sales. How do you know which fields to include in the GROUP BY clause? Oracle (and ANSI SQL) forces you to GROUP BY all the non-aggregate columns listed in the column list of the SELECT statement. HAVING The SELECT statement's HAVING clause is used to limit the rows returned by a GROUP BY clause. Its relationship to the GROUP BY clause is similar to the relationship between the WHERE clause and the SELECT statement itself. The HAVING clause works like a WHERE clause on the rows in the result set rather than on the

rows in the query's tables. There is usually a better way of qualifying a query than using a HAVING clause. In general, HAVING is less efficient than WHERE because it qualifies the result set after it has been organized into groups, whereas WHERE does so beforehand. Here's an example of an appropriate use of the HAVING clause: SELECT CUSTOMER.LastName, COUNT(*) NumberWithName FROM CUSTOMER GROUP BY CUSTOMER.LastName HAVING COUNT(*) > 1 The best use of HAVING is to restrict rows returned from the query based on the results of an aggregate calculation. In this case, WHERE is unable to handle the task because the information doesn't exist until after the query has executed and its aggregate columns have been computed. Page 441 ORDER BY You use the ORDER BY clause to order the rows in the result set. Here's an example: SELECT * FROM CUSTOMER ORDER BY State Without the use of ORDER BY, there's no guarantee that rows will be returned in a particular order. Even the exact same SELECT statement, issued at two different times, can return result sets that are ordered differently. Column Aliases You might have noticed that I use logical column names for aggregate functions such as COUNT() and SUM(). Labels such as these are known as column aliases and serve to make the query and its result set more readable. In Oracle SQL, you place a column alias immediately to the right of its corresponding column in the SELECT statement's field list. For example, in the following query, the column alias of the COUNT() aggregate is the NumberWithName label: SELECT CUSTOMER.LastName, COUNT(*) NumberWithName FROM CUSTOMER GROUP BY CUSTOMER.LastName HAVING COUNT(*) > 1 You can use column aliases for any item in a result set, not just aggregate functions. For example: SELECT CUSTOMER.LastName LName, COUNT(*) NumberWithName FROM CUSTOMER GROUP BY CUSTOMER.LastName This query substitutes the column alias LName for the LastName column in the result set. Note that you can't use

aliases in other parts of the query such as the WHERE or GROUP BY clauses. You must use the actual column name or value in those parts of the SELECT statement. Table Aliases Rather than having to specify the full name of a table each time you reference it in a SELECT command, you can define an abbreviation for it instead. You do this by specifying a table alias for the table in the FROM clause of the SELECT statement. Place the alias to the right of the actual table name, as illustrated here: SELECT C.LastName, COUNT(*) NumberWithName FROM CUSTOMER C GROUP BY C.LastName Notice that the alias can be used in the field list of the SELECT list before it is even syntactically defined. This is possible because references to database objects are resolved before a query is executed. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 451 If you're able to ping using the server's IP address but not its host name, chances are its host name is either missing from your HOSTS file or references a different IP address. In either case, fixing your HOSTS file should solve the problem. 6. If you have no luck pinging the host machine using either its host name or its IP address, you probably have a network problem. Be sure to check obvious things such as whether the IP address you have for the server is correct. Try pinging the TCP/IP loopback address, 127.0.0.1, to see whether your TCP/IP stack is working properly. This address loops back to your machine, enabling you to ping yourself. If it fails, you have a serious problem with your protocol configuration. You might need to consult your network administrator for help with your network connection. 7. If WISQL fails but PING works fine, attempt to telnet into the server machine using the TELNET utility that accompanies both Windows 95 and Windows NT. The syntax for TELNET is the following: TELNET 100.100.100.100 or TELNET hostname where 100.100.100.100 is the TCP/IP address of the host computer or hostname is its name. If TELNET fails, you might have a problem with the inet daemon on the server machine. 8. If TELNET succeeds but WISQL still fails, make sure, first of all, that the InterBase server software is installed correctly and running on the host computer. Second, check to see that required entry in the TCP SERVICES file is present. It should look like this: gds_db 3050/tcp

9. If these steps fail to resolve your problem, you should consult with your network administrator or your database administrator for further assistance.

SQL Primer
The primary means developers use to communicate with database servers is SQL. This section introduces you to InterBase's own rich SQL dialect and touches on a few of the features that distinguish it from the SQL implementations of other vendors. I assume that your server is running and that you know how to connect to it using InterBase's WISQL utility. Creating a Database You may already have a database in which you can create some temporary tables for the purpose of working through the examples in this chapter. If you don't, creating one is easy enough. In SQL, you create databases using the CREATE DATABASE command. The exact syntax varies from vendor to vendor; here's the basic InterBase syntax: CREATE DATABASE `C:\DATA\IB\SALES' USER SYSDBA masterkey; Page 452 Because InterBase's WISQL utility can't prepare the CREATE DATABASE command for execution, you use the Create Database option from its File menu instead. Follow these steps to create a database using WISQL: 1. Start the Interactive SQL tool (WISQL) from either your InterBase program group or from the Task menu of the InterBase Server Manager. 2. Select the Create Database option from the File menu. 3. Type the full pathname of the database you want to create (for example, C:\DATA\IB\SALES. GDB) into the Database field of the Create Database dialog. You may want to create a special directory in advance in which to store your databases. 4. Supply a username and password. The default username is SYSDBA; unless you've changed it, masterkey is SYSDBA's password. 5. Click the OK button. InterBase should create the database and connect you to it. After the database is created, you can use the Connect to Database option from the File menu to connect to the database without creating it first. You can specify special options in the Create Database dialog's Database Options box. For example, you can set the size in bytes of the pages that will make up the database. The default is 1024 (1K), but you can change this to 2048, 4096, or 8192 (that is, PAGE_SIZE=2048). You can also specify additional secondary files to create along with the database's primary file. You can specify initial lengths for these files as well as for the primary file. PASSWORD

TIP Even though WISQL doesn't support the use of the CREATE DATABASE command, its command line cousin, ISQL, does. ISQL allows you to execute many commands that can only be accessed via menus in WISQL. You'll find ISQL in the C:\Program Files \Borland\IntrBase\Bin directory by default.

Shadows InterBase supports a special type of object, known as a shadow, that allows you to mirror a database onto a second drive or drives. This helps ensure that the system will remain available in the event of a catastrophic failure of the database or the drives on which it resides. You create shadows using the CREATE SHADOW command. When CREATE SHADOW is issued, it constructs a shadow for the most recently connected database. Here's the syntax: CREATE SHADOW 1 "sales.shd" LENGTH 5000; You must supply a unique shadow number as the command's first parameter. If you omit the number or it isn't unique, InterBase won't create the shadow. Page 453 Because a shadow mirrors its associated database, adding a shadow doubles the number of physical writes that must occur when changes are made in the database. Be careful of where you place the shadow file. Placing it on an extremely slow drive can impact system performance. CREATE SHADOW supports a special parameter for configuring what is to occur if the shadow becomes unavailable for some reason. AUTO specifies that client apps should continue to be able to connect and that the shadow should simply be deleted. MANUAL indicates that all client connections to the database should be refused until the shadow becomes available or it's dropped from the database using the DROP SHADOW command. CONDITIONAL specifies that connections to the database are to continue to be permitted and that a new shadow is to be created. If a database fails and its shadow takes it place, the CONDITIONAL setting will cause the newly promoted shadow-database to itself to be shadowed. Here's another example: CREATE SHADOW 1 CONDITIONAL "d:\shadows\sales.shd" LENGTH 5000; Note the use of the CONDITIONAL keyword to stipulate what happens when the shadow becomes unavailable. The CONNECT Command

You use the InterBase CONNECT command to connect to an already existing database. The command syntax is CONNECT ServerAndDatabasePath USER "ValidUser" PASSWORD "UserPassword" Replace ServerAndDatabasePath with the full path to the server and database file to which you want to connect. If you're connecting to a local server, specify only the database pathname. Replace ValidUser and UserPassword with a valid username and password on your database server. Here's an example of a CONNECT to a local database: CONNECT "C:\DATA\IB\SALES.GDB" USER "SYSDBA" PASSWORD "masterkey"; Each time you CONNECT to another database, you disconnect from your current one. Use the DISCONNECT command to explicitly break a database connection established with the CONNECT command, as in the following: DISCONNECT ALL;

You can replace ALL with the word DEFAULT to accomplish the same thing. In WISQL, you use the Connect to Database and Disconnect from Database File menu options to connect to and disconnect from databases (the CONNECT and DISCONNECT commands don't work in WISQL, though you can use them in ISQL). Creating Tables After a database connection has been established, you're ready to begin building database objects. Virtually any relational database concept can be demonstrated with a maximum of three Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 454 tables. For the purpose of working through this chapter, begin by creating the following three tables. Tables are created by using the SQL CREATE TABLE statement. Enter the following command in WISQL to create the CUSTOMER table: CREATE TABLE CUSTOMER ( CustomerNumber int LastName char(30), FirstName char(30), StreetAddress char(30), City char(20), State char(2), Zip char(10) ) Next, build the SALE table using this command: CREATE TABLE SALE ( SaleNumber int NOT NULL, SaleDate date, CustomerNumber int NOT NULL, ItemNumber int NOT NULL, Amount float ) Now that the SALE table is built, only one table remains. Create the ITEM table using this command:

NOT NULL,

CREATE TABLE ITEM ( ItemNumber Description Price ) External Tables

int char(30), float

NOT NULL,

InterBase supports some interesting variations of the CREATE TABLE command. One alternate form of the command allows you to create tables that are external to the database, like so: CREATE TABLE SALE_HISTORY EXTERNAL FILE `C:\DATA\IB\SALEHIST.DAT' ( SaleNumber int NOT NULL, SaleDate date, CustomerNumber int NOT NULL, ItemNumber int NOT NULL, Amount float ) If the external file you specify doesn't exist, InterBase creates it. If the file already exists, it's left untouched. One nifty use of this command is to reference data created outside InterBase, for example, a table dump from another DBMS. By specifying an InterBase table structure that matches the format of the external data, you can copy rows from the external file to an internal table using the INSERT...SELECT command. Page 455 Computed Columns Another nuance of the InterBase CREATE TABLE command is that it allows you to define computed columns. For example, here's a variation of the ITEM table that includes a computed column for government pricing: CREATE TABLE ITEM ( ItemNumber Description Price GovernmentPrice COMPUTED BY

int NOT NULL, char(30), float, (Price _ Price * .15)

) In this case, the GovernmentPrice column computes a 15 percent discount from the Price column for government customers. Because no data type was specified, InterBase infers one from the involved columns. Columns referenced by computed columns must appear before them in CREATE TABLE's list of columns. The ability to create computed columns is similar to what you could do in a view or standard SELECT statement but embeds the calculation directly into the table definition. This is a better method of ensuring that a computational business rule is enforced since client apps and other SQL objects can simply query the computed column. Array Columns In addition to simple data types, InterBase allows you to define columns that are actually arrays of a given data type. Defining a column as an array allows you to treat data elements that are naturally grouped together as a set. Here's a variation of the CUSTOMER table that includes an array column: CREATE TABLE CUSTOMERARRAY ( CustomerNumber LastName FirstName StreetAddress City State Zip )

int char(30), char(30), char(30)[3], char(20), char(2), char(10)

NOT NULL,

Notice the [3] to the right of the StreetAddress column definition. This defines StreetAddress as a three-element array of char(30). You might use this, for example, to support multiple-line street addresses. You can also explicitly define the minimum and maximum bounds for array columns. Here's an example: CREATE TABLE CUSTOMERARRAY ( CustomerNumber LastName

int char(30),

NOT NULL,

Page 456 FirstName StreetAddress City State Zip ) char(30), char(30)[1:3], char(20), char(2), char(10)

In this case, StreetAddress is again a single-dimension, three-element array, but its lower bound is 1 (rather than the default of 0), and its upper bound is 3 (rather than 2). Array columns can also be multidimensional; up to 16 dimensions are supported. Here's an example: CREATE TABLE HOURLY_SAMPLES ( SampleNo int NOT NULL, SampleDate DATE NOT NULL, WeeklySampleValues int [7,24] ) In this example, WeeklySampleValues is a two-dimensional array capable of storing values for every hour of the day for an entire week. CAUTION Consider all the ramifications of implementing array columns before using them pervasively in your databases. Array columns are essentially repeating groups. Technically speaking, they violate First Normal Form as far as relational database normalization is concerned. Array columns require extra work to assign (you cannot assign array column values using SQL), to format (on reports, for example), and to query. There are also a number of restrictions placed on array columns that don't apply to other data types (they can't be passed into or returned from stored procedures, for example). Carefully weigh the pros and cons of introducing unwieldy elements such as these into your database schemas before doing so.

Special Column Defaults In addition to defining constant values as column defaults, InterBase also allows you to use

three special column default keywords: USER, TODAY, and NOW. USER allows you to assign a column's default value to the current user's name. TODAY allows you to set a column's default value to the current date (this is analogous to the Delphi Date function). NOW allows you to set up the current date and time as a column's default (this coincides with the Delphi Now function). Here's an example using all three: CREATE TABLE REPORTLOG (ReportUser VARCHAR(20) DEFAULT USER, ReportDate DATE DEFAULT "TODAY", ReportDateTime DATE DEFAULT "NOW") Page 457 Notice how "TODAY" and "NOW" are required to be enclosed in quotes, whereas USER can't be. If you enclosed USER in quotes, the constant value "USER" would become the column's default rather than the name of the current user. Adding and Dropping Table Columns You use the SQL ALTER TABLE command to add and drop columns from an existing table. Not all servers support dropping columns after the fact (Microsoft SQL Server doesn't, for example), but InterBase does. Use the following syntax to add a column: ALTER TABLE CUSTOMER ADD PhoneNumber char(10) Use the following syntax to drop it: ALTER TABLE CUSTOMER DROP PhoneNumber Note that you can't add a NOT NULL column to a table that already has rows because it would necessarily have to allow NULLs immediately after being added to the table. Constraints A constraint is the mechanism by which you limit, or constrain, the type of data a column may store. A constraint can also be used to define a default value for a column. Constraints can be defined when a table is first created using the CREATE TABLE command or afterward using the ALTER TABLE command. Here's an example of a primary key constraint: ALTER TABLE CUSTOMER

ADD PRIMARY KEY (CustomerNumber) This syntax adds a primary key constraint to the CUSTOMER table, defining its CustomerNumber field as the table's primary key. This causes a unique index to be created over the table using the CustomerNumber column as the key. Note that you cannot define a column that accepts NULL values as a table's primary key. A foreign key constraint defines a column in one table whose values must exist in a second, or foreign, table. A foreign key doesn't uniquely identify rows as a primary key does. On the contrary, its key columns must be a primary or unique key in the table that it references. Adding a foreign key constraint to a table causes InterBase to automatically build a secondary index over its key columns. The following is an example of the syntax: ALTER TABLE SALE ADD FOREIGN KEY (CustomerNumber) REFERENCES CUSTOMER This constraint defines the CustomerNumber field in the SALE table as a foreign key that references the same column in the CUSTOMER table. This means that customer numbers entered into the SALE table must first exist in the CUSTOMER table. It also means that customer numbers that are being used in the SALE table cannot be deleted from the CUSTOMER Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 458 table. This capability to enforce the relationship between the two tables by merely declaring their relationship via SQL is called declarative referential integrity. The term simply means that the integrity of the relationship between the tables is ensured by defining (or declaring) it, not by user program code. A third type of constraint is one that checks a column against a list of predefined values. Here's an example of such a constraint: ALTER TABLE CUSTOMER ADD CONSTRAINT INVALID_STATE CHECK (State in (`OK','AR','MO')) Note the negative vantage point of the naming convention used for the constraint. This is done so front-end tools that report the constraint name will give a somewhat meaningful message to the user. If the message were to read VALID_STATE, the user might wonder what the problem is. By using a simple message as the name of the constraint, you allow for the possibility that the message might actually give the user a hint as to what the problem is. This might also save you the effort of having to replace the Delphi exception generated due to the constraint with your own. Testing Constraints You should test every constraint that you place on a database. You do this by attempting to add values to the database that the constraint is supposed to disallow. For example, to test the preceding INVALID_STATE constraint, enter this command in WISQL: INSERT INTO CUSTOMER (CustomerNumber, State) VALUES (123,'CA') Because the constraint limits states entered to `OK', `AR', and `MO', it should reject your attempted row insertion with an error. If a constraint you've defined fails to function as expected, verify that you successfully added it in the first place and that it's checking the data in the way you intended.

Creating Indexes You create indexes in InterBase SQL using the CREATE INDEX command. Here's the basic syntax: CREATE INDEX SALE02 ON SALE (SaleDate) SALE02 is the name of the new index, SALE is the name of the table on which to build the index, and SaleDate is the index key. Note that InterBase index names must be unique across the database in which they reside. You can create an index that prohibits duplicates by using the CREATE UNIQUE INDEX variation of the command, as in the following: CREATE UNIQUE INDEX SALE01 ON SALE (SaleNumber) Page 459 Index keys, by default, are arranged in ascending order. InterBase also supports creating descending indexes using the DESCENDING keyword. For example, CREATE DESCENDING INDEX SALE03 ON SALE (Amount) This helps queries like the following execute more quickly: SELECT * FROM SALE ORDER BY Amount DESCENDING Activating and Deactivating an Index InterBase supports a useful mechanism for taking an index offline to speed updates to its base table. You do this by deactivating the index, then reactivating (and thereby rebuilding) it later. This capability allows for quick addition of a large number of rows without incurring the penalty of index maintenance with each new row. Here's the syntax: ALTER INDEX SALE02 INACTIVE To reactivate it, use the following: ALTER INDEX SALEO2 ACTIVE Reactivating an index causes it to be rebuilt. Note that the deactivation of an index is delayed until it's no longer in use—this includes use by a primary or foreign key constraint. To deactivate an index used by a constraint, first drop the constraint, then deactivate the index. Inserting Data

The SQL INSERT statement is used to add data to an InterBase table. You can add data one row at a time using INSERT's VALUES clause, or you can insert several rows at once by selecting them from another table. Use the following syntax to add data to each of the three tables. First, add three rows to the CUSTOMER table by executing the following commands separately in WISQL: INSERT INTO CUSTOMER (CustomerNumber, LastName, FirstName, ÂStreetAddress, City, State, Zip) VALUES(1,'Doe','John','123 Sunnylane','Anywhere','MO','73115') INSERT INTO CUSTOMER (CustomerNumber, LastName, FirstName, ÂStreetAddress, City, State, Zip) VALUES(2,'Doe','Jane','123 Sunnylane','Anywhere','MO','73115') INSERT INTO CUSTOMER (CustomerNumber, LastName, FirstName, ÂStreetAddress, City, State, Zip) VALUES(3,'Philgates','Buck','57 Riverside','Reo','AR','65803') Now, add three rows to the ITEM table with these commands: INSERT INTO ITEM (ItemNumber, Description, Price) VALUES(1001,'Zoso LP',13.45) Page 460 INSERT INTO ITEM (ItemNumber, Description, Price) VALUES(1002,'White LP',67.90) INSERT INTO ITEM (ItemNumber, Description, Price) VALUES(1003,'Bad Co. LP',11.45) Finally, add four to the SALE table using these statements: INSERT INTO SALE (SaleNumber, SaleDate, CustomerNumber, ItemNumber, Amount) VALUES(101,'10/18/90',1,1001,13.45) INSERT INTO SALE (SaleNumber, SaleDate, CustomerNumber, ItemNumber, Amount) VALUES(102,'02/27/92',2,1002,67.90) INSERT INTO SALE (SaleNumber, SaleDate, CustomerNumber, ItemNumber, Amount) VALUES(103,'05/20/95',3,1003,11.45) INSERT INTO SALE (SaleNumber, SaleDate, CustomerNumber, ItemNumber, Amount) VALUES(104,'11/27/97',1,1002,67.90) Note that you don't have to include all the columns or follow the order in which they appear in the table when you specify a column list, but the list of values you specify must match the content and order of the column list. Here's an example:

INSERT INTO ITEM (Price, ItemNumber) VALUES(13.45, 1001) Special Values As with column defaults, InterBase supports the use of the USER, TODAY, and NOW special column values. Here's an example: INSERT INTO REPORTLOG VALUES(USER, "TODAY","NOW") The UPDATE Command You use the SQL UPDATE command to change data that's already in a table. UPDATE can include a WHERE clause to qualify which rows to update. Here's the syntax: UPDATE CUSTOMER SET Zip='65803' WHERE City='SpringField' Though an UPDATE's WHERE clause might cause it to change only a single row, depending on the data, you can update all the rows in the table by omitting the WHERE clause: UPDATE CUSTOMER SET State='MO' You can also update a column using other columns in its host table. You can even use the column itself. Let's say you wanted to increase the price of each item in the ITEM table by seven percent. You could issue this UPDATE command to get the job done: UPDATE ITEM SET Price=Price+(Price*.07) Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 483

CHAPTER 18

Delphi on Sybase SQL Server
Page 484 In this chapter, we'll explore the Sybase SQL Server RDBMS. I'll touch on issues specific to using SQL Server and Delphi together as well as delve into SQL Server's Transact-SQL dialect.

Starting the Server
The exact method of starting Sybase SQL Server varies from platform to platform. On UNIX, you usually run a script named RUN_servername. On Windows NT, you use the Sybase Services Manager to start and stop the server. You'll find this facility in your Sybase SQL Server program group or folder. You can also use the Services applet in the Windows NT Control Panel to start and stop SQL Server. Because SQL Server runs as a Windows NT service, you can start and stop it via the Services option in the Windows NT Server Manager app as well. In addition to managing SQL Server as a Windows NT service, you can also use the Transact-SQL SHUTDOWN command to stop an SQL Server when necessary. SHUTDOWN offers a NOWAIT option to stop a server immediately. Normally, the command waits for running processes to finish

before stopping the server.

Getting Connected
To connect from Delphi apps on Windows 95 or Windows NT client machines to a Sybase server, you need to install Sybase's Net Library software. There are two versions of this software for the Windows 95/NT family, and they are not the same. The version for Windows NT (it comes as part of Sybase's Open Client for Windows NT package) installs only for NT; it will not install or work under Windows 95. Attempting to install it under Windows 95 will generate an error 422 message. The second version installs under both Windows 95 and Windows NT. You know you have the dual-purpose version of the software because its splash screen says that it works for either OS. This version of the software is the later of the two and is the preferred one to use because it works with both operating systems.

Net Library/Open Client Error 422 Under NT
The Windows NT_only version also has the dubious distinction of sometimes not installing correctly under NT. As of early 1996, Sybase's installation program often generated an error 422 message the first time through the installation process—even under Windows NT. It turns out that the solution is simple. Despite the fact that the Sybase Release Bulletin accompanying the software instructs you to empty your Windows TEMP directory before installing, you need Page 485 to do just the opposite. If the software crashes with error 422 the first time you run it, it leaves a handful of temporary files in your TEMP directory. Don't delete them. Run the installation again, and all will go well.

Configuring the Client
Whichever version of the software you end up using, you'll use Sybase's SQLEDIT program to edit your connection information. Here's a quick rundown of how to set up a server connection by using SQLEDIT: 1. Start SQLEDIT and type the name you want to use for your database

2. 3. 4. 5.

6.

server into the Input Server Name box. Click on the Add button to add it to the Server Entry list. Click on the Service Type drop-down list and select Query from the list. In the Platform list, select Windows 95 or NT, depending on which one you're running. Select the appropriate Net-Library driver from the Net-Library Driver drop-down list. Most likely, this is either the NLWNSCK (TCP/IP Winsock) or NLNWLINK (IPX) driver. Type your database server's network address and port number (separated by commas) into the Connection Information/Network Address box. This is a TCP/IP address if you are using the TCP/IP protocol and an IPX network address if you're using IPX. Here's a sample connection string: 100.10.15.12,3000 In this example, 100.10.15.12 is the TCP/IP address, and 3000 is the port number. Consult with your network or system administrator if you're unsure of your database server's address or port number.

NOTE You can also use HOSTS file entries in place of actual TCP/IP addresses in your server connection strings. For example, assuming you had a HOSTS entry of this: 100.10.15.12 marketing

you can use the following: marketing,3000 for your connection string.

7. After you set up the connection information, click on the Add Service button to add the service to the Connection Service Entry list. Page 486 8. Once the service is added, you're ready to test it. Click on the Ping button in the lower right of the screen. Ping attempts to open, then

close, a connection with the server you specify. If you properly configure your client, it should succeed. 9. Now press Ctrl+S to save your connection information, then exit SQLEDIT. NOTE These same instructions apply whether you configure a server connection when you install the software or by running SQLEDIT separately.

SYBPING You can also use Sybase's SYBPING utility to test your server connections. It should be located in your SYBASE program folder. Simply click on the name of your server in the list presented by SYBPING, then click on the Ping button. If your machine can "see" the server in question, all is well. Win 3.x Drivers If you still have 16-bit clients (Win 3.x) that you must support, here are a few tips that may save you some grief:
q

q

All releases of Sybase SQL Server prior to System 10 use Sybase's DBLibrary to establish client/server connections. System 10 and later releases use CT-Library. Although you can connect from Win 3.x clients to System 10 servers using DB-Library, you likely won't have access to System 10-specific features. Delphi 1.0 connects using DBLibrary; Delphi 2.0 and later can connect using either DB-Library or CT-Lib. Be sure you know which library is in use on a client before you attempt to configure it. All versions of DB-Library are not created equal. Some work better than others. I've had the best luck with the one dated 3-1-94, although you may find that a different one works best for you. One sure sign that you need to check your DB-Library version is data type conversion problems. For example, I once experienced a problem with date fields when using Delphi 1.0 and Sybase's June 1994 DB-Library. That particular DB-Library appended the time portion of a datetime value twice when converting from a string data type. If you have odd conversion-related problems when running Windows 3.x applications against your Sybase server(s), you might try a different DB-Library version.

q

Versions of SQL Server prior to System 10 used a different method of canceling queries. These versions relied on Out-Of-Band Data (OOBD) support in order to cancel a query and properly discard its unretrieved results. OOBD support is built Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 481 3. When the validation process has finished, click the Repair button to mark/repair corrupted structures. 4. If you experienced checksum errors, select the Database Validation option again, this time with the Ignore checksum errors option checked. 5. When the process is done, click the Repair option to repair any new errors that are discovered. 6. Run the Database Validation facility again, this time with the Read-only validation option selected. You'll notice that free pages are no longer reported, and broken records are marked as damaged. Any records marked as damaged are excluded from backups you make with Server Manager. 7. Back up your repaired database using Server Manager's backup facility. Damaged records will be permanently lost since they're excluded from the backup. 8. Restore your database from the backup you just made. This will rebuild certain internal structures and should remove any corruption from the database. 9. Run the Database Validation facility one last time to verify that the database has indeed been repaired. Select the Read-only validation option when you check the database. Database Statistics This Server Manager option and those that follow require an active database connection. In addition to being logged in to the server, you must also be connected to a database. Select the File | Database Connect menu option in Server Manager to connect to a database.

Selecting Server Manager's Tasks | Database Statistics menu option displays statistical and background information about a database. It lists such items as the database's creation date, its oldest active transaction, the database's page size, and the number of shadows defined for it. Some of this same information is available via Server Manager's Maintenance | Database Properties menu option. Figure 17.6 shows a sample Database Statistics window. Figure 17.6. The Database Statistics window lists basic information about a database.

Page 482 When the Database Statistics window is onscreen, you can select the Database Analysis option on the View menu to see the number of pages allocated to the database and their fill distributions. This can help you understand how your database's design affects the physical storage of its data. Database Sweeping Sweeping a database (Maintenance | Database Sweep) frees up space held by out-of-date versions of records and by records rolled back within transactions. You can think of Server Manager's sweep facility as a database garbage collector. Although you don't have to shut down a database to sweep it, you should still do so when it will least impact users. Sweeping a large database can slow down the server dramatically. Transaction Recovery Whenever you invoke a transaction that spans multiple databases, InterBase automatically sets up a two-phase commit process to ensure that either all the databases are changed or none of them is. Two-phase commits can fail if a network connection is broken or a disk crashes that prevents access to one or more databases. The Maintenance | Transaction Recovery menu option is designed to deal with the transactions left in limbo by failed two-phase commit routines. It allows these transactions to be recovered—to be rolled back or

rolled forward, depending on what's appropriate.

Summary
You've just been on a whirlwind tour of the InterBase DBMS. You learned how to start the InterBase server, how to connect to it, and how to troubleshoot connection problems. You also received a thorough introduction to InterBase's brand of SQL, and you learned how to perform basic database administration tasks. InterBase is the type of subject that merits books of its own, but hopefully the crash course presented here will make life easier for you as you build Delphi applications that interface with InterBase.

What's Ahead
Chapter 18, "Delphi on Sybase SQL Server," takes you through the many issues involved with producing Delphi apps that connect to Sybase database servers. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 477 Figure 17.1. Use Server Manager to back up your databases.

Managing User Accounts Before you can connect to an InterBase database, you must have a valid username and password. You use the Server Manager's Task | User Security menu option to create new user accounts. Follow these steps to add a new InterBase user account: 1. 2. 3. 4. 5. 6. Start the Server Manager program. Click File | Server Login and log in to the server. Click Tasks | User Security to display the InterBase Security dialog. Click the Add User button to begin a new user definition. Specify a username and password for the new user. Click OK to add the new user. Figure 17.2 illustrates this.

Figure 17.2. Add new InterBase users in Server Manager's User Configuration dialog.

Page 478 After you've added a user account, that account can connect to any InterBase database. However, you'll still have to grant specific rights to the new account (or to PUBLIC) in order for the account to access database objects. Server Configuration There are a couple of ways to access the InterBase Server Configuration dialog. You can get to it via the Server Manager's Task | Server Config menu option or you can access it by right-clicking the InterBase server icon on the Windows taskbar and selecting Properties. The Server Configuration dialog divides InterBase's server parameters into two classes: IB (InterBase) Settings and OS (Operating System) Settings. IB Settings consist of two parameters: Database Cache and Client map size. Database Cache specifies the number of pages to reserve for caching databases. It's conceivable that all databases could end up residing in RAM if the cache is set high enough. Of course, this could bring about a dramatic performance improvement. Setting the cache too high will cause the operating system to swap InterBase pages to disk (page faults), reducing overall server performance. Client map size specifies the size of the buffer to allocate for each client connection. You shouldn't normally need to change this, though you might want to increase it in situations where you know you're transferring large chunks of data (for example, BLOBs) to and from a client app. OS Settings consists of three items: Min Process Working Set, Max Process Working Set, and Process Priority Class. Min and Max Process Working Set controls how much physical memory (in kilobytes) is dedicated to the server. Increasing the maximum setting can allow for larger Database Cache settings, but setting it too high can cause page faults. If your server runs on Windows NT, you can gauge whether or not the working set parameters need adjustment using the NT Performance Monitor tool. To determine whether Min and Max Process Working Set need adjustment, follow these steps: 1. With your InterBase server already started, start the NT Performance Monitor tool (preferably on the same machine). 2. Start a new chart and press Ctrl+I to add a new counter to the chart.

3. Select Object: Process, Instance: IBSERVER, Counter: Page File Bytes, and click the Add button. 4. Add counters for IBSERVER's Page Faults/sec and % Processor Time indicators as well. (See Figure 17.3.) If a large number of Page Faults are occurring or if the % Processor Time indicator is less than 50 percent, you might want to increase the Process Working Set range in order to improve server performance. Changes you make to the Process Working Set parameters will take effect the next time you start your server. Page 479 Figure 17.3. You can use NT's Performance Monitor to measure InterBase RAM requirements.

Process Priority Class affects the priority of the InterBase server process among the other processes currently running on the server machine. Changing the priority class to High may yield a performance boost, especially on machines dedicated as InterBase servers and in situations where a large number of client apps are active on the server machine. This setting takes effect the moment the Apply button is clicked. Viewing Lock Statistics Because of the pessimistic concurrency control mechanisms employed by client/server DBMSs, it's not unusual for support issues to arise relating to resource contention and locking. InterBase, like most multi-user DBMSs, locks resources to keep one user from overwriting another user's changes. If two users appear to be blocking one another (a situation known as a deadlock), you might need to inspect the server's resource locks to ensure they're as they should be. You view InterBase locks by clicking the Server Manager's Tasks | Lock Manager Statistics menu option. (See Figure 17.4.) The InterBase lock manager makes use of a lock table to track resource locks on the server. This table stores status information for all the server's resource locks. The LOCK_HEADER BLOCK (at the top of the listing) lists system-wide summary information, including the lock table's current and maximum size, the remaining free

locks, the total number of deadlocks, and so on. The detail section of the window lists process-level information, including which processes own which locks, which locks have been granted and which ones are waiting, and so forth. As I've said, you can use this information to track down locking problems such as deadlock situations. Page 480 Figure 17.4. The Lock Manager Statistics window lists important information regarding InterBase locks.

Database Validation In concert with making regular backups, another measure you can take to ensure the veracity of your data is to check its validity on a regular basis, preferably immediately before or after a backup. You do this via the Maintenance | Database Validation menu option in the InterBase Server Manager. Figure 17.5 shows the Database Validation dialog. Figure 17.5. Check your databases using the Database Validation dialog.

The Database Validation facility performs three basic functions: It inventories corrupted storage structures, it lists data pages that have been misallocated, and it reassigns orphaned pages to free space so that they can be reused. Normally, the facility makes corrections for you that don't involve user data. You can disable this by selecting the Read-only validation option.

Any errors detected by the validation facility are displayed in a window. You can click the Repair button to instruct the facility to attempt to repair the errors that have been detected. If your database contains checksum errors, you might want to instruct the facility to ignore checksum errors so that the validation routine runs to completion. Follow these steps to repair a corrupted InterBase database: 1. Copy the database to a backup using a method other than the Server Manager's Backup facility (for example, the COPY command or drag-and-drop within Explorer). InterBase can't back up corrupted databases. 2. Select the Server Manager's Maintenance | Database Validation option to check the database for corruption. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 474
q

Return values can be returned by reference or by value. If you elect to return by reference, remember to return a pointer to the return value, not the value itself (including, for example, integers), or InterBase will balk when you try to use the function.

You can test the MinInt and MaxInt functions by executing something like this in WISQL: SELECT MaxInt(3,5) FROM CUSTOMER; Because InterBase requires that all SELECT statements reference at least one table, this code makes use of the CUSTOMER table. Note that MaxInt will execute once for each row in CUSTOMER, so you may see several copies of the function result. TIP You can create a single-row InterBase table for the express purpose of coding non-table queries. This would work along the lines of the Oracle DUAL table, and could be used to create SELECT statements that don't actually need to query tables. Here's an example of the creation and use of such a table (execute each statement separately in WISQL): CREATE TABLE DUAL (A CHAR(1)); INSERT INTO DUAL (A) VALUES (`A');

SELECT 22 / 7 AS ApproximatePI FROM DUAL;

Use the DROP EXTERNAL FUNCTION command to remove an external function declaration from a database. Cursors Cursors are set-oriented SQL's approach to row-oriented processing. Cursors enable you to work with tables one row at a time. Because the BDE automatically creates and maintains cursors for you, you won't generally create your own cursors using SQL. However, you may find them handy in stored procedures. There are four basic operations you can perform on cursors: You can declare them, open them, fetch from them, and close them. You can also use a cursor to UPDATE and DELETE individual table rows. NOTE Don't try to execute these commands individually from WISQL. They're for use in stored procedures only.

Page 475 A cursor declaration consists of a SELECT statement and, for updatable cursors, a list of updatable columns. Here's the syntax: DECLARE CUSTOMER_SELECT CURSOR FOR SELECT * FROM CUSTOMER Before you can retrieve rows using the cursor, it must be opened. You use the OPEN command to fire off the query that makes up the cursor declaration: OPEN CUSTOMER_SELECT OPEN doesn't actually retrieve any rows back to the client application. You use the FETCH command for that. Here's the syntax:

FETCH CUSTOMER_SELECT This retrieves a single row from the cursor result set. Each subsequent call to FETCH retrieves the next row in the set. InterBase supports one-way cursors only; you cannot FETCH backward. If you want to move back up in a set, you must CLOSE and re-OPEN the cursor. NOTE The fact that InterBase doesn't support bidirectional cursors doesn't prevent your Delphi applications from using them anyway. The BDE emulates bidirectional cursoring at the application level regardless of whether your server back-end supports it. This is why you're able to scroll both backward and forward in TDataSets such as TQuery and TTable.

The rows returned by updatable cursors can be updated using special versions of the UPDATE and DELETE commands. A cursor must be declared using the FOR UPDATE OF clause in order to be updatable. Here's an example: DECLARE CUSTOMER_UPDATE CURSOR FOR SELECT * FROM CUSTOMER FOR UPDATE OF LastName NOTE Be sure to list only those columns in the FOR UPDATE OF clause that you actually intend to update. Declaring more updatable fields than you need wastes server resources.

In order to update or delete the current row of an updatable cursor, you use the WHERE CURRENT OF cursorname syntax to qualify the command, as in UPDATE CUSTOMER SET LastName="Cane" WHERE CURRENT OF CUSTOMER_UPDATE Page 476 or

DELETE FROM CUSTOMER WHERE CURRENT OF CUSTOMER_UPDATE When you finish with a cursor, you use the CLOSE command to close it. Closing a cursor also releases any system resources it uses. Here's the syntax: CLOSE CUSTOMER_UPDATE

InterBase Administration
Out of necessity, database developers often end up doing at least a minimal amount of database administration. This section isn't meant to be exhaustive by any means; hopefully, it'll provide you with the basic administrative tools you need to get by in a pinch. Database administration is a complex task best left to Database Administrators. DBAs have the knowledge and training to properly manage databases on a daily basis. Backing Up and Restoring Of primary concern to the database developer, administrator, and user alike is the safety of the database. What would happen if the drive or drives on which the database is stored failed? We protect against this by making backup copies of the database. It's not sufficient to merely copy the operating system files that make up the database because some of them may be in use and reside in memory on the server. Also, operating system copies don't make "snapshot" backups; they don't guarantee that the database is consistent as of a given point in time. The proper way to make InterBase database backups is through the Server Manager program. To back up a database using Server Manager, follow these steps: Start the Server Manager program. Click File | Server Login and log in to the server. Click Tasks | Backup to display the Database Backup dialog. Type the full pathname of the database you want to back up in the Database Path entry box. Click the Remote button to specify a remote database for platforms other than NetWare. 5. Type the name of the operating system file or backup device you want to use in the Backup File or Device entry box. If you're on NT, you can specify the default tape device as \\.\tape0. 6. Click OK to back up your database. Figure 17.1 illustrates this. Restoring a database backup is pretty much a reversal of this process. You 1. 2. 3. 4.

select the Restore option on the Tasks menu and supply your backup as the Restore Source and your database as the Restore Destination. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 487 into the IPX protocol, but you need to ensure its presence in your particular TCP/IP connectivity package. For example, only later versions of Novell's LAN WorkPlace TCP/IP software support OOBD out of the box; earlier ones require a patch.
q

In order to make use of OOBD to cancel queries on Sybase Win 3.x clients, you use the URGENT connection string parameter. Basically, you just tack URGENT on to the end of the connection string: 100.10.15.12,3000,URGENT This ensures that queries that are aborted using DB-Library's dbcancel() function return control as quickly as possible to the application.

q

q

Use WSYBPING, not SYBPING, to troubleshoot 16-bit connection problems. SYBPING is for 32-bit connections only. Win 3.x versions of DB-Library all require 386 Enhanced mode, although they give no indication of this when you attempt to use them. If you attempt to connect to a Sybase SQL Server using DB-Library under Windows 3.x in Standard mode, your connection will simply fail; DB-Library won't bother to tell you that you need to switch modes.

Setting Up a BDE Alias Once you're able to connect to your database server, you're ready to build a BDE alias so that your Delphi applications can access it. This subject is covered elsewhere in this book, but let's review it here for your convenience and to establish the relationship between configuring your database driver

software and setting up a BDE alias. You can create BDE aliases using either the BDE Administrator utility or Delphi's Database Explorer. The following instructions use the BDE Administrator program. 1. Start the BDE Administrator, right-click the Databases tab, and select New from the menu. When the New Database Alias dialog box comes up, select SYBASE in the Alias type drop-down list and then click OK. 2. Once the new alias is added, type a name for it. 3. After you've named the alias, configure it by setting the parameters on the Definition page. 4. Click the SERVER NAME parameter and set it to the name of your SQL Server. This will be the same name you used in the SQLEDIT utility. 5. Optionally, you can also set the USER NAME parameter to the name of the server user you want to log in as by default. The username you specify is displayed as the default whenever Delphi's built-in login dialog box is shown. Finish by clicking the Apply button to save your changes and exit the Administrator. Page 488 Special BDE Alias Settings The following are several BDE alias settings that you may find useful when connecting to Sybase SQL Server. Though you can begin by accepting the defaults for these settings, it's helpful to know what's available to you in the event you encounter problems. Note that some of these are driver-level settings that you can access via the Configuration page tab in the BDE Administrator utility. APPLICATION NAME You can set the APPLICATION NAME parameter to specify the name that appears in SQL Server's sysprocesses table for your process. This helps distinguish your process from other processes on the server. CONNECT TIMEOUT

This controls the amount of time the client will continue to attempt to connect when attaching to an SQL server. This setting defaults to 60 seconds; you may find that increasing it improves the capability of client workstations, especially those dispersed across a WAN, to connect. HOST NAME You can set the HOST NAME parameter to specify the workstation name that appears in SQL Server's sysprocesses table. This helps distinguish your connection from other connections in listings such as those generated by the sp_who stored procedure. NATIONAL LANG NAME This setting specifies the language that's used when error messages are displayed. If you leave this blank, SQL Server's default language is used. TDS PACKET SIZE Use this setting to configure the size of Tabular Data Stream (TDS) packets. TDS is the high-level packet protocol that SQL Server uses to exchange data with client connections. Although you can specify package sizes from 0 to 65535 in the BDE Administrator, SQL Server cannot use packets smaller than 512 bytes. You may find that larger packet sizes increase system throughput by lowering the number of packets required to transmit large amounts of data (for example, BLOB fields). The default packet size is 512, and you should probably keep it between 512 and 8192 for the best performance. Page 489 NOTE If you increase the maximum network packet size, you'll also need to increase the additional network memory setting. This setting is used to allocate memory for buffers that SQL Server allocates internally to service client connections. If additional network memory is not set proportionally to max network packet size, you won't be able to use packets larger than the 512 byte default.

You can use SQL Server's sp_configure stored procedure to determine the

current maximum packet size supported by your server. Execute sp_configure `max network packet size' to list the current maximum packet size. Adjust this parameter before you change its corresponding setting in the BDE Administrator. If TDS PACKET SIZE in the BDE Administrator and SQL Server's max network packet size don't agree with one another (or if additional network memory hasn't been configured correctly), you may encounter the following errors:
q q q

Error: unknown user name or password Server error—4002 Login failed Server error—20014 Login incorrect

DATABASE NAME Use this option to specify the name of the SQL database to which you want to connect. When people build SQL Server aliases, the normal approach is to set up a separate one for each database they might want to access. If you take this approach, you'll use the DATABASE NAME setting to specify which database you want to access with each alias. BLOB EDIT LOGGING You can disable the logging of changes to BLOB fields by switching this parameter to False. Setting this option to False minimizes BLOB storage requirements and improves performance. This option causes BLOBs to be transmitted using SQL Server's bulk copy facility, so you'll need to set select into/bulk copy on in the target database if you plan to use it. You can turn select into/bulk copy on using the sp_dboption stored procedure. MAX QUERY TIME The parameter controls the amount of time that SQL Links will wait for an asynchronous query to complete before canceling it. The Sybase SQL Links driver now executes queries synchronously by default. You can force asynchronous query execution by incrementing the DRIVER FLAG parameter by 2048. Set the TIMEOUT parameter (a driver-level parameter) to specify a synchronous query time-out value. By default, SQL Links will give an asynchronous query five minutes (300 seconds) to finish. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 490 Miscellaneous Sybase SQL Server Issues Beyond alias configuration issues, there are a number of special usage nuances and caveats of which you should be aware when building Delphi apps that access Sybase databases. The following are some of the miscellaneous issues you may encounter when interacting with Sybase SQL Server from your Delphi applications. Numeric and Decimal Data Types Sybase System 10 introduced the new Numeric and Decimal data types. The BDE supports these by translating them to its floating-point type, fldFloat. Note, however, that both types are retrieved from the server in their native formats (xltNONE), so no data is lost in the trans-lation. Cross-Database/Server Tables You can access Sybase objects in other databases by fully qualifying them. For example, you can open ACCOUNTING.DEMO.CUSTOMER, a table that lives in the ACCOUNTING database and is owned by user DEMO, just by fully specifying its name. Troubleshooting Sybase Connection Problems If you have problems connecting to a Sybase SQL Server, try the following: 1. Use Sybase's SYBPING utility to ping your server. If the server pings fine, you probably have a problem with your BDE alias configuration.

Return to the BDE Configuration program and examine the parameters you specified (particularly the server name) to ensure that they're correct. NOTE Don't mistake the WSYBPING utility for SYBPING. WSYBPING is for 16-bit connections only, so it's of no use to you with 32-bit Delphi apps.

2. If SYBPING fails, you probably have a protocol problem. If you're using TCP/IP to connect to the server, use the PING utility that comes with Windows 95 and Windows NT to attempt to PING the host computer. Try both the machine's name, as it appears in the HOSTS file, and its IP address. 3. If the machine pings appropriately with the IP address, but not with its host name, you may have a problem with your HOSTS file. You can fix the problem temporarily by changing your connection string (with SYBEDIT) to use the IP address rather than the host name, to connect. You should resolve the HOSTS file problem as soon as possible because this is a more flexible way of connecting to remote machines. Page 491 4. If you're using IPX to connect with your server, be sure that the Novell server's IPX network number matches the one you're specifying with SYBEDIT. If this is the case and you still can't connect, or if you're using some other protocol to connect, consult with your network administrator for further assistance. 5. If you're using TCP/IP and you have no success when trying to ping the host machine using either its host name or its IP address, you probably have a network problem. Be sure to check obvious things such as whether the IP address you have for the server is correct. Try pinging the TCP/IP loopback address, 127.0.0.1, to see whether your TCP/IP stack is working properly. This address loops back to your machine, enabling you to ping yourself. If it fails, you have a serious problem with your protocol configuration. You may need to consult your network administrator for help with your network connection. 6. If SYBPING fails, but PING works fine, check to see that the port number you specified in your connection string is the one on which the database server is listening. You can consult with your database administrator to determine what port(s) your host machine is listening

on for connections. Listener ports are configured under System 10 using the Sybase SYBINIT utility. 7. If everything seems to be configured properly, you might try switching protocols if that's an option for you. For example, I have seen a version of System 10 for Novell NetWare that will not work reliably using Novell's own homegrown protocol, IPX—yet it works fine with TCP/IP. Because the server can be configured to listen on both protocols simultaneously, this isn't an either/or situation; you can configure the server to listen on a protocol that works until you resolve problems with other ones.

SQL Primer
This section introduces you to Sybase SQL Server's Transact-SQL dialect and touches on a few of the features that distinguish it from the SQL implementations of other vendors. I assume that your server is running and that you know how to connect to it using SQL Server's ISQL utility. Creating a Database You may already have a database in which you can create some temporary tables for the purpose of working through the examples in this chapter. If not, creating one is easy enough. In SQL, you create databases using the CREATE DATABASE command. The exact syntax varies from vendor to vendor; here's the basic Transact-SQL syntax: CREATE DATABASE dbname ON datadevice=size LOG ON logdevice=size Page 492 Before you can create a database, though, you need logical devices on which it can reside. SQL Server's logical devices allow you to avoid having to refer to physical disk locations in the databases you build. They provide a layer between your disk drives and your databases. You use the DISK INIT command to create SQL Server devices. Here's some sample syntax: DISK INIT name='saltrn00', physname='c:\sybase\data\saltrn00.dat',

vdevno=5, size=2048 The name parameter specifies the logical handle that you'll use elsewhere to refer to the device. The physname parameter details the physical location and name of the device file. This can be a file in a file system or a raw partition on operating systems that support raw partitions. vdevno is a logical virtual device number. Its only significance is that it must be unique among the devices defined on the server. The range of acceptable values is 1 to 255; 0 is reserved for the master device. Note that you'll need to increase the maximum number of allowed devices to use device numbers higher than 10. You can change this setting using the sp_configure stored procedure. Size is the number of 2K pages the device is to occupy. For example, if you wanted to create a device that's 4M in size (4096K), you'd specify a size of 2048. Data devices and log devices are created using the same syntax. For example, here's a command to create an SQL Server log device: DISK INIT name='sallog00', physname='c:\sybasesql\data\sallog00.dat', vdevno=6, size=512 Once the devices that are to contain it exist, you're ready to create the database. Here's a CREATE DATABASE command that uses the devices we just defined. CREATE DATABASE sales ON saldat00=4 LOG ON sallog00=1 If you don't already have a database where you can store objects for working through this section, go ahead and create this database now. The USE Command The SQL Server USE command changes the active database context. Simply

put, this means that it switches the current database. The command syntax is USE dbname Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 493 Creating Tables After you've created your database and issued the USE command to make it your active database, you're ready to begin building database objects. Virtually any relational database concept can be demonstrated with a set of three tables. For the purpose of working through this chapter, begin by creating the following three tables using the SQL CREATE TABLE statement. Enter the following command in ISQL to create the CUSTOMER table: CREATE TABLE CUSTOMER ( CustomerNumber LastName FirstName StreetAddress City State Zip )

int char(30) char(30) char(30) char(20) char(2) char(10)

NOT NULL, NULL, NULL, NULL, NULL, NULL, NULL

Next, build the SALE table using this command: CREATE TABLE SALE ( SaleNumber int NOT NULL, SaleDate datetime NULL, CustomerNumber int NOT NULL, ItemNumber int NOT NULL, Amount money

) Now that the SALE table is built, only one table remains. Create the ITEM table using this command: CREATE TABLE ITEM ( ItemNumber int NOT NULL, Description char(30) NULL, Price money NULL ) Adding and Dropping Columns You use the SQL ALTER TABLE command to add and drop columns from an existing table. The following syntax shows how to add a column to a table: ALTER TABLE CONTACT ADD PhoneNumber char(10) NULL Here's how you drop a column: ALTER TABLE CONTACT DROP PhoneNumber Page 494 Note that you cannot add a NOT NULL column to a table that already has rows because it would have to allow NULLs immediately after being added to the table. WARNING The ALTER TABLE...DROP column syntax isn't officially supported by Sybase. Technically, it's an undocumented feature. As with any undocumented feature, be wary of writing code that depends on it.

Constraints A constraint is the mechanism by which you limit, or constrain, the type of data a column may store. A constraint can also be used to define a default value for a column. Constraints can be defined when a table is first created using the CREATE TABLE command or

afterward using the ALTER TABLE command. Here's an example of a primary key constraint: ALTER TABLE CUSTOMER ADD PRIMARY KEY (CustomerNumber) This syntax adds a primary key constraint to the CUSTOMER table, defining its CustomerNumber field as the table's primary key. This causes a unique index to be created over the table using the CustomerNumber column as the key. Note that you cannot define a column that accepts NULL values as a table's primary key. A foreign key constraint defines a column in one table whose values must exist in a second, or foreign, table. A foreign key doesn't uniquely identify rows as does a primary key. On the contrary, its key columns must be a primary or unique key in the table that it references. Adding a foreign key constraint to a table causes SQL Server to automatically build a secondary index over its key columns. The following is an example of the syntax: ALTER TABLE SALE ADD FOREIGN KEY (CustomerNumber) REFERENCES CUSTOMER This constraint defines the CustomerNumber field in the SALE table as a foreign key that references the same column in the CUSTOMER table. This means that customer numbers entered into the SALE table must first exist in the CUSTOMER table. It also means that customer numbers that are being used in the SALE table cannot be deleted from the CUSTOMER table. This capability to enforce the relationship between the two tables by merely defining it is called declarative referential integrity. The term simply means that the integrity of the relationship between the tables is ensured by defining (or declaring) it, not by user program code. A third type of constraint is one that checks a column against a list of predefined values. Here's an example of such a constraint: ALTER TABLE CUSTOMER ADD CONSTRAINT INVALID_STATE CHECK (State in (`OK','AR','MO')) Page 495 Note the negative vantage point of the naming convention used for the constraint. This is done so front-end tools that report the constraint name will give a somewhat meaningful message to the user. By using a simple message as the name of the constraint, you allow for the possibility that the message could give the user a hint as to what the problem is when the constraint is violated. Additionally, this might save you the effort of having to replace

the Delphi exception generated due to the constraint with your own. Testing Constraints You should test every constraint that you place on a database. You do this by attempting to add values to the database that the constraint is supposed to disallow. For example, to test the preceding INVALID_STATE constraint, enter this command in ISQL: INSERT INTO CUSTOMER (CustomerNumber, State) VALUES (123,'CA') Because the constraint limits States entered to `OK', `AR', and `MO', it should reject your attempted row insertion with an error. If a constraint you've defined fails to function as expected, verify that you successfully added it in the first place and that it's checking the data in the way you intended. Creating Indexes You create indexes in SQL Server SQL using the CREATE INDEX command. Here's the basic syntax: CREATE INDEX SALE02 ON SALE (SaleDate) SALE02 is the name of the new index, SALE is the name of the table on which to build the index, and SaleDate is the index key. Note that SQL Server index names must be unique across the database in which they reside. You can create an index that prohibits duplicates by using the CREATE UNIQUE INDEX variation of the command, as in the following: CREATE UNIQUE INDEX SALE01 ON SALE (SaleNumber) Inserting Data The SQL INSERT statement is used to add data to an SQL Server table. You can add data one row at a time using INSERT's VALUES clause, or you can insert several rows at once by selecting them from another table. Use the following syntax to add data to each of the three tables. First, add three rows to the CUSTOMER table by executing the following commands separately in ISQL:

INSERT INTO CUSTOMER (CustomerNumber, LastName, FirstName, ÂStreetAddress, City, State, Zip) Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 513

PART IV

Advanced Topics
Page 514 Page 515

CHAPTER 19

Business Reports
Page 516 In addition to being a full-featured form designer, Delphi is a very capable report writing tool. Its QuickReport components offer a full bevy of reporting tools from which you can create sophisticated business reports. Delphi also includes a powerful decision cube component that makes building cross-tab reports a snap. And if you need business charts, Delphi also has you covered there. You can even integrate business charts with decision cubes to create cross-tab graphs. You can do all this without ever leaving the Delphi IDE.

Types of Reports
In this chapter, we'll build four types of business reports using the InterBase employee database that ships with Delphi Client/Server. We'll build a simple

list report to list the customer table, a grouped report to sort the customer table by country, a master/detail report to list sales by customer, and a cross-tab report to summarize sales information by country. We'll also create a pie chart that shows sales by country.

The Employee Database
The InterBase employee database is included with both Delphi Client/Server and InterBase. It's used in the tutorial that accompanies InterBase and contains a handful of tables. Figure 19.1 shows a database schema for the employee database. Figure 19.1. The employee database schema.

Page 517 Out of the box, Delphi provides a BDE alias named IBLOCAL that's designed especially for interfacing with the employees database. We'll use IBLOCAL to build reports and graphs over the tables in the employee database.

The Customer List Report
Follow these steps to get started building the Employee List Report: NOTE

The methods provided for building reports in this chapter don't use the built-in QuickReport templates in the Delphi repository. This is due to the fact that I've found these to be a bit unwieldy. Unlike the other members of the Object Repository's Forms page, the QuickReport items aren't actually forms—they're descendants of TQuickRep itself. This means that, even though you get what appears to be a new form when you copy one of these templates to start a new report, you actually get an element that's about halfway between a TPanel and a TForm, with a good dose of TQuickRep thrown in. The upshot of this is that your report forms don't show up in the Forms property of the Screen component. This makes them somewhat difficult to access and will cause them to be absent from the list of reports presented by the REPORTS app that you'll build later in this chapter. The bottom line is, if you want to be able to change your reports en masse using visual form inheritance or handle them polymorphically using Runtime Type Information (RTTI), it's far preferable to build reports using forms and TQuickRep components that are separate from one another.

1. Click File | New Application to start a new app just for housing your reports. 2. Change the default form's Name property to CustomerReport (this will also change the Caption property). 3. Drop a TQuickRep component onto the default form and set its ReportTitle property to Customer List Report. 4. Drop a TQuery component onto the form and set its Name to MasterQuery. 5. Set MasterQuery's DatabaseName property to IBLOCAL. Key this code into its SQL property: SELECT * FROM CUSTOMER 6. Double-click MasterQuery's Active property to open it (be sure that your InterBase server is running and remember to supply a valid username and password when prompted). Page 518 7. Set the QuickRep component's DataSet property to reference your MasterQuery component.

8. Drop four QRBand components onto the QuickRep component. Set the BandType property of the first QRBand to rbTitle, the second to rbColumnHeader, the third to rbDetail, and the fourth to rbPageFooter. 9. Drop a QRSysData component onto the report's title band and set its Alignment property to taCenter. Set its Data property to qrsReportTitle. 10. Drop four QRDBText components onto the report's detail band and set their DataSet properties to MasterQuery. These components will constitute the report's columns. Remember that you can Shift+click components to select more than one of them at a time. 11. Set the DataField property of the first QRDBText to CUSTOMER, the second to CITY, the third to STATE_PROVINCE, and the fourth to COUNTRY. 12. Drop four QRLabel components onto the column header band. These components will serve as column headings for the database fields listed on the report. Position each one over a different QRDBText control and change its caption to describe the database field it identifies. For example, the Caption of the first QRLabel component should be set to Customer because it will list the CUSTOMER field. 13. Change the Font property of all four QRLabel components to include both the Bold attribute and the Underline attribute. This will help them stand out on the report as column headings. 14. Drop a QRLabel onto the upper-left corner of the report and set its Caption to Report Date:. 15. Drop a QRLabel just below the Report Date label and set its Caption to Report Time:. 16. Drop a QRSysData component to the right of the Report Date label and set its Data property to qrsDate. The QRSysData component is specially designed for returning system-level variables such as the current page number, the report's title, and so on. Here, we're using it to print the current date in the report's title area. 17. Drop a second QRSysData component to the right of the Report Time label. Its Data field should default to qrsTime. This will cause the report's print time to be included in the report header. 18. Save your new form's source code unit as LISTREPO.PAS. After you've completed these steps, the Customer List Report is basically done. Figure 19.2 shows what the completed report should look like. Page 519 Figure 19.2. Your Customer List Report as it looks when

finished.

You can easily preview your new report's runtime appearance by right-clicking it and selecting Preview from the pop-up menu. Figure 19.3 shows what your report will look like at runtime. Figure 19.3. The Customer List Report as it will look at runtime.

Now that the list report is complete, you're ready to move on to the group report. When we're done building reports, I'll show you how to construct an application to launch them. Close the report preview window and return to the Delphi form designer. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 511 OPEN doesn't retrieve any rows back to the client application. You must use FETCH for that. Here's the syntax: FETCH CUSTOMER_SELECT This retrieves a single row from the cursor result set. Each subsequent call to FETCH retrieves the next row in the set. SQL Server supports one-way cursors only; you cannot FETCH backward. If you want to move back up in a set, you must CLOSE and re-OPEN the cursor. NOTE The fact that SQL Server doesn't support bidirectional cursors doesn't prevent your Delphi applications from using them anyway. The BDE provides bidirectional cursoring at the application level regardless of whether your server back-end supports it. This is why you're able to scroll both backward and forward in TDataSets such as TQuery and TTable.

The rows returned by updatable cursors can be updated using special versions of the UPDATE and DELETE commands. A cursor must be declared using the FOR UPDATE OF clause in order to be updatable. Here's an example: DECLARE CUSTOMER_UPDATE CURSOR FOR SELECT * FROM CUSTOMER FOR UPDATE OF LastName

NOTE Be sure to list only those columns in the FOR UPDATE OF clause that you actually intend to update. Declaring more updatable fields than you need wastes server resources.

In order to update or delete the current row of an updatable cursor, you use the WHERE CURRENT OF cursorname syntax to qualify the command, as in UPDATE CUSTOMER SET LastName="Cane" WHERE CURRENT OF CUSTOMER_UPDATE or DELETE FROM CUSTOMER WHERE CURRENT OF CUSTOMER_UPDATE When you finish with a cursor, you use the CLOSE command to close it. Here's the syntax: CLOSE CUSTOMER_UPDATE Page 512 Closing a cursor doesn't release the system resources it uses. You use the DEALLOCATE cursor syntax for that, as in DEALLOCATE CUSTOMER_UPDATE

Summary
In this chapter, you learned how to start Sybase SQL Server, how to connect to it, and how to troubleshoot connection problems. You also received a thorough introduction to Sybase's SQL dialect, Transact-SQL.

What's Ahead
Chapter 19, "Business Reports," explores building client/server reports with Delphi. You'll learn to construct everything from simple table lists to

sophisticated cross-tab reports. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 506 Listing 18.1. continued unused_space = convert(numeric(13),((reserved_pgs(i.id, Âi.doampg)+reserved_pgs(i.id, i.ioampg))-(data_pgs(i.id, Âi.doampg)+data_pgs(i.id, i.ioampg)))), owner=user_name(o.uid), sequencer=0.0 into #sp_dir from sysobjects o, sysindexes i where o.name like @mask and o.type like @obtype and o.id*=i.id and i.indid<=1 select @orderby=upper(@orderby) /* declare @tablename char(30),@row_len int set rowcount 1 while exists (select @tablename=name from #sp_dir where Âtype in (`U','S') and row_len = null) begin exec sp_estspace @tablename,1,@rowlength=@row_len output update #sp_dir set row_len=@row_len where name=@tablename end set rowcount 0 */ if @orderby = `/N' begin create clustered index sp_dirind on #sp_dir (name,id) end else if @orderby = `/R'

begin create clustered index sp_dirind on #sp_dir (row_count,id) end else if @orderby = `/S' begin create clustered index sp_dirind on #sp_dir (size,id) end else if @orderby = `/D' begin create clustered index sp_dirind on #sp_dir (date_created,id) end else if @orderby = `/DS' begin create clustered index sp_dirind on #sp_dir (data_space,id) end else if @orderby = `/IS' begin create clustered index sp_dirind on #sp_dir (index_space,id) end Page 507 else if @orderby = `/US' begin create clustered index sp_dirind on #sp_dir (unused_space,id) end else if @orderby = `/O' begin create clustered index sp_dirind on #sp_dir (owner,id) end

alter table #sp_dir drop sequencer alter table #sp_dir add sequencer numeric(10,0) identity insert into #sp_dir (name,row_count,size,data_space, Âindex_space,unused_space,id,type,date_created) select `TOTAL:',row_count=isnull(sum(row_count),0),size=isnull(sum(size),0), Âdate_space=isnull(sum(data_space),0),index_space= Âisnull(sum(index_space),0), unused_space= Âisnull(sum(unused_space),0),id=0,type=@obtype,date_created=getdate()

from #sp_dir select name,type,date_created, row_count, size, data_space, index_space, unused_space, owner from #sp_dir order by sequencer go IF OBJECT_ID(`dbo.sp_dir') IS NOT NULL PRINT `<<< CREATED PROC dbo.sp_dir >>>' ELSE PRINT `<<< FAILED CREATING PROC dbo.sp_dir >>>' go sp_dir can be used to return a list of any type of object from a given database on the server. You can sort the listing using any of the returned columns. Figure 18.1 shows an example of the output from sp_dir. Notice that the USE statement at the top of the file establishes the database in which the procedure will be created. The GO command batch terminator is used to separate the different sections of the script. NOTE The complete source to this procedure is included on the CD-ROM accompanying this book.

Page 508 Figure 18.1. You can use the sp_dir procedure to list objects on the server.

Running Stored Procedures You can run SQL Server stored procedures using the EXECUTE command. The syntax is EXECUTE procedurename parameters You can abbreviate EXECUTE to EXEC, and you can omit it altogether when the EXEC is the first command in a command batch. For example, you can run the listcustomers procedure in the ISQL utility with this syntax:

listcustomers You can pass parameters to a stored procedure by position or by name, like so: exec and exec listcustomers @LastNameMask='%',@State='MO' listcustomers `MO','%'

System Procedures A system procedure is a special stored procedure whose name begins with sp_ and executes within the context of the current database. For example, sp_spaceused is a system procedure. When executed from the pubs2 database, sp_spaceused lists the used space in pubs2. When executed from the master database, the procedure lists the used space in master. System procedures can reside in the master or sybsystemproc databases. Page 509 TIP Rather than having to change to a database via the USE command in order to run a system stored procedure within its context, you can simply prefix the name of the stored procedure with the database in which you want to run it. For example, let's say you want to run the sp_dir procedure above in the master database, but you're currently in the pubs2 database. Simply prefix the name of the procedure with the master database like so: master..sp_dir `%','S'

Although the sp_dir procedure actually resides in the sybsystemprocs database, the context will temporarily change to the master database while the procedure runs. Figure 18.2 illustrates this. Notice the use of the two periods between the database and procedure name, as is customary with Sybase stored procedure calls. This causes the owner, which could also be specified, to default to dbo.

Figure 18.2. You can use a shorthand method of changing databases to launch stored procedures.

Page 510 Triggers Not unlike stored procedures, triggers are SQL routines that are activated when data in a given table is inserted, updated, or deleted. You associate a trigger with a specific operation on a table: a row insertion, update, or deletion. Here's an example using Transact-SQL: CREATE TRIGGER SALEDelete ON CUSTOMER FOR DELETE AS BEGIN DELETE FROM SALE WHERE CustomerNumber=(SELECT CustomerNumber FROM deleted) END This trigger deletes the sales made to a customer from the SALE table when the customer's record is deleted from the CUSTOMER table. This sort of delete is known as a cascading delete: A delete operation on one table cascades through others using a common key. Note the use of the deleted logical table. Whenever a DELETE trigger fires, SQL Server creates a logical table named deleted that contains the row(s) about to be deleted. In this trigger, deleted is queried to determine the CustomerNumber being deleted and used to remove rows from the SALE table. When INSERT and UPDATE triggers fire, a similar logical table named inserted is constructed by the server. The table doesn't exist anywhere except memory, but it's accessible like any other table by the trigger. Cursors Cursors are set-oriented SQL's approach to row-oriented processing. Cursors enable you to work with tables one row at a time. Since the BDE automatically creates and maintains cursors for you, you generally won't create your own cursors using SQL. However, you may find them handy in stored procedures. There are four basic operations you can perform on cursors: You can declare them, open them, fetch from them, and close them. You can also use a cursor to UPDATE and DELETE rows in its base table. A cursor declaration consists of a SELECT statement and, for updatable cursors, a list of updatable columns. Here's the syntax: DECLARE CUSTOMER_SELECT CURSOR FOR SELECT * FROM CUSTOMER

Before you can retrieve rows using the cursor, it must be opened. You use the OPEN command to initiate the query that makes up the cursor declaration: OPEN CUSTOMER_SELECT Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 503 Column Aliases You might have noticed that I use logical column names for aggregate functions such as COUNT() and SUM(). Labels such as these are known as column aliases and serve to make the query and its result set more readable. In SQL Server SQL, you can place a column alias immediately to the right of its corresponding column in the SELECT statement's field list, or you can place it to the left of the column, followed by an equal sign. For example, in the following query, the column alias of the COUNT() aggregate is the NumberWithName label: SELECT CUSTOMER.LastName, NumberWithName=COUNT(*) FROM CUSTOMER GROUP BY CUSTOMER.LastName HAVING COUNT(*) > 1 You can use column aliases for any item in a result set, not just aggregate functions. Here's an example of a column alias that's located to the right of the referenced column: SELECT CUSTOMER.LastName LName, COUNT(*) NumberWithName FROM CUSTOMER GROUP BY CUSTOMER.LastName This query substitutes the column alias LName for the LastName column in the result set. Note that you can't use aliases in other parts of the query such as the WHERE or GROUP BY clauses. You must use the actual column name or value in those parts of the SELECT statement. Table Aliases Rather than having to specify the full name of a table each time you reference it in a SELECT command, you can define a shorthand moniker for it to use instead. You do this by specifying a table alias for the table in the FROM clause of the SELECT statement. Place the alias to the right of the actual table name, as illustrated here: SELECT C.LastName, COUNT(*) NumberWithName FROM CUSTOMER C GROUP BY C.LastName Notice that the alias can be used in the field list of the SELECT list before it is even syntactically defined. This is possible because references to database objects are resolved before a query is executed.

Views An SQL view consists of a SELECT statement that you can treat as a table and, in turn, query with SELECT statements. In some cases, you can also issue INSERT, DELETE, and UPDATE statements against the view. The view itself does not actually store any data; it's a logical construct only. Think of a view as a small SQL program that runs each time you query it. It's similar to Page 504 an SQL Server select procedure, which is discussed in the following section, "Stored Procedures." When you query a view, the query optimizer takes the SELECT used to create the view, blends in the one you are executing against it, and optimizes the two as a single query. SQL views are created using the CREATE VIEW command. Here's an example: CREATE VIEW MOCUSTOMERS AS SELECT * FROM CUSTOMER WHERE State='MO' After the view is created, it can be queried just like a table, as in SELECT * FROM MOCUSTOMERS When you run this query, notice that, even though the SELECT against the view didn't include a WHERE clause, the result set appears as though it did due to the WHERE clause that's built into the view. The SELECT statement that makes up a view can do almost anything a normal SELECT statement can do. One thing it cannot do is contain an ORDER BY clause. This limitation exists not only on Sybase SQL Server, but also on the InterBase, Microsoft, and Oracle platforms. When you create an updatable view, you can tell the server to ensure that rows that are updated or added using the view meet the selection criteria imposed by the view. That is, you can ensure that an updated or added record doesn't "go out of scope"—that it doesn't vanish from the view after it's changed or added. You do this by using the WITH CHECK OPTION clause of the CREATE VIEW command. Here's the syntax: CREATE VIEW MOCUSTOMERS AS SELECT * FROM CUSTOMER WHERE State='MO' WITH CHECK OPTION Now, any record updates or inserts that specify a State column containing anything but `MO' will fail. Stored Procedures A stored procedure is a compiled SQL program that's stored in a database with other database objects. Stored procedures are created using the CREATE PROCEDURE command. Here's an example of the SQL Server syntax: CREATE PROCEDURE listcustomers AS

BEGIN SELECT LastName FROM CUSTOMER END Page 505 If the procedure receives parameters from the caller, the syntax changes slightly. Here's the SQL Server syntax: CREATE PROCEDURE listcustomersbystate (@State char(2), @LastNameMask char(30)) AS BEGIN SELECT LastName FROM CUSTOMER WHERE State=@State AND LastName LIKE @LastNameMask END Scripts It's a good idea to construct Data Definition Language (DDL) statements, including stored procedures, using SQL script files. You can create these scripts using a text editor; most good SQL editors support saving their contents to disk. Remember that these scripts must include any necessary USE statements and GO command batch terminators. You can execute SQL Server SQL scripts by passing them on the command line to ISQL, like so: ISQL -Usa -Imyscript.sql -Omyscript.out The -I command-line parameter specifies the name of the script to execute; -O specifies the name of the script's output file. Listing 18.1 shows an example of such an SQL script file. The sp_dir stored procedure defined in the script provides an object listing similar to the DOS/Windows DIR command. Listing 18.1. AN SQL script file containing the sp_dir stored procedure. /* * DROP PROC dbo.sp_dir */ use sybsystemprocs go IF OBJECT_ID(`dbo.sp_dir') IS NOT NULL BEGIN DROP PROC dbo.sp_dir PRINT `<<< DROPPED PROC dbo.sp_dir >>>' END go create procedure sp_dir @mask char(30) = `%', Â@obtype char(2) = `U', @orderby char(3)='/N' as select o.id, o.name, o.type, date_created=o.crdate, row_count = rowcnt(i.doampg), -row_len=1*null, size = convert(numeric(13),(reserved_pgs(i.id,

Âi.doampg)+reserved_pgs(i.id, i.ioampg))), data_space = convert(numeric(13),data_pgs(i.id, i.doampg)), index_space = convert(numeric(13),data_pgs(i.id, i.ioampg)), continues Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 520

The Customer Group Report
You can save yourself a lot of time with the remainder of the reports in this chapter by saving your list report to the Object Repository and inheriting from it to create your other reports. Let's try this now with the Customer Group Report: 1. Right-click the Customer List Report and select the Add To Repository menu option. 2. Title the repository entry something witty like Customer Report and set its Description to Generic Customer Report Class. Assign the new form to the Forms page in the repository, and then click OK. 3. Click the File | New | Forms menu option and click the Customer Report form icon in the New Items dialog. 4. Click the Inherit radio button and click OK. You should see a new Customer Report form opened in the Delphi form designer. Notice that the titles, DataSet specifications, and other report details from the original Customer Report are already present in your new report form. Delphi's visual form inheritance has saved you the trouble of having to reconstruct the basic details of each customer report separately. Now that the new form is onscreen, let's customize it to create the Customer Group Report. To set up the Customer Group Report, follow these steps: 1. Change the new form's Name to CustomerGroupReport. 2. Change the QuickRep component's ReportTitle property to something novel like Customer Group Report.

3. Drop a TQRGroup component onto the report and set its Master property to reference the QuickRep component. Set its Expression property to MasterQuery.Country. 4. Drop a TQRExpr component onto the QRGroup component and set its Expression component to MasterQuery.Country. 5. Change the QRExpr component's Font property to include the Bold attribute and increase its size to 12 points to help it stand out in the report listing. 6. Close the MasterQuery component if it's currently open (set its Active property to False). 7. Edit its SQL property and add the phrase ORDER BY Country to the end of the existing SELECT statement. When you're finished, the SELECT statement should read SELECT * FROM CUSTOMER ORDER BY Country 8. Open the MasterQuery property by setting its Active property to True. Your new report is complete. Save your form's source unit to disk as GRPREPO.PAS, then right-click the report and select the Preview option. Figure 19.4 shows what you should see. Page 521 Figure 19.4. The Customer Group Report as it appears when previewed.

Notice that the customer list is now organized by country. This grouping is known as a level- or control-break. Using groups in your reports allows you to summarize the report's detail data by each grouping level. For example, you could set up a group footer that rendered a count of the total number of customers for each country. Groups can have both headers and footers. Headers and footers can make use of calculated, system, and regular fields. NOTE

Notice that we changed the SQL accompanying the report so that its result set is sorted by the Country column. If you intend to group by a particular field, be sure that your result set is ordered correctly. If it isn't, you'll get multiple group breaks for the same group at different places in the report. If your report has multiple level breaks, be sure to sort by all the fields involved, in the same order as their groups in the report.

The Master/Detail Report
The next report you'll build is the sales by customer master/detail report. This report will list each customer followed by the sales for that particular customer. Follow these steps to create your new master/detail report: 1. Create a new descendant of the original Customer Report via the File | New | Forms | Customer List Report | Inherit | OK menu sequence. 2. Set the new form's Name property to CustomerMDReport. Page 522 3. Change the ReportTitle property of the Report component to Sales by Customer Report. 4. Drop a TDataSource component onto the form and change its DataSet property to reference the MasterQuery component. 5. Drop a TQuery component onto the form and set its DatabaseName property to IBLOCAL. Key the following code into its SQL property: SELECT * FROM SALES WHERE Cust_No=:Cust_No ORDER BY Order_Date 6. Set its DataSource property to reference the TDataSource component you dropped earlier. This will establish a master/detail relationship between the CUSTOMER table (queried by the MasterQuery component) and your new query of the SALES table. 7. Toggle the TQuery component's Active property to True to open it. 8. Drop a TQRSubDetail component onto the report and set its Master property to reference the CustomerMDReport component. Set its DataSet property to reference your new TQuery. 9. Drop three TQRLabel components and three TQRDBText components onto the QRSubDetail component. Set the DataSet property of all three QRDBText components to the TQuery component you just dropped.

10. Set the DataField property of the first QRDBText component to OrderDate, the second to Discount, and the third to Total_Value. 11. Set the Mask field of the Discount QRDBText to .#0 and set the Mask field of the Total_Value QRDBText component to #,##0.00. The Mask property formats the numbers displayed by these components. In the case of the Discount field, it's being returned as a floating point value, so we use a mask to limit the number of digits it produces. In case of the Total_Value column, it represents a large money value, so we format it accordingly. NOTE You'll have to set the IBLOCAL alias's Enable BCD property to True in order for InterBase numeric and decimal fields (such as the Total_Value column) to be properly handled by your Delphi apps. Failing to do this causes the BDE, and therefore Delphi, to treat these types of fields as integer values. This will prevent your reports from being able to display their decimal points and will cause edit components such as DBEdit to reject decimal points as input. See Chapter 17, "Delphi on InterBase" for more information.

12. Position the QRLabel components over each of the QRDBText components so that they become headings for those columns. Set each QRLabel's Caption property to match the column it labels. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 523 13. Change the Font property of the three QRLabel components on the QRSubDetail band to include the Bold Italic attribute to help them stand out on the report. Your new report is basically finished at this point. Figure 19.5 shows the report as it should look in the Delphi form designer. Figure 19.5. Your master detail report in the Delphi form designer.

Now save your form's unit as MDREPO.PAS; then right-click the report and select the Preview option to view it. Figure 19.6 illustrates your new master/detail report. TIP While writing this chapter, I discovered what appeared to be a bug in Delphi's QuickReport components. When I first tried to run the master/detail report, I received an Access Violation message. The message mentioned something about an empty pointer. On returning to the form designer, I noticed that the Order_Date TQRDBText component had been shrunk to zero pixels in width. Subsequent attempts to widen it failed. There was obviously a problem with this field. I deleted and re-added the component, to no avail. Finally, I decided to try explicitly adding TField components for the query's fields by double-clicking the SALES TQuery and pressing Ctrl+A. For whatever reason, this resolved the problem. If you run into this yourself, perhaps knowing this will save you some frustration. If you encounter bugs of your own with Delphi's QuickReport components, you might want to pay a visit to the QuickReports Web site at http://www.qusoft.com. Also, Delphi includes a file named QRPT2MAN.DOC (located in the \Program Files\Delphi 3\Quickrpt directory) that contains additional information on using the QuickReport components in Delphi apps.

Page 524 Figure 19.6. Your new master/detail report in all its glory.

Your master/detail report is now complete. Close the Preview window and return to the Delphi form designer.

The Cross-Tab Report
Of the reports explored in this chapter, the cross-tab report you're about to build is the most unusual. This is due to the fact that it doesn't use Delphi's QuickReport components; it uses the DecisionCube components, instead. This means that you won't call a component print method in order to print the report, as you do with the QuickReport components; you'll call the form's Print method, instead. Follow these steps to set up your cross-tab report: 1. Start a new form and change its Name to CustomerCTReport. 2. Drop three TPanel components on to the new form. Set the Align property of the first panel to alTop, the second to alBottom, and the third to alClient. 3. Delete the Caption properties of both the second and third panels. 4. Set the Caption of the first TPanel to Sales by Country and Year and set its Font to Arial 18 pt. Regular. 5. Drop a DecisionQuery component (located on the DecisionCube component palette page) and set its DatabaseName property to IBLOCAL. 6. Drop a DecisionCube component onto the form and set its DataSet to the DecisionQuery component you just dropped. Page 525 7. Drop a DecisionSource component and set its DecisionCube property to the DecisionCube you just dropped. 8. Drop a DecisionGrid component onto the third TPanel component (it should be occupying the majority of the middle of the form) and set its Align property to alClient. 9. Set the DecisionGrid's DataSource property to the DecisionSource component you just dropped. 10. Drop a DecisionPivot onto the second TPanel (it should be located at the bottom of the form) and set its Align property to alClient. 11. Set the DecisionPivot's DataSource property to the form's DecisionSource component. 12. Right-click the DecisionQuery component and select Decision Query Editor from the pop-up menu. 13. Click the Query Builder button in the ensuing dialog.

14. When the Visual Query Builder's Add Table dialog displays, double-click the CUSTOMER table and the SALES table to add them to query, and then click Close. 15. Drag the Cust_No field from the CUSTOMER table to the Cust_No column in the SALES table to establish a join between them. 16. Drag the Country column from the CUSTOMER table to the Visual Query Builder's lower panel to add it to the report. 17. Drag the Order_Date and Total_Value columns from the SALES table to the Query Builder's lower panel to add them to the query, and then click the OK button (the one with the checkmark bitmap) to exit. 18. Once you're back in the Decision Query Editor, drag the Country and Order_Date columns to the Dimensions list box. Drag the Total_Value column to the Summaries list box. When prompted for the type of summarization to make, choose the sum option. Selecting the sum option causes the Total_Value column to be totaled. When you're done, the Query Text window on the SQL Query page table should display this SQL code: SELECT CUSTOMER.COUNTRY, SALES.ORDER_DATE, SUM( SALES.TOTAL_VALUE ) FROM CUSTOMER CUSTOMER INNER JOIN SALES SALES ON (CUSTOMER.CUST_NO = SALES.CUST_NO) GROUP BY CUSTOMER.COUNTRY, SALES.ORDER_DATE FROM CUSTOMER CUSTOMER If it doesn't, something's gone awry in the Decision Query Editor dialog itself or in the Visual Query Editor. 19. If the SQL looks as it should, click the OK button to close the Decision Query Editor. 20. Right-click the DecisionCube component and select the Decision Cube Editor option. Click the SUM column and set its Format property to #,##0.00. This will ensure that totals of the Total_Value column display correctly. Click OK to exit the dialog. Page 526 21. Toggle the DecisionQuery component's Active property to True. You should then see your DecisionGrid fill with data. If you're prompted for a username and password, supply valid ones as before. Figure 19.7 shows what you should see. 22. Save your form's unit as CTREPO.PAS when you're finished. Figure 19.7. Your cross-tab as it looks in design mode.

Constructing a Report Front-End Program

To really see how adept the DecisionCube component is at creating cross-tab reports, you need to view it at runtime. This brings up another task that's related to report writing—building report front-end applications. Listings 19.1 through 19.3 list the source code for the REPORTS app. This app makes use of all the reports you've built in this chapter (for brevity, their source isn't included here) and allows you to preview or run them from a central form. NOTE To build the REPORTS app, you'll either need to type in its source code as listed here or load the source from the CD-ROM accompanying this book.

Listing 19.1. The source to Reports.DPR—the Reports project file. program reports; uses Forms, repo00 in `repo00.pas' {Form1}, listrepo in `listrepo.pas' {CustomerReport}, grprepo in `grprepo.pas' {CustomerGroupReport}, mdrepo in `mdrepo.pas' {CustomerMDReport}, Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 527 ctrepo in `ctrepo.pas' {CustomerCTReport}, salegrap in `salegrap.pas' {CustomerGraphReport}; {$R *.RES} begin Application.Initialize; Application.CreateForm(TForm1, Form1); Application.CreateForm(TCustomerReport, CustomerReport); Application.CreateForm(TCustomerGroupReport, CustomerGroupReport); Application.CreateForm(TCustomerMDReport, CustomerMDReport); Application.CreateForm(TCustomerCTReport, CustomerCTReport); Application.CreateForm(TCustomerGraphReport, CustomerGraphReport); Application.Run; end. Listing 19.2. The source to repo00.PAS—the app's main form. unit repo00; interface uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, QuickRpt; type TForm1 = class(TForm)

btPreview: TButton; btPrint: TButton; ListBox1: TListBox; btCustomerCTPreview: TButton; btCustomerCTPrint: TButton; Label1: TLabel; Label2: TLabel; procedure btPreviewClick(Sender: TObject); procedure FormShow(Sender: TObject); procedure btPrintClick(Sender: TObject); procedure btCustomerCTPreviewClick(Sender: TObject); procedure btCustomerCTPrintClick(Sender: TObject); private { Private declarations } public { Public declarations } end; var Form1: TForm1; implementation uses listrepo, ctrepo;

continues Page 528 Listing 19.2. continued {$R *.DFM} procedure TForm1.btPreviewClick(Sender: TObject); var c,f : Integer; begin if (ListBox1.ItemIndex<>-1) then With Screen do For f:=0 to FormCount - 1 do With Forms[f] do

For c:=0 to ComponentCount - 1 do if (Components[c] is TQuickRep) and (TQuickRep(Components[c]).ReportTitle = ÂListBox1.Items[ListBox1.ItemIndex]) then begin TQuickRep(Components[c]).Preview; break; end; end;

procedure TForm1.FormShow(Sender: TObject); var c,f : Integer; begin With Screen do For f:=0 to FormCount - 1 do With Forms[f] do For c:=0 to ComponentCount - 1 do if Components[c] is TQuickRep then ListBox1.Items.Add(TQuickRep(Components[c]).ReportTitle); If (ListBox1.Items.Count <> 0) then ListBox1.ItemIndex:=0; end; procedure TForm1.btPrintClick(Sender: TObject); var c,f : Integer; begin if (ListBox1.ItemIndex<>-1) then With Screen do For f:=0 to FormCount - 1 do With Forms[f] do For c:=0 to ComponentCount - 1 do if (Components[c] is TQuickRep) and (TQuickRep(Components[c]).ReportTitle = Â ListBox1.Items[ListBox1.ItemIndex]) then begin TQuickRep(Components[c]).Print; break; end; end; procedure TForm1.btCustomerCTPreviewClick(Sender: TObject); begin CustomerCTReport.Show;

end; Page 529

procedure TForm1.btCustomerCTPrintClick(Sender: TObject); begin CustomerCTReport.Print; end; end. Listing 19.3. The source to the form associated with the app's main form. object Form1: TForm1 Left = 200 Top = 109 Width = 544 Height = 375 Caption = `Reports' Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText Font.Height = -11 Font.Name = `MS Sans Serif' Font.Style = [] OnShow = FormShow PixelsPerInch = 96 TextHeight = 13 object Label1: TLabel Left = 0 Top = 40 Width = 79 Height = 13 Caption = `Sales by Country' end object Label2: TLabel Left = 0 Top = 120 Width = 34 Height = 13 Caption = `Others:' end

object btPreview: TButton Left = 0 Top = 288 Width = 75 Height = 25 Caption = `Pre&view' TabOrder = 0 OnClick = btPreviewClick end object btPrint: TButton Left = 96 Top = 288 Width = 75 Height = 25 Caption = `&Print' continues Page 530 Listing 19.3. continued TabOrder = 1 OnClick = btPrintClick end object ListBox1: TListBox Left = 0 Top = 136 Width = 169 Height = 137 ItemHeight = 13 TabOrder = 2 end object btCustomerCTPreview: TButton Left = 0 Top = 56 Width = 75 Height = 25 Caption = `Preview' TabOrder = 3 OnClick = btCustomerCTPreviewClick end

object btCustomerCTPrint: TButton Left = 88 Top = 56 Width = 75 Height = 25 Caption = `&Print' TabOrder = 4 OnClick = btCustomerCTPrintClick end end Figure 19.8 shows what the new app looks like at runtime. Figure 19.8. The Reports app in all its splendor.

A couple of techniques are employed in the REPORTS apps that you might want to use yourself. First, take a look at the code associated with the main form's OnShow event: Page 531 procedure TForm1.FormShow(Sender: TObject); var c,f : Integer; begin With Screen do For f:=0 to FormCount - 1 do With Forms[f] do For c:=0 to ComponentCount - 1 do if Components[c] is TQuickRep then ListBox1.Items.Add(TQuickRep(Components[c]).ReportTitle); If (ListBox1.Items.Count <> 0) then ListBox1.ItemIndex:=0; end; This snippet of code uses the predefined Screen variable to loop through the available forms and determine which ones contain TQuickRep components. Those that contain TQuickRep components are added to the form's ListBox1 component. You'll recall that TCustomerReport contains a TQuickRep

component and is the basis for the other report forms in this chapter. After the list is built, its count is checked to see whether any TQuickRep components were found. If any exist (Count <> 0), the ListBox is set up to highlight the first item. The interesting thing about this code is that it uses Runtime Type Information (RTTI) to build a dynamic list of available reports. Provided you build your reports as forms that contain QuickRep components, you can do the same thing in your own apps. Now let's have a look at the Preview button's OnClick code: procedure TForm1.btPreviewClick(Sender: TObject); var c,f : Integer; begin if (ListBox1.ItemIndex<>-1) then With Screen do For f:=0 to FormCount - 1 do With Forms[f] do For c:=0 to ComponentCount - 1 do if (Components[c] is TQuickRep) and (TQuickRep(Components[c]).ReportTitle = Â ListBox1.Items[ListBox1.ItemIndex]) then begin TQuickRep(Components[c]).Preview; break; end; end; This code again loops through the list of available forms and components to find a match for the report currently selected in the ListBox component. If it finds a match, the code typecasts the component as a TQuickRep form and calls its Preview method. Again, this allows reports to be executed dynamically. It allows reports to be added to the application and to be immediately available for execution without any additional coding. The key here is the inspection of the form and component lists using RTTI. Page 532

Viewing Your Reports at Runtime
Now that we have a nice report front-end, let's have a look at that cross-tab report. Click the Preview button beneath the Sales by Country label. Figure 19.9 shows what you should see. Figure 19.9. Your cross-tab report at runtime.

Now let's try a few things to see just how powerful the DecisionCube component is: 1. Drag the Order_Date column heading to the Country column heading. You'll see that their corresponding rows and columns exchange places in the grid. This is called pivoting and is one of the main reasons people use decision cubes (also known as pivot tables) in the first place. 2. Pop up the Order_Date or Customer button on the DecisionPivot component at the bottom of the form. These buttons indicate which dimensions are included in the cube. By default, they're all included, but you can change that at runtime with the DecisionPivot control. 3. Right-click the Order_Date or Customer button and select the Drilled In option on the pop-up menu. Next, click the button with the left mouse button to display a list of possible drill-down values. For example, you can drill down into the Order_Date column and restrict the data in the grid to a particular year. You could also drill down into the Country column and view the data for a single country. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 533

Business Charts
In addition to allowing you to build sophisticated business reports, Delphi also provides facilities for building business graphs and charts. Delphi's DBChart, QRChart, and DecisionGraph components are specially designed for building data-aware charts. I'll show you how to use these components to build sophisticated business charts. Charting with QRChart Let's begin by building a simple chart using the QuickReport QRChart component. Because QRChart is a QuickReport component, you can include it on reports and preview and print it using TQuickRep's Preview and Print methods. Follow these steps to create a chart report using QRChart: 1. Create a new form descendant of your CustomerReport form and name it CustomerGraphReport. 2. Click the TQuickRep component and set its ReportTitle property to Graph of Sales by Country. 3. Select all four of the QRLabel components in the report's column heading band and delete their Caption property values. This will have the visual effect of removing them from the report. We can't simply delete them because they're inherited from another form. 4. Select all four QRDBText components in the detail band of the report, clear their DataField properties, and set their Width properties to 0. This will have the visual effect of removing them from the report. 5. Select the page footer, detail, and column header bands and set their Enabled property to False. Change their Height property to 0. This will have the visual effect of deleting them from the report. 6. Resize the title band so that it occupies at least half the form. This is where you'll locate the chart, so be sure to give yourself plenty of room. 7. Drop a QRChart component onto the title area and resize it to occupy as much of the title area as possible. 8. Close the MasterQuery component and change its SQL to the following: SELECT C.COUNTRY, SUM(S.TOTAL_VALUE) FROM SALES S, CUSTOMER C WHERE S.CUST_NO=C.CUST_NO GROUP BY C.COUNTRY Page 534 9. Now, reopen it by setting Active back to True. This query returns a list of countries from the CUSTOMER table with the total value of their sales from the SALES table. We'll base the new chart on this query. 10. Double-click your QRChart component and click the Add button on the Chart | Series page. 11. Choose Pie as the chart type and click OK. 12. Click the Chart page's Titles tab and type Country Sales Figures for the graph's title.

13. Click the Series tab (the one to the right of the Chart tab), then click the DataSource page tab on the Series page. 14. On the DataSource page, choose DataSet as the chart's DataSource type. Click the DataSet drop-down list and select your MasterQuery component as the chart's DataSet. 15. Change the Labels property to reference the query's Country column and change its Pie property to reference the SUM column. Doing this will cause the sizes of the slices in the pie to be based on the SUM column, while the number of slices will be based on the Country column. Each slice will represent the sales numbers for a different country. At this point, you've finished constructing your simple pie chart. Click the Close button to continue. Figure 19.10 illustrates your new chart. Figure 19.10. Your new chart as it appears in the Delphi form designer.

Now that the graph is complete, save its source code unit as SALEGRAP.PAS. Next, press F9 to run the Reports app. When the app is onscreen, select your graph report from the report list and click the Preview button. Figure 19.11 shows what you should see. When you're finished wondering at the pristine beauty of your new creation, close both the Preview window and the app and return to Delphi. Page 535 Figure 19.11. Your graph as it appears when previewed from Reports.

Graphing with DecisionGraph You can use the DecisionGraph component to graph the cross-tab information rendered by the DecisionCube component. We'll add a DecisionGraph to the CustomerCTReport form you designed earlier. We'll reuse the DecisionCube components already on the form. Follow these steps to add your new chart to the CustomerCTReport form: 1. Press Shift+F12 and select CustomerCTReport from the list of forms to load it. 2. Change the Align property of the DecisionGrid component to alTop and raise its bottom edge so that it occupies only about half the free space in the middle of the form. 3. Drop a DecisionGraph component onto the empty form space vacated by the DecisionGrid component and set its Align property to alClient. It should take up the remainder of free space on the form. 4. Set its DecisionSource to the form's DecisionSource component. You should immediately see a new bar graph displayed. 5. Double-click the new chart to edit it, click its Titles tab, set the new chart's Title to Sales by Country and Year, then

click Close. Your new graph is now done. It uses the DecisionCube's dimensions to label the X-axis and the graphs bars. It uses the cube's SUM field as its Y-axis. Page 536 Let's see how the new chart looks at runtime. Save your work, run the REPORTS app, and click the Preview button for the Sales by Country report. Figure 19.12 shows what you should see. Figure 19.12. Your new DecisionGraph at runtime.

To see how closely related your DecisionGrid and DecisionGraph components are, drag the Order_Date column over to the Country column in the DecisionPivot component and drop it. This will have the effect of exchanging them (as indicated by the mouse cursor), which pivots the table. Notice that both the grid and the chart change when you pivot the DecisionCube, as shown in Figure 19.13. Figure 19.13. The DecisionGrid and DecisionGraph can both be pivoted by the DecisionPivot component.

Your new chart is now complete. Exit the REPORTS app and return to Delphi. Page 537

Enhancing Your Reports
Because you were wise enough to base as many of your reports as possible on a common ancestor, enhancing them en masse is a snap. We'll take advantage of the fact that all your reports descend from the CustomerReport class to add the username of the person running the report to each report's title area. Follow these steps to set up the username field on your reports: 1. Load the CustomerReport back into the Delphi form designer. 2. Drop two QRLabel components onto the right side of the title area on the same line as the report print date. Name the first one UserNameLabel and the second one laUserName. Because these components will be inherited by other forms, they must have unique names among all the forms in the hierarchy. It's not safe to assume that the default names

assigned by Delphi on one form won't conflict with those of another. 3. Situate the controls side by side, with the rightmost one ending on the right margin of the report. 4. Set the Caption of UserNameLabel to User Name:. Set the Alignment property of laUserName to taRightJustify. 5. Double-click the OnPrint event of the laUserName component and change the event handler procedure to look like this: procedure TCustomerReport.laUserNamePrint(sender: TObject; var Value: String); var MaxNameLen : Integer; begin MaxNameLen := 30; SetLength(Value,MaxNameLen); GetUserName(PChar(Value),MaxNameLen); SetLength(Value,Pred(MaxNameLen)); // The name comes back null-terminated end; This routine makes a call to the GetUserName Windows API routine to set the Value parameter that's been passed into it. Whatever is placed in Value will print on the report. Note the use of typecasting to pass Value to the routine as a Cstyle string. After GetUserName returns, we use the MaxNameLen variable (into which GetUserName has placed the number of bytes copied into Value) to set Value's length before returning from the event code. This is just a safety precaution to ensure that the string is properly terminated. To see the effects of your change, run the REPORTS app again and preview some of your CustomerReport-based reports. With the exception of the cross-tab report, you'll notice that every one of them now has a user field in its upper-right corner. You added it in just one place, Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 538 but your change rippled through to all the descendent forms. Think of the work that could be saved if Delphi applications developers would design systems that made full use of this powerful feature. Figure 19.14 shows your Customer List Report with the new field added. Figure 19.14. You can make widespread changes in seconds via form inheritance.

Summary
You've learned to create a number of different types of reports using Delphi's QuickReport components. You built a simple list report, a report with group breaks, a master/detail report, and a cross-tab report. You also constructed a basic pie chart and a DecisionCube-based bar chart. You learned the value of using visual form inheritance to build report forms, and you learned how to make global changes to your report hierarchy. You also built a nifty front-end app for testing your reports. The wide range of reporting topics covered here should equip you well for building your own client/server database reports in Delphi.

What's Ahead
Chapter 20, "Business Rules on the Database Server," explores the many ways that you can implement server-based business rules that complement the Delphi apps you build. You'll learn the many benefits of server-side business rules and you'll learn to construct triggers, constraints, views, and stored procedures to implement them. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 539

CHAPTER 20

Business Rules on the Database Server
Page 540 Business rules are restrictions, qualifiers, or tests that describe the business policies of an organization. They can be expressed in natural language terms that nontechnical people who are knowledgeable of the relevant business processes can understand. Database practitioners take these sentence-oriented rules and codify them in databases and applications. Some examples of common business rules include the following:
q q q

q q q

q q

All tenants must supply complete employer information. All property rentals will default to $950 per month. The security deposit on a property must be equal to at least half the rent on the property. Rent is past due five days after the rent due date in the lease contract. There will be a $5 late fee assessed for each day rent is past due. Tenants who rent more than one property receive a 10 percent discount on each additional property. All lease contracts are to be in effect for a minimum of six months. A new tenant may not move into a property until the lease contract period on that property actually begins.

Each of these rules has a corresponding database or program element. Their expressions as natural language sentences are meaningful to business persons.

Their representations as database and application elements are meaningful to database and application builders. The skilled client/server architect will be able to comprehend business rules expressed either way. This chapter discusses the server side of implementing business rules in database applications. There are those who discuss business rule implementations as a theoretical concept separate from the database constraints and program objects necessary to implement them. Although I agree that one must understand an organization's business rule needs completely before attempting to implement them, I think separating them from their practical application is a mistake. This is especially true given that they must eventually be reduced to real database or program objects anyway. Completely separating business rules from the data integrity constraints necessary to implement them seems unwise given the close relationship between the two. A fundamental policy of any organization is that the integrity of its data must be assured. No business wants a database riddled with orphaned rows, invalid column values, or other data anomalies. On the contrary, basic data integrity and reliability is a foundation upon which the organization expects to be able to rely. Thus, the two subjects—business rules and data integrity—are inseparably linked. There are basically three schools of thought regarding the proper place for business rules: server placement, client placement, and middleware placement. That a client/server application of any complexity will mix these approaches is a certainty. It's as certain as the partitioning of the application into separate client and server portions in the first place. The menagerie of Page 541 incarnations that business rules can take presents one of the biggest challenges to sound client/server development. Gone are the days of simple dBASE @... SAY...GET data validation. This chapter discusses the strengths and weaknesses of each of these approaches and then explores server-based business rules in detail. Make no mistake about it—you will mix client, server, and perhaps even middleware business rule implementations when building sophisticated client/server applications. Hopefully, exploring each tier separately will equip you for the task.

Business Rules Further Defined
Before discussing server-side business rules, let's define in a little more detail what business rules are. I'll explore the different manifestations that business rules can have and talk about their respective functions. An effective business rules implementation ensures that data residing in a database complies with the rules and policies of an organization. One means of accomplishing this is to set up each column in each table in a database so that it protects against values that violate the organization's policies. In database terms, this means applying constraints to the data to ensure that it contains valid values. This is what usually comes to mind when one hears the term business rules. NOTE Throughout this chapter, I use the terms business rule and constraint interchangeably. Constraints are the chief form business rules take in databases and applications. I've never really liked the term constraint—sometimes constraints don't constrain. Sometimes they define columnar defaults, and sometimes they establish relationships between tables. In this chapter, you'll often see business rule used generically to refer to the database and application elements that implement them.

Merely protecting the database from invalid data isn't enough. Business rules, as they relate to databases, actually have three separate functions: to keep unwanted data out of the database, to define relationships between columns and tables, and to describe the way that data originates in the database. I'll discuss each of these separately. Constraints keep invalid data out of a database. They also define default values for columns if no values are supplied. An example of a constraint is: "All invoices must contain valid customer information." In database terms, this would limit, or constrain, the values that can be stored in the INVOICE table. Another example of a constraint would be: "Values in the PaymentType column must be in the following list: `C', `H', `R'." A constraint that defines a default for a column could be expressed as: "The default payment type is cash." Page 542

Business rule definitions can also define relationships between tables (for example, "For every invoice, there must be at least one item ordered") and intra-table relationships between columns (such as, "The Commission column in the SALES table equals the AmountOfSale column multiplied by .07"). I prefer to avoid intra-table relationships between columns, usually performing calculations like these on-the-fly, but there are situations where this isn't possible. In this example, the Commission column is dependent on the AmountOfSale column, so it could be argued that such dependencies prevent the table from being fully normalized. Business rules can be used to describe a data element's origin. Describing an element's origination indicates its source. Where did the database element begin? How was it derived, calculated, or produced? Is it a summation of column X in table Y? A simple example might be, "The general ledger accounts payable balance is updated by adding new accounts payable transaction totals to it." This defines the origin or source of the general ledger accounts payable balance. As I've said, you should avoid basing one data element on another. I don't mean to say that this should never be done, but that you should carefully consider all the ramifications of doing so. Deriving one data element from another creates a dependency between the elements involved. This might be unavoidable, but it may also cause you headaches. If the source data changes, the dependent data needs to change as well. Typically, you protect against this by implementing data "posting" mechanisms. Data that's been posted updates its dependent elements (as in the example of the GL accounts payable balance), thereby rendering itself uneditable. No further modifications are permitted after the dependent elements have been updated. Systems that avoid dependencies of this type are commonly referred to as bucketless systems because they do not derive data elements for storage elsewhere in the database (in "buckets"). Now that you understand a little more about what business rules are, what's the best way to implement them? As I've mentioned, there are three schools of thought regarding the proper place for business rules. Determining the proper location for business rules largely influences the manner in which they will be implemented. Obviously, if you implement business rules on the server, you'll do it via constraints and the like. If you put your business rule implementation on the client, you'll do it via program code. If you implement business rules in a middleware product, you'll do it by using the facilities provided by the middleware product.

Server-Based Business Rule Implementations

I guess it's the DBA in me, but I am an unabashed proponent of server-based business rule implementation. This means that I take the "fat server" approach to implementing business rules. My basic philosophy is this: Business rules should be placed on the server when possible, and supplemented as necessary in middleware and in client applications, in that order. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 543 Naturally, the corollary to this principle is quite similar: A client-side business rule that can be moved to middleware or to the database server should be moved. Likewise, a middleware business rule that can be moved to the server should be. I think the reasons for this philosophy will become more evident as the pros and cons of the three models are discussed.

Server Implementation Strengths
One of the benefits of placing business rules on the database server is that the data itself is protected against tampering and accidental invalidation. Because the controls are intrinsically linked to the data, server-based constraints ensure that data stored in the database will always contain valid values and will be protected against invalid deletions. Another benefit of implementing business rules on the server is that all client applications that communicate with the database automatically have access to the rules. One of the big problems with the approach of locating business rules in client software is that client development tools are often platform or operating system dependent. Not so with server-based implementations. All the major client/server DBMS vendors support client connections from a number of different operating systems and hardware platforms. No matter what the client's host operating environment actually is, it is both restricted by and has access to the business rule information on the database server. Another advantage of implementing business rules on the server is speed.

Because organizations typically view a database server as an infrastructure investment, they are more willing to purchase the hardware and software needed to ensure that the server performs optimally. If placing business rule constraints on a server severely impedes performance, a corporation is usually more willing to buy the extra resources needed to remedy this than it would be to put high-powered machines on every desktop, even if the cost of the former exceeds that of the latter. Another point to consider is that database servers are usually, by design, scalable. Thus, if your needs exceed the maximum performance capabilities of a particular hardware platform, you can move your server and its data bank to a more powerful hardware platform without switching DBMS vendors or redesigning your database. This same sort of scalability would need to be present in middleware and client-based solutions for them to be viable alternatives. At present, this just isn't so. If your clients are Windows clients, you're pretty much locked into the Intel platform. True, Windows NT is also available on a handful of other platforms, but it isn't as portable as Sybase SQL Server, which is available on nearly every major hardware platform and operating system. Page 544

Server Implementation Weaknesses
One of the weaknesses of going the server-based route is that database server performance is inversely proportional to the size of its load. As you add constraints to database objects, access to them (particularly updates) is bound to slow down. This could be so severe that it makes the server unusable. The answer to this, though, is not to jump to either client- or middleware-based business rules. The answer lies in the same approach one would take if a sudden increase in data volume slowed the server to the point of being unusable: Invest in the additional hardware and software resources necessary to bring the server's performance up to par. In the example of a sudden increase in data volume, tossing the server out in favor of a client-based solution is usually not an option. The same should be true of business rule implementations: Treat the business rule implementation on the server as an integral part of the database and an essential server element. With that mentality, making the necessary investment to support server-based business rule implementations is less difficult. Another weakness that's often pointed out by opponents of server-based

business rules is that server implementations tend to be either database or server centric. This means that conveying business rule constraints between databases or across servers is either difficult or impossible. A table in one database could, presumably, not reference a table in another for the purpose of validating a column entry. Nor could a table on one server acquire a default value from a table on another. Because organizations have a tendency to organize servers by department, this presents a problem. The answer to these concerns, though, lies in pushing DBMS vendors to improve cross- database and cross-server interoperability. Sybase, for example, already supports cross-database queries and has for years. Furthermore, technology such as Sybase's Replication Server addresses the cross-server problem by allowing key tables to be automatically replicated from one server to another. And server gateway technology makes heterogeneous queries possible. It allows tables located on different vendor DBMSs to be queried as though they were all on the same server. The answer to database and server dependence is not in jumping ship to yet another immature technology but in influencing existing vendors to improve their cross-database and cross-server facilities to the point that they're usable. Another problem with server-based business rule implementations is that there is often a lack of integration between the client software and the server. One result of this is that server messages related to business rules are often improperly handled by the client or ignored altogether. The solution here, though, is not in switching away from a server-based implementation. It lies instead in pushing client software tool vendors (such as Borland) to fully integrate with their supported back ends. That is, if Delphi provides support for a particular client/server DBMS, that support should be complete and should support all the client-related facilities the platform provides, including business rule violation detection. Page 545

Client Implementation Strengths
One of the strengths of implementing business rules on the client is the level of customization and control one has in dealing with reporting and responding to business rules. Field-by-field control is usually quite easy in front-end tools. For example, preventing the user from exiting a form until its entries are valid is normally a simple task.

Another advantage of client-side implementation is the richness of application programming languages when compared with SQL. Object Pascal is vastly richer as a language than SQL. Even BASIC is a better language than SQL. A final advantage of implementing business rules on the client is that they can be encompassed in components that can then be reused in all applications developed with the tool. Adding business rules to an application then becomes as simple as dropping a component onto a form. Chapter 21, "Business Rules in Delphi Applications," illustrates how this is done.

Client Implementation Weaknesses
The chief fallacy in placing business rules exclusively on the client is that doing so requires increasingly more powerful client machines on the desktop. Client machines end up requiring the type of resources that would have made a machine a good server just a few years ago. You end up putting the latest, greatest hardware you can on the desktop because the client continually complains about performance. Client applications that are abundant with business rules have a larger memory footprint and run slower than those that aren't. This is especially true with applications that must run server-based queries to support their client-based implementations. For example, if a client application must query its back-end server to determine the data type of a particular column so that it can apply the appropriate data constraints, it has to do too much work. How much better it would be if the server was allowed to do the job it obviously already knows how to do. When you build applications that needlessly interrogate the server to get constraint support information, you have the problem of the "chatty nurse." The nurse attempts to perform surgery, but because she doesn't have the proper training, she must continually bug her boss, who is a surgeon. Because the surgeon spends all her time assisting her nurse, she has no time for surgery. And, since the nurse, no matter how closely she follows instructions, will never do everything the surgeon could have done, the surgery gets botched, as well. This is a clear case of elements not serving their intended purposes—not doing what they do best. This is the situation when you implement business rules in client apps that belong on database servers. With the constant upgrading of the client hardware come all the headaches associated with constantly changing hardware and software configurations. Historically, the constant tuning and tweaking of hardware has been relegated to server machines, but this isn't the case in the world of the "fat client." You virtually end up with a server on each desktop, requiring additional personnel

and expertise to support. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 546 This approach also violates one of the main tenets of the client/server philosophy—that of the reduced need for client resources. The natural outgrowth of client/server DBMS usage, so the thinking goes, is that because the real work of the DBMS happens on the server, client machines can afford to be less capable. The idea is that if resource needs increase, they will do so primarily on the server, not on the client. The "fat client" model runs directly counter to this thinking. Another problem with building business rule implementations into client applications is that such implementations are rarely cross-tool, let alone crossvendor, in nature. This means that you'll have to reinvent the wheel if, for example, you need to develop an application that updates a given table in Delphi one week and one that updates it in Oracle's Developer/2000 the next. Because the Developer/2000 app can't access the business rules you built into your Delphi application, you'll have to redo them using its tools. The situation is even worse when the prospect of working with tools on different platforms is considered. You might be able to encapsulate an application's business rule logic in a set of Delphi components and then use the set in both Delphi and Borland C++ Builder apps, thereby enforcing business rules across tools. But what happens when you need to enforce the same business rules in a character mode C program running on UNIX? Building business rules into applications, rather than on a server, also removes the capability of viewing the entirety of a database or data bank's business rules from a single vantage point. Because the rules are all encapsulated in program code or objects, you cannot easily tell which constraints are in place on what data, unlike the server-based approach. You end up using a CASE tool simply to keep track of where your business rules are implemented.

A final point to be made regarding the fallacy of client-based solutions is that even though a given application or tool may insulate the database from invalid data, the database itself remains unprotected. This creates an inordinate dependency on client software development tools that is not easily alleviated. It stipulates that all further development of client software must occur with the tool in question; otherwise, the business rules will be either neglected or duplicated in another tool. Though tool vendors would certainly have no problem with this, it's not a good business strategy. It's far better to have the rules on the server where they can be enforced regardless of the client tool and accessed and shared by all applications, as they were intended to be.

Middleware Strengths
Middleware refers to a layer of software between the client and the server. Specifically, middleware abstracts clients from servers and servers from clients; they communicate with each other through the middleware layer. Middleware can consist of anything between the client application itself and the database server. It might be a database connectivity API, such as the BDE, or it might be an application server, such as the OLEnterprise server that's included with Delphi 3 Client/Server. Middleware can take a number of different forms. Conceptually, the middleware Page 547 approach might be the best of the available options. In a perfect world, business rules would be defined in operating system objects that would then reference database server objects. Certain operating systems—for example, Steve Job's NeXTSTEP—have made bold strides in this direction. A middleware approach avoids the current difficulties with cross-server and cross-database business rule implementation, because the rules do not reside in a given database or on a given database server. The middleware approach also avoids both the fat client and fat server problems. Presumably, neither the server nor the client will be encumbered by the presence of business rules. You won't have to invest in powerhouse PCs for the desktop, and your server-based resources should be free from the burden of enforcing business rules. Delphi's constraint and data brokers offer a method of constructing application servers that is both powerful and easy. They allow you to build multi-tiered client/server applications as easily as you've built two-tiered applications in the

past. See Chapter 22, "Beyond the Two-tiered Model," for more information.

Middleware Weaknesses
The biggest problem with the middleware approach is that it requires yet another layer of development, aside from the mandatory client and server development efforts. Also, since your business rules will no longer be located in one place, you won't be able to view them from a single perspective. That is, because the rules will no longer live exclusively on the server or exclusively on the client—since they're distributed across the client, middleware, and server layers—they'll be more difficult to manage and administer. So you have two problems. You have redundant work in your development effort—many of the rules implemented in your middleware will also need to be implemented on the server—and you have increased difficulty in managing your app's business rules because they'll be spread across multiple layers of the architecture.

Implementing Server-Based Business Rules
Now that I've offered my views on what I think the proper business rules strategy is, what's the best way to implement it? Determining the proper location for business rules largely determines the way in which they will be implemented. If you go the server route, obviously you'll build database objects to support your implementation. The following section discusses those database objects and their respective roles in the business rules equation. Page 548 Getting Started The first thing to do in developing a business rules scheme is to model the business processes your app will address. This modeling step will no doubt require user interviews and interaction. Going through an application's relevant business processes with your users will aid you in determining the necessary object relationships and the requisite database and application constraints early on. The two general types of business rules you'll be looking for are those that ensure domain integrity and those that ensure relational integrity. Domain integrity refers to the type of data a column may contain. For example, a date column must contain valid dates, and a payment type column might be limited to cash, check, or credit card. Domain integrity business rules can be implemented through database column constraints.

Relational integrity constraints ensure that relationships between tables are respected. For example, if an INVOICE row references a row in the CUSTOMER table, a relational (or referential) integrity constraint ensures that the CUSTOMER table row cannot be deleted as long as the INVOICE row exists. As early as possible, you'll want to exhaustively develop every rule and every policy that can be applied to the relevant data. The best way to do this is to use simple sentences written in plain English. Some examples of English-like business rules follow:
q q

q

q

q

q

q

Each invoice number must be unique in the INVOICE table. Every customer number in the INVOICE table must have a corresponding row in the CUSTOMER table. The credit cards accepted are VISA, MasterCard, and American Express. Each line item on an invoice is totaled by multiplying the cost of the item by the quantity ordered. An invoice total is derived by summing all extended line item totals, adding 7 percent sales tax and $5 shipping and handling. An invoice header record may not be deleted as long as there are invoice detail records that correspond with it. The default method of shipping products is Federal Express.

Every column in every table should be reviewed. Each column must allow only valid data. Relationships between tables must be established and protected. Every aggregation and all data origination should be worked out in advance. Your client should check the list for accuracy and completeness before you begin work. Later, when you develop the constraints to place into the database design, you can check off each business rule on the list, one by one. Being thorough now will save you time (and heartache) later. Many CASE tools include substantial support for implementing business rules. Everything from keying in natural language sentences that describe business rules to setting up trigger tem- plates for implementing them is supported in popular client/server CASE tools. If you need to Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 549 establish a large number of business rules in a complex database model, a good CASE tool can save you many hours of work. Primary Key Constraints Table and column constraints are the vehicle most often used in implementing server-based business rules. Although it's true that triggers can do all that constraints can do and more, my philosophy regarding the trigger versus constraint debate is as follows: Constraints always have preference over triggers. Address as much of your business rules implementation as you can using constraints. Address the remainder with views, triggers, or stored procedures, in that order. Give views preference over triggers and triggers preference over stored procedures. Constraints are preferable to triggers because they're faster. This is because they're enforced by the server's kernel itself (usually natively compiled C). Triggers, by contrast, are composed of semi-compiled SQL. Machine code will nearly always be faster than semi-compiled code. Views are preferable to triggers for the same reason. The WITH CHECK OPTION syntax of ANSI SQL views is enforced by the server itself—not with SQL. Despite all this, triggers are still preferable to stored procedures because they're automatically enforced. Building business rules into stored procedures leaves open the possibility that a process could bypass the procedure to update its underlying table, thus circumventing the intended business rules. The first type of constraint to apply is the primary key constraint. It denotes which columns in a table uniquely identify each row. A primary key is the default method of accessing a table. Examples of primary keys are the InvoiceNumber field in an INVOICE table and the CustomerNumber field in a CUSTOMER table. By adding a primary key constraint, you cause the database server to ensure that the values inserted into the primary key column or columns are unique within the table. Although you would normally define a primary key constraint when you create a table, there are times when you might define it after the fact. The SQL syntax to add a primary key constraint after a table has been created looks like this:

ALTER TABLE CUSTOMER ADD PRIMARY KEY (CustomerNumber) Foreign Key Constraints Foreign key constraints are popular for defining relationships between tables. They force a value in a column in one table to exist in another table. Likewise, they prevent the deletion of data from the second table that is referenced by the first. You use a foreign key constraint, for example, to ensure that all the customer numbers listed in the CustomerNumber field of the INVOICE table exist in the CUSTOMER table. You would normally create a foreign key reference when you construct a table, but you might need to wait and do so afterward instead. Here's the SQL syntax to add a foreign key constraint after the fact: ALTER TABLE ORDERS ADD FOREIGN KEY (CustomerNumber) REFERENCES CUSTOMER Page 550 Check Constraints Another type of constraint is the check constraint. A check constraint ensures that a value inserted into a column exists in a given set of fixed values. Suppose that a given retail organization accepts only certain credit cards—for example, VISA, MasterCard, and American Express. Because this is a fixed set of values, it's a good candidate for a check constraint. You could code it as a check constraint using the following SQL syntax: ALTER TABLE ORDERS ADD CONSTRAINT INVALID_CREDIT_CARD CHECK (CreditCardType in (`V','M','A')) Check constraints also can be constructed at table creation time. Here's an example: CREATE TABLE NYSE_EVENTS ( EventNo INTEGER NOT NULL, Year INTEGER NOT NULL CHECK (YEAR >= 1790), Description VARCHAR(80) NOT NULL, Resolution VARCHAR(80) NOT NULL PRIMARY KEY (EventNo) ); Check constraints can be defined at the table level or for individual columns, as in the previous example. Defaults Default values for fields are important in that they determine the value a column gets during an INSERT operation if one is not supplied. Supplying default values helps ensure that a row's columns initially

contain valid entries when the row is created. There is a tendency among developers to establish column defaults using program code. For example, you could easily assign a column a default value using Delphi's OnNewRecord event. Regardless of how easy it is, defining column defaults in applications is a bad practice. It requires one to dig through program code to find the default values for a table's columns. It's a far better practice to store those defaults on the server where they can be easily viewed and altered. Though some vendors provide platform-specific means of establishing column defaults, I'll stick with the ANSI syntax. The ANSI syntax for establishing a column default is as follows: ALTER TABLE ORDERS ADD CreditCardType char(1) DEFAULT `V' Views There are times when implementing a particular business rule is best done via an SQL table view. Simply put, a view is an SQL SELECT statement you may compile and query as though it Page 551 were a table itself. The uses of SQL views are many and varied, though one key use of views in implementing business rules is that of performing computations involving a table's columns. For example, here's a view to calculate invoice line item totals: CREATE VIEW INVOICEDETAIL AS SELECT InvoiceNumber, LineNumber, PriceEach * UnitsOrdered ExtendedTotal This view implements the business rule, "Each line item on an invoice is totaled by multiplying the cost of the item by the quantity ordered." Triggers When all else fails, a trigger can usually get the job done. Many platforms include special extensions to SQL that give triggers and procedures special capabilities. Sybase, for example, provides Transact-SQL, whereas Oracle includes PL/SQL. You should always use a constraint rather than a trigger, when possible. If you can't do what you need to do using a constraint, chances are a trigger will do the job. Here's an example of a trigger that goes beyond the simple capabilities of a column constraint: CREATE TRIGGER ORDERSInsert FOR ORDERS BEFORE INSERT AS BEGIN

IF (New.CreditCardType='V' AND New.Amount<50.00) THEN EXCEPTION BELOW_MINIMUM_VISA; END This trigger ensures that orders made using a VISA credit card total $50 or more. Because of the conditional nature of the rule, it would be difficult if not impossible to implement as a simple column constraint. Thus, the use of a trigger is appropriate in this situation. Stored Procedures There is a trend in some SQL circles to use stored procedures for implementing business rules. The thinking is that one should use stored procedures for all DML (Data Manipulation Language) operations, building a separate INSERT, UPDATE, and DELETE procedure for each table. This procedure would then ensure that the proper integrity constraints and business rules were observed with each operation. The problem with this approach is that it prevents the viewing of your database security and business rule implementation from a single vantage point. It also nullifies the use of tools such as Delphi, because their capability to bind program objects to database elements goes largely unused. Though you can make use of DML stored procedures via Delphi's TUpdateSQL component, doing so removes much of the incentive for using tools like Delphi in the first place—their ability to automatically bind to data, regardless of its structure. You have to mirror your table structures in their companion procedures, which is precisely what you attempt to avoid with RAD tools like Delphi. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 552 Creating a minimum of three stored procedures for every table in a database also makes managing the database more difficult. In databases with hundreds of tables, you could literally have thousands of stored procedures. These are a maintenance nightmare and a constant source of grief for the DBA. The performance benefits from building stored procedures in a piecemeal fashion like this are over-touted and apply only in certain very unique circumstances. They're usually minuscule if perceptible at all. Usually, the headaches associated with creating stored procedures in Tribble-like quantities far outweigh any advantages. Remember that data maintenance stored procedures have to be debugged and supported, just like any other programming element. If a table turns up with bad data in it, you'll need to begin by checking its maintenance procedures for bugs. In addition to your Delphi code, you could end up with a sizable body of needless stored procedure code to support, as well. I advise you to avoid building unnecessary stored procedures. Shy away from creating procedures on a whim. Instead, use stored procedures only in situations where neither a traditional constraint nor a trigger can do the job. An example of such a situation is that of the infamous posting routine. If you want to accumulate the values in a column or columns for posting in a separate table, you'd probably do so via a stored procedure because you'd want to be able to control exactly when the posting occurred. Consider the general ledger example mentioned earlier. If you were to write a stored procedure to handle the posting of accounts payable transactions to the general ledger, you might do something like this: CREATE PROCEDURE POSTAP (APACCOUNT)

AS DECLARE VARIABLE APTOTAL FLOAT BEGIN SELECT SUM(AmountOfTransaction) TranAmount INTO :APTOTAL FROM APTRANS; UPDATE GLBAL SET APMonthEndBalance=APMonthEndBalance+:APTOTAL WHERE AccountNumber=:APACCOUNT; END This procedure defines the APMonthEndBalance column as originating from the AmountOfTransaction column in the APTRANS table. It implements the business rule that defines how the general ledger accounts payable account balance is derived each month. Stored procedures are good for situations like this one because they can perform as many SELECTs as necessary to perform the data operations you need to complete and because you decide when they execute. They're particularly well suited for batch-oriented operations like posting routines and generating complex reports. Page 553

Summary
If you take each concept outlined here, break it down, and apply it to your specific circumstances, you should find that implementing server-based business rules is intuitive and straightforward. Envision the database objects I've mentioned here as increasingly complex tools for implementing business rules. Procedures are more complex than triggers, and triggers are more complex than table constraints. With each business rule you implement, you use the simplest tool that's capable of fully implementing the rule. If a given tool can't do what you need, move up to the next one. For example, if a simple column constraint doesn't do the trick, you might need to move up to a view or trigger. Whatever the case, the examples given in this chapter should get you well on your way to successful server-side business rules implementation.

What's Ahead
Chapter 21, "Business Rules in Delphi Applications," discusses client-side business rules. I'll cover the OLEnterprise application server middleware product that's bundled with Delphi Client/Server. I'll also explore what's involved with building business rule enforcement directly into your Delphi applications. It's inevitable that you'll have to fill in the gaps in a server-based implementation with business rules at the middleware and client levels. Chapter 21 gives you some tips on how to go about this.

Page 554 Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 555

CHAPTER 21

Business Rules in Delphi Applications
Page 556 This chapter discusses the application side of successful business rules implementation. As I stated in the chapter on server-based business rules, I believe that business rules should be implemented on the server side of an application whenever possible. However, in real applications it's inevitable that you'll need to supplement server-side business rules with middleware or clientside rules. It's my hope that someday enough synergy will exist between database servers, middleware products, and software development tools that this will be a nonissue—that the developer won't have to be concerned with where business rules are implemented but can instead concentrate on how they're implemented. There is a propensity among longtime software developers to place a significant amount of an application's business rule logic in the application itself. The development tool world is more familiar territory to them, and DBMS platforms have historically lacked the sophisticated business rule facilities that polished applications demand. Propensities notwithstanding, as I stated earlier, moving an application's business rules wholesale to the application itself is not the answer. Developers must coerce DBMS and middleware vendors to provide the comprehensive

business rule interfaces that professional applications require. The answer isn't to simply give up and produce a hackneyed solution that relies on inferior technology. Instead, the best approach is to force vendors to meet the needs of their customers—to provide business rule support that real applications can use. Until this is done, client/server systems will continue to be little more immune to business rule infractions than their flat-file counterparts of yesteryear. To their credit, DBMS and middleware vendors are making great strides in shoring up their business rule strategies. In the past, most server-based business rule implementations were fairly airtight—they were safe—but they weren't "developer-friendly." Developers had difficulty getting database objects and application objects to work together seamlessly. For example, if a Sybase default object was used to supply values for an inserted row, how would the application know this and display the default column values onscreen when a user added a row to the table? The developer was left to choose the lesser of two evils: Either ignore the server-based business rule and redundantly implement it in applications, or blow off the need to synchronize the appearance of the application onscreen with its database counterparts. Neither of these options was particularly attractive, and, as Jerry Garcia once observed, "Choosing the lesser of two evils is still choosing evil." Today, DBMS and middleware vendors are becoming increasingly more developer-friendly. Sound business rule design is easier now than ever before. At the same time, database development tools like Delphi are becoming more aware of and more intelligent about their database and middleware pen pals on the other end of the line. The result is that development tools and server products are converging in their attempts to satisfy people's needs. The winners in all this are you and I. Implementing a sound business rule strategy is becoming increasingly easier as DBMS platforms, middleware products, and development tools learn to work together. Despite where things are going, you have to deal with where they are right now. The situation at present is that you will probably have to implement part of an application's business rules in Page 557 the application itself. As I said in the chapter on server-side rules, the approach I take is this: I design all the business rule strategy that I can on the DBMS platform itself, then I supplement this in middleware and on the client as needed. It makes no difference whether you're dealing with Paradox tables or a

full-blown client/server implementation. What I can't implement on the server, I implement in middleware and in the client. I fill in the gaps of the serverbased implementation with middleware and application constructs. This chapter addresses the application side of proper business rules implementation. It takes you through the various means of constructing an effective business rule strategy in your applications. You'll learn that there are four major levels of application-based business rule design in Delphi: the data type level, the component level, the TField level, and the TDataSet level. I'll explore each of these in-depth so that you'll see how to apply them in your own applications.

Types of Business Rules
Business rules come in two distinct types: simple business rules and complex business rules. Simple rules provide for simple entity integrity. They ensure, for example, that numeric data goes into numeric fields, that date columns contain valid dates, and so on. Simple rules apply no matter what the application is or what the database is being used for. Complex rules govern more advanced aspects of database access. Complex rules help ensure referential integrity. They tend to be database specific or even table specific. In this book, I use the term business rules in its broadest meaning. That is, I include both simple and complex rules in the same discussion. Everything from simple data validation to complex relational integrity is included under the business rules umbrella. Although I could separate data validation techniques from the general business rules discussion, there's nothing to be gained by doing so. The methods used to implement simple business rules are the same methods used to forge complex ones. My approach, therefore, is to discuss effective business rule design in general. Regardless of whether they're simple rules or complex ones, they both need careful attention. Delphi provides a number of ways to construct simple business rules with no coding whatsoever and provides a nice suite of tools for constructing complex rules using a minimal amount of code. This chapter explores both types of rules from the perspective of the application.

Two Rules About Business Rules
The first level of client-side business rule implementation is the data-type level.

There are two fundamental rules to keep in mind that demonstrate the importance of data-type selection. You'll solve a number of the typical problems that developers run into before they even occur if you follow these two very basic rules. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 558 Rule Number One The first rule for establishing client-side business rules is Use the appropriate database data types in the first place. Delphi's VCL enforces a number of built-in, simple restrictions based on the type of data a column represents. For example, data-aware components won't permit invalid dates in date-type columns, alphabetic characters in numeric fields, or invalid values in Boolean fields. You don't have to do any coding to enforce these constraints; Delphi enforces them automatically. The key is that you must use the appropriate data types to begin with. There is a tendency among veteran developers to make too much use of string types when building databases. For example, I've seen tables containing date columns that were defined using string data types. I've also seen numeric data stored in string fields. If a column is to contain dates and dates alone, define it using a date data type. If a field can contain only numbers, define it using a numeric data type. There's no reason to store numeric values in alphabetic columns. Another situation where using the right data type is crucial to proper business rule design is in dealing with columns that contain sequential values. Although you could certainly effect a sequential numbering system through Object Pascal code, it's preferable to use data types that are inherently sequential in the first place. For example, Sybase and Microsoft support the use of identity columns—special columns that are automatically incremented by the server. By using auto-incrementing data types, you relieve yourself from having to

maintain the sequential numbers yourself, and you ensure that the basic business rule you're after—the numbers in column Y of table X must be sequential—is enforced with no real effort on your part. Rule Number Two The second major rule to follow when implementing application-based business rules is Use Delphi components that best match their underlying data types. The most common error that developers make in this regard is in using plain text entry components too extensively. These text entry components (such as DBEdit, TEdit, and TMaskEdit) can allow data to be keyed into a field that is invalid for the component's underlying data type. You can usually avoid this by using the right component in the first place. For example, you shouldn't use a DBEdit for a Boolean field. The field can have just two values: True and False. Use a checkbox, radio buttons, or a dropdown list instead. And don't use a DBEdit for a field that's always read-only; use a DBText component instead. This conserves system resources and saves you the trouble of setting the component's ReadOnly property. Also, you shouldn't use a DBEdit for a numeric column that can have only a handful of valid values. Use a DBRadioGroup instead. Table 21.1 summarizes which components you should use with which basic data types. Page 559 Table 21.1. Column data types and their appropriate VCL components. Data Type Boolean Date Numeric (allows large number of values) Numeric (allows only a handful of values) Components DBCheckBox, DBRadioGroup, DBComboBox, DBListBox DBEdit, Calendar, SpinEdit, DataTimePicker DBEdit, SpinEdit DBRadioGroup

String (allows large number DBEdit of values) String (allows only a handful DBComboBox, DBListBox of values) TIP You can configure the type of control that's used for a particular field by editing the TControlClass property in the Database Explorer. After you've set this property, dragging a TField from the Fields Editor onto a form will cause the component you specified to be created.

The main idea here is to use the component that ensures the validity of the data entered into it as much as possible without preventing valid entries.

Custom Components
The second tier of application-side business rule implementation is the custom component layer. Because of Delphi's component-based architecture, a number of quality tools and libraries have emerged that enable you to bind business rules directly into components. They usually do this by descending new components from Delphi's DataSet component class and building the desired business rule logic into these descendant components. These tools typically enable you to specify everything from individual field input masks to complex relationships between tables. Especially with Delphi 1.0, this is a very practical approach to constructing sound business rule implementations. The downside to this approach, however, is that because developers aren't forced to use the components, Delphi applications can be constructed that do not respect the business rules ingrained in them. Furthermore, because the component approach is a Delphi-specific solution, applications built in other tools (with the exception of Borland's C++Builder) can't make use of the business rule logic embedded in these TDataSet descendants. Page 560 My word of advice on these types of tools is the same as for application-side business rules in general: If you elect to use one of them, you should still

implement all you can of your business rule strategy on your DBMS itself and use these custom components as a means of enhancing that implementation.

TField Properties and Business Rules
The third level of the client-side business rule strategy is the TField property level. Business rules normally refer to more complex entities than mere field masks and display labels. Still, many properties of the TField component class are relevant in some way to proper business rule implementation. Properties like TField's edit mask attribute are relevant in that they force the data that a given column receives to conform to a specific format. This is a simple business rules concern, as mentioned earlier. Upon the foundation of these simple business rules, you build the structure of the more advanced rules, like those that ensure referential integrity between two tables or those that supply a given column with a default value. Successful business rule implementation requires a devoted attention to detail. There are two ways to implement simple business rules through TField component property settings. The first method is to use Delphi's Data Dictionary and set relevant TField properties using Attribute Sets. The second method is to set those TField properties in your application's DataSet components themselves. Although they both amount to the same thing—the TField Attribute Settings you specify end up becoming a part of your application's TField components—your first choice should be to use the Data Dictionary to define your TField settings. Establishing TField properties using the Data Dictionary is more flexible and of more general use than doing so via the Fields Editor of DataSet components. What you cannot define via the dictionary or what makes no sense to define in the dictionary can be set up in the DataSet components themselves. I'll explore both methods so that you can see the relative benefits of each approach. Importing Dictionary Information You can import data dictionary information directly from your host database into Delphi's Data Dictionary. The constraints, defaults, data types, and other pertinent information from your server will be copied into the dictionary en masse. This saves you the trouble of duplicating server-based constraints in the dictionary and helps keep your server and application-based business rules synchronized. Follow these steps to import Data Dictionary information from a database: 1. Click Database Explorer's Dictionary tab, and then expand the

Databases branch. 2. Select a database to house the Data Dictionary information. Data Dictionary information can be stored in a new dictionary database that you register via the Dictionary | New menu option, or in an existing one such as the DBDEMOS database that ships with Delphi. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 561 3. Click the Import from Database option on the Explorer's Dictionary menu. 4. When the Import Database dialog is displayed, select the BDE alias that corresponds to your database and click OK. Importing your database into Delphi's Data Dictionary brings in the constraints and other field-level information from your database. It also brings in domains with associated constraints as Attribute Sets. This process enables you to use imported Data Dictionary information (and any changes you make to it) in your Delphi apps. So, if you import a column for which a CHECK constraint has been defined in your database, that constraint will also appear in Delphi apps that reference the column. Rather than having to react to the server-side constraint rejecting invalid entries, your apps will prevent the invalid data from even getting to the server. Creating Your Own Attribute Sets Besides importing Attribute Sets from databases, you also can create your own and associate them with columns in your tables. Follow these steps to construct and use your own Attribute Sets: 1. Right-click the root Attribute Sets branch on the Dictionary page and select New from the menu. 2. Key in a name for the new Attribute Set. 3. Configure the TField properties you want it to specify. 4. Click the Database Explorer Apply button to save it. 5. Expand the Database branch on the Dictionary page and click the table column with which you want to associate the Attribute Set.

6. Click the Attribute Set drop-down list on the Explorer's Definition page and select your new Attribute Set. 7. Click the Explorer's Apply button to save your changes. There are numerous TField attributes that you can establish using Attribute Sets. Delphi's Data Dictionary provides a very comprehensive facility for defining business rules through TField properties. The advantage to defining business rules in the Data Dictionary rather than doing so in the DataSets themselves is that the business rules you set up in the Data Dictionary can be used by any Delphi application that references the tables in question. This provides a similar benefit to the approach taken by some tool vendors of bundling business rules into custom components, but without any of the drawbacks of using third-party code. Defining Business Rules Using TFields In addition to using Delphi's Data Dictionary to define TField attributes, you can also define business rules using TFields themselves. Delphi adds TField components to a form's class when Page 562 you select Add fields from a DataSet's Fields Editor menu. When you create TField components in this manner, any attributes you defined in the Data Dictionary are reflected in Delphi's Object Inspector. You can follow these steps to set up TField-based business rules: 1. Double-click a DataSet component (TTable, TQuery, or TStoredProc) to bring up the Fields Editor. 2. Press Ctrl+A to create TField definitions for the DataSet's fields. 3. Click on each one of them to view/set their properties in the Object Inspector. Required One of the key TField properties that you can set in the Object Inspector is the Required property. This property determines whether a field must have a value. An example of a time when you'd want to change the default value for this property is when the server is supplying the value for a key or other NOT NULL column. Because the column is mandatory on the server, Delphi assumes it's mandatory in the application, so the TField's Required property

defaults to True. If the server supplies a value for the field, this is unnecessary and will actually prevent your app from working correctly. Switching Required to False places the burden of ensuring the field gets a value back on the server, which is where it belongs anyway. Calculated Fields Calculated fields are useful for everything from deriving simple field values to performing complex computations involving the fields in a table. You can perform mathematical operations, table lookups, data validation, and so on, using Delphi's calculated fields mechanism. Calculated fields are especially handy for data that you want to display but have no need to store. To set up a calculated field, you must accomplish two basic tasks: 1. Define the field in the appropriate DataSet's Fields Editor. 2. Assign the field a value in its DataSet's OnCalcFields event. Here are the specific steps you need to follow to define a calculated field: 1. Double-click a DataSet component to bring up its Fields Editor. 2. Right-click the list of fields and select New field from the menu. 3. In the New Field dialog, type the name you want to use for your new calculated field, select its data Type, and click the Calculated radio button in the Field type radio button group. 4. Click OK to finish your definition. When you return to the Fields Editor, you should see the new field's properties listed in Delphi's Object Inspector. Notice that the Calculated property is set to True. Page 563 5. Now that that the field is defined, set up an OnCalcFields event for your DataSet that assigns a value to the new field. Here's an example of an OnCalcField assignment: taORDERSOrderDateTime.Value := Now;

This line of code assigns a calculated field named taORDERSOrderDateTime the value of Delphi's Now function—the current date and time.

Calculated fields are particularly useful for performing fixed computations involving a table's columns. You could, for example, use a calculated field to encode the amount of money your organization pays per mile when an employee takes a personal vehicle on company business. Or, you might build the amount of sales commission your firm typically pays its salespeople into an OnCalcFields event. The possibilities are endless. An important thing to remember is to keep these calculations as lean as possible so that you don't slow down your apps any more than necessary. OnValidate Another method of establishing TField business rules is through the OnValidate event. Every TField has an OnValidate event associated with it that you can use to verify its value. The code you place here can do just about anything—it can compare the value the field is receiving with constants, other fields, or the results of a query. If you determine that the value the field is receiving is not valid, simply raise an exception. This displays an error message and prevents the posting of the row. You can create your own custom exception types or use those provided by Delphi. OnExit Another common place to implement field-based business rules is in the OnExit event of data-aware components such as DBEdit. You can attach code to the OnExit event that executes when the user exits the control. This code can assign values to other fields (for example, assigning an ending date based on a supplied beginning date), validate values or make other components on the form visible or invisible based on the entry.

DataSets and Business Rules
The next level of application business rule design is the DataSet level. There are three events of the DataSet component class (from which the Table, Query, and StoredProc components all descend) that merit special consideration: OnNewRecord, BeforeDelete, and BeforePost. These correspond roughly to the INSERT, DELETE, and UPDATE triggers that you can link to database tables. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 564 OnNewRecord The OnNewRecord event occurs anytime a new record is added to a table. You can use it to supply values for a table's columns. Here's a code sample that supplies a default value for the PetDeposit and LawnService columns of the "Tutorial" section's LEASE table: procedure TForm1.taLEASENewRecord(DataSet: TDataSet); begin taLEASEPetDeposit.Value:=0; taLEASELawnService.Value:=True; end; BeforeDelete BeforeDelete occurs just prior to a row deletion, as its name suggests. You can use it to verify that a deletion the user is attempting is a valid one. For example, you can use it to prevent the deletion of a row in one table while rows in other tables depend on it. You would be effecting a code-based referential integrity scheme by doing this. By raising an exception within this event, you thwart the pending deletion. For example, here's a code snippet that uses the LEASE table and the PROPERTY table from Part II of this book to effect a simple RI scheme: taLEASE.IndexName:='Property_Number'; If taLEASE.FindKey([taPROPERTYProperty_Number.AsInteger]) then raise EDatabaseError.Create(`Property_Number is in use - delete failed'); This code prevents the deletion of PROPERTY rows that have referencing LEASE rows. BeforePost The BeforePost event, as its name suggests, occurs just prior to a row being saved to its table. You can use the BeforePost event to validate the values in a new row or the changes to an existing one. If you raise an exception within this event, you stop the post. Here's a code sample that ensures that the LeaseEndDate always falls chronologically after the LeaseBeginDate: procedure TForm1.taLEASEBeforePost(DataSet: TDataSet); begin If (taLEASELeaseBeginDate.AsDateTime > taLEASELeaseEndDate.AsDateTime) then

raise EDatabaseError.Create(`Lease ending date must come after its beginning date'); end; There are several other special DataSet events that you might want to use to implement application-based business rules. Table 21.2 includes a list of events supported by the DataSet component class that are relevant to business rule design. Page 565 Table 21.2. DataSet methods that you might find useful in implementing application-based business rules. Catalyst Occurs following a Cancel Occurs following the close of the DataSet Occurs following a Delete ccurs following an Edit Occurs following an Insert or Append Occurs after a DataSet is opened Occurs following a Post Occurs prior to a Cancel Occurs before the close of the DataSet Occurs prior to an Edit Occurs prior to an Insert or Append Occurs before a DataSet is opened Occurs when calculated fields need values Occurs when there is a problem deleting a record Occurs when there is a problem editing a record Occurs when filtering is active and the DataSet needs a OnFilterRecord row Event AfterCancel AfterClose AfterDelete AfterEdit fterInsert AfterOpen AfterPost BeforeCancel BeforeClose BeforeEdit BeforeInsert BeforeOpen OnCalcFields OnDeleteError OnEditError

Summary
In this chapter, you've learned about the four major levels of application-based business rule design in Delphi: the data type level, the component level, the TField level, and the TDataSet level. Each of these is cumulative—it simplifies the levels that follow it. If you've picked the correct data type for a field, picking the best user-interface component with which to service it becomes much easier. Likewise, if you set TField's properties appropriately, the work you must do at the DataSet level becomes less complex. I tend to think of constructing business rules this way as similar to building a pyramid: As the lower levels are built correctly, the higher ones become fundamentally simpler. Likewise, if you botch the lower levels, the higher ones are more difficult to build and less stable. It's important to pay careful attention to the fine points of application-based business rules. Even the smallest of details can make a big difference in the robustness of your applications. Page 566

What's Ahead

In the next chapter, you'll learn to build multi-tier applications using Delphi's data, constraint, and business object brokers. Referred to collectively as MIDAS (Multi-tier Distributed Applications Services Suite), these technologies allow you to go beyond the simple two-tier client/server model and build applications that are scalable across the enterprise. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 567

CHAPTER 22

Beyond the Two-tiered Model
Page 568 When client/server computing first came into vogue, it seemed reasonably certain that technology would evolve to fill the wide gap that existed then between the client and server sides of the equation. Business rules could be constructed on the server, but integrating server-side business rules into clientside applications proved to be a tricky task—especially in the early days of the client/server revolution. It was assumed that someday servers and clients would work well enough together to overcome these limitations and provide seamless applications to end-users. Unfortunately, as more and more companies moved to client/server technology, this never really happened. Frustrated with the lack of integration between database servers and application development tools, developers began building business rules into applications on a regular basis. It was not uncommon to find systems with their business rules and constraints set up entirely on the client side, with few or none on the database server. Although this was the best approach for delivering polished apps to the enduser, the disadvantage to it was that client-side constraints could not be as easily shared among multiple tools or propagated over multiple platforms as server-side constraints could. Developers ended up redundantly constructing

business rule implementations for every development tool they used and for every platform they supported. The end result was a lot of wasted time and errors made transcribing business rules from one place to another. The answer to this conundrum and various others not originally foreseen in the early days of client/server computing is the multi-tier model. Rather than having only two levels—the client and the server—the multi-tier approach allows for many levels. In a multi-tier system, there may be several levels between the client and server, each filling a unique role. Delphi supports multi-tier applications development through four key technologies:
q q q q

The Remote Data Broker The Constraint Broker Briefcase computing support Partial data package support

I'll discuss each of these separately and show you how to make use of them in real applications.

Remote Data Broker
Delphi's Remote Data Broker technology allows you to build thin-client apps that require no database engine or drivers on the client machine. Instead, they communicate over the network with an application server that contains the TTable, TQuery, and TStoredProc components needed to access your database server. Within the application server, you can set up constraints and business rules that can be shared by the apps that rely on it for access to your server. This centralizes business rules, while still allowing each client application to function as though the business rules resided in it. Page 569 NOTE

Although it's true that Delphi's thin-client model allows you to build apps that don't require the Borland Database Engine to be included with them, thin-client apps still require a mechanism of some sort to communicate with the application server. This facility is located in the DBCLIENT.DLL file, which you must distribute with thin-client applications. It appears that DBCLIENT.DLL is only required when a thin-client app makes changes to data and sends those changes to the server, but you should include it with all thin-client apps just to be on the safe side.

To build a Remote Data Broker, follow these steps: 1. Make sure that your InterBase database server is currently running. 2. Start Database Explorer and define a new BDE alias called IB_EMPLOYEE that references the EMPLOYEE.GDB InterBase database that ships with Delphi (this database is in C:\Program Files \Borland\IntrBase\Examples, by default). 3. Right-click the new alias and select Import to Dictionary. This will import the database's constraints and other data dictionary information from InterBase into Delphi's data dictionary. 4. Exit Database Explorer and return to Delphi. 5. Click the File | New Application menu option to start a new Delphi app. 6. Click the File | New menu option and double-click the Remote Data Module option in the New Items dialog. 7. Supply a Class Name of RemoteData Broker in the Remote Dataset Wizard, then click OK. 8. Drop a TTable component, a Database component, and a Session component onto the form. 9. Set the Session component's AutoSessionName to True. 10. Set the Database component's AliasName property to IB_EMPLOYEE and its DatabaseName property to IB_EMPLOYEE. Set its LoginPrompt property to False and then key these two parameters into its Params field: USER NAME=SYSDBA PASSWORD=masterkey 11. Set its HandleShared property to True. 12. Set the Table component's DatabaseName property to IB_EMPLOYEE and its TableName property to DEPARTMENT.

13. Right-click the Table component and select the Export...from data module option. Page 570 14. Save the project to disk. Save Unit2 as RDB01.PAS, Unit1 as RDB00. PAS, and the project as RDB.DPR. 15. Now run RDB to register its COM server. After it loads, you can close it immediately Once your data broker is created, you're ready to build an application that uses it. Follow these steps to build the RDC (Remote Data Client) app: 1. Click File | New Application to start a new Delphi app. 2. Drop a ClientDataSet, a RemoteServer, and a DataSource onto the default form. 3. Drop a DBGrid onto the form. 4. Set the RemoteServer component's ServerName property to RDB. RemoteDataBroker. 5. Set the ClientDataSet component's RemoteServer property to RemoteServer1 and its ProviderName to Table1 (Table1 references the DEPARTMENT table in your RDB app). 6. Set the DataSet property of the DataSource component to reference the ClientDataSet component and set the DataSource property of the DBGrid to reference DataSource1. 7. Open the ClientDataSet component by setting its Active property to True. You should see data from the DEPARTMENT table in the DBGrid. Congratulations—you've just built a Remote Data Broker and matching client app. Save your project to disk before proceeding. Save Unit1 as RDC00.PAS and the project as RDC.DPR ("RDC" signifies "Remote Data Client").

Constraint Broker
In addition to brokering data, Delphi's remote data module component can also broker constraints. It can use the constraint information imported from your database server to save you from even having to send invalid data to the server. This reduces network traffic and speeds up your apps. To see how this works, follow these steps:

1. Reload the RDB app into Delphi. 2. Double-click the Table component to bring up the Fields editor, then press Ctrl+A to add fields. Press OK to add all the DEPARTMENT table's fields to the form. 3. Click the Budget field in the Fields editor. You should see an ImportedConstraint for it in the Object Inspector. 4. Close the Fields editor, save your app, and re-compile it. 5. Next, re-open the RDC app and run it. 6. Once the app is onscreen, edit the DEPARTMENT table using the DBGrid control and attempt to supply an invalid value for the Budget column (any value less than 10001 is invalid). Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 571 You'll notice that the value is rejected immediately without ever having been sent to the server. The Constraint Broker implements its own version of your server constraint and intercepts the invalid data value before it's sent to the server. Note the inclusion of the constraint in the error message that's displayed.

Briefcase Computing Support
In addition to brokering data back and forth from an application server, Delphi can also store data locally. This allows you to work with server data offline, using the "briefcase" approach. To see how this works, follow these steps: Close the RDC app if it's still running. Drop three Button components onto RDC's main form. Name the first button btSave, the second btLoad, and the third btSend. Set btSave's Caption property to `&Save locally', btLoad's to `&Load local data', and btSend's to `Send changes to server'. 5. Size the buttons so that their captions display completely. 6. Double-click btSave and set its OnClick event to: ClientDataSet1.SaveToFile(`C:\TESTLOCAL.DAT'); 7. Double-click btLoad and set its OnClick code to: ClientDataSet1.LoadFromFile(`C:\TESTLOCAL.DAT'); 8. Double-click btSend and set its OnClick event to: 1. 2. 3. 4.

ClientDataSet1.ApplyUpdates(0); 9. Save your app and run it. 10. Once the app is onscreen, click btSave. 11. Next, find your RDB app (which was launched via automation) and close it. Ignore the subsequent error message and click Yes to shut down your app server. 12. Back in RDC, you'll notice that you can continue work with the loaded set of data. Make a few changes to the data, then click the btSave button to save them locally. 13. Close the RDC app, then re-run it. This will reopen your RDB app server. Notice your changes are gone because the data was refetched from your database server. 14. Click the btLoad button to load the local copy of your data. You should see your changes. 15. Click the btSend button to apply your changes to the server. If you now exit the app and reload it, you'll see that the changes you originally saved locally have now been applied to the copy of the data on the server. This is what's meant by the use of the "briefcase" metaphor—data can be saved off and then reapplied later when it's convenient, and all the while, you're allowed to work with it uninterrupted. Page 572

Partial Data Packages
Another nifty feature of Delphi's data broker technology is the capability to download partial packages of data to the client app. Rather than having to grab an entire result set, the application designer can opt to retrieve a small subset of rows at a time. This is particularly beneficial with apps that tend to grab small sets of data in the first place, such as master-detail apps. To see how this works, follow these steps: 1. Close the RDC app if it's still running. 2. Drop a TStatusBar component onto the form. It will automatically align itself with the bottom of the form. 3. Set its SimplePanel property to True in the Object Inspector. 4. Click the DataSource component, then double-click its OnDataChange event in the Object Inspector. 5. Type the following code into its OnDataChange event: With DataSource1.DataSet do

StatusBar1.SimpleText := Format(`%d / %d',[RecNo,RecordCount]); 6. Set the ClientDataSet component's PacketRecords property to 5 in the Object Inspector. This will cause five rows to be retrieved from the server at a time. Setting it to _1 (the default) causes all rows to be retrieved, whereas 0 retrieves data dictionary information only. 7. Run your app. As you scroll through the records in the DBGrid component, you'll notice that the app retrieves five more rows each time you cross a multiple of five. This level of control allows you to completely configure how much of a result set is brought down to the client at a time. This can have a dramatic impact on performance, especially when working with extremely large tables and when accessing data over a WAN. Figure 22.1 illustrates what your RDC app should look like. Figure 22.1. Your remote data client app at runtime.

Page 573

Summary
You can use Delphi's powerful Data and Constraint Broker technology to build true multi-tier client/server applications. You can import constraints and other data dictionary information from a database directly into an application server. Delphi's Constraint Broker can then enforce these imported constraints without ever sending invalid data to your database server. This cuts down on network traffic and improves application performance. Also, the ability to work with data offline can be used to implement mobile systems, such as field sales apps, that regularly need to work offline. The ability to control how much data is fetched at a time further improves the developer's ability to customize an application's behavior to its specific work environment.

What's Ahead

In the next chapter, you'll learn the difference between optimistic and pessimistic concurrency control and why the difference is important. You'll learn how Delphi controls concurrent access to database objects, and you'll see how your server prevents database changes from being lost. Page 574 Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 575

CHAPTER 23

Concurrency Control
Page 576 The term concurrency control refers to sharing resources among multiple users simultaneously. This broad subject can be broken into two major sections: transaction isolation and concurrency control systems. Transaction isolation levels control the degree of access a transaction has to database changes made by concurrent transactions. Closely related to transaction isolation, concurrency control systems affect not only the capability of one transaction to access another transaction's updates, but also the way in which database updates are performed.

Transaction Isolation
Transaction isolation refers to the mechanism used by database management systems to insulate one transaction from the effects of another. Transaction isolation in Delphi is organized into three distinct levels, each with its own characteristics. These transaction isolation levels (TILs) affect the accessibility of one transaction to database changes made by other concurrent transactions. Normally, Delphi handles transaction-related issues for you by establishing a default transaction isolation level and by automatically starting and committing transactions when your application updates a database. If you need more control than this, you can control transaction processing through the TDatabase

component or via Passthrough SQL. Controlling transactions via the TDatabase component is the preferred method because it ensures that Delphi "sees" the transaction processing you do. Choosing an Appropriate Transaction Isolation Level Most database servers, including InterBase, support three separate TILs. Speaking strictly from a management point of view, it's advantageous to use a single TIL across an entire application. Using a single TIL for all database applications is even more airtight. This strategy protects the integrity of the database and ensures that changes you attempt to make occur as they are supposed to. Nevertheless, depending on the requirements of a particular application, you might not be able to use this simplistic approach. Taking a one-size-fits-all approach to transaction isolation may result in overkill or questionable database integrity. You might end up needlessly locking tables or preventing harmless database updates. TIP The key to a successful TI implementation is to first ensure the integrity of the database and then address performance concerns by optimizing your TI scheme.

Page 577 Classic Transaction Isolation Problems The types of problems that transaction isolation schemes encounter can be divided into five basic groups:
q

q

Dirty reads: These occur when the uncommitted changes made by one transaction are read by another. Because the uncommitted changes can be rolled back, they can cause the transaction that read them before to possess dirty reads. Nonrepeatable reads: These occur when one transaction is allowed to change rows that another transaction is continually reading. Because the iterative reads by the second transaction are not reproducible due to the changes by the first transaction, they are said to be nonrepeatable. By their very design, READ COMMITTED transactions permit

q

q

q

nonrepeatable reads because they can read changes made by other transactions as they are committed. Phantom rows: These occur when a transaction is allowed to select an incomplete set of the new rows written by a second transaction. Phantom rows are not prevented by the READ COMMITTED TIL. Lost updates: These occur when one transaction inadvertently overwrites a change made by another simultaneous transaction. Update side effects: These can occur when the values in one row depend on the values in another and these dependencies are not protected by the proper integrity constraints. When two (or more) simultaneous transactions read and update the same data, undesirable side effects can occur if the first transaction copies a value in one row to another row that the second transaction subsequently changes in the first row. These types of integrity glitches are known as interleaved transactions.

The manner in which the TIL you choose with TDatabase's TransIsolation property will handle these problems depends on your RDBMS platform and on the server TIL to which TransIsolation is translated. Both of these subjects are discussed in more detail in the following sections. Transaction Management with TDatabase You manage transactions via the TDatabase component using the TransIsolation property and the StartTransaction, Commit, and Rollback methods. The TransIsolation property controls the transaction isolation level on the database server for your particular connection. As mentioned, the TIL in use for each connection on the server controls the accessibility of transactions concurrent with yours to changes you've made and your transaction's capability to see changes they've made. Page 578 TransIsolation has three possible values: tiDirtyRead, tiReadCommitted, and tiRepeatableRead. It defaults to tiReadCommitted. The three possible values of TransIsolation have the following significance:
q q

tiDirtyRead—Uncommitted changes by other transactions are visible. tiReadCommitted—Only committed changes by other transactions are visible.

q

tiRepeatableRead—Changes by other transactions to previously read data are not visible, which means that every time a transaction reads a given record, it always gets the exact same record.

The TDatabase component's StartTransaction method marks the beginning of a transaction—a group of database changes you want treated as a unit. Either they will all be applied to the database or none of them will be. Commit makes permanent the database changes that have occurred since the transaction was started. Think of it as a database save command. Rollback discards the database changes that have been made since the transaction began. Think of it as a database undo command. TransIsolation and DBMS TILs The isolation levels supported by TDatabase's TransIsolation property may be different or not supported at all on your database server. When a TransIsolation level you request is not supported by your server, it's promoted to the next highest isolation level. Table 23.1 cross-references the TILs supported by the TransIsolation setting with their implementations on the various DBMS platforms. Table 23.1. TransIsolation and DBMS TILs. InterBase Read tiDirtyRead committed Read tiReadCommitted committed tiRepeatableRead TransIsolation Oracle Read committed Read committed Repeatable read Sybase & Microsoft Read committed Read committed Repeatable read (READ ONLY) Error (unsupported)

Controlling Transactions with SQL You can also control transaction processing on your server using Passthrough SQL. To do this, you issue SQL commands that change the transaction

processing on your server. The examples in the following sections use InterBase's SQL syntax. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 579 CAUTION Be aware that setting the transaction isolation level with SQLPASSTHRUMODE set to SHARED AUTOCOMMIT or SHARED NOAUTOCOMMIT could cause your new TIL setting to inadvertently affect other transactions initiated by your application.

Transaction Isolation To set the TIL you want to use via SQL, use the SET TRANSACTION command. The three TILs supported by InterBase, for example, are SNAPSHOT, SNAPSHOT TABLE STABILITY, and READ COMMITTED. To install one of these as the new TIL, use the SET TRANSACTION ISOLATION LEVEL SQL command. Here's a summary of what each setting means:
q

q

q

SNAPSHOT: Restricts the view of the database to a snapshot of the way it appeared when the transaction started. Changes made by other active transactions aren't visible using this TIL. SNAPSHOT TABLE STABILITY: Places a table lock on tables this transaction is reading and writing, allowing other transactions read-only access to the tables. READ COMMITTED: Shows the most recently committed version of a row during updates and deletions, and allows the transaction to make changes provided there are no update conflicts with other transactions.

READ COMMITTED supports the following two optional parameters: r NO RECORD_VERSION (default): Shows only the most recent version of a row. If SET TRANSACTION's WAIT parameter has been specified, the transaction waits until the most recent version of a record is committed or rolled back and then retries the read. r RECORD_VERSION: Reads the most recent committed version of a row, even if a newer, uncommitted version is present in the database. Server Transaction Isolation and the Classic TI Problems As a rule, each DBMS vendor has its own way of dealing with the classic transaction isolation problems previously mentioned. Each TIL supported by the server is designed to address the problems mentioned previously in some fashion. Table 23.2 is a summary of how each InterBase TIL deals with them. Page 580 Table 23.2. InterBase transaction isolation levels and the five classic transaction-management problems. TIL SNAPSHOT Problem Lost updates Solution Other transactions can't update rows updated by this transaction Doesn't read changes made by other transactions;other transactions see a previous version of a row updated by this transaction Can read only the version of a row committed when the transaction began Can read only the version of a row committed when the transaction began

Dirty reads

Nonrepeatable reads Phantom rows

READ COMMITTED

Doesn't read changes made by other transactions; other Update side effects transactions see a previous version of a row updated by this transaction Other transactions can't update Lost updates rows updated by this transaction Other transactions see a previous Dirty reads or committed version of a row updated by this transaction Nonrepeatable Are allowed by design reads May be encountered because this Phantom rows TIL sees committed changes by other transactions Other transactions see a previous Update side effects or committed version of a row updated by this transaction

Page 581 TIL SNAPSHOT TABLE STABILITY Solution Prevents updates by other Lost updates transactions on tables it controls Prevents access by other Dirty reads transactions to its updated tables Can read only the version of a row committed when the transaction began; Nonrepeatable prevents access by other reads transactions to its updated tables Prevents access by other Phantom rows transactions to tables it controls Prevents updates by other Update side effects transactions to tables it controls Problem

Selecting the Right Transaction Isolation Level As you can see, with few exceptions each transaction isolation issue is covered in some way by all three InterBase TILs. The one you should choose depends largely on your application's needs. The default TIL is SNAPSHOT. For most applications, either SNAPSHOT or READ COMMITTED should be chosen. SNAPSHOT TABLE STABILITY can indefinitely lock other users out of tables they might need access to. It, therefore, should be avoided unless you actually need its specialized features. The choice between SNAPSHOT and READ COMMITTED should be based on whether you need to see committed updates by other transactions during your transaction. If not, use SNAPSHOT. If you do need to see updates, READ COMMITTED is your best choice. Generally speaking, the READ COMMITTED TIL will produce a lower amount of lock contention. Transaction Control You control InterBase transactions by using the SET TRANSACTION, COMMIT, and ROLLBACK SQL commands. SET TRANSACTION has a variety of uses, including setting the transaction isolation level, as mentioned previously. COMMIT works just like TDatabase's Commit method; it acts as a database save command. ROLLBACK functions just like the TDatabase Rollback method; it discards changes made to a database since the last COMMIT. Page 582 SET TRANSACTION You use the SET TRANSACTION command to begin a transaction, like so: SET TRANSACTION If you need to start a read-only transaction, you can also include the optional READ ONLY keyword: SET TRANSACTION READ ONLY Many RDBMS platforms also support named transactions. This enables you,

for instance, to nest transactions within other transactions. In InterBase SQL, data-modification commands (including INSERT, UPDATE, and DELETE) can also make direct use of a named transaction. Here's the InterBase syntax for starting a named transaction: SET TRANSACTION :UpdateCustomers Note that :UpdateCustomers must be a previously declared and initialized host language variable. Here's the Sybase Transact-SQL syntax for doing the same thing: BEGIN TRANSACTION UpdateCustomers COMMIT and ROLLBACK The SQL COMMIT command makes the changes that occurred during a given transaction permanent. It saves the modifications you've made to the database and ensures that other users can see them. ROLLBACK, on the other hand, throws away the changes a transaction might make to the database. In most systems, it simply discards pending data modifications from the transaction or redo logs. Both of these commands affect the changes made since the last COMMIT; you can't ROLLBACK changes you've just committed. NOTE You should attempt to include only statements that actually modify data between a SET TRANSACTION and its corresponding COMMIT or ROLLBACK. Perform all lookups or other data gathering before you initiate the SET TRANSACTION. This helps ensure that you lock other users out of database resources for as short a time as possible.

InterBase's WISQL utility begins a transaction automatically (by issuing the equivalent of the InterBase SET TRANSACTION command) when it first loads. When you exit the utility, it asks whether you'd like to commit your work. You can commit or roll back your work at any time by using the Commit Work and Rollback Work options on WISQL's File menu. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 583

Concurrency Control Systems
The concepts behind concurrency control systems are relatively simple if you reduce them to everyday terms. For example, you might compare a concurrency control system to the system that manages trains on a railroad system. If there were just one train, there'd be no concurrency problem. Add several trains to the system, though, and you must quickly devise a way to prevent the trains from colliding while still picking up and delivering their cargoes in a timely fashion. The same types of problems you encounter in this scenario apply equally well to databases. You need to find a way to prevent the updates users make to the database from "colliding"—overwriting one another—while still providing the functionality users expect. Obviously, if you only query a database, you have no concurrency problems. Concurrency problems rear their ugly heads when two or more users attempt to change the same data at the same time. When a user's changes to the database are lost or prevented because of changes made by another user, a lost update or update conflict occurs. Effective concurrency control systems address these problems without reducing the usability or functionality of the database. Concurrency control systems come in two basic flavors: pessimistic control systems and optimistic control systems. You'll often hear these referred to as pessimistic and optimistic locking systems, because locking database resources is the most prevalent method of handling concurrency issues. These systems are distinguished by their different assumptions regarding whether concurrent transactions are likely to attempt to change the same data at the same time. A pessimistic concurrency control system assumes that this is a likely occurrence, whereas an optimistic system regards such contention as abnormal and treats it

as an exception. An optimistic concurrency control system assumes that most queries are read-only and that updates to a given data element rarely occur at the same time. Pessimistic Concurrency Control Because a pessimistic control system assumes update conflicts are likely, it locks the resources a given transaction uses in order to remove the possibility of a conflict. Other transactions needing the resources locked by the transaction will need to wait until it completes before proceeding. Although a pessimistic control system can utilize both read-inhibit and writeinhibit locks, you never see a read-inhibit lock in the real world. Instead, you normally see locks that permit only the reading of data, or permit both reading and writing. This locking is usually maintained at a given level—ranging from multiple table locks all the way down to single columns. Table, row, and page (SQL Server) locks are the most common, sometimes occurring in sequence. That is, you often will see several row locks followed by the locking of the entire table Page 584 in which they reside. This is called lock escalation and occurs when your database server believes that enough row or page locks exist to justify replacing them with a complete table lock. Sybase, for example, promotes a transaction's page locks to a table lock after the transaction accumulates more than 200 page locks (by default) on a given table. In practice, you never see locks on individual columns; they're too resourceintensive and serve no real purpose. Preventing a change to a column in a row that is not being updated by another transaction is rarely useful. Typically, as the locking used within a database becomes more granular, it permits more users to access more database resources simultaneously. As this usage increases, the number of locks also increases. Obviously, if you lock just one table, you have only one lock. But if you lock multiple rows in multiple tables, you have many locks. As the number of locks increases, the server resources needed to manage them also increases, as does the time required to establish and remove them. Like many things, increased functionality brings with it increased resource requirements.

Because of their propensity to lock entire tables, pessimistic control systems are susceptible to deadlock situations. A deadlock occurs when transaction 1 locks tables needed by transaction 2, and vice versa. Because neither transaction is able to lock the tables it needs to complete, the two are locked in a deadly embrace, resulting in neither of them ever completing without outside intervention. Database servers, as a rule, implement pessimistic control schemes. This is a throwback to the days when the resources did not exist at the workstation level to implement optimistic control mechanisms. Because Delphi itself implements a type of optimistic control scheme, you might be faced with the challenges of both approaches in the database applications you develop. Most database servers lock the appropriate elements for you automatically as you make changes to a database. For example, most servers will lock a row while you are changing it to prevent other users from updating it simultaneously. Depending on the platform, the server may lock the entire table if you delete a large number of rows. Both Sybase and Microsoft SQL Server rely on table and page locking rather than row-level locking, which platforms like Oracle use. When you attempt to change a given row, the page on which it resides is locked during the update. Updates usually occur in batches to rows that are concentrated in a given area of the database. By locking these rows by the page, one lock typically suffices for several rows. This conserves server resources, but it can prevent access to rows unrelated to the update. Updates to single rows are particularly a problem with this approach. Despite the trade-offs, SQL Server's approach works about as well in practice as those taken by other DBMS vendors. Page 585 NOTE Microsoft SQL Server has recently added row locks in certain limited situations, but this facility still falls well short of Oracle's row-level lock support. Likewise, Sybase currently has a row-level locking mechanism in beta, but it is not yet commercially available.

Optimistic Concurrency Control

Optimistic concurrency control systems assume that most updates are nonconflicting and that most users read, but do not update, the database. The best way to visualize the workings of an optimistic control system is with an example. Imagine an editor who supervises the editing of book manuscripts to be published by a publishing house. As manuscripts are received, the editor duplicates and distributes them to other editors to be edited. These editors mark up the duplicates and send them back to the supervising editor, who must consolidate the changes and apply them to the original manuscript. After the changes are applied, editors who receive the manuscript will get the revised version so that they don't repeat editing work that's already been done. This is a good example of optimistic concurrency control for a couple of reasons. First, notice that the original manuscript never leaves the supervising editor. In optimistic concurrency control systems, a copy of the data is initially changed, not the data itself. When a change is applied to the server, it is done in such a way as to prevent it from overwriting changes made by other users. Also notice that changes made by the editors are naturally partitioned. One editor might review the manuscript's grammar, another might check it for technical accuracy, and so on. As a rule, two editors would never edit the same manuscript in the same manner simultaneously. This is a basic premise of optimistic concurrency control systems. Database updates are normally partitioned by department, management level, or some other criteria. This works out well for optimistic systems because database resources are not initially locked. Optimistic Concurrency Control in Delphi Applications Delphi implements its version of optimistic concurrency control using the UpdateMode property of the TTable and TQuery DataSet components. As with all optimistic concurrency systems, Delphi retrieves a copy of a row from the server, enables you to change it, and then sends your changes to the server. Changes are sent to the server using the SQL UPDATE command. This UPDATE command contains a WHERE clause that locates the row that is to be changed. UpdateMode controls which columns from the table are listed in the WHERE clause. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 586 UpdateMode can have three possible values:
q q

q

upWhereAll (default)—The WHERE clause includes every column in the table. upWhereChanged—The WHERE clause includes the DataSet's key columns and columns that have changed. upWhereKeyOnly—The WHERE clause includes only the DataSet's key columns (use this only if you have exclusive use of the database table).

Both the TTable and TQuery components publish an UpdateMode property. It defaults to upWhereAll, which means that updating a row in a table causes a WHERE clause to be generated that lists every column in the table. This can be quite cumbersome, especially with large tables. An alternative, and faster, approach is the use of the upWhereChanged setting. It generates a WHERE clause that includes only the table's key fields and the fields that have changed. This is best demonstrated by way of example. Let's say your Delphi application had just modified the LastName field of the CUSTOMER table. With upWhereAll, here's the type of SQL that would be generated—notice the lengthy WHERE clause: UPDATE CUSTOMER SET LastName='newlastname' WHERE CustomerNumber=1 AND LastName='Doe' AND FirstName='John' AND StreetAddress='123 SunnyLane' AND City='Anywhere' AND State='OK' AND Zip='73115' By contrast, here's the statement generated by upWhereChanged: UPDATE CUSTOMER SET LastName='newlastname' WHERE CustomerNumber=1 AND LastName='Doe'

Notice how much shorter it is. Notice also that it avoids the possibility of overwriting another user's changes to the LastName field by including LastName's original value in the WHERE clause. If another user changes the LastName field in between the time the row is read and when it is updated by the current user, the UPDATE generated by upWhereChanged will fail, which is what you'd want. Obviously, this isn't as bulletproof as the upWhereAll method. Another user could delete the row after your application has read it and then add a new record to the table that happens to have the same key and LastName value as the original record. When your record applied its UPDATE, it would be updating the wrong record. But this scenario is usually a very rare occurrence. Page 587 UpdateMode's other setting, upWhereKeyOnly, is even less bulletproof, but it does have its use. Because it checks only the key value of the row you're changing, it doesn't allow for the possibility that another user has changed the field you're updating in the time since you originally read the record. This might be a safe assumption, and then again, it might not be. In most multi-user applications, it would be unsafe to assume that another user has not changed a record you've previously read into a client application. This is, therefore, the kind of optimization that you perform only on a rare occasion and only out of necessity. Because the WHERE clause generated by the upWhereKeyOnly setting would naturally be shorter, it would tend to be faster than one that included additional columns. Nevertheless, you should check with your database administrator before using this option; it could lead to data loss if used improperly. In practice, you should stick with upWhereAll unless you have a specific reason for deviating from it. upWhereChanged is a good alternative if the table you're working with has a large number of columns and upWhereAll is just too slow. upWhereKeyOnly is safe to use only when you have exclusive use of the table—don't use it otherwise. One practical application of upWhereKeyOnly would be in the collection of data from a machine of some type. If you need to update values in a database table using collected data, you'll probably be the only user updating the table, and quick execution of the SQL UPDATE may be critical. If so, perhaps upWhereKeyOnly is what you need, but I'd recommend that you consult with your database administrator first.

Transaction Log Management
Ever wonder how database servers avoid permanently saving to a database changes that are interrupted by a power outage or machine crash? Does the server keep some type of undo list? How does it know what transactions to roll back when the database is recovered? The answer to all these questions is that changes you make to a database are normally made to the database's transaction (or redo) log first, and then later committed to the database itself after the transaction completes successfully. This means that, despite its name, the transaction log is more than a mere log; it's where all the action is! If the power fails on the server machine before a change you've made has been committed to the database, there's no change to reverse off the database; the change never occurred. Because the uncommitted changes you were making were applied strictly to the transaction log, the server software can easily roll back incomplete transactions without affecting the database. One subject that doesn't usually come to mind when developers think of database application development is transaction log management. Usually relegated to the area of database administration, transaction log management tends to be neglected by database application developers—much to the chagrin of DBAs. Page 588

There are, however, a couple of areas of transaction log management that the competent client/server database developer will need to be familiar with and design his or her applications to take into consideration. The first area to be concerned with is in keeping transaction log information to a minimum. The second is in breaking large transactions into smaller ones to avoid filling the transaction log. I'll discuss each of these separately. Keeping Transaction Logs to a Minimum A common mistake made by client/server developers coming from flat-file databases such as dBASE and Paradox is using programming constructs, rather than SQL, to perform batch data updates. This is best illustrated by the following example. Let's say that you need to convert all the last name fields in the CUSTOMER table to uppercase. If you're a former dBASE developer, you might code the following Object Pascal to perform your updates: With taCUSTOMER do begin First; While not EOF do begin FieldByName(`LastName').AsString := UpperCase(FieldByName(`LastName'). AsString); Next; end; end; This approach not only would be slow, it also would have the undesirable effect of beginning and committing a transaction for every row in the table. Furthermore, if a problem occurred while your loop was executing, part of the rows would be updated, and part would not; some last names would be uppercased, and some would remain unchanged. You could remedy the partial update problem by calling your TDatabase component's StartTransaction method prior to the loop, but you'd still have the problem of inefficient updates. For every iteration of your loop, a separate SQL UPDATE statement would be generated, complete with its own WHERE clause to locate the very next row in the table. With an extremely large CUSTOMER table, the process would probably crawl along very slowly. A far better approach, and one that uses the transaction log as it was intended, is to use a TQuery or TStoredProc component to carry out your update. The same update written in SQL would look like this: UPDATE CUSTOMER SET LastName=UPPER(LastName) Because the statement will itself be treated as a single transaction, either all the updates will occur or none of them will. Furthermore, the transaction log will escape being thrashed by the constant initiation and committal of one transaction after another. Another means of limiting the log information generated by a transaction is to avoid unqualified SQL DELETE commands. Each row deleted with the DELETE command is copied to the transaction log first so that the transaction may be rolled back should the need arise. With a large Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 589 table, it's easy to quickly run out of transaction log space. If you need to delete all the rows in a table, use your server's table-truncation command rather than the DELETE command. Many servers support a command similar to the dBASE ZAP command that quickly empties a table of its contents. Sybase and Microsoft SQL Server, for example, provide the TRUNCATE TABLE command for this purpose. Not only is it always faster than the DELETE command, it isn't saved to the transaction log, so there's no log activity associated with it. If your server doesn't support any type of table-truncation command, you might find that simply dropping and re-creating the table in question is an option, depending on the way it's used elsewhere in the database and by other applications. On the Sybase and Microsoft platforms, another way of avoiding log activity is the use of SELECT INTO rather than INSERT SELECT when copying rows from one table to another. Rows inserted using INSERT SELECT are stored in the transaction log, so large inserts are to be avoided. SELECT INTO, as implemented by SQL Server, is a nonlogged operation; it adds no entries to the transaction log. Not all DBMS platforms support this syntax (InterBase doesn't, for instance), so you may not be able to use this technique. The SQL Server Transact-SQL syntax for using SELECT INTO in this manner is as follows: SELECT LastName, FirstName INTO NEWCUSTOMER FROM CUSTOMER This creates a table called NEWCUSTOMER and inserts into it the LastName and FirstName columns from the CUSTOMER table. It's functionally equivalent to this syntax: CREATE TABLE NEWCUSTOMER (LastName CHAR(30),

FirstName CHAR(30)) INSERT INTO NEWCUSTOMER (LastName, FirstName) SELECT LastName, FirstName FROM CUSTOMER You can use SELECT INTO to avoid creating transaction log entries in the following scenario:
q q q

You need to create a table and then copy rows from a second table into it. All the columns in the new table need to receive values from the second table. You aren't interested in being able to roll back the row inserts.

Breaking Up Large Transactions There are times when you must perform a mass data modification of some kind that unavoidably creates large numbers of transaction or redo log entries. Unchecked, these types of updates can cause your transaction log to fill and create problems on your server. They can also generate locks that prevent access to entire tables by other users. Because of this, it's a good Page 590 idea to break these types of large transactions into smaller ones. There are a couple of ways to do this; which one you use depends on your DBMS platform and the specific needs of your application. One way of breaking a large update into smaller ones is to horizontally partition data modification commands—to break down an UPDATE or DELETE into several UPDATEs or DELETEs that each reference a subset of the rows referenced by the original WHERE clause. Using the previous example, if you have a CUSTOMER table of a million records and need to uppercase each last name in each record, you might do the following: SET TRANSACTION; UPDATE CUSTOMER SET LastName=UPPER(LastName) WHERE CustomerNumber between 1 and 100000; COMMIT; SET TRANSACTION; UPDATE CUSTOMER SET LastName=UPPER(LastName) WHERE CustomerNumber between 100001 and 200000; COMMIT;

SET TRANSACTION; UPDATE CUSTOMER SET LastName=UPPER(LastName) WHERE CustomerNumber between 200001 and 300000; COMMIT; This is a simple technique that can make the difference between an update being feasible and not being feasible. Another technique for limiting the rows affected by an UPDATE or DELETE statement is to limit the number of rows the command can change in one pass. Not all DBMS platforms support this (SQL Server does, for example; InterBase doesn't), so you might not be able to use it. To do this, you use a command specific to the server that limits the rows processed by your UPDATE or DELETE. You then repeat your UPDATE or DELETE as many times as necessary to process all rows. Here's an example using SQL Server's Transact-SQL: SET ROWCOUNT 50000 /* Limits the UPDATE to 50000 rows at a time */ WHILE (EXISTS (SELECT * FROM ORDERS WHERE Amount<>0)) BEGIN UPDATE ORDERS SET Amount=0 WHERE Amount<>0 /* Keeps the UPDATE from looping infinitely */ END Because the rows being changed are still in the table with each iteration of the loop, you must devise a way of excluding them after they're processed. One way to do this is to code the UPDATE's WHERE clause so that it ignores the very rows it changes, after it has changed them. In other words, test the Amount column using <>0 because you are assigning Amount to 0 with the UPDATE. This allows the UPDATE to essentially flag each row as processed simply by updating it. This flagging prevents a row from being processed twice. Page 591 Here's an example using the DELETE command: SET ROWCOUNT 50000 /* Limits the DELETE to 50000 rows at a time */ WHILE (EXISTS (SELECT * FROM ORDERS WHERE Amount=0)) BEGIN DELETE FROM ORDERS WHERE Amount=0 END

Summary
As you can see, there are many issues to consider regarding transaction isolation and concurrency control. Elements such as the TransIsolation and UpdateMode properties need to be fully appreciated in order to properly design Delphi client/server database applications. Although tools like Delphi greatly insulate the developer from having to worry about the low-level details of database access, you'll still need to understand how transaction isolation and concurrency control work in order to build robust client/server applications. This chapter has provided you with the basic information you need to do so.

What's Ahead
In the next chapter, I'll return to a discussion of SQL, specifically advanced SQL. You'll further the SQL skills you've garnered thus far by exploring stored procedure creation, database triggers, index construction, view definition, and the like. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 592 Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 593

CHAPTER 24

Advanced SQL
Page 594 This chapter doesn't attempt to pick up exactly where Chapter 5, "A No-Nonsense Approach to SQL," left off. There is a wide gap between entry-level and advanced SQL, so there's a substantial gap between this chapter and the introductory SQL chapter. This chapter is targeted for the person with advanced SQL skills; the introductory chapter is targeted for the beginner. I assume that you're already familiar with how to connect to a database and execute SQL commands against it using your favorite SQL editor. The examples in this chapter make use of InterBase and its WISQL editor. Feel free to use whatever suits you. This chapter makes extensive use of the database and tables first created in Chapter 5. You might want to create these objects before proceeding in order to work through the following examples. See Chapter 5 for more information. Some of the tables used in this chapter were defined in the "Tutorial" section of this book. Though they're not essential to understanding the concepts presented, you can refer to Chapter 8, "Your First Real Client/ Server Database Application," for information on creating tables. This chapter contrasts the two main families of SQL syntax: the ANSI family and the Sybase family. InterBase is highly ANSI-compliant, so examples using InterBase syntax will work on most other ANSIcompliant platforms such as Oracle. Sybase SQL Server, on the other hand, deviates from ANSI in many important ways, as does its licensed cousin, Microsoft SQL Server. NOTE

Though I often contrast the different SQL dialects in this chapter, see Chapters 15 through 18 for in-depth information on each DBMS. Each platform's chapter—whether it's Oracle, InterBase, Sybase, or Microsoft SQL Server—provides lots of vendor-specific goodies, including the nuances of each platform's SQL dialect.

I've expressly tried to avoid repeating what you can already easily find in your SQL platform's documentation. The goal of this chapter is to touch on a variety of advanced SQL topics—some are "advanced" because they are not beginner topics, and some are "advanced" because they are indeed complicated and difficult to grasp. Advanced SQL is a huge subject capable of filling several books all by itself; this chapter covers enough topics that there should be something for everyone. You'll notice that I go to great lengths to "speak" in SQL. Rather than attempt to explain SQL with mere English, I explain SQL with SQL. I liken this to a French teacher teaching in French and forcing all her students to converse in French. If SQL is the language you're supposed to speak, let's talk SQL! Page 595

DDL Versus DML
I'll address two broad categories of advanced SQL statements: Data Definition Language (DDL) commands and Data Manipulation Language (DML) commands. You use DDL to create and manage database objects. For example, CREATE TABLE is a DDL statement. DML commands query and modify data in these objects. The SQL UPDATE statement is an example of a DML command. I've organized the following discussion into advanced DDL syntax and advanced DML syntax to make it easier to keep them distinct. Though, in the larger sense of the term, SQL encompasses both DDL and DML, it's still helpful to know what class of SQL a particular command belongs to. DDL commands tend to be similar to other DDL commands, and DML commands tend to be similar to other DML commands.

Advanced DDL Syntax
The following statements have to do with creating and modifying database objects. They don't have to do with updating or modifying data; they relate instead to defining the way data is stored and the way it's accessed by users and other objects. Databases As Chapter 5 pointed out, you use the CREATE DATABASE command to construct new data- bases. As your data capacity needs change over time, you'll probably need to expand the databases you've created in the past. Doing so can help improve performance by spreading the database over a greater number of disk drives and (depending on the platform) allowing the database to store more data. You use the ALTER DATABASE command to extend an existing database. Here's the InterBase syntax: ALTER DATABASE

ADD `C:\DATA\IB\ORDENT2' The preceding command adds a second operating system file to the database. Sybase and Microsoft SQL Server support a similar syntax. Here's an example: ALTER DATABASE ORDENT ON ORDENT2=100 In the case of Sybase and Microsoft, ALTER DATABASE actually increases the physical size and capacity of the database, so not only is the database distributed over additional devices, it can also contain a larger volume of data. Page 596 Segments and Tablespaces A popular way of improving performance on a large database is to spread it over several different disk drives and assign specific database objects to specific drives. In Microsoft and Sybase SQL Server, this is done by using database segments. Dramatic performance improvement can be seen by placing a table on one disk drive and its indexes on another, especially if the disks have separate disk controllers. Here's how you'd separate indexes and tables on Sybase SQL Server (System 10 and later): 1. Create a new link to a physical drive (this link is known as a device in SQL Server terminology) using the DISK INIT command: DISK INIT name = "INDDEV", physname = "SYS:DATA\INDDEV.DAT", vdevno = 25, size = 51200 2. Expand the database onto the drive using the ALTER DATABASE command: ALTER DATABASE ORDENT ON INDDEV=100 3. Create a new segment that resides on the newly allocated device using the sp_addsegment command: sp_addsegment `indexeseg','ORDENT','INDDEV' where `indexeseg' is the name of the new segment, `ORDENT' is the database name, and

`INDDEV' is the name of the newly added device. 4. Create a database object on the newly created segment. For example, you could create an index on the new segment using the following syntax: CREATE INDEX INVOICES02 ON INVOICES (CustomerNumber) ON indexeseg The INVOICES table and the INVOICES02 index now reside on different segments and, it's hoped, on different disk drives, thereby eliminating head contention when the table is accessed using the INVOICES02 index. In Oracle, this same thing can be accomplished by creating tablespaces that reside on different drives, and then creating database objects within those tablespaces. You use the CREATE TABLESPACE command to set up a new tablespace and the CREATE TABLE and CREATE INDEX commands to assign new objects to it, for example: CREATE TABLESPACE indexspace DATAFILE `c:\oracle\indexspace.dat' SIZE 500K REUSE AUTOEXTEND ON NEXT 500K MAXSIZX 10M; creates the tablespace, while CREATE INDEX emp02 ON emp (ename, job) TABLESPACE indexspace; Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 597 creates an index that resides in it. Because the tablespace is apportioned a specific data file, the net effect is that the index is assigned to this file. If the file exists on a separate drive from the table itself, you've effectively segmented the table from its index. This should help alleviate the I/O bottlenecks that frequently occur with high-use tables. Proper database design mandates the consideration of segmentation before database objects are actually built, because they cannot be easily moved afterward. There are three rules to follow when deciding how to segment a database:
q

q

q

Keep transaction/redo log devices and data devices separate. Storing the logs on the same physical device as data is not only dangerous, because of the potential for a single point of failure, but is also detrimental to performance, because log and data activity will compete with one another. For the sake of disaster recovery and performance, locate log and data allocations on different drives. Attempt to split tables from their non-clustered indexes. The most basic way of doing this is to place all the tables in a database on one disk drive and all the indexes on another. Note that you cannot place a table's clustered index on a different device than the table itself. If you attempt to, the table will "follow" its clustered index and migrate to the segment you've specified for the clustered index. (Clustered indexes are discussed in the section "Clustered Versus Nonclustered Indexes," later in this chapter.) Attempt to further separate major tables, especially those that are likely to be accessed simultaneously, from each other onto separate drives. If your database has three major tables and scads of other less-significant tables, the ideal disk drive configuration includes at least eight separate disks: one each for the three major tables, one for the less-significant tables, and a

corresponding index disk for each table disk. Note that InterBase doesn't support segmentation, per se, though you can still spread a database over multiple drives. What you can't do is assign specific database objects to specific drives. Indexes You create indexes in SQL using—you guessed it—the CREATE INDEX command. Here's the basic syntax: CREATE INDEX INVOICES02 ON INVOICES (CustomerNumber)

where INVOICES02 is the name of the new index, INVOICES is the name of the table on which to build the index, and CustomerNumber is the index key. In InterBase, index names must be unique across the database in which they reside. In Sybase and Microsoft SQL Server, they need only be unique across each table. Page 598 Unique Indexes Unique indexes are created using the CREATE UNIQUE INDEX variation of the command, as in the following: CREATE UNIQUE INDEX INVOICES01 ON INVOICES (InvoiceNo) Descending Indexes Index keys are arranged in ascending order by default. You can create descending indexes in InterBase using the DESCENDING keyword. Here's an example: CREATE DESCENDING INDEX INVOICES03 ON INVOICES (Amount) This helps queries such as the following execute faster: SELECT * FROM INVOICES ORDER BY Amount DESCENDING

Activating and Deactivating an Index In InterBase, you can temporarily disable an index and then reactivate and rebuild it later. While an index is deactivated, you can add a large number of rows to its table without incurring the penalty of index maintenance with each new row. Here's the syntax: ALTER INDEX INVOICES03 INACTIVE Just replace INACTIVE with ACTIVE to reactivate it, like so: ALTER INDEX INVOICES03 ACTIVE Deactivating and reactivating an index causes it to be rebuilt. You'll want to wait until an index is not in use before attempting this because index deactivations are delayed until they're no longer in use. Generators and Sequences Most DBMSs provide some means of automatically incrementing columns. Sequential columns are a reality of relational databases, and most of the major players provide some type of system-level support for them. Sybase and Microsoft support auto-incrementing columns via the identity column attribute. Identity columns are just like normal columns except that the system supplies ascending values for them. Here's an example of how identity columns are defined: CREATE TABLE CUSTOMER ( CustomerNumber numeric(18) NOT NULL identity, LastName char(30) NOT NULL, FirstName char(30) NOT NULL, Page 599 StreetAddress char(30) NOT NULL, City char(20) NOT NULL, State char(2) NOT NULL, Zip char(10) NOT NULL )

Though the values aren't guaranteed to be consecutive, they will be ascending. CAUTION Creating indexes (especially clustered indexes) using autoincrementing keys can cause concurrency problems on both the Sybase and Microsoft SQL Server platforms. This is because both of these platforms adhere to a page-based locking model. (Microsoft recently introduced Insert Row Locking, but it has problems of its own.) Page-based pessimistic concurrency implementations have their advantages, but one of their chief disadvantages is that unrelated transactions can block one another because they affect rows that happen to reside on the same page. That is, an update to the address of CustomerNumber 1000 may block an update to CustomerNumber 1001 simply because both rows reside on the same page in the database. If the clustered index is created on a more random key, say the StreetAddress field in our example, the likelihood of this happening is lessened. Note that DBMSs that implement a row-level locking architecture (such as Oracle) are immune to this problem.

InterBase supports auto-incrementing columns via generators. To set up an autoincrementing field in InterBase, first create a generator, like so: CREATE GENERATOR CustomerNumberGen; After the generator is created, you can use it in an insert trigger to supply values to a column, like so: SET TERM !! ; CREATE TRIGGER CUSTOMERInsert FOR CUSTOMER BEFORE INSERT POSITION 0 AS BEGIN NEW.CustomerNumber = GEN_ID(CustomerNumberGEN, 1); END SET TERM ; !! Oracle takes a similar approach using its sequence objects. To create an Oracle sequence, use this syntax:

CREATE SEQUENCE CustomerSEQ; As with InterBase, you can then utilize this newly created sequence object to supply values in Oracle PL/SQL code. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 600 Views Views consist of compiled SELECT statements you can query as though they were tables. Views do not actually store any data; they consist of compiled SQL only. Views are similar to stored procedures, especially select procedures. When a view is queried, the server combines the SELECT used to create the view with the one used to query it and executes the combined query against the view's underlying tables. Use the CREATE VIEW command to construct views. Listing 24.1 illustrates. Listing 24.1. A CREATE VIEW example. CREATE VIEW NEWLEASES AS SELECT * FROM LEASE WHERE MovedInDate >"12/01/96" After the view is created, it can be queried just like a table, as in the following: SELECT * FROM NEWLEASES The SELECT statement that makes up a view can do almost anything a normal SELECT can do, except include an ORDER BY clause. This limitation applies to most client/server DBMS platforms, including InterBase, Oracle, Sybase, and Microsoft SQL Server. Updatable views impose additional restrictions. For a view to be updatable on

the InterBase platform, it must meet the following conditions:
q

q

q

The SELECT statement must reference either a single table or another updatable view. In order for the view to support the INSERT command, columns in the underlying table that are excluded from the view must allow NULL values. Subqueries, the DISTINCT predicate, the HAVING clause, aggregate functions, joined tables, user-defined functions, and SELECTs from stored procedures are not supported.

To create an updatable view that ensures that rows inserted or updated through it meet the selection criteria imposed by the view, use the WITH CHECK OPTION clause of the CREATE VIEW command. The WITH CHECK OPTION clause causes the view to refuse updates or inserts that would create a row its selection criteria would exclude. Here's the syntax: CREATE VIEW OKPROPERTY AS SELECT * FROM PROPERTY WHERE State="OK" WITH CHECK OPTION Now, any record updates or inserts whose State column is not equal to "OK" will fail. Page 601 Microsoft and Sybase SQL Server views are a little more flexible in that they can span multiple tables and remain updatable. Still, there are many limitations. Here's a summary of the limitations that exist with SQL Server views:
q

q q q

The ORDER BY and COMPUTE clauses, the keyword INTO, and the UNION operator are not allowed in the SELECT statement that makes up a view. Views cannot be created over temporary tables. Triggers and indexes cannot be built over views. Readtext and writetext cannot be used on text or image columns in views.

There are additional limitations with updatable views:

q q

q

q

q

q

q q

Views over multiple tables do not support the DELETE command. Columns from the underlying table that are excluded from the view must allow NULL values in order for the view to support the INSERT command. Views that contain calculated columns do not support the INSERT command. Views over multiple tables that were created using the DISTINCT or WITH CHECK OPTION switches do not support the INSERT command. Views created with the DISTINCT switch do not support the UPDATE command. When updating a view over multiple tables, all affected columns must belong to the same table. Columns in a view that consist of a calculation are not updatable. Views that contain aggregate columns are not updatable.

Dynamic Views When a view is queried, the SQL that comprises it is retrieved and executed by the server. Though the data returned by the query may vary dramatically, the SELECT statement executed on the server does not. A dynamic view is a view that renders a different SELECT statement based on conditions at the time it is accessed. The view shown in Listing 24.1 limited the properties returned to just those located in Oklahoma. The criteria the view used by the server never changes, even though the rows it returns might—it's a static view. A dynamic view, on the other hand, changes the criteria it passes to the server, based on conditions external to it when it's accessed. Here's an example, written in Sybase/Microsoft's Transact-SQL dialect: CREATE VIEW CONTACTLISTV SELECT * FROM CONTACTLIST WHERE EnteredBy=suser_name() Page 602 EnteredBy is a column in the CONTACTLIST table that records the name of the user entering a new record. It is automatically set by a DEFAULT constraint each time a record is added. Each time a user of the application adds a record, her suser_name() is stored in this field. Because of this, you're able to restrict the view a user sees of the entire CONTACTLIST to just those records she entered. The search criteria used by the server varies based on who the

current user of the application is—that is, it's dynamic. Views and Access Rights A user need not have rights to a view's underlying tables to access the view. Access permissions on views are separate from those on their underlying tables. The one exception to this is that users who create views must have the proper access rights to the underlying tables. If users have access rights to perform a given operation on a view, even if they lack those same rights on its underlying tables, they can access the underlying tables through the view. In fact, no less than Sybase itself recommends using views in this way as security mechanisms. This is not a good practice for several reasons. By placing part of your access permissions in views, you make it impossible to manage your system security from a single central point—you cannot see all access rights from any one perspective. You must instead look in at least two different places—the traditional placement of rights via the GRANT and REVOKE commands, as well as your system's table views—to have a complete system access picture. Furthermore, most administrative tools aren't advanced enough to point out to you that a user does not have rights to a table that he is accessing via a view. If you want to give a user limited rights to a table, I recommend you do so via table permissions, not views. TIP One clever use of views is in preparing data for export. Most client/server DBMSs support some type of bulk import/export facility. Oracle includes the EXPORT and IMPORT facilities, while Sybase and Microsoft SQL Server provide their BCP (Bulk Copy Program) utility. A common problem with exporting SQL Server data that contains dates is that the BCP utility omits parts of datetime fields when exporting in ASCII format. The solution to this problem is to create views over the tables to be exported, formatting datetime fields so they include complete date/time information. These views can then be exported, ensuring that nothing is lost in the translation from the server to the ASCII files produced by the BCP utility.

Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 603 Stored Procedures Stored procedures are compiled SQL programs. They are often made up of numerous SQL statements and are stored in a database with other database objects. There are two types of stored procedures: select procedures and executable procedures. Select procedures can be used in place of a table in a SELECT statement. Executable procedures, by contrast, are executed; they may or may not return data. You create stored procedures using the CREATE PROCEDURE command. Here's an example of the InterBase syntax: CREATE PROCEDURE CLEARCALLS AS BEGIN DELETE FROM CALLS WHERE CallDateTime < '01/01/96'; END Here's the same procedure coded in SQL Server's Transact-SQL: CREATE PROCEDURE CLEARCALLS AS DELETE FROM CALLS WHERE CallDateTime < '01/01/96' If the procedure receives parameters from the caller, the syntax changes slightly. Here's the InterBase syntax: CREATE PROCEDURE CLEARCALLS (BeginningDate DATE) AS BEGIN DELETE FROM CALLS WHERE CallDateTime < :BeginningDate; END And here's the SQL Server syntax: CREATE PROCEDURE CLEARCALLS (@BeginningDate datetime) AS DELETE FROM CALLS WHERE CallDateTime < @BeginningDate

Select Procedures Select procedures define data to return to the caller using the RETURNS keyword, like so: CREATE PROCEDURE LISTPROP (State char(2)) RETURNS (PROPERTYNO INTEGER, ADDRESS VARCHAR(30)) AS BEGIN FOR SELECT PropertyNo, Address FROM PROPERTY WHERE State = :State INTO :PropertyNo, :Address DO SUSPEND; END Page 604 You use the FOR SELECT...DO syntax to return results to the caller. SUSPEND pauses execution of the procedure until the caller requests another row. Output parameters are returned before execution is paused. It's a good practice to store the source code to stored procedures in SQL script files. These scripts can be created and maintained using nearly any text editor. Remember to include any necessary CONNECT and SET TERM statements. These can then be executed using WISQL's Run an ISQL Script option. Listing 24.2 provides an example of what your scripts might look like. Listing 24.2. A stored procedure residing in an SQL script file. CONNECT "C:\DATA\ RENTMAN\RENTMAN.GDB" USER SYSDBA PASSWORD masterkey; SET TERM ^ ; /* Stored procedures */ CREATE PROCEDURE LISTPROP RETURNS (PROPERTYNO INTEGER, ADDRESS VARCHAR(30)) AS BEGIN FOR SELECT PropertyNo, Address FROM PROPERTY INTO :PropertyNo, :Address DO SUSPEND; END ^ SET TERM ; ^ The CONNECT statement at the top of the file establishes a connection to the database. The SET TERM command temporarily changes the SQL statement termination character to a caret (^) from the default semicolon (;). This keeps commands within the stored procedure definition from executing when the CREATE PROCEDURE command itself is executed. After CREATE PROCEDURE executes, SET TERM is again called to restore the default SQL command terminator.

Executable Procedures Executable stored procedures differ somewhat from select procedures in that they're not required to include a RETURNS statement. Here's an example of an InterBase executable procedure: CREATE PROCEDURE insertWORKTYPE (WorkTypeCode smallint, ÂDescription char(30), TaskDuration float) AS BEGIN INSERT INTO WORKTYPE VALUES (:WorkTypeCode, Â:Description, :TaskDuration); END Page 605 The same procedure written in SQL Server's Transact-SQL would look like this: CREATE PROCEDURE insertWORKTYPE (@WorkTypeCode smallint, Â@Description char(30), @TaskDuration float) AS INSERT INTO WORKTYPE VALUES (@WorkTypeCode, Â@Description, @TaskDuration) Stored Functions Both SQL Server and Oracle support creating routines that return values similar to functions in traditional programming languages. In Oracle PL/SQL, these routines are actually called functions. Here's some sample syntax to create a function: CREATE FUNCTION get_bal(CustomerNo IN NUMBER) RETURN NUMBER IS custbal NUMBER(11,2); BEGIN SELECT balance INTO custbal FROM CUSTOMER WHERE CustomerNo = get_bal.CustomerNo; RETURN(custbal); END SQL Server's method of doing things is a little more restrictive but resembles traditional function calling methods, nonetheless. Here's some sample Sybase syntax: CREATE PROCEDURE get_bal @CustomerNo int, @Balance money=NULL OUTPUT AS SELECT @Balance = Balance FROM CUSTOMER WHERE CustomerNo = @CustomerNo

RETURN 100 * @Balance

-- Convert to integer

The procedure returns the customer's balance in both the @Balance output variable and via the procedure's RETURN status. Note that procedure status values must be integers, so Balance is multiplied by 100 to ensure that the decimal portion of the customer's balance isn't lost when returned as an integer. You can call this routine using a couple of different approaches. The first one looks very much like a traditional function call: declare @mybal money select @mybal = null exec @mybal = get_bal @CustomerNo=3 _ Supply a customer number, "3" in this case select @mybal / 100 Note the division of the resulting variable by 100 to return the decimal point to its original position. Page 606 The second method of calling an SQL Server function procedure resembles any other stored procedure call. It looks like this: declare @mybal money select @mybal = null exec get_bal @CustomerNo=3, @mybal OUTPUT select @mybal Note the use of the OUTPUT keyword to indicate that the parameter is to receive a return value from the procedure. Note also that dividing the result by 100 isn't necessary because SQL Server automatically handles the conversion from integer to money. Packages Oracle supports a nifty mechanism for grouping stored procedures, functions, and other related objects together in a single entity within a database. You can then add, drop, or modify the procedures in this package in one fell swoop, greatly simplifying updates to groups of routines. Here's the syntax for creating an Oracle package: CREATE OR REPLACE PACKAGE CustomerPKG AS FUNCTION addcust(CustomerName VARCHAR2, Address VARCHAR2, City VARCHAR2, State CHAR(2), Zip CHAR(10)) RETURN NUMBER; PROCEDURE deletecust(CustomerNumber NUMBER); Invalid_State EXCEPTION; Invalid_City EXCEPTION; END CustomerPKG The Oracle package mechanism also separates the header of a package from its body. This approach is analogous to Delphi's separation of the Interface and Implementation sections in units. The CREATE PACKAGE statement defines a package's public interface. The public interface is what external routines see; it's what they'll use to reference the package's internal objects. You use CREATE PACKAGE BODY to implement the package itself, like so:

CREATE OR REPLACE PACKAGE BODY CustomerPKG AS tot_custs NUMBER; FUNCTION addcust(CustomerName VARCHAR2, Address VARCHAR2, City VARCHAR2, State CHAR(2), Zip CHAR(10)) RETURN NUMBER is NewCustomerNo NUMBER(4); BEGIN IF State IS NULL THEN RAISE Invalid_State; IF City is NULL THEN RAISE Invalid_City; SELECT CustomerSEQ.NEXTVAL INTO NewCustomerNo FROM DUAL; INSERT INTO CUSTOMER (CustomerNo, CustomerName, Address, City, State, Zip) VALUES (NewCustomerNo, CustomerName, Address, City, State, Zip); tot_custs := tot_custs + 1; RETURN(NewCustomerNo); END; Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 633

CHAPTER 25

Delphi Client/Server Performance Tuning
Page 634 This chapter introduces several means of tuning Delphi client/server applications for performance. It's divided into three sections: Delphiapplication performance tuning, server tuning, and network performance tuning. Note that the performance tips included here are by no means exhaustive. I cover a number of optimization techniques in a relatively small space. The idea is to introduce you to the many performance tuning weapons at your disposal. There's no substitute for getting intimately familiar with your DBMS, your network, and your server platform to learn each layer's peculiarities and nuances. NOTE In this chapter, I touch lightly on a number of tuning options on several different DBMSs. For detailed information on client/ server app tuning as it relates to a particular DBMS platform, see Chapters 15 through 18.

How Fast Is Fast Enough?

The first thing you need to do to begin tuning is determine what your performance goals are. Unfortunately, performance tuning is rarely a proactive exercise; people don't get too concerned about performance tuning until the system doesn't perform as expected. So, you need to begin by determining what exactly it is you're expecting. How fast is fast enough? After you've determined what it is you want to accomplish, you can set about to make it happen.

Determine Performance Variables
The second thing you need to do is determine what things are within your power to change. You need to know what the performance variables are. These are items you can change to enhance performance. Server configuration variables, Delphi property settings, network settings, and so on, are all good examples of performance variables. One of the chief goals of this chapter is to alert you to the many variables you can change to improve the performance of Delphi client/server apps.

Build a Test Environment
Another essential weapon in the tuning war is that of the test server. When you begin your effort, don't change production code or server parameters. There are a couple of very good reasons for avoiding this. The most obvious reason is that you don't want data loss or corruption to arise as a result of your tests. You never run questionable code on production systems or systems that other people are using. A second reason to avoid intermingling test and Page 635 production systems is that use of your system by those outside the tests—normal users—will skew your test results. You'll get bizarre numbers from your tests with seemingly no explanation.

Defining Performance Tuning
Now that we've covered some of the bare essentials for tuning a system for performance, what exactly do we mean by performance? How will you know when you've arrived? System performance is measured in a variety of ways, but it usually involves one or more of the following:
q

Transactions per second (TPS)—Overall system throughput is a common metric for measuring system performance. If so, things like

q

q

q

q

streamlining the application's server access, tuning the server itself, and providing for concurrency will be the focal points of your tuning approach. Query response time—Measuring the time it takes a particular query to execute may be your method of gauging system performance. If so, reducing this time is the general goal of your tuning effort. Batch job execution time—Your client may require that a given batch job execute within a fixed window of time. This batch job may involve several queries, interaction with external devices, or other processes. If this is the case, reducing the overall batch execution time becomes a focus of your tuning effort. Application responsiveness—The actual speed with which screens are displayed and reports run may be what your client perceives as performance. If so, you may have to focus on these not-so-trivial aspects of the client/server game. Concurrency—A system you build may have a predetermined simultaneous user requirement. It may be that, say, 1,000 users need to be able to add rows to the same tables at the same time. If so, you'll obviously have to tune the system to reduce blocking processes and resource contention as much as possible.

Usually, performance is defined and measured by a combination of these elements. It's rare for any one element to exclusively define system performance. One reason for this is that the different performance metrics are so interrelated. For example, though TPS may be the single most important performance benchmark to a particular client engagement, overall throughput in multi-user systems is often directly dependent on concurrency. If processes block one another, it's likely that the system's average TPS will drop. So, while you may emphasize one tuning area over another, chances are that you'll end up tuning a number of interrelated performance variables. Page 636

Application Performance Tuning
The following tips concern the application side of the client/server equation. Since the app is the gateway to the information on the server, the approach it takes to acquiring and presenting data can have a dramatic impact on overall system performance. Keep Server Connections to a Minimum

It makes sense to avoid opening unneeded connections to a server that has limited connection resources to begin with. This wastes resources on the server, slows the client application down, and can saturate your network. Here are some techniques for reducing server connections. Use a TDatabase One way to protect against opening unneeded server connections is to use a single TDatabase component for an entire application. Here are the steps for doing this: 1. Drop a TDatabase component onto your application's main form. 2. Set the AliasName property of the TDatabase to point to the BDE alias you want the application to use. 3. Set its DatabaseName property to the name you want published elsewhere in the app as a local alias. 4. When you use a DataSet (for example, TTable, TQuery, or TStoredProc), choose the DatabaseName you chose for the TDatabase, instead of a regular BDE alias, for the TDataSet's DatabaseName property. When a TDataSet that has been set up this way is opened, it will use the connection provided by the TDatabase component, rather than creating its own. There is one caveat you should be aware of when using a TDatabase rather than a separate BDE alias to provide connections to your server. If you open a TDataSet that refers to the application-specific alias while in the Delphi form designer:
q

q

The form that contains the TDatabase component must be open in the form designer, as well. Opening a DataSet (by setting its Active property to True in the Object Inspector) that refers to a TDatabase causes the TDatabase to connect automatically to the server. Because TDatabase's KeepConnection property is set to True by default, subsequently closing the TDataSet will not close the connection to the server. Instead, when you later save your project and exit Delphi, the TDatabase's connection status is saved with the project. When the project is later reloaded, you'll be immediately prompted for a password because the TDatabase is trying to reestablish its previous connection to the server. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 630 day, credit card number, and transaction location. You need to summarize both the total number of transactions and the total amount. That is, if John Doe had three purchases at Foley's on January 1st, you want to see only one record. It would list John's credit card number, the date, the location number for the Foley's store, the number of transactions, and the total amount of the purchases. Rather than store the summary information in a separate table, you can store it back in the original table with only a few changes to the table design. For example, let's say the original layout of the table looked like this: CREATE TABLE CARDTRANS (CardNumber TransactionDate Location Amount

char(20) date int float

NOT NOT NOT NOT

NULL, NULL, NULL, NULL)

And let's say you'd query the data either for reporting or summarization using a SELECT statement like this one: SELECT CardNumber, TransactionDate, Location, ÂCOUNT(*) NumberTrans, SUM(Amount) Amount FROM CARDTRANS WHERE TransactionDate between '01/01/95' AND '02/01/95' GROUP BY CardNumber, TransactionDate, Location You can accommodate summary data in this table and your query by making only minor changes. Here's a revised table layout that supports in-line summarization: CREATE TABLE CARDTRANS (CardNumber TransactionDate Location NumberTrans Amount

char(20) date int int float

NOT NOT NOT NOT NOT

NULL, NULL, NULL, NULL, NULL)

Notice the inclusion of the NumberTrans column. What does it store in a detail row? It stores the same thing a summary row does—a transaction count for a given row in the table. For detail rows, NumberTrans will always be

set to 1; you might even define a DEFAULT constraint to ensure this. For summarized rows, NumberTrans would contain the number of transactions represented by the row's key fields. In the case of our John Doe example, the value would be 3, but it could be any number, up to the maximum supported by the int data type. Now, here's the query mentioned previously, revised to handle either detail or summary rows: SELECT CardNumber, TransactionDate, Location, ÂSUM(NumberTrans) NumberTrans, SUM(Amount) Amount FROM CARDTRANS WHERE TransactionDate between '01/01/95' AND '02/01/95' GROUP BY CardNumber, TransactionDate, Location Note that the only change was to go from using COUNT(*) to determine the number of transactions to using SUM (NumberTrans) instead. This approach works for both summary and detail rows because of our inclusion of the NumberTrans column in detail rows. Page 631 Typically, the summarization process would summarize a month's data into a temporary table using a query similar to the previous one. Then, the original detail data would be deleted from the CARDTRANS table. Finally, the summarized data would be inserted back into the table. This would all be executed as a single transaction on the server to prevent accidental deletion of the data in the event of an unforeseen problem. NOTE On some platforms, the summarization process I've described would also be responsible for stripping the time information from the TransactionDate column during summarization. Some platforms (such as Sybase and Microsoft SQL Server) store date and time information with a single data type. This means that TransactionDate might include both a transaction date and time in detail rows. Since the desired summary level is to the day, the time information would have to be removed. You can use syntax like this to remove SQL Server time information: SELECT CardNumber, CONVERT(char(8),TransactionDate,112), ÂLocation, SUM(NumberTrans) NumberTrans, SUM(Amount) Amount FROM CARDTRANS WHERE TransactionDate between `19950101' AND `19950201' GROUP BY CardNumber, CONVERT(char(8),TransactionDate,112), Location Note the use of the CONVERT function to translate the TransactionDate column to a CHAR(8) type. SQL Server's implicit CHAR to DATETIME conversion can automatically convert the value back as necessary. CONVERT's third parameter, 112, puts the date in CCCCMMDD format. Storing it as a CHAR(8) data type truncates the time information.

Note that you may have to increase the size of fields such as the Amount field to allow them to hold summary data. For example, if you had defined Amount as a smallmoney Sybase data type, you'd probably want to redefine it using the larger money data type. This is a viable method for avoiding the headaches commonly associated with summarizing detail data into

separate summary tables or databases. It trades a small amount of storage per record for the convenience of being able to treat detail and summary data uniformly.

Summary
This chapter covered a wide range of SQL-related topics. We delved into a number of advanced topics completely absent from the introductory chapter on SQL. We also attempted to venture beyond advanced SQL to optimal SQL. Like any programming code, the best kind of SQL is code that not only runs fast but is also maintainable. Page 632

What's Ahead
In the next chapter, you'll learn how to tune Delphi client/server applications for performance. You'll learn a number of techniques for making your applications run faster and more reliably on client/server platforms. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 626 Local-Variable Assignments Stored procedures that use SELECT statements to set local variables should do so in a single SELECT, if possible. On some platforms, there is overhead associated with the first variable assignment of each SELECT statement. Grouping the variables into a single SELECT is more efficient. Here's an example in InterBase SQL: SELECT City, State, Zip INTO :City, :State, :Zip FROM CUSTOMER and one in SQL Server's Transact-SQL: SELECT @City=City, @State=State, @Zip=Zip FROM CUSTOMER This also cuts down on the total number of SELECTs in a stored procedure, making it easier for server's query optimizer to parse and optimize the procedure. Looking Up Secondary Information Look up ancillary information last in stored procedures. The idea is to reduce the set of data first, and then use it to look up secondary information. For example, if you are summarizing a credit card table that contains several million rows, don't look up each card's customer name during the summarization; look it up after you've reduced the result set as much as possible and are ready to return the rows to your client application. The most effective way of doing this is to

execute the query in multiple passes, storing the reduced result set in temporary tables with each successive pass. After the data has been fully qualified, join it with the CUSTOMER table to return the supplemental customer information to the client application. Though it might seem counter-intuitive to make multiple passes over a large result set, keep in mind that the server itself makes multiple passes when necessary. All you do by making them yourself is effectively become your own query optimizer. The single biggest thing you can do to optimize stored procedure queries is to reduce the set of data with which you're working as quickly as possible. This sometimes requires multiple passes and the use of temporary tables. Cross-Tab Queries There are times when the standard method of grouping data via a SELECT statement is not enough. In particular, cross-tab queries can be difficult to implement in SQL. A cross-tab query organizes data into rows and columns, much like a spreadsheet. If your data is organized linearly, as most data is, it may be challenging to represent it in a grid-like fashion. This is best explained by way of an example. Let's say you are writing a query that returns sales information for the Big Three American automakers: Ford, General Motors, and Chrysler. The client wants the type of automobile (subcompact, compact, full size, truck, and so on) to be Page 627 listed on the left of a report, with columns for the sales figures for each of the automakers to the right. Normally, the SQL to derive the needed information would look something like this: SELECT CarType, Maker, Sales TotalSales FROM BIGTHREESALES However, due to your report writer's inability to generate cross-tabs on its own (fortunately, Delphi includes a nice cross-tab component), the data can't be formatted in the way the client wants without jumping through some hoops. Specifically, you'll have to create a work table that mimics the output format the client is after and then fill it with the appropriate data. Here's an example: CREATE TABLE CARCROSS (CarType

CHAR(10)

NULL,

FordSales GMSales ChryslerSales

FLOAT FLOAT FLOAT

NULL, NULL, NULL)

Typically you'd make multiple passes over this table, filling it with the appropriate data, before finally returning the completed data set to your report writer. Here's an example: INSERT INTO CARCROSS (CarType, FordSales) Select CarType, Sales FordSales FROM BIGTHREESALES WHERE Maker='Ford' Then you'd either use a cursor and update the appropriate columns for the other two manufacturers or you'd use UPDATE statements within loops to update their respective columns en masse, like so: SELECT Sales INTO :GMSales FROM BIGTHREESALES WHERE Maker='GM' AND CarType='SC' UPDATE CARCROSS SET GMSales=:GMSales WHERE CarType='SC' Or, alternately, you might use the following SQL Server syntax: UPDATE CARCROSS SET GMSales=Sales FROM SALES S, CARCROSS C WHERE Maker='GM' AND S.CarType=C.CarType There is, however, a better way. It leverages one of SQL's greatest strengths—its capability to easily group and summarize data—to give you data in a cross-tab format with a minimal amount of effort. It does this by means of query folding or flattening. Here's an example of the previous query using the flattening technique: Page 628 First, collect and place the Ford information in the appropriate column:

INSERT INTO CARCROSS (CarType, FordSales) Select CarType, Sales FordSales FROM BIGTHREESALES WHERE Maker='Ford' Now, do the same for GM: INSERT INTO CARCROSS (CarType, GMSales) Select CarType, Sales GMSales FROM BIGTHREESALES WHERE Maker='GM' And, now, for Chrysler: INSERT INTO CARCROSS (CarType, ChryslerSales) Select CarType, Sales ChryslerSales FROM BIGTHREESALES WHERE Maker='Chrysler' At this point, the rows in CARCROSS look something like that shown in Figure 24.1. Figure 24.1. The CARCROSS table before being flattened.

You flatten the CARCROSS table using this query: SELECT CarType, SUM(FordSales) FordSales, SUM(GMSales) ÂGMSales, SUM(ChryslerSales) ChryslerSales FROM CARCROSS GROUP BY CarType The result set returned by the query is shown in Figure 24.2. Page 629

Figure 24.2. The result set following the flattening operation.

As you can see, you now have the format the report required without any of the kludge-like techniques mentioned previously. Data Warehousing and SQL One of the classic problems facing data warehouse designers is the best way to store and query summary data. Data warehouses typically store a large amount of summary data. Usually, in addition to being able to query this data while in detail form, users also want to access it after it's been summarized. This presents a suite of interesting problems for the data warehouse architect. The first problem is finding an appropriate place to store the summarized data. If it's stored in the same database as the system's detail data, should it reside in special summary tables? Perhaps it should reside in a special summary database. How, then, will it be accessed? Will this double data administration work? And what of the stored procedures and reports that were developed for the detail data—should they be redeveloped to work with summary data, as well? An answer to this conundrum trades slightly higher storage requirements for an optimal data warehousing solution. It lies in what I call in-line summarization. It basically consists of organizing detail tables in such a way that summarized rows can reside in the same table with detail rows and be queried by the same queries that query detail rows. This eliminates the need for summary tables or databases and makes separate, summary-oriented stored procedures or reports unnecessary. The best way to explain this is by way of example. Let's say you have a database that stores millions of credit card transactions. Each month, you want to summarize the transactions by Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 623 WHERE r.CardType = c.CardType and c.Type='I' GROUP BY UseDate This puts all the work on the server and eliminates the subquery altogether. ORDER BY The ORDER BY clause should be avoided. It's an inefficient mechanism that can cause a server to needlessly sort a large result set. Unless you actually need the ordering provided by the clause, don't use it. Note that clustered indexes implicitly order result rows Sybase and Microsoft SQL Server. Also note that the GROUP BY clause orders data on some platforms. Though SQL as a language guarantees no natural row order, some platforms do provide for implicit row ordering by one means or another. Check your server documentation for specifics. HAVING There is almost always a better way of qualifying a query than by using a HAVING clause. In general, HAVING is less efficient than the WHERE clause because it qualifies the result set after it has been organized into groups, while WHERE does so beforehand. Here's an example of an unnecessary use of the HAVING clause: SELECT CUSTOMER.LastName, COUNT(*) NumberWithName FROM CUSTOMER GROUP BY CUSTOMER.LastName HAVING CUSTOMER.LastName<>'Citizen'

Now, here's the same query properly rewritten using the WHERE clause: SELECT CUSTOMER.LastName, COUNT(*) NumberWithName FROM CUSTOMER WHERE CUSTOMER.LastName<>'Citizen' GROUP BY CUSTOMER.LastName This is a better approach because it does not include data in the grouping phase that we know we do not want. In the case of the HAVING clause, most servers would group the data first and then filter it by the criteria specified in the HAVING clause. This is less efficient than simply ignoring the unwanted data to begin with. The only valid use of HAVING is to qualify the result set by aggregate columns—columns that are the result of computations over the result set. Because these aggregates aren't known until after the server has created the result set, you can't use them in the WHERE clause. Instead, if you want to qualify a result set using an aggregate, you must do so via the HAVING clause. Here's an example: SELECT CUSTOMER.LastName, COUNT(*) NumberWithName FROM CUSTOMER WHERE CUSTOMER.LastName<>'Citizen' GROUP BY CUSTOMER.LastName HAVING COUNT(*) > 2 Page 624 Because NumberWithName isn't known until after the result set is built, you use HAVING to qualify it. NOTE Even though servers are becoming increasingly more savvy in optimizing inefficient queries, you should still write efficient code, nonetheless. Though your particular RDBMS may translate an errant HAVING clause into a WHERE clause, that may not be the case on other platforms. Getting into the habit of always building efficient SQL will save you from having to re-learn the same lessons every time you switch DBMS platforms.

COMPUTE Sybase/Microsoft Transact-SQL supports a way of totaling columns returned by a

SELECT statement without actually using aggregates in the SELECT itself. This is done via the COMPUTE clause. It follows the other clauses that make up the SELECT statement and simply totals a returned column using a normal aggregate function such as SUM(). Here's an example: SELECT CustomerNumber, Amount FROM ORDERS COMPUTE SUM(Amount) There are several good reasons not to use this facility. First, most front-end tools don't know how to properly process it as part of a result set. Second, COMPUTEs have some nonsensical limitations that prevent them from being as useful as you might expect. Third, and most important, the query can be designed to return the desired total without the need of a COMPUTE clause, as in SELECT O.CustomerNumber, O.Amount, SUM(O2.Amount) TotalOrders FROM ORDERS O, ORDERS O2 GROUP BY O.CustomerNumber, O.Amount This keeps the data in a format that most front-ends can deal with. An even better and more efficient approach would be to make two separate passes over the data: one to retrieve a subset of the data into a temporary table and a second to total it up. In any event, there are a number of ways of deriving the desired figures without resorting to the clumsy COMPUTE clause. GROUP BY Group columns according to index keys, if possible. For example, given the two queries SELECT OrderDate, CustomerNumber, SUM(Amount) TotalOrders FROM ORDERS GROUP BY OrderDate, CustomerNumber and Page 625 SELECT OrderDate, CustomerNumber, SUM(Amount) TotalOrders FROM ORDERS GROUP BY CustomerNumber, OrderDate

only the second approach will use an index over the ORDERS table that was created using this syntax: CREATE INDEX ORDERS03 ON ORDERS (CustomerNumber, OrderDate) If no index exists that was created using OrderDate as the high-order key, the query optimizer won't use an index to resolve the query. Stored Procedures The following techniques relate to stored procedure performance optimization. Sometimes just switching to stored procedures from interpretive SQL can greatly improve an application's performance. Additionally, there are several things you can do to further improve performance by optimizing the stored procedures themselves. Go Easy on the Parameters The parameters you pass to a stored procedure should be as small and as few as possible. As with traditional programming languages, parameters passed on the stack should be kept to a minimum. This is even more true of stored-procedure parameters, because they must be sent over the network to your database server, possibly causing network bottlenecks. Pass small integers where possible and expressly avoid passing large character strings. Some platforms allow you to pass parameters to stored procedures both by name and by order. When you aren't calling a procedure lots of times in succession, pass parameters by name for clarity. If you are calling a stored procedure repetitively, pass parameters by position, as this affords a slight performance benefit on most platforms. Those small gains add up to huge ones when you make hundreds or thousands of calls to a given procedure. SQL Server's SET NOCOUNT ON By default, Sybase and Microsoft SQL Server pass a count of the rows processed by each statement in a stored procedure back to the client that executed the stored procedure. Normally, the client throws these counts away. You can improve the performance of SQL Server stored procedures by using the SET NOCOUNT ON syntax. The only side effect of using SET NOCOUNT is that the READTEXT command, when used in conjunction with the dbreadtext() DB-Library function, may not operate properly. Because this circumstance is extremely rare, SET NOCOUNT ON should be regarded as safe to use and should be included in stored procedure definitions by default. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 637 You can remedy this by setting the TDatabase's KeepConnection property to False; however, this brings with it the undesirable side effect of forcing you to log back into the server each time the last active DataSet has been closed and you try to open another. You can override having to log back in, but, again, this is the default behavior. SQLPASSTHRU MODE Another way you can limit the number of connections your application makes to your back-end server is through the SQLPASSTHRU MODE database alias setting. SQLPASSTHRU can be set for an entire driver family or for specific aliases. Setting it for a driver family affects only new aliases that you define afterward—it has no effect on existing aliases. SQLPASSTHRU has one of three possible values: NOT SHARED, SHARED AUTOCOMMIT, and SHARED NOAUTOCOMMIT. Setting it to either of the SHARED settings helps minimize connections to the server because it allows the BDE to share connections made by your application to the server for its own use. Dynamic Forms Another way of reducing an application's overall number of server connections is to delay creating forms until they're actually needed. By default, all the forms in an application are created when the application starts, even though only a handful are ever used simultaneously. It's far better to explicitly create and destroy nonessential forms than to have them needlessly wasting database server connections and other system resources. You'll have to use program code to create and destroy forms that you don't

want to be created automatically. This can be quite simple—especially for modal forms. Here's a method of dynamically creating and destroying forms: 1. Bring up the Project Options dialog from within Delphi. 2. Move all but the most essential forms out of the Auto-create forms list and into the Available forms list. 3. On each form that you wish to create and destroy dynamically, establish OnClose event code that sets Action := caFree. 4. Use the Application.CreateForm(TForm1, Form1) syntax to create a form when you need to show it, replacing TForm1 with the form's class type and Form1 with the form's instance variable. 5. Show the form using its ShowModal method. 6. Destroy the form by closing it. SQL Indications—An Economy of Words The next several tips have to do with tuning the way that applications communicate with servers using SQL. Since SQL is the lingua franca of client/ server DBMSs, it's critical to be fully fluent in it. Page 638 Use Stored Procedures Compiled code is generally faster and more efficient than interpreted code, regardless of the programming language. The same is true of SQL. Rather than using TQuery components and Delphi's dynamic SQL, use stored procedures when possible and pass the parameters instead. This is faster than sending dynamic SQL statements because the server compiles and optimizes stored procedures in advance. Interpreted code must go through this same process every time it needs to be executed, so the more of the process you can accomplish in advance, the better. There is one area where I feel it's inappropriate to use stored procedures extensively. This is in the area of commonplace data modification—the kind normally done with INSERT, UPDATE, or DELETE statements. A trend has arisen in the last few years to perform data modification with stored procedures instead of data-aware controls. The reasons given for this range from improved security and speed to greater control over how DML statements are executed. These are all valid reasons, but the one thing not mentioned is that doing this basically enervates client/server-aware development tools like Delphi. In

Delphi apps, the only automatic way to use stored procedures for normal data modifications is through the TUpdateSQL component. Though TUpdateSQL handles this task quite well, it still requires you to build hard-coded SQL to update your tables. Your stored procedure calls will no doubt require a list of field values for insertion, deletion, or modification of table rows. As the table structure changes, your SQL will have to change with it. This is problematic in nature and should be avoided. This isn't to say that there aren't occasions where using TUpdateSQL is the right approach. But you should first attempt to use Delphi's built-in mechanism for updating tables, resorting to performing data modifications through TUpdateSQL only when absolutely necessary. If you perform stored procedure-based updates without using TUpdateSQL, you lose the ease of Delphi's data-aware controls altogether. You must then manually set up normal controls to simulate data-aware controls by limiting the types of data they accept, retrieving values for them when a form is first displayed, and sending changed values to the server via stored procedures when the form is closed. This is all very tedious and highly error prone. It removes one of the biggest reasons for using a tool like Delphi in the first place—its capability to retrieve and act on your server's meta-data. You might as well be using a tool that has no inherent capability to communicate with the server and make direct database API calls instead. Another disadvantage to this approach is the fact that you circumvent the security mechanisms provided by the server. Most administrative tools aren't going to show whether stored procedure XYZ has the DELETE privilege on table XYZ. You lose the ability to view the system-security configuration—rights that have been granted or revoked for users or groups on individual database objects—from a single vantage point. You must look not only at the rights you've granted using GRANT or REVOKE, but also at the contents of the stored procedures in your database. Page 639 NOTE

Unlike Sybase, Oracle, and Microsoft SQL Server, InterBase allows rights to database objects to be granted and revoked from triggers as though they were users. This security information is stored as part of the server's meta-data and could be queried by a database administration tool in order to provide a comprehensive view of the server's security. I still believe, however, that it's a bad idea to implement database security in this manner.

There was a time when the tools available for client/server development were primitive enough that doing simple data modification via stored procedures was a necessary evil. With the advent of tools like Delphi, those days are mostly gone, so I recommend you take full advantage of Delphi's built-in data modification mechanisms. As a rule, use stored procedures for complex queries and for tasks other than simple data manipulation. Use Prepare Call the Prepare method of TQuery components before opening them. Prepare sends an SQL query to the database engine for parsing and optimization. If Prepare is explicitly called for a dynamic SQL query that will be executed more than once, Delphi sends only the query's parameters—not the entire query text—with each successive execution. If Prepare is not explicitly called in advance, the query is automatically prepared each time it's opened. By preparing it first, you remove the need for the database engine to do so, allowing the query to be opened and closed several times in succession without being re-prepared. This is bound to make query-intensive operations run faster. UpdateMode Both the TTable and TQuery components publish an UpdateMode property. This property determines the type of SQL WHERE clause that's used to perform data modifications made using data-aware controls. It defaults to UpWhereAll, which means that the BDE generates an SQL WHERE clause that lists every column in a table. With large tables especially, this can be quite cumbersome. An alternative, faster approach is the use of the UpWhereChanged setting. It generates a WHERE clause that mentions only the table's key fields along with the fields that were changed. This is best demonstrated with an example. Let's say your Delphi application had just modified the LastName field of the

CUSTOMER table. With UpWhereAll, here's the type of SQL that would be generated—notice the lengthy WHERE clause: UPDATE CUSTOMER SET LastName='newlastname' WHERE CustomerNumber=1 AND LastName='Doe' Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 640 AND AND AND AND AND FirstName='John' StreetAddress='123 SunnyLane' City='Anywhere' State='OK' Zip='73115'

By contrast, here's the statement generated by UpWhereChanged: UPDATE CUSTOMER SET LastName='newlastname' WHERE CustomerNumber=1 AND LastName='Doe' Notice how much shorter it is. Notice also that it avoids the possibility of overwriting another user's changes to the LastName field by including its original value in the WHERE clause. If another user changes the LastName field in between the time the row is read and when it is updated by the current user, the UPDATE generated by UpWhereChanged will fail, which is what you'd want. This is a little less bulletproof than the UpWhereAll method. Another user could delete the row after your application has read it, then add a new record to the table that happens to have the same key and LastName value as the original record. When your record applied its UPDATE, it would be updating the wrong record. But this scenario is a remote possibility at best. UpdateMode's other setting, UpWhereKeyOnly, is even less bulletproof, but it does have its use. Because it checks only the key value of the row you're changing, it doesn't allow for the possibility that another user has changed the field you're updating in the time since you originally read the record. This may

be a safe assumption, and, then again, it may not be. CAUTION In most multi-user applications, it's not safe to assume that a record hasn't been changed since you first read it into your client application. Therefore, use UpWhereKeyOnly with caution. Fully understand its ramifications before enabling it in a multi-user application.

Using UpWhereKeyOnly is the kind of optimization that you do only on a rare occasion and out of necessity. Because the WHERE clause generated by the UpWhereKeyOnly setting would be shorter, it would naturally tend to be faster than those that include additional columns. Check with your Database Administrator before using this option; it could lead to disastrous results if used improperly. Updatable TQuerys As a rule, avoid updatable TQuerys. Use views on your server instead. I say this for a couple of reasons. First, updating a TQuery puts the burden of dissecting an SQL query and updating its underlying table(s) on the client application (specifically on the Borland Database Engine), not on the server. In client/server systems, the server is the proper place for this SQL Page 641 reverse-engineering. It's the place that resources have been dedicated to do complex database work. The server will also tend to understand its own SQL dialect better than a client application. This leads to more flexible updatable queries and faster updates. Cached Updates You can use Delphi's cached updates mechanism to help minimize the SQL being sent to your server and reduce the database locks caused by your application. Cached updates are stored locally until you explicitly apply them to the database. They're then sent to the server en masse. This reduces server locks and overall lock duration and can speed up your application considerably. To make use of cached updates in a Delphi application, follow these steps:

1. In the Object Inspector, set the CachedUpdates property to True for the DataSet whose updates you want to cache. 2. Set the DataSet's UpdateRecordTypes property to control the visible rows in the cached set. UpdateRecordTypes can have the following values: rtModified, rtInserted, rtDeleted, and rtUnmodified. 3. Establish an OnUpdateError event handler to handle any errors during a call to ApplyUpdates. 4. Make changes to the DataSet's data when you run the application. 5. Call the ApplyUpdates method to save your changes and CancelUpdates to discard them. Use SQL Monitor Delphi's SQL Monitor tool is handy for getting a peek at the SQL that's generated by your applications. You can use it, for example, to inspect the SQL generated for operations that seem inordinately slow. Depending on the situation, you may find that you need to switch the SQL to a view or stored procedure on the server. You may also discover that the way you're searching for or updating data is inefficient—perhaps the application can be streamlined to remedy this. Whatever the case, be sure to take advantage of the behind-thescenes information that SQL Monitor provides. You can find the SQL Monitor tool on Delphi's Database menu. Schema Caching The Borland Database Engine now supports schema caching—storing structural information about database objects locally. Enabling schema caching can reduce the queries sent by your app to the server to retrieve database catalog information. This can speed up your applications significantly because the BDE is not forced to constantly re-retrieve meta-data from the server. You can enable schema caching in the BDE Administration utility. There are four settings that are related to schema caching. Table 25.1 lists them. Page 642 Table 25.1. BDE Configuration settings that affect schema caching. Setting ENABLE SCHEMA CACHE Action Enables/disables schema caching (this is a driver-level setting)

SCHEMA CACHE SIZE SCHEMA CACHE TIME SCHEMA CACHE DIR

Specifies the number of tables for which to cache schema data Specifies the number of seconds to cache schema information Specifies a directory in which to store schema information (this is a driver-level setting)

Note that SCHEMA CACHE TIME defaults to -1, which means that schema information is retained in the cache until the database is closed. Valid values for SCHEMA CACHE TIME range from 1 to 2,147,483,647 seconds. Especially over WAN connections, enabling schema caching can make a remarkable difference in how your applications perform. Note that the BDE's schema cache facility naturally expects your database schema to remain static. This means that some databases aren't good candidates for schema caching. In particular, you shouldn't use schema caching for databases
q q q

Where table columns are often added or dropped Where table indexes are frequently added or dropped Where column NULL/NOT NULL designations are frequently changed

If you attempt to use schema caching with databases that aren't appropriate for it, you can expect the following unknown SQL errors:
q q q q q

Unknown Column Invalid Bind Type Invalid Type Invalid Type Conversion Column Not a Blob

Filters Use Delphi's filtering facility to qualify result sets on the client side of your client/server applications. For small DataSets, using an OnFilterRecord event handler can be faster than re-querying the database because it limits interaction with the database server and network. Small result sets Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 643 will often be cached on the client machine in their entirety, anyway, so it makes sense to filter them locally when possible. To filter records locally in a Delphi application, follow these steps: 1. Set up a DataSet's OnFilterRecord event handler to include/exclude rows using its Accept parameter. 2. Set the DataSet's Filtered property to True. 3. When you access the DataSet from within the application, it will appear as though it only contains rows that match the filter criteria. NOTE The steps above enlist the OnFilterRecord event rather than the Filter property to limit the rows seen by the application. This is because setting the Filter property does not automatically create local filters. Though the rows returned will certainly be limited by the filter expression, setting Filter with Table components connected to database servers merely changes the WHERE clause that's created. This amounts to a remote filter rather than a local one.

Use TFields Define and use TField components when possible, rather than using FieldByName or TDataSet's Fields property. Persistent TFields are more efficient because they store basic information about the field with the application, saving it from having to be retrieved from the BDE. They're also safer because they automatically raise an exception if a column's underlying data type has changed. The FieldByName function and the Fields property, by contrast, must scan the table's schema information to derive a column's basic information. They're slower and less reliable. Use TFields instead—they're easier, safer, and more immune to changes in the underlying database objects. Use the Data Dictionary I recommend you always first attempt to locate your application's business rules on the server if at all possible. However, there will undoubtedly be times that you'll have to set up business rules in your client apps. When you build business rules into your apps, use Delphi's Data Dictionary and its Attribute Sets as much as possible. When you've defined all you can of your client-side business rules in the Data Dictionary, define the remainder of your business rules strategy using DataSet components and TField attributes. By defining client-side business rules using the Data Dictionary rather than within a specific application, you make the rules more readily available to other applications. Page 644

DBText and Read-Only Fields If a field is to be read-only, don't use a DBEdit component for it; use a DBText component instead. DBText fields are leaner and provide the same function. You also might want to consider using static Label components for column data that cannot change once a form is onscreen. You need a DBText if the data might change; otherwise, you can assign a TLabel component's Caption property in the form's OnShow event and save yourself the resources needed by data-aware controls, even lean ones like DBText. Using the leanest possible data controls in your applications will not only help your applications have a smaller resource footprint, but will also help them run faster since there will be less interaction with the database. Multi-Threading Database Apps One of the more powerful aspects of 32-bit Windows is the ability to create multi-threaded applications. Delphi enables you to create threads easily and safely using its TThread object. Multi-threading can also be used in database applications. A useful application of threading in database apps is in background query execution. If you have a query that executes for a long time, you can execute it on its own thread and allow your application to continue running. Threads can also be used to speed access to the database. For example, if you're reading records from a set of operating system files and inserting them into tables on your SQL server, you might allocate a separate thread for each file so that insertions from one file don't wait on those from another. There are a number of good uses of multi-threading in database applications. Listings 25.1 through 25.4 show a simple database program that uses multi-threading to access the rows in three different tables at once. NOTE This sample app utilizes the IBLOCAL database alias that ships with Delphi. In order to use it, you'll need to ensure that your InterBase Server is started and that the IBLOCAL database is available to it.

Listing 25.1. Project source code for the multi-threaded sample program, thrdex. program thrdex; uses Forms, thrdex00 in `thrdex00.pas' {Form1}, thrdex01 in `thrdex01.pas'; {$R *.RES} begin Application.Initialize; Application.CreateForm(TForm1, Form1); Application.Run; end. Page 645 Listing 25.2. Unit source code for thrdex00.pas, the first of the two units in the multi-threaded sample program. unit thrdex00; interface

uses Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs, StdCtrls, DB, Grids, DBGrids, DBTables, Thrdex01, ExtCtrls; type TForm1 = class(TForm) Query1: TQuery; DataSource1: TDataSource; Button1: TButton; Query2: TQuery; DataSource2: TDataSource; Database1: TDatabase; Session1: TSession; Session2: TSession; Database2: TDatabase; Query3: TQuery; DataSource3: TDataSource; DBGrid3: TDBGrid; Button2: TButton; Database3: TDatabase; procedure Button1Click(Sender: TObject); procedure FormClose(Sender: TObject; var Action: TCloseAction); procedure Button2Click(Sender: TObject); procedure Database1Login(Database: TDatabase; LoginParams: TStrings); private { Private declarations } public { Public declarations } end; var Form1: TForm1; implementation var QueryThread1, QueryThread2 : TQueryThread; {$R *.DFM} procedure TForm1.Button1Click(Sender: TObject); begin Database1.Open; Database2.Open; QueryThread1:=TQueryThread.Create(Query1); QueryThread2:=TQueryThread.Create(Query2);

QueryThread1.OnTerminate:=QueryThread1.OnTerm; QueryThread2.OnTerminate:=QueryThread2.OnTerm; Button1.Enabled:=False; end;

continues Page 646 Listing 25.2. continued procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction); begin QueryThread1.Terminate; QueryThread2.Terminate; end; procedure TForm1.Button2Click(Sender: TObject); begin with query3 do begin if active then close; open; end; end; procedure TForm1.Database1Login(Database: TDatabase; LoginParams: TStrings); begin LoginParams.Values[`USER NAME'] := `SYSDBA'; LoginParams.Values[`PASSWORD'] := `masterkey'; end; end. Listing 25.3. Form .DFM file for thrdex00.pas, the first of the two units used by the multi-threaded sample program, thrdex. object Form1: TForm1 Left = 106 Top = 67 Width = 595 Height = 434 Caption = `Form1' Font.Charset = DEFAULT_CHARSET Font.Color = clWindowText

Font.Height = -11 Font.Name = `MS Sans Serif' Font.Style = [] OnClose = FormClose PixelsPerInch = 96 TextHeight = 13 object Button1: TButton Left = 72 Top = 374 Width = 121 Height = 25 Caption = `Start Query Threads' TabOrder = 0 OnClick = Button1Click end object DBGrid3: TDBGrid Left = 72 Top = 248 Width = 497

Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 647 Height = 113 DataSource = DataSource3 TabOrder = 1 TitleFont.Charset = DEFAULT_CHARSET TitleFont.Color = clWindowText TitleFont.Height = -11 TitleFont.Name = `MS Sans Serif' TitleFont.Style = [] end object Button2: TButton Left = 240 Top = 374 Width = 145 Height = 25 Caption = `Open Query in Main Thread' TabOrder = 2 OnClick = Button2Click end object Query1: TQuery DatabaseName = `dbthread1' SessionName = `Ses1' SQL.Strings = ( `select * from EMPLOYEE') Left = 16 Top = 8 end object DataSource1: TDataSource DataSet = Query1 Left = 16 Top = 40 end object Query2: TQuery DatabaseName = `dbthread2' SessionName = `Ses2' SQL.Strings = ( `select * from SALES') Left = 16

Top = 80 end object DataSource2: TDataSource DataSet = Query2 Left = 16 Top = 120 end object Database1: TDatabase AliasName = `IBLOCAL' DatabaseName = `dbthread1' LoginPrompt = False Params.Strings = ( `USER NAME=SYSDBA' `PASSWORD=masterkey') SessionName = `Ses1' OnLogin = Database1Login Left = 16 Top = 152 end object Session1: TSession continues Page 648 Listing 25.3. continued Active = True SessionName = `Ses1' Left = 16 Top = 224 end object Session2: TSession Active = True SessionName = `Ses2' Left = 16 Top = 256 end object Database2: TDatabase AliasName = `IBLOCAL' DatabaseName = `dbthread2' LoginPrompt = False Params.Strings = ( `USER NAME=SYSDBA' `PASSWORD=masterkey') SessionName = `Ses2' OnLogin = Database1Login Left = 16 Top = 192 end object Query3: TQuery

DatabaseName = `dbdefaultthread' SQL.Strings = ( `SELECT * FROM CUSTOMER') Left = 16 Top = 288 end object DataSource3: TDataSource DataSet = Query3 Left = 16 Top = 320 end object Database3: TDatabase AliasName = `IBLOCAL' DatabaseName = `dbdefaultthread' LoginPrompt = False Params.Strings = ( `USER NAME=SYSDBA' `PASSWORD=masterkey') SessionName = `Default' OnLogin = Database1Login Left = 16 Top = 352 end end Listing 25.4. Unit source code for thrdex01.pas, the second of the two units used by the multi-threaded sample program, thrdex. unit thrdex01; interface Page 649 uses Classes, Forms, DBTables, Windows; type TQueryThread = class(TThread) private { Private declarations } FQuery : TQuery; protected procedure Execute; override; procedure OpenQuery; public constructor Create(Query: TQuery); procedure OnTerm(Sender : TObject); end; implementation

{ TQueryThread } constructor TQueryThread.Create(Query: TQuery); begin inherited Create(False); FQuery := Query; end; procedure TQueryThread.OpenQuery; begin FQuery.Open; With FQuery do begin With Owner.Owner as TApplication do ProcessMessages; While not EOF do Next; Close; With Owner.Owner as TApplication do ProcessMessages; end; end; procedure TQueryThread.Execute; var Counter : Integer; begin { Place thread code here } For Counter:=0 to 100 do begin OpenQuery; If Terminated then exit; end; end; procedure TQueryThread.OnTerm(Sender : TObject); begin Application.MessageBox(PChar(`Thread running `+FQuery.Name+' Âfinished.'),PChar (FQuery.Name),IDOK); end; end. Page 650 This code defines two TSession components and two background threads. Each of these threads opens and closes a given query 100 times in succession. A third query can be opened in the foreground. If you key in this source code (or load it from the accompanying CD-ROM), you'll notice that the background and foreground threads don't interfere with one another. They each execute independently. You can click the Open button for the foreground query multiple times to re-execute it without impacting your background queries. You can use query threading in Delphi client/server applications to provide lengthy tasks their own execution threads. Study these source code listings for the details on how this is done.

Server Performance Tuning

Server-side performance tuning consists of tweaking your SQL, network settings, database configuration, and so forth. Naturally, making server-based optimizations generally helps any app that uses the server, not just Delphi apps. Server Configuration Tuning There are a number of ways of tuning database servers for better performance. Out of the box, few are optimally tuned for any but the simplest applications. Here are a few potential tuning targets. Memory I've actually walked into client sites only to discover that though the server machine had lots of RAM in it, the database server software hadn't been configured to use it. System memory usage isn't automatic on most platforms; it must be configured. Generally speaking, you want to give all the RAM you can to the server software without causing page swapping. Caching The rule of thumb is usually to disable operating system level caching and leave that to the database server. Your database server knows more about your data and its logical construction and can usually make better decisions about what to keep cached. Another item to watch for regarding caching is procedure cache bloat. Both Sybase and Microsoft SQL Server divide cache memory into two pieces: the data cache and the procedure cache. The procedure cache is defined as a percentage of cache memory. What's left after the procedure cache has been set aside is given to the data cache. This often means that the procedure cache is oversized on systems with lots of RAM. For example, if you increase your system memory from 128MB to 256MB, you've effectively doubled the size of your procedure cache because it's computed as a percentage. If you added the RAM with the intent that it be earmarked for data cache and your procedure needs haven't changed, part of your RAM is being wasted. Consider lowering the procedure cache percentage to compensate for this. Page 651 Processors Most modern DBMSs support spreading their load over multiple processors. Microsoft SQL Server, for example, allows you to configure the server's "affinity mask"—a bit mask that determines which processors in a multi-processor computer the server can utilize. Sybase allows you to configure its "max online engines" setting to specify how many of the processors in a multi-processor computer it can use. Oracle, for its part, is the most flexible of all—even allowing you to spread the server's load across multiple machines via its parallel server technology. Note that there is a point of diminishing returns as you add more processors into the mix. You won't double performance by doubling the number of processors. Why? Because most DBMS operations are I/O bound, not CPU bound. Nevertheless, I/O is managed by the CPU, so cranking more instructions through the system will usually improve performance to some degree. Asynchronous I/O All the major DBMS players support asynchronous input/output operations to disk drives. Obviously, reading and writing data asynchronously is preferable to doing so in a synchronous fashion. If your platform supports asynchronous I/O, experiment with it to see if it improves performance. Noticeable performance gains are quite likely when using "smart" drive or RAID technology. Since there are typically a greater number of devices to begin with, reading and writing asynchronously becomes physically more viable.

Transaction Logging Locate transaction/redo logs on separate devices from data. This is not only a good idea for the sake of performance, it also helps ensure data recoverability. Also, for development databases or databases that you're able to completely back up in your regular back routine, consider reducing some of the server's transaction management overhead. For example, on both Microsoft and Sybase SQL Server, enabling the "trunc. log on chkpt" option causes completed transaction records to be removed from the transaction log each time a system checkpoint occurs. This keeps the log as small as possible, speeds up log-intensive operations, and generally improves system performance. CAUTION Be careful with log truncation in production systems because you effectively give up your ability to back up the transaction log when log truncation is enabled. This means that you won't be able to do incremental backups and, should there be a system failure, there's no possibility of up-to-the-minute recovery. You'll have to be the judge of whether automatically truncating the transaction log fits your needs. It has its use; you just have to know what you're doing.

Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 652 Query Tuning I've used the term query optimizer throughout this book. You may be wondering what exactly a query optimizer is and how such a beast works. All leading database servers optimize the SQL queries passed to them by clients. They look at the SQL you pass them and attempt to determine the most effective way of executing it. The facility that performs this analysis is what I'm referring to when I mention the server's query optimizer. Most servers refer to the strategy derived by the optimizer as a query execution plan. Factors such as available indexes, the amount of disk that must be traversed, statistical information regarding index key distribution, and so forth are taken into account when this plan is developed. More often than not, the server comes up with the best plan for resolving your query, but it may need some assistance from time to time. Helping the Optimizer One way you can help your server's query optimizer is to update the statistics it stores regarding index selectivity. Index selectivity refers to the distribution of key values within an index. By keeping this information up-to-date, you ensure that the query optimizer bases its decisions on the most accurate information possible. The syntax for updating index selectivity in InterBase is SET STATISTICS INDEX INVOICES03 where INVOICES03 is the name of the index to recompute.

In Sybase and Microsoft, it's UPDATE STATISTICS INVOICES.INVOICES03 where INVOICES is the name of the table on which the index is built, and INVOICES03 is the name of the index itself. Because SQL Server indexes are unique only within the table on which they're built, the name of the table is required with this command. On the other hand, all the indexes for a given table can be created at once by omitting the name of a specific index: UPDATE STATISTICS INVOICES Update Oracle index statistics using the ANALYZE command. ANALYZE gathers statistics about tables, indexes, and clusters. Use this syntax to update the statistics for a single index: ANALYZE INDEX CUSTOMER03 COMPUTE STATISTICS; You can compute the statistics for a table and its indexes using this syntax: ANALYZE TABLE CUSTOMER COMPUTE STATISTICS CASCADE; And you can gather statistics for an entire cluster (including its tables and indexes) using this syntax: ANALYZE CLUSTER acctrecv COMPUTE STATISTICS CASCADE; Page 653 Displaying the Query Optimizer's Plan With most SQL servers, you can display the server's execution plan when you execute a query. You can do this with InterBase from within WISQL. To view the plans InterBase generates to service your queries, check the Display Query Plan option in the Basic ISQL Set Options dialog box on WISQL's Session menu, as illustrated in Figure 25.1. Figure 25.1. The Basic ISQL Set

Options dialog box.

This is equivalent to the SET PLAN ON command. You also can display statistical information about each query after it executes using the SET STATS ON command. This command is not to be confused with the SET STATISTICS command (which updates index information) mentioned previously. On Sybase and Microsoft SQL Server, you use the SET SHOWPLAN ON command to display the optimizer's plan. In conjunction with the SET NOEXEC option, you can review the server's query execution plan without actually executing the query. You can also review statistical information related to query execution via the SQL Server SET STATISTICS command. SET STATISTICS IO ON causes SQL Server to display statistical I/O information for each query it executes. SET STATISTICS TIME ON causes SQL Server to display timing information for each query executed. Oracle's query execution plan information can be accessed using the EXPLAIN PLAN PL/SQL command. You pass EXPLAIN PLAN the statement you're wanting to execute, and Oracle records the execution plan it arrives at in a special table. If you have cost-based optimization turned on, the relative cost of the query is computed, as well. Reviewing the plan generated by your query optimizer helps you better understand why queries behave the way they do and assists you in fine-tuning them. Figure 25.2 illustrates how seeing the optimizer's query plan can alert you to the fact that a given query is not utilizing an index. The presence of the word NATURAL in the execution plan indicates that the table is being searched using natural row order (without an index). Now let's rewrite the query to use an index. Figure 25.3 shows the new query and the plan InterBase derives for it. Page 654

Figure 25.2. The query plan without an index.

Figure 25.3. The query plan with an index.

This example shows the value of being able to see the plan the server develops for executing a query. By carefully reviewing query plans, you should be able to come up with ways to optimize your queries. Forcing the Use of a Plan InterBase supports an extension to the SELECT statement that enables you to tell the optimizer to use a specific query execution plan rather than develop its own. Here's the syntax: SELECT LastName, FirstName FROM CUSTOMER PLAN (CUSTOMER ORDER CUSTOMER03) ORDER BY LastName Notice the PLAN clause. The first argument tells the optimizer which of the query's tables the plan is for, and the second argument tells the optimizer what it is you're doing—in this case, you're ordering the table. The last argument tells the optimizer what it is you want to use to carry out your plan—in this case, the CUSTOMER03 index. Page 655

This facility can be useful in situations where you don't think the optimizer is taking the best approach to optimizing a query. SQL Server has a couple of ways of doing the same thing. The first method involves forcing the use of a particular index. The syntax to do this looks like this: SELECT LastName, FirstName FROM CUSTOMER (3) ORDER BY LastName Notice the 3 surrounded by parentheses after the table name. This tells the optimizer to use the third index created over the CUSTOMER table. (This is the reason that I name indexes using the table as a base, followed by a number. Remembering the order of the index is a snap.) Specifying a 0 here forces a table scan, while specifying 1 forces the optimizer to use the table's clustered index, if it has one. SQL Server also allows you to specify the index to use by name, like so: SELECT LastName, FirstName FROM CUSTOMER (INDEX = CUSTOMER03) ORDER BY LastName The second method involves forcing the SQL Server optimizer to join tables in a particular order using the SET FORCEPLAN command. Setting FORCEPLAN on causes tables to be joined in the order they're specified in a query's FROM clause. Usually, the optimizer will make the best guess regarding join orders, but not always. If your inspection of SHOWPLAN indicates that the optimizer isn't making good choices, you can turn on the FORCEPLAN option and control the join via your query's FROM clause.

Network Performance Tuning
This section provides a number of tips for tuning the performance of your client/server apps over networks. WANs in particular are a common source of client/server bottlenecks. NOTE

The term WAN, as used here, refers to a Wide Area Network that typically runs over 56KB or faster digital lines. Because even a speedy T-1 line is only about one-seventh as fast as a 10MB/second Ethernet network, you have to be a lot more conscientious about bandwidth utilization in apps you target for WANs.

Here are a few tips related to optimally configuring database connections over networks:
q

Obviously, a faster network (say, 100 megabits/sec) can improve the performance of applications that use the network, including client/server apps. Previous | Table of Contents | Next

To access the contents, click the chapter and section titles.

Client/Server Developer's Guide with Delphi 3
(Publisher: Macmillan Computer Publishing) Author(s): Ken Henderson ISBN: 0672310244 Publication Date: 07/30/97

Previous | Table of Contents | Next Page 656
q

q

q

q

q

q

Faster network cards, better network segmentation, and other physical resources can also have a significant impact on performance. I suppose these are obvious choices, but sometimes we overlook the obvious. If your network connection protocol supports oversized packets (such as Novell's Large Internet Packet support), you might experiment with this to lower the number of packets traversing the network. The fewer packets you pass over relatively slow WAN connections, the better. Consider creating nonessential forms dynamically rather than automatically. Every database connection you establish uses up network bandwidth. Over relatively speedy local network connections, this may be barely perceptible. However, the difference it makes can be very substantial over WAN connections. You have to weigh the slowness of creating and destroying forms as you need them against the reduced overall bandwidth requirements to see whether this is worth doing, but it's something you should consider. When building applications that will communicate over a WAN, grid controls, such as DBGrid and DBCtrlGrid, should be viewed with a skeptical eye. Though certainly appropriate in local network applications, grids often retrieve much more data than is actuall