DEBUGGING DATAWINDOWS
Transcription
DEBUGGING DATAWINDOWS
DEBUGGING DATAWINDOWS U.S. $14.00 (CANADA $15.00) PowerBuilderJournal.com Enterprise Application Studio February 2000 - Volume: 7 Issue: 2 www.PowerBuilderJournal.com Announcing... Coming June 25–28, 2000 September 24–27, 2000 From the Editor PowerDesigner: Neck and Neck with the Competition by John Olson pg. 3 Guest Editorial Fat Client Is Not Dead by Richard Brooks pg. 5 Focus Story: Java vs PowerScript Kouros Gorgani Part 2: How to leverage your knowledge of PowerScript to quickly learn and build Java programs DataWindow Magic: Concepts in Graphing Richard Brooks How to create a simple graph From Sybase Extending PowerBuilder Applications to the Web by Rob Veitch pg. 27 PBDJ News by Bruce Armstrong pg. 33 10 Distributed Technologies: Handling Aborted Transactions in Jaguar Michael Barlotta A technique that works with any Jaguar client or component SYS-CON PUBLICATIONS 18 Feature: Beyond Showplan Brian Davignon The next step in Sybase query tuning 22 PFC Corner: Debugging DataWindows–Live! Vince Fabro Useful enhancements to the DW properties dialog Slick Tricks by Bernie Metzger pg. 38 6 Introduction to PowerBuilder: Using the PFC Multitable Update Service 28 USER 1 USER 2 Update orders Set ship_name =’Foy’ Where order_id = 100 and ship_name = ’Bates’ Update orders Set ship_city ship_city =’Wayne’ Where order_id = 100 and ship_city = ’Boogers Holler’ ship_city Foy Wayne order_id 100 How one of the least used PFC services works ship_name Bates Bob Hendry ship_city ship_cit Boogers Holler 34 Dutton Software www.duttonsoftware.com 2 PBDJ volume7 issue2 www.PowerBuilderJournal.com FROM THE EDITOR JOHN OLSON, EDITOR-IN-CHIEF PowerDesigner:Neck and Neck with the Competition hough PowerDesigner 7 was released a few months ago, not many people are aware that it’s much more feature-rich than past versions. Sybase has been pushing hard to get the word out that PD7 is a great product, but it’s getting only mediocre media coverage. We haven’t even mentioned it yet in this magazine, so I’m going to focus on it now. First, a little history… Back in 1994, Powersoft was looking to broaden its toolset by adding relational design capabilities, a database and various other components needed for full-cycle development. Rather than taking years to build them from the ground up, they chose to find and purchase existing products and integrate them with PowerBuilder. A well-advertised acquisition was Watcom of Waterloo, Ontario. The company had one of the best stables of C/C++ developers in the world and some products, Watcom C and Watcom SQL, that were best in their fields. Of course, their Watcom SQL database has evolved and is now called Adaptive Server Anywhere, and it’s still the best departmental-size and mobile computing database available. The relational design tool that met their expectations for quality and features was SDesignor by SDP, a French company. They purchased SDP and quickly started offering SDesignor in a bundle with PowerBuilder and Watcom SQL. Soon after, Sybase purchased Powersoft and all the Powersoft products became part of a much larger collection of front-end and back-end tools. As the Sybase toolset evolved and became more integrated, SDesignor was renamed PowerDesigner. Though SDesignor was a good product, by the time Powersoft started promoting it several other design tools, led by Computer Associates’ ERWin, had already won the hearts (and market) of PowerBuilder developers. Initially, SDesignor was lagging a little in features but has slowly caught up in both features and acceptance. However, Sybase hasn’t promoted it much outside of its customer base. As a result, it’s virtually unknown to developers who don’t use Sybase development tools. Today, PowerDesigner is an entirely different tool than it was in version 6. The French development team rewrote the entire program and greatly enhanced its feature set. It’s no longer just a very good relational design tool, but is now also a very good object-design tool. The huge changes resulted in a bit of a rocky start to the PD7 release. However, version 7.01 is now available and the bugs common to first-version products have been taken care of. The Sybase development and support teams are proud of this product, and for good reason. It can now compete head to head with ERwin for relational design and Rational Software’s Rose for object design. Hopefully it won’t be marketed just to the PowerBuilder industry but will also find acceptance in the general software development industry. In addition to features you’d expect to find in every relational and object modeling tool, here are just a few of the exceptional features PowerDesigner 7 has. T Relational Design (many of these were also in PD6): • Conceptual modeling: Physical modeling is a staple of relational tools but not all offer conceptual modeling. It allows for designs to be database independent and leads to the next great feature. • Support for multiple databases from a single model: From a single conceptual model PD can build DDL specific to several targets including Oracle, MS-SQL Server, ASE, ASA, DB2 and more. • Powerful reengineering capabilities: The tool not only builds databases for many targets but it can also reengineer physical models from many database types. From the physical model it can create a conceptual model, then build a physical model for a new target, automatically performing data type conversions. It effectively removes most of the work of porting a database from one type to another. • Embedded entities: With PB7 you can embed an entity from an entirely different project (CDM or PDM file) into your current project. The link is persistent so changes to the entity in the original model are promulgated to the linked model. • Schema difference reporting: Databases, schema exports and physical models can be compared for differences – a feature ERwin has had for several years. It’s nice to finally have it in PowerDesigner. Object Design: • Java support: Not only does PD7 support PowerBuilder, and more specifically PFC, for code generation and reengineering, but it also supports Java. PD7 will generate Java classes and reengineer Java. • Code reengineering: PD7 can read Java and PowerBuilder code, then create object models from it. If you’ve already begun coding, it’s not too late to start using PD7 for object modeling. • Code generation: Once you’ve modeled your objects, PD7 will actually generate your code for you. This gives you a good head start on coding Java and PowerBuilder applications and components. • UML: PD follows the industry standard by supporting the Unified Modeling Language. Obviously, that’s a very brief list of standout features. For more information on the features that PD7 offers and to download an evaluation version, go to www.sybase.com/products/powerdesigner. In the next few months we’ll have some in-depth articles on the features of PD7, as well as provide some tips on its use. Sybase Success Sybase released their fourth-quarter earnings on January 21. They blew away financial analysts’ expectations for profits by beating the per-share estimate by almost 50%. That’s a huge surprise for a quarter in which earnings were expected to be down due to Y2K and a cyclical revenue downturn. After the earnings announcement, Sybase stock immediately gained 25% and suddenly Sybase is the talk of Wall Street. The media frenzy will blow over but hopefully the increased consumer confidence will not. With over $300,000,000 in the bank, it’s clear that Sybase isn’t going out of business any time soon. On the downside, the service revenues are increasing while the licensing revenues are flat. For a company whose success has been based on its products rather than services, that could signal a change in direction. ▼ [email protected] AUTHOR BIO John Olson is a principal of Developower, Inc., a consulting company specializing in software solutions using Sybase development tools. He’s a CPD professional, charter member of TeamSybase and contributing author to SYS-CON’s Secrets of the PowerBuilder Masters books. www.PowerBuilderJournal.com PBDJ volume7 issue2 3 Starbase Corporation www.starbase.com 4 PBDJ volume7 issue2 www.PowerBuilderJournal.com GUEST EDITORIAL WRITTEN BY RICHARD BROOKS EDITORIAL ADVISORY BOARD MICHAEL BARLOTTA, BILL BARTOW, ANDY BLUM, RICHARD BROOKS, JOE CELKO, KOUROS GORGANI, BAHADIR KARUV, PH.D., DAVID MCCLANAHAN, BERNIE METZGER, JOHN OLSON, SEAN RHODY EDITOR-IN-CHIEF: EXECUTIVE EDITOR: ART DIRECTOR: PRODUCTION EDITOR: ASSOCIATE EDITOR: TECHNICAL EDITOR: WATCOM SQL EDITOR: DATA WINDOWS EDITOR: WRITERS IN THIS ISSUE BRUCE ARMSTRONG, MICHAEL BARLOTTA, RICHARD BROOKS, BRIAN DAVIGNON, VINCE FABRO, KOUROS GORGANI, BOB HENDRY BERNIE METZGER, , JOHN OLSON, ROB VEITCH SUBSCRIPTIONS FOR SUBSCRIPTIONS AND REQUESTS FOR BULK ORDERS, PLEASE SEND YOUR LETTERS TO SUBSCRIPTION DEPARTMENT SUBSCRIPTION HOTLINE:800 513-7111 COVER PRICE: $14/ISSUE DOMESTIC: $149/YR. (12 ISSUES) CANADA/MEXICO: $169/YR. OVERSEAS: BASIC SUBSCRIPTION PRICE PLUS AIRMAIL POSTAGE (U.S. BANKS OR MONEY ORDERS). BACK ISSUES: $12 EACH PUBLISHER,PRESIDENT AND CEO: VICE PRESIDENT,PRODUCTION: VICE PRESIDENT,MARKETING: ACCOUNTING MANAGER: ADVERTISING ACCOUNT MANAGERS: ADVERTISING ASSISTANT: GRAPHIC DESIGNER: GRAPHIC DESIGN INTERN: WEBMASTER: WEB EDITOR: WEB SERVICES CONSULTANT: WEB SERVICES INTERN: CUSTOMER SERVICE: JDJSTORE.COM: ONLINE CUSTOMER SERVICE: Fat Client Is Not Dead JOHN OLSON M’LOU PINKHAM ALEX BOTERO CHERYL VAN SISE NANCY VALENTINE BERNIE METZGER JOE CELKO RICHARD BROOKS FUAT A. KIRCAALI JIM MORGAN CARMEN GONZALEZ ELI HOROWITZ ROBYN FORMA MEGAN RING CHRISTINE RUSSELL JASON KREMKAU AARATHI VENKATARAMAN ROBERT DIAMOND BARD DEMA BRUNO Y. DECAUDIN DIGANT B. DAVE ANN MARIE MILILLO JACLYN REDMOND AMANDA MOSKOWITZ EDITORIAL OFFICES SYS-CON PUBLICATIONS, INC. 39 E. CENTRAL AVE., PEARL RIVER, NY 10965 TELEPHONE: 914 735-7300 FAX: 914 735-6547 [email protected] POWERBUILDER DEVELOPER’S JOURNAL (ISSN#1078-1889) is published monthly (12 times a year) for $149.00 by SYS-CON Publications, Inc., 39 E. Central Ave., Pearl River, NY 10965-2306. Periodicals Postage rates are paid at Pearl River, NY 10965 and additional mailing offices. POSTMASTER: Send address changes to: POWERBUILDER DEVELOPER’S JOURNAL, SYS-CON Publications, Inc., 39 E. Central Ave., Pearl River, NY 10965-2306. ©COPYRIGHT Copyright © 2000 by SYS-CON Publications, Inc. All rights reserved. No part of this publication may be reproduced or transmitted in any form or by any means, electronic or mechanical, including photocopy or any information storage and retrieval system, without written permission. For promotional reprints, contact reprint coordinator. SYS-CON Publications, Inc., reserves the right to revise, republish and authorize its readers to use the articles submitted for publication. WORLDWIDE DISTRIBUTION BY CURTIS CIRCULATION COMPANY 739 RIVER ROAD, NEW MILFORD NJ 07646-3048 PHONE: 201 634-7400 All brand and product names used on these pages are trade names, service marks or trademarks of their respective companies. SYS-CON Publications, Inc., is not affiliated with the companies or products covered in PowerBuilder Developer’s Journal. SYS-CON PUBLICATIONS www.PowerBuilderJournal.com s programmers we tend to live in the fast lane, especially where technology is concerned. If you’re anything like me, any mention of the “wave of the future” gets your immediate attention. The last thing we want is to be left behind, floundering in a dead or dying technology. I know that’s what drives me. That’s why, in the wee hours of the morning, you can often find me scanning hundreds of messages in the usenet groups looking for those little hints – “How is the new technology being used? Who is using it? Are the big companies getting involved? What problems are the programmers facing with this new technology and will I face those problems soon?” I have the Sybase knowledge base bookmarked. I’ve set up “My Support” on Sybase. I’ve read every faq there and every white paper. I routinely scan Amazon.com for books with words like PowerBuilder, Jaguar, PowerJ, Distributed, COM and CORBA. I buy and read these books without a second thought. By the time this emerging technology becomes mainstream, I’ll already be there with the experience and knowledge, perfectly poised to offer my clients the solutions they need. This brings me to my point. This morning my client told me she was afraid that I might be driving my current project too far toward this technology. She wanted to remind me of her priorities. Actually she has only one – get this application up and running and make sure it’s airtight. Then move on to those other applications that need to be done. It was an epiphany for me, one of those moments when reality comes shining in, throws you back in your chair and makes you reconsider your motivations and actions. I stumbled my way through the rest of that meeting, trying to allay her very justifiable fears while at the same time asking myself if she was right, at least on some level. A My mind swirled with a sudden realization of this client’s world. Sure, she’d like to use the most modern technology. It would be nice to be able to use all the buzzwords and to tell her supervisors she had delivered the very best that could be found. Given the choice though, she would rather say that she delivered on time and under budget using old technology rather than late and over budget using new. The single biggest nightmare, though, would be to admit that in her efforts to bring the very best, she didn’t have anything at all. I could explain to her how this technology works and the flexibility she would gain. I could explain instance pooling and wax poetic about components and how she could get her Java programmers working on the same team as her PowerBuilder programmers, all of them striving for the same goal. She doesn’t know these words; she hasn’t seen them working. She hasn’t seen any real proof that this technology is more than piein-the-sky dreams and hopes. What she knows is PowerBuilder, client/server, PFC and MDI. These are proven and safe. She knows that they work and she has faith in them. She knows how much effort is required with these tools. She knows that when I leave, her programmers can maintain and extend what I give her – as long as I stick with their skill set. For her these things are real. They are today. Let’s talk about tomorrow when it gets here. For now, she needs her application. So yes, tomorrow morning in the wee hours, you’ll find me at my desk in my home office once again scouring the usenet groups for hints, tidbits and head starts. I’ll be one of those programmers out there talking about how to implement this new technology and I’ll be, believe me, one of the greatest supporters of it. But for now I need to keep her needs in mind and focus. Let’s see, how many arguments did OpenSheet have again? ▼ [email protected] AUTHOR BIO Richard (Rik) Brooks is the owner of Brooks & Young, a Sybase Code Partner. He’s been using PowerBuilder since 1990 and has worked as an independent consultant for major corporations in the U.S. for the last five years. He has authored several books on PowerBuilder including PFC Programmer’s Reference Manual and The Definitive DataWindow. PBDJ volume7 issue2 5 P B D J P A R T F O C U S V Leverage your knowledge of PowerScript WRITTEN BY KOUROS GORGANI n Part 1 of this article (PBDJ,Vol. 7, issue 1) I compared Java to PowerScript.We covered some of the fundamentals of Java language – identifiers, primitive data types and primitive data type wrappers – and PowerBuilder’s any data type, and we began to discuss relational operators, so let’s continue. I Boolean Logical Operators expression is not evaluated. To illustrate this point, consider the following example using the && operator: In Java, the boolean logical operators as seen in Table 1 may look different; however, they perform the same as the AND, OR and NOT in PowerBuilder. The operator not present in PowerBuilder is the exclusive OR, XOR that returns TRUE if and only if one of its two operands is TRUE. boolean IsPersonEligibleForDriversLicense( String name, int age ) { int minimumAge = 16; boolean ok; Conditional Logical Operators The conditional logical operators shown in Table 2 work the same as the Boolean logical operators with one difference: conditional operators are known to short-circuit. Short circuiting is a performance optimization common to many languages. When one condition can determine the outcome of the entire expression, the remainder of the Operator Name Example & AND if ((age >= 9) & (age <= 9)) System.out.println("Age is 9."); | OR if ((age == 9) | (age == 10)) System.out.println("Age is 9 or 10."); ! NOT if ( !(age == 9) ) System.out.print ln("Age is not 9"); ^ XOR if ((age == 9) ^ (age == 10)) Sys tem.out.println("Age is 9 or 10"); TABLE 1 Operator Name Example && AND if ((age > 0) && (age <= 9)) System.out.println("Age is 9".); || OR if ((age == 9) || (age == 10)) System.out.println( "Age is 9 or 10." ); if (( age >= minimumAge ) && (HasNoCriminalRecord(name)) { ok = true; } else { ok = false; } return ok } In order for a && expression to evaluate to true, both sides must evaluate to true. In the above example, for a person to be eligible for a driver’s license, he or she must be older than 16 and not have a criminal record. For this to occur the left-hand side should be (age >= minimumAge) and the right-hand side – (HasNoCriminalRecord(name)) – must evaluate to true. Since the conditional and && are used, Java can skip the criminal record check if the person’s age is less than 16. Thus, if the expression (age >= minimumAge) evaluates to false, then the overall expression can’t be true and the criminal record check isn’t performed. In this scenario, the method (HasNoCriminalRecord name) isn’t even called, which could result in a significant performance increase. If our expression had been coded with the logical and &, then the criminal record check would always be performed, regardless of the person’s age. if ( age >= minimumAge ) & (HasNoCriminalRecord(name)) { TABLE 2 6 PBDJ volume7 issue2 www.PowerBuilderJournal.com I I S to quickly learn and build Java programs Bitwise Logical Operators –-a; // prefix decrement The third class of operators is the bitwise operators that operate on individual bits in an integral value. Collectively, the individual bits are referred to as a bitfield. Bitfields are an extremely efficient and compact way to use and store properties. Each individual bit in the bitfield could represent a single property that is either on or off. The bold, italic and underline properties of fonts could be represented by a single bit in a bitfield since their values are either on or off. Bits could also be combined to represent a property with more than two values. The alignment property, such as justify, right, left or center for a paragraph, requires two bits in order to represent the four ( (2bits)2 = 4properties ) values. The Java data type byte, whose length is 8 bits, could be used to store all the properties in the example mentioned above. To adjust a property’s setting, its corresponding bit must be turned on or off. If we wanted to change the font to bold, then the first bit needs to be enabled or set to 1. If we wanted to change the paragraph alignment to right justification, then the fifth and sixth bits need to be 1 and 0, respectively. Java also allows the prefix and postfix operators to exist within expressions or statements. Arithmetic Operators Java’s arithmetic operators are standard among today’s programming languages. In Table 3 the Java language defines standard arithmetic operators. In addition, PowerBuilder defines an exponentiation operator, “^”, and Java does not. In Java, exponentiation is performed by the pow() method in the java.lang.Math package. // To calculate 23 result = 2^3 // In PowerBuilder result = java.lang.Math.pow( 2, 3 ); // In Java In Java the expression A-B is always parsed as A minus B. PowerBuilder will treat the expression A-B as either an identifier or an expression. PowerBuilder’s interpretation depends on the value of the DashesInIdentifiers property. If DashesInIdentifiers is set to 1, then A-B is an identifier, and if DashesInIdentifiers is set to 0, then A-B is the same as Java. Both PowerBuilder and Java allow the use of postfix increment and decrement operators. In order to use the postfix decrement operator in PowerBuilder, the DashesInIdentifiers must be turned off. Java also permits the use of the prefix increment and decrement operators. int a; a++; // postfix increment a--; // postfix decrement ++a; // prefix increment // In Java the following is legal. int a; a = 0; System.out.println( "Value of a is " + ++a ); Both PowerBuilder and Java permit the use of the com– pound assignment operator that, in general, is a statement of the form: variable operator= expression. Some examples include the following: int a; a += 1; a *= 2; // Equivalent to a = a + 1; // Equivalent to a = a * 1; Note: The DashesInIdentifiers property is located in the PB.INI and valid values for the property are: • 1 = Allows dashes in identifiers and the expression a– is interpreted as an identifier. • 0 = Disallows dashes in identifiers and the expression a– is interpreted as an expression. Flow Control Statements Java’s flow or control statements have practically been copied from C or C++. If you have any C or C++ experience, these will all look familiar. If your background is using PowerScript, then you’ll have to learn only the new syntax of these statements. The common flow-control statements to both languages include if, for and while. If Statement Table 4 compares Java’s standard if statement to PowerBuilder’s if statement. Operator Meaning Example + Addition result = a + 5; + Concatenation newString = "Hello " + "World"; - Subtraction result = a – 5; * Multiplication result = a * 5; / Division result = a / 5; % Remainder result = a % 5; TABLE 3 PBDJ volume7 issue2 www.PowerBuilderJournal.com 7 Java PowerBuilder if <condition> { <action>; } IF <condition> THEN <action> END IF if <condition> { <action1>; } else { <action2>; } IF <condition> THEN <action1> ELSE < action2> END IF if <condition1> { <action1> } else if <condition2> { <action2>; } IF <condition1> THEN <action1>; ELSEIF <condition2> THEN <action2> END IF if <condition1> <action1>; } else if <condition2> { <action2>; } else { <action3>; } {IF <condition1> THEN <action1> ELSEIF <condition2> THEN <action2> ELSE <action3> END IF choose. Java behaves the same way with one fundamental difference. When the <statementblock> finishes, control is not transferred to the end of the switch statement. Instead, control falls through to the next label and executes its respective <statementblock>. This “fall through” continues until either the end of the switch or a break statement is encountered. If a break statement is encountered, control is transferred to the end of the switch. Switch.java int yearsOfService, numberOfHolidays; numberOfHolidays = 0; yearsOfService = 10; switch (yearsOfService) { case 10: numberOfHolidays += 5; case 5: numberOfHolidays += 5; TABLE 4 Java PowerBuilder case 2: numberOfHolidays += 5; switch <expression> case <item>: <statementblock>; } {CHOOSE CASE <expression> CASE <item> <statementblock> END CHOOSE case 1: numberOfHolidays += 5; break; switch <expression> case <item>: <statementblock>; break; {CHOOSE CASE <expression> CASE <item> <statementblock> CASE ELSE default: <statementblock>; } <statementblock> END CHOOSE TABLE 5 Java PowerBuilder for( <varname>=<start>; FOR <varname>=<start> TO <varname>=<end>; <end> <statementblock> NEXT <varname>++ ) { <statementblock>; } for( <varname>=<start>; <end> <varname>=<end>; <varname> = <varname> + <increment> ) { <statementblock>; } FOR <varname>=<start> TO STEP <increment> <statementblock> NEXT TABLE 6 No magic here! Java defines all standard conditions when dealing with if statements. Remember that <condition> must be boolean. Switch Statement Switches in Java behave slightly different than PowerBuilder’s choose statement (see Table 5). In Java the <expression> must evaluate to a char, byte, int or short. If it doesn’t, then compile-time error will be generated. At runtime, Java’s switch behaves differently than PowerBuilder’s choose statement. When PowerBuilder finds a case <item> that matches the <expression>, it immediately executes the next <statementblock>. After the <statementblock> is executed, control is transferred to the end 8 PBDJ volume7 issue2 default; numberOfHolidays += 5; } In the above example, the number of days allotted for vacation is based on the number of years in service. The example sets the yearsOfService to 10 years and then calculates numberOfHolidays. The switch statement will execute case blocks 10, 5, 2 and 1 since all the blocks, except case 1, don’t have a break statement. When case 1 executes, it transfers control to the end of the switch block once the break is executed. For Statement Table 6 compares Java’s standard for statement to PowerBuilder’s for statement. Java’s for loop includes all the functionality of PowerBuilder’s for loop with a couple of extra features. A standard for loop that outputs integers 1 through 10 is as follows: int j; for( j=1; j<=10; j++ ) { System.out.println( "j = " + j ); } In the above example the starting condition is j = 1. The loop repeats while j <= 10, and every time the loop iterates, j is incremented by 1. Also, there’s one expression to represent the starting condition and one to represent the increment condition. Like C and C++, Java allows multiple starting and increment expressions. In the following example, there are two start conditions and two increment conditions that count two variables from 1 to 10. int j, k; for( j=1, k=1; j<=10; j++, k++ ) { www.PowerBuilderJournal.com System.out.println( "j = " + j + ", k = " + k ); } Notice that the two start expressions are separated by a comma, as are the increment expressions. This technique is commonly used when dealing with data structures that involve “linked lists” to other objects. The following example counts the number of nodes in a linked list. LinkedNodes int nodeCount; ln; for( ln=headNode, nodeCount=1; ln!=null; nodeCount++, ln=ln.getNextNode() ) { } System.out.println( "Number of nodes = " + nodeCount ); Do-While Statement Table 7 compares Java’s standard do-while statement to PowerBuilder’s do-while statement. This loop will always execute the <statementblock> once and will continue to execute <statementblock> while the <condition> is true. int j = 10; do { System.out.println( "T-Minus " + j " + seconds until blastoff!" ); } while ( --j >= 0 ); Java PowerBuilder do { DO <statementblock> <statementblock> } while <condition>; LOOP WHILE <condition> TABLE 7 Java PowerBuilder while <condition> { DO WHILE <condition> <statementblock>; <statementblock> } LOOP Jdj Store Ad www.jdjstore.com TABLE 8 While Statement Table 8 compares Java’s standard while statement to PowerBuilder’s loop while statement. Unlike the do-while loop, the while loop may or may not execute the <statementblock> once depending on the value of <condition>. While <condition> is true the while loop continues to iterate. int j = 11; while ( j-- > 0 ) { System.out.println( "T-Minus " + j " + seconds until blastoff!" ); } Now that we’ve covered the basics, we’ll discuss object orientation in PowerBuilder versus Java in the third and final part of this series. ▼ ABOUT THE AUTHOR Kouros Gorgani is a software engineering manager focusing on Enterprise Application Server and the mobile and embedded computing platform. He is a CPD and the author of several technical books. [email protected] www.PowerBuilderJournal.com PBDJ volume7 issue2 9 D A T A W I N D O W M A G I C Concepts in Graphing Creating a simple graph hen I sit down to write this column I always ask myself, “What can I write that will be the most help to DataWindow programmers?” At the moment I’m also in the final production stages of my new book, The Definitive DataWindow.This morning, while glancing at my chapter on “Graphing,” I remembered that this is an area not well understood by many people. In addition, an awful lot can be said about graphing, maybe enough for a series of articles. W WRITTEN BY RICHARD BROOKS My goal right now is to make sure you’re an expert at creating graphs. I’m going to show you how to create them, how to use computed columns to make the data “fit” into your graph and how to make them interactive. I probably won’t be able to fit it all into just one article – but I’ll try. We’re going to develop an application that holds examples of graphs. Actually, this is my typical test application. I write one for every application I work on. In fact, I’ve done this since I developed my first one decades ago. I have an empty application I call “tester”; it’s sitting in its own folder, empty except for the skeleton. Every time I start a new application, I copy tester.pbl to the appropriate folder and begin to build on it. First we’ll create our tester.pbl, which I’ll explain how to use, then we’ll get to our first simple graph. For our test application we’ll have two windows, one to log in and the other as a jumping-off point for our test windows. The idea is that we create a window for each individual test, then go back to the main window and add the test in there. I’ve created the test application in my own folder under “personal apps” called, quite appropriately, tester.pbl. In fact, the only things in that folder are tester.pbl and connection.ini. That way, when I start an application, I just copy both of those files into my new application folder and I’m ready to go. Figure 1 is the login window and Figure 2 is the main window, both from tester.pbl. When I create a new tester application I have to set some of the properties to customize them. Let’s take a look at the login window. I call mine w_login. There’s one checkbox for the autoCommit, a series of single-line edits for the values, some static texts and a couple of buttons – one for canceling the login and one for executing the connect. 10 PBDJ volume7 issue2 You can find the code for the open event of the window in Listing 1. It simply loads the connection parameters from the connection.ini file into the controls on the login window. The only other code required in this window is in the clicked event for the Connect button. You’ll find it in Listing 2. The next step is the main window. I call mine w_main. You can see from the figure that it just has a listbox, a couple of staticText objects and a button. In this case I’ve named the staticText objects st_help and st_window. The former holds text that will describe the test when the user clicks on the listbox; the latter holds the name of the window so I don’t forget where to go to see the code. The code for the open event is found in Listing 3. The only other code in this window is found in the listbox control. There’s code in the selectionChanged event, in which we’ll change the values in the StaticTexts, and there’s code in the DoubleClicked events, in which we fire off our example window. These are found in Listings 4 and 5, respectively. That’s all there is to the template test application. As I said before, when I start an application I copy the pbl and the connection.ini file into my new application folder, and whenever I need to test something I put it in there. Our First Graph Exercise Before we get started on graphs we need to understand a few terms. When you’re using graphs in a DataWindow, you’ve moved away from the traditional row-and-column metaphor. You’re now dealing with categories and values. What’s worse is that these terms are so flexible, they can be just about anything. I think one of the hardest things about graphs, at least for me, is we’re essentially dealing with the data in a different way. Now we talk about directions and dimensions rather than rows and columns. It gets even worse – we’re often talking about multiple lines or planes. To put it frankly, once you’re dealing with graphs you’re no longer in Kansas, Dorothy. Let’s define our terms. • A category is normally the span of your graph. It’s the opposite of a FIGURE 1 Standard login window www.PowerBuilderJournal.com Ecrane Computing www.ecrane.com www.PowerBuilderJournal.com PBDJ volume7 issue2 11 D A T A W I N D FIGURE 2 Main test application window value. For instance, if you graph sales between a set of dates, the category would be the dates. • A value is the data that you’re graphing. In our case of sales, it would be the sales amount or perhaps the number of sales. • A series is the collection of graphing lines. For example, you might want to compare the sales of one product with another and the series would be the product. With these definitions in mind let’s do our first little graph. In our example we’ve been asked to create a graph that allows a manager to view shipments that occurred between a user-supplied pair of dates. The manager doesn’t care which product is shipped; his only concern is that there have been shipments. First create the DataWindow, a graph with a SQL source. The SQL is shown in Figure 3, but just in case you prefer text, here’s the SQL statement: SELECT "sales_order_items". "quantity", "sales_order_items"."ship_date", "product"."name", "product"."description" FIGURE 4 Specifying category and value 12 PBDJ volume7 issue2 FROM "sales_order_items", "product" WHERE ( "sales_order_items"."prod_id" = "product"."id" ) and ( ( "sales_order_items"."ship_date" between :adate_start and :adate_end ) ) ORDER BY "sales_order_items". "ship_date" ASC O W M A G I C Figure 4 shows the next dialog we get. In this case we aren’t concerned with series. Our manager told us he didn’t care about which products, just all of them grouped together. So the category (the span) is going to be the dates. The value then would be the sum of the shipments for that date. The next dialog is shown in Figure 5. All we need here is a title for our graph and to specify which style it is. Since we’re going to do a line graph, that’s what we select. This dialog is followed by a summary of the options we’ve chosen. Click past that one and you’ll be in a DataWindow painter for your DataWindow. It’s time to put our test program to work. We need a window to hold this DataWindow and a couple of controls to provide us with the dates. Think about it – we’re about to create a window. Should we have a superclass for this (an ancestor)? Do you suppose you’ll ever need a similar window again? I think you might. In fact, since I know what I’ll be writing in the coming months, I can tell you for certain that you will. We should pause right here and create an ancestor window – a new one (or inherit from an appropriate existing FIGURE 3 SQL for your line graph As you can see, we’re selecting from two tables: thesales_order_items and the product. The reason we want the product is so we can display the name and description of the products. We also have two retrieval arguments – the start and end dates. Thus we have quite a simple little DataWindow here. one) – and put in the two edit masks for the start and end dates. Wait a minute! Two edit masks for the start and end dates? Will we ever need that combination of edit masks again? Just how many times have you created them? Okay, just leave that window painter on your screen. We’ll come back FIGURE 5 Selecting the graph style www.PowerBuilderJournal.com Java Con 2000 www.javacon2000.com www.PowerBuilderJournal.com PBDJ volume7 issue2 13 D A T A W I N dw_1.setTransObject(sqlca) In the OK button you need to close only your parent (the parent of the button is the window). The only real code in this window is in the clicked event of the Run button. You can find that in Listing 7. Note: I made the Run button default so the user can enter the dates, then hit Enter to run the report. Close and save this window. I call mine w_super_report_daterange because I like to keep all my ancestor (superclass) windows together in my libraries. Now that our infrastructure is set up, we need to use it, which is the point of W_login::open() sle_serverName.text = profileString("connection.ini", "database", "servername", "") sle_logpass.text = profileString("connection.ini", "database", "logpass", "") sle_logid.text = profileString("connection.ini", "database", "logid", "") sle_dbpass.text = profileString("connection.ini", "database", "dbpass", "") sle_dbparm.text = profileString("connection.ini", "database", "dbparm", "") sle_dbms.text = profileString("connection.ini", "database", PBDJ volume7 issue2 W M A G I C FIGURE 7 Your graph windows Listing 1 14 O this article. You might note that in the future, if you ever have to do another similar task, you can skip most of the preceding steps. Inherit from your superclass window, set the title for the window (mine is “Simple Date Range Graph Test”), then click on the DataWindow and assign it the dataObject of the graph you created earlier. Now close and save your window. I called mine w_product_shipments. That’s all there is to it. No code at all! In the future you can create any date range report you want by simply creating the DataWindow, inheriting from w_super_report_daterange, and setting the title and the dataObject. The final step is to add this to our test application. Open w_main again and add “Product Shipments” to your listbox. Now go to your DoubleClicked event FIGURE 6 Creating your custom “from-to” control to it soon enough. For now, click on New, then the Object tabpage, and finally the Custom Visual (see Figure 6). The next step is to put two static texts on the object for the From and To, and then two edit masks. We need to give the programmer some way to get the values of those edit masks. This means one function for getting the From Date, another for getting the To Date. Those functions are found in Listing 6. We’re finished with this object. Close it and save it as u_dateRange and you’ll be back at the new window you just left, ready to place your object on it. Put your date range object on your window, a couple of buttons for running your report and another for closing the window, then put a DataWindow control on the window to run. You don’t have to set the dataObject property of the DataWindow control yet; we’re still in the ancestor. However, you should set the control name for your date range object to uo_dates (see Figure 7). In the open event you have only one line of code: D for that listbox and add your case statement for this new item. You can find mine in Listing 8. Finally, go to the selectionChanged event for the same listbox and add your case statement for the item again. You’ll find my code for this in Listing 9. This gives us a start, which we’ll enhance in my next column. We’ll clean up a few things in the graph to make it look better, then we’ll make another similar, but more complex, graph. ▼ [email protected] "dbms", "") sle_database.text = profileString("connection.ini", "database", "database", "") if upper(left(profileString("connection.ini", "database", "autocommit", "F"), 1)) = "F" then cbx_autocommit.checked = FALSE else cbx_autocommit.checked = TRUE end if Listing 2 W_login.cb_connect::clicked() if not fileExists("connection.ini") then int li_fileHandle www.PowerBuilderJournal.com Lecco www.leccotech.com www.PowerBuilderJournal.com PBDJ volume7 issue2 15 li_fileHandle = fileOpen("connection.ini", lineMode!, write!, shared!, replace!) if li_fileHandle < 1 then messagebox("Error", "Could not create connection.ini file to store your values as defaults") else fileWrite(li_fileHandle, " ") fileClose(li_fileHandle) end if end if string ls_str if len(sle_servername.text) > 0 then ls_str = sle_servername.text setProfileString("connection.ini", "database", "servername", ls_str) if len(sle_logpass.text) > 0 then ls_str = sle_logpass.text setProfileString("connection.ini", "database", "logpass", ls_str) if len(sle_logid.text) > 0 then ls_str = sle_logid.text setProfileString("connection.ini", "database", "logid",ls_str ) if len(sle_dbpass.text) > 0 then ls_str = sle_dbpass.text setProfileString("connection.ini", "database", "dbpass", ls_str) if len(sle_dbparm.text) > 0 then ls_str = sle_dbparm.text setProfileString("connection.ini", "database", "dbparm", ls_str) if len(sle_dbms.text) > 0 then ls_str = sle_dbms.text setProfileString("connection.ini", "database", "dbms", ls_str) if len(sle_database.text) > 0 then ls_str = sle_database.text setProfileString("connection.ini", "database", "database", ls_str) if cbx_autocommit.checked then setProfileString("connection.ini", "database", "autocommit", "TRUE") else setProfileString("connection.ini", "database", "autocommit", "FALSE") end if Listing 4 W_main.lb_1::doubleClicked(int index) string ls_text ls_text = text(index) // Add your cases below. Then open the appropriate window. choose case ls_text case "" end choose Listing 5 W_main.lb_1.selectionChanged(int index) string ls_text ls_text = text(index) // Add your cases below. In them set st_help.text with a // description of what your test will do. Then set // st_window.text to be the name of the window that will // run your test choose case ls_text case "" end choose Listing 6 U_dateRange.of_From() date ldate_retVal int li_status li_status = em_from.getData(ldate_retVal) return ldate_retVal u_dateRange.of_to() date ldate_retVal int li_status li_status = em_to.getData(ldate_retVal) return ldate_retVal Listing 7 W_super_report_daterange.cb_run::clicked() date ldate_from, ldate_to ldate_from = uo_dates.of_from() ldate_to = uo_dates.of_to() if isNull(ldate_from) or isNull(ldate_to) then messagebox("ERROR", "Please enter a valid date range") end if if len(sle_servername.text) > 0 then sqlca.servername = sle_servername.text IF LEN(sle_dbms.text) > 0 then sqlca.dbms = sle_dbms.text if len(sle_logpass.text) > 0 then sqlca.logpass = sle_logpass.text if len(sle_logid.text) > 0 then sqlca.logid = sle_logid.text if len(sle_dbpass.text) > 0 then sqlca.dbpass = sle_dbpass.text if len(sle_dbparm.text) > 0 then sqlca.dbparm = sle_dbparm.text if len(sle_database.text) > 0 then sqlca.database = sle_database.text sqlca.autocommit = cbx_autocommit.checked dw_1.retrieve(ldate_from, ldate_to) connect using sqlca ; W_main.lb_1.selectionChanged(int index) string ls_text ls_text = text(index) if sqlca.sqlcode <> 0 then messagebox("Connection Error", "[" + string(sqlca.sqldbcode) + "] " + sqlca.sqlerrText) else closeWithReturn(parent, "success") end if Listing 3 W_main::open() open(w_login) string ls_status ls_status = message.stringParm if ls_status = "failure" then close(this) Listing 8 W_main.lb_1::doubleclicked(int index) string ls_text ls_text = text(index) // Add your cases below. Then open the appropriate window. choose case ls_text case "Product Shipments" open(w_product_shipments) end choose Listing 9 // Add your cases below. In them set st_help.text with a // description of what your test will do. Then set // st_window.text to be the name of the window that will // run your test choose case ls_text case "Product Shipments" st_help.text = "Demonstrates a simple date range line graph" st_window.text = "w_product_shipments" end choose ! d the Code a o l n Dow The code listing for this article can also be located at www.PowerBuilderJournal .com 16 PBDJ volume7 issue1 www.PowerBuilderJournal.com Sybase Techwave www.sybase.com/techwave2000 www.PowerBuilderJournal.com PBDJ volume7 issue2 17 D I S T R I B U T E D T E C H N O L O G I E S Handling Aborted Transactions in Jaguar A technique that works with any Jaguar client or component n this article we’ll examine how to handle a Jaguar CORBA_TRANSACTION_ROLLEDBACK exception that is thrown when a Jaguar transaction is aborted. We’ll use the BTFBank sample application developed in an article I wrote for PBDJ in October 1999 (Vol. 6, issue10).This example is written in PowerBuilder, but the technique covered here is relevant for any Jaguar client (Java,Web, etc.) or Jaguar component that’s involved in a transaction. I WRITTEN BY MICHAEL BARLOTTA AUTHOR BIO Michael Barlotta, director of information technologies at AEGIS Consulting, has worked with PowerBuilder since version 3.0 and is a CPD associate.The author of several books, including Jaguar Development with PowerBuilder 7 (Manning Publications), Mike also serves as a representative of AEGIS for the Application Service Provider Industry Consortium. Visit his Web site at www.erols.com/m.barlotta. 18 When developing a Jaguar component that uses Jaguar transactions, a CORBA_TRANSACTION_ROLLEDBACK exception is generated each time a transaction managed by the Jaguar server is aborted and rolled back. A transaction is marked as requiring a ROLLBACK by a PowerBuilder component’s invoking the SetAbort function on the TransactionServer object. This exception is thrown when the method that’s called on a Jaguar component causes the transaction to be rolled back. In a PowerBuilder client this exception is captured by the connection object error event. If the exception isn’t handled, the client application will terminate. As a result of throwing the exception, the method return value and any arguments passed by reference (output parameters) are unusable, making it difficult to handle such situations gracefully. This situation is best illustrated with an example. In our BTFBank example we were able to deposit, withdraw and transfer money from a bank account. In this example a withdrawal of money was canceled if the account didn’t have enough funds or wasn’t found, or if the SQL to update the database failed. In all cases the SetAbort function was invoked to terminate the transaction. However, in the last article we didn’t return a very useful message that would indicate to the client that called the withdrawal why it had failed. The code in Listing 1 modifies the function to accept by reference a string argument in which a message can be returned to be displayed to the user. On the BTFBank client window we can make the following changes to the call to the withdraw method on the account component so the message returned by the method call is displayed with a MessageBox. // Withdraw command button int li_rc int li_acct, li_tellerid decimal ld_amount PBDJ volume7 issue2 FIGURE 1 string CORBA_TRANSACTION_ROLLEDBACK exception ls_msg li_tellerid = Integer(sle_tellerid.text) li_acct = Integer(sle_withdraw_account.text) ld_amount = Real(sle_withdraw_amount.text) li_rc=in_account.withdraw(li_acct, ld_amount, ls_msg) IF li_rc <> 1 THEN MessageBox("BTFBank",ls_msg) END IF After making these changes we can run the example. However, you’ll notice that after attempting to withdraw too much money from an account, you don’t get your error message. Instead, you get the CORBA_TRANSACTION_ROLLEDBACK exception as shown in Figure 1. Handling the Aborted Transaction As a result of throwing the exception, the withdraw method return value and any arguments passed by reference (output parameters) aren’t available, since the values aren’t sent to the client. In order to allow return parameters to be marshaled and accessible by the client, we need to set a component property on the Jaguar server. The www.PowerBuilderJournal.com Xml Dev Con xmldevcon2000.com www.PowerBuilderJournal.com PBDJ volume7 issue2 19 D I S T R I B U T E D T E C H N O L O G I E S and type failed into the Property Value field. A value of “failed” tells the Jaguar server not to throw a CORBA TRANSACTION_ROLLEDBACK exception when the component issues a SetAbort. This allows the component to raise a different exception or pass back values to the client. Once the information is added, click the OK button. The tx_outcome property will be visible in the Component Properties dialog on the All Properties tab. Once the property is added, refresh the package or server through the Jaguar Manager and retest the application. When a withdrawal of too much money is attempted, the transaction is aborted and the error message is displayed on the client, as shown in Figure 4. Conclusion FIGURE 2 Component Properties for the Account component FIGURE 3 New Property dialog property, com.sybase.jaguar.component.tx_outcome, needs to be added to the account component through the Component Properties dialog on the All Properties tab, as shown in Figure 2. The tx_outcome property isn’t listed in the All Properties list, but is available to the component and set to “always” by 20 PBDJ volume7 issue2 A method that results in an aborted transaction will throw the CORBA_ TRANSACTION_ROLLEDBACK exception. To avoid throwing the exception and to access the return value or output parameters on a method that’s part of an aborted transaction, the Jaguar Component property com.sybase.jaguar.component.tx_outcome must be set to failed for each compo- FIGURE 4 BTFBank sample application default, which tells the Jaguar server to throw a CORBA TRANSACTION_ ROLLEDBACK exception when a transaction is rolled back. In order to add the property click on the Add button, which will open the dialog shown in Figure 3, type com.sybase.jaguar.component.tx_ outcome into the Property Name field nent on the Jaguar server using the Jaguar Manager. After setting the property, be sure to refresh the component. The BTFBank sample code used in this article is available on my Web site at www.erols.com/m.barlotta under Code Samples. ▼ [email protected] www.PowerBuilderJournal.com Listing 1 // // // // // // // // // // // END IF Withdrawal Args: integer ai_accountid decimal ad_amount string as_message (BY REF) Returns: 1 for OK -1 for a SQL error -2 for an error (account not found) -3 for an error (insufficient funds) Decimal ld_balance integer li_result // Clear Message as_message = "" SELECT INTO FROM WHERE balance :ld_balance account id = :ai_accountid; IF SQLCA.sqlcode <> 0 THEN // An error occurred or the customer does not exist IF SQLCA.SQLCode = 100 THEN as_message = "Withdraw Failed - Account " + String(ai_accountid)+ " was not found." iel_jag.log(ClassName() + " SetAbort (withdraw - account not found)") li_result = -2 ELSE as_message = "Withdraw Failed due to SQL Error." iel_jag.log(ClassName() + " SetAbort (withdraw - SQL Error)") li_result = -1 iel_jag.log(SQLCA.SQLErrText) its_jag.SetAbort() ELSE IF ad_amount > ld_balance THEN // Insufficient funds as_message = "Withdrawal Canceled due to Insufficient funds." iel_jag.log(ClassName() + " SetAbort (withdraw - insufficient funds)") iel_jag.log(SQLCA.SQLErrText) its_jag.SetAbort() li_result = -3 ELSE UPDATE account SET balance = balance - :ad_amount WHERE id = :ai_accountid; IF SQLCA.sqlcode <> 0 THEN as_message = "Withdraw Failed due to SQL Error." iel_jag.log(ClassName() + " SetAbort (withdraw - SQL Error)") iel_jag.log(SQLCA.SQLErrText) its_jag.SetAbort() li_result = -1 ELSE iel_jag.log(ClassName() + " SetComplete (withdraw)") its_jag.SetComplete() li_result = 1 END IF END IF END IF RETURN li_result ! d the Code a o l n Dow The code listing for this article can also be located at www.PowerBuilderJournal .com Sub Ad Note It All www.sys-con.com www.PowerBuilderJournal.com www.note-it-all.com PBDJ volume7 issue2 21 P B D J WRITTEN BY BRIAN DAVIGNON F E A T U R E The next step in Sybase query tuning O kay, so you’ve used showplan, statistics i/o and statistics time to identify how your query has been optimized and what it’s costing you. That’s a good starting point. Sometimes all the answers are right there in the query plan or resource counts. They’ll point out a table scan, the fact that your query is using a different index than you expected or that most of your I/O relates to one particular table or SQL statement. You know you have an index on the search columns, so why did it choose a tablescan? You’ve done your homework and identified that one index is more selective than another, yet the optimizer decided to access the table with the less selective index. You suspect that one join order is better than another, based on your knowledge of the data and relationships, but it didn’t choose that join order. Unfortunately, these tools won’t help you in these situations because they fail to answer the question “Why?” They only answer the questions “How?” and “How much?” You’ve reached the point in tuning your query where you need to dig into your other toolbox and break out the dbcc traceflags. This article will focus on traceflag 302, although it also includes output from 310. The 302 traceflag provides information on index selection, while the 310 traceflag relates to join order selection. The definitions and commands used for this article are shown: create (a int b int c int d int table abc not null, not null, not null, not null) create index ix1 on abc(b,c) create (a int b int c int d int create index ix3 on xyz(a,b) create index ix2 22 table xyz not null, not null, not null, not null) PBDJ volume7 issue2 on abc(d) (200,000 rows) (1300 rows) dbcc traceon(3604,302,310) set showplan on declare @d int select @d = 2000 select abc.a, abc.b, abc.d, xyz.c, xyz.d from abc, xyz where abc.b=xyz.a and abc.c=xyz.b and abc.d > @d The optimizer’s job is to find the least expensive access method given the tables, indexes and query provided. The following output shows that the optimizer is entering a scoring routine to determine how to access table “abc.” Varno=0 simply means that “abc” is the first table listed in the “from” clause. However “abc” won’t necessarily end up being first in the chosen join order, because Sybase uses a cost-based optimizer. That cost is measured in terms of physical and logical I/O, which is really what this dbcc report is all about. How much did the optimizer estimate that accessing a table in a certain manner would cost? Was it an accurate estimate? The report starts by displaying the table name along with page and row counts. These counts will be very close to actual values, possibly exact if dbcc checktable() has been run recently. They will form the basis for estimating total I/O. DBCC execution completed. If DBCC printed error messages, contact a user with System Administrator (SA) role. www.PowerBuilderJournal.com ******************************* Entering q_score_index() for table 'abc' (objectid 1954626552, varno = 0). The table has 198060 rows and 1579 pages. The optimizer will then score each sarg, or search argument, provided in the query. Here it begins by scoring the “Greater Than” clause, which references column “d” of table “abc”. Scoring the SEARCH CLAUSE: d GT Regardless of what indexes are available, the optimizer will always estimate the cost of doing a tablescan. It may sometimes turn out that a tablescan will actually be faster than using existing indexes. For example, the table may occupy only two data pages, but every index access requires at least three or four pages to be read (root, intermediate, leaf and data). Or your query may be returning a high percentage of the table’s rows, which would certainly be faster using a tablescan. In the next few lines of the report you can see that an estimate is being performed against indid 0, which is always the base table, so this is an estimate of pages that need to be read for a tablescan. The optimizer estimates that 1,579 pages will need to be read, but it also shows that these reads will be done with a 4K I/O size, which is two pages at a time. Thus the actual cost will be 790 reads, not 1,579 reads. Keep that in mind as you read through the output. The optimizer is trying to reduce the number of reads the query must perform, and using large I/O buffer pools can have a substantial impact on the final estimate. For instance, if a large index is bound to a cache that contains a 16K buffer pool, and another index, only half its size, is not, then a query like “select count(*) from abc” will perform fewer reads by scanning the leaf level of the larger index. (The reason is that you can read eight times as many pages into cache with a single read.) Most of your query’s time is spent waiting in the I/O queue, not actually performing the read, so if you can reduce the number of reads, you will reduce the amount of wait time. That’s not to say that you should run out and start adding large I/O buffer pools all over the place; it’s merely that the optimizer will consider things like that when generating its estimates. (As you look over the following, you can ignore the Relop bits, which is just a bitmap value representing the logical operator “>”. ) Base cost: indid: 0 rows: 198060 pages: 1579 prefetch: S I/O size: 4 cacheid: 0 replace: LRU Relop bits are: 11 This next section states that my sarg is using a subquery, expression or local variable. In my case it’s a local variable, but in any case the problem is that the optimizer doesn’t know the value of the sarg until runtime. It can’t be determined at optimization time, even though I clearly initialize it to 2,000 before it gets to that statement. TIP: This problem can easily be rectified by moving the “select” into its own stored procedure, then passing that value into the stored procedure after it has been initialized. Thus the optimizer can “see” those values before the stored procedure is optimized. Unfortunately, that is not how the code was written, so the optimizer can deal only with what’s in front of it. There are two options for estimating the I/O cost in this situation: the magicSC method and the densitySC method. The magicSC scoring method uses default percentages based on the logical operator. Using an equality operator (=), the optimizer estimates that 10% of the rows will be returned. The estimate for a closed-end query (>= and <=, >= and <, > and <=, > and <) will be 25%, and for an open-end query (>, >=, <, <=) an estimate of 33% will be used. These estimates may be considerably far from the truth, but the optimizer has no way of knowing this because I used a local variable. It can only “guesstimate.” www.PowerBuilderJournal.com The densitySC method can be used only if a valid distribution page and density table exist. The density table is located on the distribution page, and contains information on the percentage of duplicates across various combinations of key columns. If an index contains columns a and b, then an entry in the density table will exist for the percentages of duplicates for the combination of columns a and b. This entry can be used to further determine an index’s usefulness when a query contains a sarg for both columns. SARG is a subbed VAR or expr result or local variable (constat = 60)--use magicSC or densitySC Sybase won’t consider using an index unless the search argument includes at least the first column of that index. The only index that qualifies in that regard is indid 3, so it is the only other access method evaluated besides the tablescan. The magicSC scoring was used for this query, since not knowing the value of the local variable rendered the density table useless. The .33 selectivity of the index is a result of the open-end query causing an estimate of 33% of the rows. The index height is 3, meaning that it takes three reads to traverse the index tree, plus a data page read. The optimizer has estimated that it will have to read a total of 65,653 index and data pages and that it will yield 65,360 rows. That’s probably pretty far off, but it wasn’t the optimizer’s fault – it was the coder’s fault for not providing a sarg that the optimizer could use to generate a valid estimate. Estimate: indid 3, selectivity 0.330000, rows 65360 pages 65653 index height 3 This process of evaluating indexes continues for all indexes that could possibly satisfy the sarg, or that could be used to cover the query in the case of a nonmatching index scan. The output in all cases would be similar to the above, but if a valid sarg were provided, the report would contain even more information about how the estimate was made. (We will see this later when we look at the dbcc report of the corrected query.) Once the optimizer has evaluated each index in turn, it will report on which index was deemed to be cheapest, along with other details of the access method. In our example below, it chose the only index that was evaluated, indid 3, and indicates that it will cost 65,653 pages, generate 65,630 rows and use 2K I/O (no prefetch) in the data cache with id=0, which is the default data cache. Cheapest index is index 3, costing 65653 pages and generating 65360 rows per scan, using no data prefetch (size 2) on dcacheid 0 with LRU replacement Search argument selectivity is 0.330000. After all the sargs are evaluated, the optimizer turns to estimating the cost of accessing each table on the join columns. Once those numbers are in hand, there will be enough information to estimate the cost of performing the join in various orders. Note below that it’s estimating the cost of accessing table “abc,” and has indicated which join clause it is performing the estimate on. Once again it will evaluate the cost of doing a tablescan, which is considered the “base cost.” Entering q_score_index() for table 'abc' (objectid 1954626552, varno = 0). The table has 198060 rows and 1579 pages. Scoring the JOIN CLAUSE: b EQ a c EQ b Base cost: indid: 0 rows: 198060 pages: 1579 prefetch: S I/O size: 4 cacheid: 0 replace: LRU Relop bits are: 5 PBDJ volume7 issue2 23 The index selectivity is derived by dividing the estimate of rows by the total rows in the table. (The estimate of rows will be discussed later when we cover the distribution page and density table.) The optimizer has estimated that with an index height of 3, it’ll require 245 index and data page reads to perform a single iteration of the join. This means that if 10 rows qualify in the outer table, and table “abc” is chosen as the inner table, it will require 2,450 reads against table “abc” and indid 2 to complete the join. The join selectivity is determined by multiplying the index selectivity by the total rows, and then dividing total rows by the result (198060 / (198060 * 0.001221)). Estimate: indid 2, selectivity 0.001221, rows 242 pages 245 index height 3 Cheapest index is index 2, costing 245 pages and generating 242 rows per scan, using no data prefetch (size 2) on dcacheid 0 with LRU replacement Join selectivity is 819.187500. Because the join can be performed with either table as the inner table, the optimizer will estimate pages, rows and join selectivity for table “xyz” as well. Based on a row total of 1,309 and an index selectivity of 0.000763, the optimizer has estimated that one row would be returned for each iteration of the join if “xyz” were the inner table chosen in the join. The cost would be two index pages and one data page for each row returned. Note: The join selectivity is a higher number for this table. Entering q_score_index() for table 'xyz' (objectid 1922626438, varno = 1). The table has 1309 rows and 24 pages. a EQ b b EQ c Base cost: indid: 0 rows: 1309 pages: 24 prefetch: S I/O size: 4 cacheid: 0 replace: LRU Relop bits are: 4 Estimate: indid 2, selectivity 0.000763, rows 1 pages 3 index height 2 Cheapest index is index 2, costing 3 pages and generating 1 rows per scan, using no data prefetch (size 2) on dcacheid 0 with LRU replacement Join selectivity is 1310.700000. The fact that the row and page costs are significantly lower for table “xyz” than they were for table “abc” does not guarantee it will be chosen as the inner table. Those numbers refer only to a single iteration of the join. The sargs will determine how many iterations will be performed. If only five rows qualify for table “xyz” based on sarg estimates, and it costs 245 page reads for each iteration of the join against table “abc,” the total cost of joining to table "abc" is only 1,225 pages. If 5,000 rows qualify for table “abc” based on sarg estimates, and it costs one page read for each iteration of the join against table “xyz,” the total cost of joining to table “xyz” is 5,000 pages. The optimizer must consider the cost of accessing each table based on sarg estimates, determine how many rows will be returned and calculate the cost of joining the qualifying rows to the inner table. The combination of these three things will determine the final cost of processing the query. You can imagine how much the optimizer must consider when there are multiple sargs and several tables involved in joins. The output generated by these traceflags can be enormous, which is why I used a simple example so that we could focus on the individual components. The 310 traceflag output below will have to be covered in detail another day, but for now I wanted to point out what type of information is contained in it. “Query Is [Not] Connected” indicates whether or not the proper number of join clauses have been supplied to avoid a Cartesian product. The output shows that two join orders were evaluated, “0-1” and “1-0”, indi- 24 PBDJ volume7 issue2 cating the varno (table) order. The lp and pp values are logical and physical I/O estimates based on the estimated rows returned for the sarg and the join selectivity for the join columns on the inner table. The report shows a second NEW PLAN cost, based on doing a tablescan on table “abc” instead of using index ix2, but both estimates are based on the same join order, with table “abc” as the outer table. The total cost is in milliseconds, and is based on reads necessary to access all qualifying rows in the outer table, reads necessary to perform the join, and includes both data and index page reads. “Total Permutations” indicates how many join orders were considered, and “Total Plans” includes all table access methods and join orders considered. “ Once the optimizer has evaluated each index in turn, it will report on which index was deemed to be cheapest, along with other details of the access method ” QUERY IS CONNECTED 0 - 1 NEW PLAN (total cost = 552319): varno=0 (abc) indexid=3 (ix2) path=0xe44f0128 pathtype=sclause method=NESTED ITERATION outerrows=1 rows=65360 joinsel=1.000000 cpages=65653 prefetch=N iosize=2 replace=LRU lp=65653 pp=1579 corder=4 varno=1 (xyz) indexid=2 (ix3) path=0xe44f05d0 pathtype=join method=NESTED ITERATION outerrows=65360 rows=65275 joinsel=1310.700000 cpages=3 prefetch=N iosize=2 replace=LRU lp=196079 pp=24 corder=1 jnvar=0 refcost=0 refpages=0 reftotpages=0 ordercol[0]=1 ordercol[1]=2 NEW PLAN (total cost = 413513): varno=0 (abc) indexid=0 () path=0xe44f0128 pathtype=sclause method=NESTED ITERATION outerrows=1 rows=65360 joinsel=1.000000 cpages=1579 prefetch=S iosize=4 replace=LRU lp=1579 pp=1579 corder=0 varno=1 (xyz) indexid=2 (ix3) path=0xe44f05d0 pathtype=join method=NESTED ITERATION outerrows=65360 rows=65275 joinsel=1310.700000 cpages=3 prefetch=N iosize=2 replace=LRU lp=196079 pp=24 corder=1 jnvar=0 refcost=0 refpages=0 reftotpages=0 ordercol[0]=1 ordercol[1]=2 1 - 0 TOTAL # PERMUTATIONS: 2 TOTAL # PLANS CONSIDERED: 7 CACHE USED BY THIS PLAN: CacheID = 0: (2K) 24 (4K) 1579 (8K) 0 (16K) 0 The “Final Plan” is the plan that will be executed. It details the join order, page and row estimates, and the tablescan or index used for each table’s access. It’s followed by the showplan output, which indicates a tablescan was chosen for accessing table “abc” first, followed by a join to table “xyz” using index 3. www.PowerBuilderJournal.com FINAL PLAN (total cost = 424170): varno=0 (abc) indexid=0 () path=0xe44f0128 pathtype=sclause method=NESTED ITERATION outerrows=1 rows=65360 joinsel=1.000000 cpages=1579 prefetch=S iosize=4 replace=LRU lp=1579 pp=1579 corder=0 varno=1 (xyz) indexid=2 (ix3) path=0xe44f05d0 pathtype=join method=NESTED ITERATION outerrows=65360 rows=65275 joinsel=1310.700000 cpages=3 prefetch=N iosize=2 replace=LRU lp=196079 pp=24 corder=1 jnvar=0 refcost=0 refpages=0 reftotpages=0 ordercol[0]=1 ordercol[1]=2 QUERY PLAN FOR STATEMENT 1 (at line 1). STEP 1 The type of query is DECLARE. QUERY PLAN FOR STATEMENT 2 (at line 2). STEP 1 The type of query is SELECT. QUERY PLAN FOR STATEMENT 3 (at line 3). STEP 1 The type of query is SELECT. FROM TABLE abc Nested iteration. Table Scan. Ascending scan. Positioning at start of table. Using I/O Size 4 Kbytes. With LRU Buffer Replacement Strategy. FROM TABLE xyz Nested iteration. Index : ix3 Ascending scan. Positioning by key. Keys are: a b Using I/O Size 2 Kbytes. With LRU Buffer Replacement Strategy. The corrected query is shown below, followed by some new information that’s now found in the dbcc output. This information shows that index ix2, with indid=3, is now being evaluated based on statistics that exist on the distribution page for that index, instead of using default percentages based on the logical operator. In the real world I would have broken the query into a separate proc, but the use of a constant in place of a local variable will suffice for demonstration purposes. select abc.a, abc.b, abc.d, abc.e, xyz.c, xyz.g from abc, xyz where abc.b=xyz.a and abc.c=xyz.b and abc.d > 2000 The excerpt below looks similar to the estimate of the cost of a tablescan that we’ve examined already. However, in this case, since a valid sarg was provided, the optimizer is now able to more accurately estimate the number of pages and rows that match the sarg. This is something that we haven’t seen yet. Entering q_score_index() for table 'abc' (objectid 1954626552, varno = 0). The table has 198060 rows and 1579 pages. Scoring the SEARCH CLAUSE: d GT www.PowerBuilderJournal.com Base cost: indid: 0 rows: 198060 pages: 1579 prefetch: S Relop bits are: 11 A distribution page (“stat page”) is the place where Sybase stores index key value samples. The optimizer will try to use those sample values to estimate how many rows will be returned. The output first shows us that a qualifying distribution page was found. Its page number, 619,713, is located in the column “distribution” of the sysindexes table. (The word “qualifying” should be taken lightly, since the information contained on the distribution page will be accurate only if the index was rebuilt or Update Statistics was run recently.) A step is merely a sample interval. The number of steps that can fit on a 2K distribution page is determined by the size of the index key. The output indicates that this index was able to store 334 steps on its distribution page. Qualifying stat page; pgno: 619713 steps: 334 Search value: 2000 Let’s talk about steps a little bit before we move on. Smaller index keys result in more steps, and therefore an estimate involving them will generally be more accurate. If a table has 100,000 rows and the distribution ‘‘ Depending on the logical operator used, the optimizer should therefore be no more than 2,000 rows off on its estimate ” page can hold 100 steps, then every 1,000th key value will be stored, including the first and last key values. Depending on the logical operator used, the optimizer should therefore be no more than 2,000 rows off on its estimate. If a smaller key is defined so that the same table’s distribution page can hold 1,000 steps, then every 100th key value will be stored and the optimizer’s estimate should be no more than 200 rows off. For instance, if a query searches for a value of 5,000, and the optimizer finds sequential steps contain values of 4,500 and 5,200, it knows that all rows with a key value of 5,000 are contained within this one step range. If it also knows that it has stored every 1000th key value as a step, then it can estimate that it will return no more than 999 rows. The reason it’s 999 and not 1,000 is that if row 1,000 contains key value 4,500 and row 2,000 contains key value 5,200, there could be 999 values in between. Again, this is only an estimate, and the estimate is only as good as the number of rows divided by the number of steps (roughly), and also depends on how recently these statistics were updated. In this example there are 198,060 rows and 334 steps, so roughly every 592nd key value will be stored as a step. Below we can see that an exact match was found on the distribution page. In fact, the search value was found on two steps. In this case the optimizer used the midseveralSC scoring method. In simple terms this means that the optimizer takes into consideration that there may be more rows with the target value prior to the first step where it was found, PBDJ volume7 issue2 25 and there may also be more rows with the target value after the last step where it was found. Based on that information, it has estimated 1,480 rows will be returned at a cost of 1,490 index and data pages. It estimates that this index is the cheapest. However, since the estimate is more accurate this time, it’s not only the cheapest index, but it’s also less expensive than the tablescan. Therefore, index 3 will be chosen as the access method for table “abc” instead of a tablescan. Match found on statistics page equal to 2 rows on the statistics page in middle of page--use midseveralSC Estimate: indid 3, selectivity 0.007473, rows 1480 pages 1490 index height 3 Cheapest index is index 3, costing 1490 pages and generating 1480 rows per scan, using no data prefetch (size 2) on dcacheid 0 with LRU replacement Search argument selectivity is 0.007473. I should also mention that prior to ASE 11.9, the steps contain values only for the leading columns of the index. If you have a composite key made up of two columns and your sarg references both columns, the optimizer can use the density table mentioned earlier to more accurately estimate resulting rows. Remember that the density table contains a percentage of duplicates found in combinations of composite key columns. If the optimizer determines that 500 rows fall between two steps for your search argument, and it also knows that the composite key contains 10% duplicates, it can estimate 50 rows instead of 500. Of course this estimate could be off as well, because it’s possible that all of your duplicates for this index fall outside the values contained in these steps. I’ve omitted most of the remaining output since it was fairly redundant, but you can see below the new Final Plan, along with the more English-like showplan. This plan was chosen because the total cost is 30,936, compared with the previous plan’s total cost of 424,170, so we can expect a significant savings on response time as well. This cost estimate is milliseconds, not reads, but is based on 18ms physical and 2ms logical I/O estimates. Whether those times are valid is trivial, since those same weights are used when evaluating each access method. FINAL PLAN (total cost = 30936): varno=0 (abc) indexid=3 (ix2) path=0xe38cb928 pathtype=sclause method=NESTED ITERATION outerrows=1 rows=1480 joinsel=1.000000 cpages=1490 prefetch=N iosize=2 replace=LRU lp=1490 pp=1046 corder=4 varno=1 (xyz) indexid=2 (ix3) path=0xe38cbdd0 pathtype=join method=NESTED ITERATION outerrows=1480 rows=1473 joinsel=1310.700000 cpages=3 prefetch=N iosize=2 replace=LRU lp=4438 pp=14 corder=1 jnvar=0 refcost=0 refpages=0 reftotpages=0 ordercol[0]=1 ordercol[1]=2 QUERY PLAN FOR STATEMENT 1 (at line 1). STEP 1 The type of query is SELECT. FROM TABLE abc Nested iteration. Index : ix2 Ascending scan. Positioning by key. Keys are: d Using I/O Size 2 Kbytes. With LRU Buffer Replacement Strategy. FROM TABLE 26 PBDJ volume7 issue2 xyz Nested iteration. Index : ix3 Ascending scan. Positioning by key. Keys are: a b Using I/O Size 2 Kbytes. With LRU Buffer Replacement Strategy. There is a wealth of information that can be derived from the traceflags, too much to cover in this space. Here are a few of the things that the traceflags can help point out, along with some possible reasons for their occurrence. • Optimizer did not find a qualifying stat (distribution) page. 1. Is there no valid index for the sarg? 2. Are your index statistics missing? 3. Was a large temp table not indexed? • An erroneous estimate was made for an index. 1. Large number of rows causing huge range of keys between steps? 2. Large key causing too few steps to be stored? 3. Too many columns or too large a composite key size? (This can produce a large density table, which reduces amount of step information that can be stored on distribution page.) 4. Erratic key distribution in your table (e.g., five rows for one key value, 100,000 for another key value)? • A subquery, expression or local variable was used as a sarg. 1. As in our example, optimizer must rely on default percentages. ‘‘ Effective use of the traceflags requires that you have a firm grasp of index and table design ” I hope this introduction has motivated you to add the traceflags to your tuning toolbox. As with any tool, they are only as good as their user. Using state-of-the-art automotive diagnostic tools won’t turn you into a mechanic, but if you’re already an accomplished mechanic, having better tools can certainly make you a more successful one and save you time and energy. Effective use of the traceflags requires that you have a firm grasp of index and table design and a basic understanding of distribution pages and density tables, as well as knowledge and experience in various tuning methods. It won’t always be as easy as adding an index, updating statistics or removing a local variable. But I think with these tools you will find it easier to get to the root of the problem, and that’s the first step in solving it. ▼ [email protected] www.PowerBuilderJournal.com F R O M S Y B A S E Extending PowerBuilder Applications to the Web Use application server technology to make e-business a reality ver the past year many companies have been leveraging their PowerBuilder skills and investments to take their businesses to the Web. Sybase’s Enterprise Application Server (EAServer) has enabled these companies to quickly and easily make e-business a reality. O WRITTEN BY ROB VEITCH Blue Cross and Blue Shield of Rhode Island (BCBSRI) is one customer who’s leveraged PowerBuilder and EAServer to develop an intranet solution. This application has provided BCBSRI with tremendous cost savings and enhanced performance. Using Data to Everyone’s Advantage BCBSRI is the state’s largest and only locally based health plan. Founded in 1939, BCBSRI provides health coverage for one out of every two Rhode Islanders. With more than 1,500 employees, it’s also one of the 20 largest employers in the area. Customer-Driven Solutions AUTHOR BIO Rob Veitch is the director of business development at Sybase Internet Applications Division. BCBSRI is driven by the needs of its customers. Toward this end, the company relies on its customer service system to monitor and record communication with its policyholders. BCBSRI’s mission is to be the local leader in coordinating and financing health-related benefits and providing quality health care and service in Rhode Island. www.PowerBuilderJournal.com To help achieve the company’s mission, BCBSRI’s IT department was tasked with replacing its existing customer service system, which was proprietary and non-Y2K compliant. In an effort to serve its customers better and improve operational efficiency, the company decided to take its existing customer service application and deploy it on a three-tier architecture. Moving to Three Tiers BCBSRI originally explored the possibility of implementing an off-the-shelf CRM application but soon realized that this option would cost in excess of $1 million. Moreover, by integrating the new application, BCBSRI would have incurred additional costs and resources. The company’s IT department convinced management that the best approach would be to use the company’s existing PowerBuilder skill sets and investments and rebuild the customer service system instead. Built with Sybase’s EAServer and PowerBuilder, the newly developed intranet application allows a variety of lightweight clients to use business logic that sits in the middle tier on EAServer. The company chose EAServer and PowerBuilder because of their rapid development capabilities, scalability and tight integration across BCBSRI’s infrastructure. EAServer combines the capabilities of a component transaction server and a dynamic Web page server to provide a single point of integration for heterogeneous back-office systems. This has allowed BCBSRI to extend its existing PowerBuilder-based system and data safely and easily to a three-tier environment. With EAServer, BCBSRI could integrate its legacy and new-customer service data with the company’s existing logic, which was located on an IBM mainframe. EAServer now acts as the focal point for access to several IBM legacy applications within the company, including IMS and DB2. Significant Savings By using its existing resources and skill sets, BCBSRI was able to implement a customer service system with a cost savings of more than $500,000. Because the new system borrowed from the previous system’s user interface, the company also saved time and money by not having to retrain users on a new application. The new EAServer-based system supports BCBSRI’s 120-user customer service department, managing 1,800 calls per day and generating approximately 20,000 transactions – a significant performance gain over the previous customer service solution. Application server technology is relatively new to the IT market, and organizations are still trying to grasp its true purpose in the enterprise. Those who can recognize the value of integrating existing data and applications stand to gain significant competitive advantage as they move to an e-business model. As an innovative e-business implementation, BCBSRI’s newly enhanced customer service system offers a clear example of how application server technology can be used to an organization’s fullest advantage – with a beneficial impact on customers as well. Are you considering the advantages of an application server in designing your business’s Web architecture? More and more PowerBuilder shops are discovering that EAServer is uniquely suited for deploying business logic via PowerBuilder NVOs in intranet- and Internet-based applications, as well as JavaBeans, COM and CORBA objects created by other development teams. You can find more success stories at www.sybase.com/success. Do you have an EAServer success story you’d like to share with us? If so, send an e-mail to [email protected]. ▼ [email protected] PBDJ volume7 issue2 27 P F C C O R N E R Debugging DataWindows – Live! Useful PFC extensions ou stumble on a bug in your application. In order to troubleshoot it you need to see the raw data inside your DataWindow but the columns you need aren’t visible. ••• You open a window in your application, and even though you don’t change any of its data it still prompts you to save when closing. What has changed? ••• A drop-down DataWindow isn’t displaying the right values at runtime, but it retrieves just fine when you preview it at design time.Where’s the data? ••• You’re pulling your hair out trying to get some modify/describe syntax to work, and it’s taking forever between changing your code, saving and rerunning your application. Can you possibly get the syntax right and still get home on time? Y WRITTEN BY VINCE FABRO The solution to all of these problems can be found in the PFC DataWindow properties dialog – after we make some enhancements, that is. The DataWindow Properties Dialog The PFC originally included a DataWindow debug window, which I found very helpful. With the major changes that came in PFC 6.0, the debug window was replaced by the DataWindow properties dialog, which both added and removed functionality. (I needn’t mention PFC 7.0 because of the paucity of changes.) One thing I like about the PFC 6.0 dialog is that it’s easy to add new debugging capabilities simply by extending existing tabs and adding new tabs. In this article I’ll describe a useful enhancement to some existing DataWindow properties tabs, which essentially restores a PFC 5.0 feature that was removed in PFC 6.0. In addition, we’ll add two new DataWindow properties tabs: a drop-down DataWindow tab and a Modify/Describe tab. But first, if you aren’t familiar with the DataWindow properties dialog, then obviously you don’t know what you’re missing! In order to access the dialog you must enable the “property” service. You can do this either by calling of_SetProper- FIGURE 1 The DataWindow popup menu 28 PBDJ volume7 issue2 ty or of_SetSharedProperty. I call of_SetSharedProperty because with one call I instantiate a shared service (referenced with the shared variable snv_Property), which makes the service accessible to all DataWindow controls in the entire application. (The function of_SetProperty, on the other hand, instantiates the same service but stores the reference in an instance variable so that it’s accessible only to the current DataWindow control.) After the service is instantiated at runtime, you can right-click on your DataWindow control and the “DataWindow Properties…” option will be visible in the popup menu (see Figure 1). Selecting the “DataWindow Properties…” option will open the DataWindow Properties dialog and its Services tab page, as in Figure 2. (Note: These figures show the DataWindow properties dialog after making the enhancements in this article.) I rarely use the Services tab, but I frequently use the “Buffers” and “StatusFlags” tabs (see Figures 3 and 4, respectively). Try them out. They’re both pretty self-explanatory, and there’s even on-line PFC help! FIGURE 3 The DataWindow Properties dialog buffers tab page FIGURE 2 The DataWindow Properties dialog services tab page FIGURE 4 The DataWindow Properties dialog StatusFlags tab page www.PowerBuilderJournal.com FIGURE 5 The u_dw KeyDown Event Now that I’ve finished explaining the “standard” way to open the DataWindow properties dialog, I can wink and nod and tell you how I really open it. The problem I have with calling of_SetSharedProperty or even of_SetProperty is that they both enable a menu option in a popup menu that users have access to. I don’t want my users saying, “I wonder what that ‘DataWindow Properties’ thingy does?” So I coded a secret key sequence that opens the DataWindow properties dialog. To maintain secrecy, I’ll include a simplified version here. Open u_dw and insert a new event called “KeyDown,” mapped to the pbm_dwnkey event ID (see Figure 5). Then add the following code to that event: // Event KeyDown: // If you press Shift-Ctrl-Alt-D, the //DataWindow property window will // display if Key = KeyD! and keyflags = 3 and & KeyDown (KeyAlt!) then // Turn on the property service for // all DWs. this.of_SetSharedProperty // (true) this.Event pfc_Properties () end if // End if the User hit the // right key combo. return 0 With this code in u_dw you won’t have to write any additional code to access the DataWindow properties dialog, and your users won’t have easy access to it. All you have to do is set focus on your DataWindow control, hit Shift-Ctrl-Alt-D and the DataWindow properties dialog will open! Now that we’re all at least vaguely familiar with the DataWindow properties dialog, let’s add some cool debugging capabilities. quently was the ability to view all of the raw data in any DataWindow. Notice in see Figures 3 and 4 that there are radio buttons labeled “Original” and “All Columns” – those options aren’t available in the base PFC. These figures show the original view, which uses the same DataWindow object that your user sees. Notice in Figures 6 and 7, however, that every column’s data is visible and editable. That’s what allows me to peer inside any DataWindow’s data, and it’s all at runtime! Let’s start by extending the Buffers tab. The GUI is quite simple, but the code is fairly involved. 1. Open u_tabpg_dwproperty_buffers. 2. Add two radio buttons(rb_original and rb_all) at the bottom of the DataWidow. 3. Set the radio buttons’ text and default rb_original to checked. 4. Shrink the DataWindows to make room for the radio buttons. 5. Place an invisible group box around the radio buttons. (This step is optional, but it’s good technique.) 6. Write the following code for rb_original. parent.Event pfc_PropertyInitialize (inv_attrib) 7. Write the code for rb_all (see Listing 1). 8. Save and close the tab page. (Note: I had to regenerate pfc_w_dwproperty and w_dwproperty because otherwise I got a runtime error.) Extending the Buffers and StatusFlags Tabs The steps are the same for the StatusFlags tab, but the code for rb_all is slightly different. One difference is that this tab page has only one DataWindow (dw_requestorview), and the second difference is that this tab page has no radio buttons for selecting which buffer to display. Basically, the code for rb_all is identical to what is on the Buffers tab, except that the following two sections need to be removed. 1. The code that refers to dw_requestorduplicate: Some useful features were lost when the PFC 5.0 DataWindow debugger became the PFC 6.0 DataWindow properties dialog. One that I used quite fre- dw_requestorduplicate.Create(ls_dw_sy ntax,ls_error) if len(ls_error) > 0 then www.PowerBuilderJournal.com FIGURE 6 The “All Columns” View on the Buffers Tab FIGURE 7 The “All Columns” View on the StatusFlags Tab MessageBox("Create:",ls_error) return end if idw_Requestor.ShareData(dw_requestorv iew) 2. The code that refers to the buffer radio buttons: if rb_primary.checked then Parent.Event pfc_PropertyBufferChanged(Primary!) elseif rb_deleted.checked then Parent.Event pfc_PropertyBufferChanged(Delete!) else Parent.Event pfc_PropertyBufferChanged(Filter!) end if Finally, some limitations to this implementation of the “All Columns” radio button are worth noting. 1. The code for rb_all assumes that the DataWindow uses SQLCA as its trans- PBDJ volume7 issue2 29 P action object, which may not be the case. If needed, you can extend u_dw and add an of_GetTransObject. 2. As noted in the code, the section of code that replaces retrieval arguments doesn’t work with all DBMSs. Different DBMS vendors have different formats for some data types, and datetime values are what I’ve had problems with in the past. In my own framework (the NewMedia PFC) I use different formats depending on the DBMS. 3. Even though the Buffers tab displays data from the Filter! and Delete! buffers, editing that data won’t actually change data in the main DataWindow. 4. The “Original” and “All Columns”don’t view display drop-down DataWindows. Regardless of these limitations, the Buffers and StatusFlags tabs are my most frequent reason for using the DataWindow properties dialog. The ability to see the raw data inside a DataWindow at runtime is awesome! FIGURE 8 The drop-down DataWindow tab page The Drop-Down DataWindow Tab I can safely say that half of the DataWindows in my applications include dropdown DataWindows. That’s probably pretty common. For the most part, using dropdown DataWindows is quite straightforward. But we spend very little time debugging straightforward things. The reason I created a drop-down DataWindow properties tab is because there are many occasions in my applications where drop- 30 PBDJ volume7 issue1 F C downs are filtered for one reason or another. Unfortunately, there have also been occasions where those filtered dropdowns don’t display the expected values. The drop-down DataWindow tab in Figure 8 permits me to see inside my dropdowns. The tab page’s GUI is not too difficult. It’s based on the Buffers tab page, which is the second tab in the DataWindow Properties dialog. The master DataWindow lists all the drop-downs in the DataWindow being debugged. The detail DataWindow displays the contents of the selected drop-down DataWindow. Then there are radio buttons for selecting which buffer to display, and buttons for sorting and filtering. Because it’s based on the Buffers tab, you can start by saving the Buffers tab under a different name. Open pfc_u_tabpg_dw-property_buffers in PFCUTIL.PBL and save it as u_tabpg_dwproperty_childdws in PFEUTIL.PBL. (Actually, mine lives in an extension layer between the PFC and the PFE.) Then make the following changes to the GUI: 1. Change the GUI by deleting the extraneous controls in the group box. 2. Add the “Child DataWindow:” text above the detail DataWindow. 3. Resize and rearrange the controls to make room for the master DataWindow (note that there are actually two detail DataWindows – one is for displaying the Primary! buffer, and the other is for the Filter! and Delete! buffers.). 4. Add the “Drop-down DataWindow Columns:” text at the top of the tab page. 5. Add the master DataWindow (dw_children, based on u_dw). 6. Create an external grid DataWindow object named d_dwproperty_childdws that has two char(40) columns (“colname” and “dataobject”). 7. Set the DataObject property of dw_children to d_dwproperty_childdws. 8. Delete the Undelete button. And now for the code. Declare an instance variable of type dwBuffer called idwb_CurrentBuffer. This will keep track of the selected buffer (Primary!, Filter! or Delete!). Insert a new function called of_LoadChild, which takes a row (al_row) as its argument. This function will load the selected child DataWindow object into the detail DataWindows and refresh the view (see Listing 2). The tab page will already have code in the following events: pfc_PropertyBufferChanged, pfc_PropertyInitialize, pfc_PropertyOpen, pfc_PropertyStats and pfc_PropertyUndelete. You can delete the code from C O R N E R the pfc_PropertyInitialize and pfc_PropertyUndelete events. Then make the following changes: 1. Change pfc_PropertyBufferChanged by deleting all references to cb_undelete. 2. Change pfc_PropertyStats to the code in Listing 3). 3. Use the script in Listing 4 for pfc_PropertyPopulate. 4. Add the following code to the master DataWindow’s RowFocusChanged event: // Event RowFocusChanged: // Load the newly selected child DataWindow. return parent.of_LoadChild (currentrow) 5. Change the code for dw_requestorduplicate by deleting all scripts except for the constructor event and set the tab page’s Text property to “DDDWs” on the General tab. And that’s it for the tab page! But in order to use it you’ll have to add it to the DataWindow properties tab control. Close and save the tab page, then open u_tab_dwproperties in PFEUTIL.PBL. Right-click near the top of the tab and select “Insert User Object” (see Figure 9). Select u_tabpg_ dwproperties_childdws and click OK. On the General tab of the Properties pane, name the tab page “tabpage_ dddws.” Close and save the tab control. Done! Now you can run your application and test it out. (Note: Once again I had to regenerate pfc_w_dwproperty and w_dwproperty. Otherwise I got a runtime error.) Don’t forget to set focus on your DataWindow and type the Shift-CtrlAlt-D shortcut to display the DataWindow Properties window. FIGURE 9 Adding the DDDWs tab page www.PowerBuilderJournal.com FIGURE 10 The DataWindow Properties modify tab The Modify/Describe Tab The power of Modify and Describe never ceases to amaze me. But occasionally I have a lot of trouble getting the syntax to work as I’d like. (And I suspect I’m not alone in that experience!) In these situations the PowerBuilder debugger is rarely helpful, and the DataWindow painter isn’t always helpful either. Then one day a light went on and I realized how easy it would be to build a Modi- fy/Describe debugging tab. It took very little time to build, as you’ll see, and it can be quite helpful. Create a new tab page by inheriting from u_tabpg_dwproperty_base in PFEUTIL.PBL. The most difficult part about constructing this tab page is the GUI (see Figure 10). Make it the same size as the other tabs (1198x1200). Add some text controls, a couple of multiline edits (mle_syntax and mle_result), few buttons and an edit mask (em_position). Don’t forget to inherit from base PFC controls! Finally, set the Text property on the tab page’s General tab to “Modify.” The code couldn’t get any easier. Here’s the code for the Modify button: (em_position.text), 0) mle_syntax.SetFocus () mle_result.text = idw_requestor.Modify ( & mle_syntax.text) Probably the biggest benefit from using a framework such as the PFC is productivity gains from code reuse. But why shouldn’t your framework also help you debug and troubleshoot problems? So I hope you find these enhancements to the DataWindow properties dialog a useful addition to your PFC. And to take PFC debugging one step further, I’d suggest that you check into the debug service and SQL Spy. (How about Shift-Ctrl-Alt-S?) ▼ Here’s the code for the Describe button: mle_result.text = idw_requestor.Describe ( & mle_syntax.text) And here’s the code for the Go to button: mle_syntax.SelectText (Integer Listing 1 // Clicked for rb_all// Clicked for rb_original. int li_Arg, li_NumArgs, li_Pos, li_Col, li_Cols string ls_sql_syntax, ls_dw_syntax, ls_error, ls_Arg string ls_ArgNames[], ls_ArgTypes[], ls_TmpSyntax u_dwldw_remote n_cst_string lnv_string if IsNull(idw_Requestor) then return if not IsValid(idw_Requestor) then return SetPointer(hourglass!) ls_sql_syntax = idw_Requestor.Describe( & 'DataWindow.Table.Select') // Make sure we have a database connection. if SQLCA.DBHandle () = 0 then MessageBox ("All Columns:", & "Transaction not connected!", StopSign!) rb_original.checked = true return end if // Make sure we have a valid SQL statement. if Pos (ls_sql_syntax, "PBSELECT") > 0 then idw_Requestor.SetTransObject (SQLCA) ls_sql_syntax = idw_Requestor.Describe( & 'DataWindow.Table.Select') end if // Replace any args so that we don't get prompted. ldw_remote = idw_Requestor ldw_remote.of_SetBase (true) ldw_remote.inv_base.of_dwArguments (ls_ArgNames, & ls_ArgTypes) www.PowerBuilderJournal.com The purpose of the Go to button is to help in those situations when you get an error message from your Modify/Describe call such as “Line 1 Column 14: incorrect syntax.” The line and column numbers are sometimes useful, but a more advanced “Go to” that accepted a line and column number would be more useful. Save the tab page as u_tabpg_dwproperties_modify, close the painter and add it to u_tab_dwproperties just as before. (Don’t forget to regenerate pfc_w_dwproperty and w_dwproperty!) Closing Thoughts [email protected] ABOUT THE AUTHOR Vince Fabro is a practice leader at the Columbus branch of NewMedia, a consulting firm headquartered in Cleveland, Ohio. He’s a CPD professional and certified PowerBuilder instructor who has been using PowerBuilder since version 2. // Loop through the arguments. li_NumArgs = UpperBound (ls_ArgNames) ls_sql_syntax = lnv_string.of_GlobalReplace ( & ls_sql_syntax, "~~~"", "~"") for li_Arg = 1 to li_NumArgs // Copy the SQL so we can search it in lower case. ls_TmpSyntax = Lower (ls_sql_syntax) li_Pos = Pos (ls_TmpSyntax, ":" + ls_ArgNames[li_Arg]) if li_Pos > 0 then choose case ls_ArgTypes[li_Arg] // CHARACTER DATATYPE CASE "string" ls_Arg = "''" // Empty string. // DATE DATATYPE CASE "date" ls_Arg = "'1900-01-01'" // Empty date. // DATETIME DATATYPE CASE "datetime" // NOTE: This is a simplification, // and won't work with all DBMSs. ls_Arg = "'1900-01-01'" // Numeric datatypes CASE "decimal", "number", "long", "ulong" ls_Arg = "0" // Default to zero. // TIME DATATYPE CASE "time", "times" ls_Arg = "'12:00:00'" // Default to noon. end choose // Change the SQL syntax. ls_sql_syntax = & PBDJ volume7 issue2 31 Left (ls_sql_syntax, li_Pos - 1) + & ls_arg + & Mid (ls_sql_syntax, li_Pos + & Len (ls_ArgNames[li_Arg]) + 1) end if next // Get the DataWindow syntax for this ls_dw_syntax = SQLCA.SyntaxFromSQL( & ls_sql_syntax, "Style(Type= Grid )",ls_error) if Len(ls_error) > 0 then MessageBox("SyntaxFromSQL:",ls_error) return end if // Put this DataWindow in the two DW controls. dw_requestorview.ShareDataOff() dw_requestorview.Create(ls_dw_syntax,ls_error) if len(ls_error) > 0 then MessageBox("Create:",ls_error) return end if dw_requestorduplicate.Create(ls_dw_syntax,ls_error) if len(ls_error) > 0 then MessageBox("Create:",ls_error) return end if idw_Requestor.ShareData(dw_requestorview) li_rc = ldwc_temp.ShareData (dw_requestorview) this.Event pfc_PropertyBufferChanged( & idwb_CurrentBuffer) // Update the onscreen stats. this.Event pfc_PropertyStats() return li_rc Listing 3 // Se// Event pfc_PropertyStats: t the text for the radio buttons to keep // track of row counts. if dw_requestorview.DataObject = "" then return 0 rb_primary.Text = & string(dw_requestorview.RowCount()) + & " &Primary" rb_filtered.Text = & string(dw_requestorview.FilteredCount()) + & " &Filtered" rb_deleted.Text = & string(dw_requestorview.DeletedCount()) + & " &Deleted" Return 1 Listing 4 // Make sure the data is editable. // NOTE: Only the primary buffer will be editable. ls_Arg = dw_requestorview.Describe("#1.TabSequence") if ls_Arg <> "10" then // Loop thru the cols and assign a tab order. li_Cols = Integer (dw_requestorview.Describe ( & "DataWindow.Column.Count")) for li_Col = 1 to li_Cols dw_requestorview.Modify ( & "#" + String (li_Col) + ".TabSequence = " + & String (li_Col)) next end if // Event pfc_PropertyPopulate: // Load the child DW objects into the master DW. int li_Col, li_Cols long ll_row string ls_Columns[], ls_Describe, ls_DDDWName // Update the statistics and the buffer. parent.Event pfc_PropertyStats () if rb_primary.checked then Parent.Event pfc_PropertyBufferChanged(Primary!) elseif rb_deleted.checked then Parent.Event pfc_PropertyBufferChanged(Delete!) else Parent.Event pfc_PropertyBufferChanged(Filter!) end if // Loop through the columns. for li_Col = 1 to li_Cols // Get the edit style. ls_Describe = idw_requestor.Describe ( & ls_Columns[li_Col] + ".Edit.Style") Listing 2 // Function of_LoadChild (long al_row): // Load a child DW object into the detail DWs. Datawindowchild ldwc_temp int li_rc string ls_Column, ls_DataObject if IsNull (al_row) or al_row <= 0 or & al_row > dw_children.RowCount () then return –1 end if // Get the column and datawindow object. ls_Column = dw_children.GetItemString (al_row, & "colname") ls_DataObject = dw_children.GetItemString ( & al_row, "dataobject") // Get a reference to the child datawindow. Li_rc = idw_requestor.GetChild (ls_Column, ldwc_temp) if li_rc < 0 then return -1 // Set the DataObject of the bottom datawindow. dw_requestorview.ShareDataOff () dw_requestorview.DataObject = ls_DataObject dw_requestorduplicate.DataObject = ls_DataObject 32 PBDJ volume7 issue2 // Use the base DataWindow service to get a list // of the columns in the DataWindow. if not IsValid (idw_requestor.inv_base) then idw_requestor.of_SetBase (true) end if li_Cols = idw_requestor.inv_base.of_GetObjects ( & ls_Columns, "column", "*", false) // Only dddws need apply. if ls_Describe <> "dddw" then continue // Get the name of the dddw. ls_DDDWName = idw_requestor.Describe ( & ls_Columns[li_Col] + ".dddw.Name") if ls_DDDWName = "!" or ls_DDDWName = "?" or & Trim (ls_DDDWName) = "" then continue // Insert this dddw into the list. ll_row = dw_children.InsertRow(0) dw_children.SetItem (ll_row, "colname", & ls_Columns[li_Col]) dw_children.SetItem (ll_row, "dataobject", & ls_DDDWName) next if dw_children.RowCount() > 0 then this.of_LoadChild (1) end if ! d the Code a o l n Dow The code listing for this article can also be located at www.PowerBuilderJournal .com www.PowerBuilderJournal.com PowerBuilder News All things of interest to the PB community BY BRUCE ARMSTRONG [email protected] 12/07/1999 11/29/1999 Sybase released Enterprise Application Server (EAServer) 3.5, which adds support for the Java 2 Platform Enterprise Edition (J2EE) standard. Sybase also introduced new EAServer pricing. Certicom Corp. announced that Sybase licensed security technology from them for use in future product enhancements. Certicom is a leading provider of next-generation encryption technology used to build strong, fast and efficient security solutions. Their technology is used in electronic commerce software, wireless messaging applications and smart cards. 12/06/1999 Sybase announced the Early Adopter release of an Enterprise Portal (EP) solution, code-named Sybase OpenDoor. 12/01/1999 Sybase announced the signing of a definitive agreement to acquire Home Financial Network (HFN) from InteliData Technologies Corp. HFN will be established as an independent, wholly owned subsidiary of Sybase. Sybase will acquire HFN for a combination of cash and an agreed number of shares of Sybase stock with a total value of approximately $130 million. 11/30/1999 Sybase joined the Wireless Application Protocol (WAP) forum, the industry association focused on developing information and telephony services for digital mobile phones and other wireless terminals. As a part of the WAP forum, Sybase will work with other forum members to define the standards for wireless ebusiness applications. 11/24/1999 MicroStrategy Incorporated announced a multiyear licensing agreement with Sybase that allows MicroStrategy to offer Sybase’s Industry Warehouse Studio as part of its Intelligent EBusiness Platform. 11/22/99 Sybase released the results of a significant performance test with Adaptive Server IQ 12. In the test a 100 gigabyte benchmark of IQ(12)-32 bit ran on a Sun 3500 with eight CPUs and 4 gigabytes of memory versus an Informix Dynamic Server AD 8.2, 64 bit running on a Sun 4500 with 12 CPUs and 12 gigabytes of memory. Sybase obtained superior performance and throughput using 60% less memory and 30% less CPU than Informix, while still only using the 32-bit version of IQ(12). ADVERTISINGINDEX Cendant www.cmjobs.com www.PowerBuilderJournal.com ADVERTISER URL PH PG DUTTON SOFTWARE WWW.DUTTONSOFTWARE.COM 805.375.0789 2 E. CRANE COMPUTING WWW.ECRANE.COM 603.226.4041 11 JAVACON 2000 WWW.JAVACON2000.COM JDJ STORE WWW.JDJSTORE.COM 888.303.JAVA 9 LECCO TECHNOLOGY WWW.LECCOTECH.COM 852.2527.0330 15 RIVERTON SOFTWARE CORPORATION WWW.RIVERTON.COM 781.229.0070 39 STARBASE CORPORATION WWW.STARBASE.COM 888.STAR700 4 SYBASE WWW.SYBASE.COM/PRODUCTS/POWERBUILDER 978.287.1871 40 SYBASE TECHWAVE WWW.SYBASE.COM/TECHWAVE2000 781.278.2607 17 SYS-CON PUBLICATIONS,INC. WWW.SYS-CON.COM 800.513.7111 21 XML DEV. CON WWW.XMLDEVCON2000.COM 13 PBDJ volume7 issue2 19 33 I N T R O D U C T I O N T O P O W E R B U I L D E R Using the PFC MultitableUpdate Service The PFC multitable service made simple I n this month’s column I’ll show you how to use the PFC multitable update service.This service, as you might expect, allows the DataWindow to update more than one table. Multitable update, one of the least used PFC services, has been around since version 5.0. I’ll demonstrate how you can use it, how it works and pitfalls you may find along the way. WRITTEN BY BOB HENDRY How the DataWindow Updates Before I introduce the DataWindow multitable update service, it’s important to understand the default way that DataWindows handle updates. When a an update is attempted, a PowerBuilder system message box will be displayed, informing the user that updates can’t be made to this DataWindow. Also, if this button is left unchecked, the DataWindow won’t maintain a delete buffer. Where Clause for Update/Delete You can indicate here how the DataWindow will determine if an update was successful. This groupbox is used to specify which of three techniques should be used to construct the Where clause on the Update statement to implement optimistic locking. These techniques vary in the degree of data integrity they ensure. (See Table 1 for a more detailed description.) Where Clause Criteria – Key Columns FIGURE 1 Specify Update Properties DataWindow is created, it contains a list of items called Update Properties. These can be viewed via the Rows >> Update Properties menu item within the DataWindow painter (see Figure 1). Simply put, this is where you, the programmer, tell this DataWindow how the updates are going to work. A more descriptive list of the Update Properties is given below. Where Clause Criteria Meaning Meaning Key Only the key column(s) have to match their original Allow Updates Key and Updatable Columns This checkbox specifies whether or not this DataWindow will be updatable. Obviously, if you want the user to be able to update the DataWindow, the box must be checked. If the box isn’t checked and 34 The Key criterion specifies that only the key column(s) must match their original value(s) for the update to be performed. The major concern with this technique is the potential for lost updates. In Figure 2, Users 1 and 2 retrieve a row at the same time and both intend to modify the last name in the orders table. If User 1 makes the change, and User 2 is unaware that the data has already been updated, any changes from User 2 will overwrite the change made by User 1 – in other words, the database update will be successful…and never mind that the changes made by User 1 are overwritten. If it so happened that Users 1 and 2 PBDJ volume7 issue2 values. If the key column(s) don’t match their original database values, the update will fail (see Figure 2). Key and Modified Columns The key column(s) and the columns whose values are being modified must match their original values. If the above-mentioned columns don’t match their original database values, the update will fail (see Figure 3). The key column(s) and all updatable columns must match their original values. If the above-mentioned columns don’t match their original database values, the update will fail (see Figure 4). TABLE 1 www.PowerBuilderJournal.com USER 1 USER 2 Set ship_name Update orders =’Hendry’ Set ship_name =’Hendry’ =’Hendry’ Where order_id = 100 Set ship_name Update orders=’Piekos’ Set ship_name =’Piekos’ Where order_id = 100 Hendry order_id 100 FIGURE 2 Piekos ship_name Bates ship_city Boogers Holler Key Columns Update USER 1 USER 2 Update orders Set ship_name =’Foy’ Where order_id = 100 and ship_name = ’Bates’ Update orders Set ship_city =’Hendry’ =’Hendry’ Where order_id = 100 and ship_city = ’Bates’ Set ship_name =’Foy’ y =’Hendry’ ship_name = ’Bates’ y = ’Bates’ Update Fails Foy order_id 100 FIGURE 3 ship_name Bates ship_city Boogers Holler Key and Modified Update retrieved the data at the same time and one of them updated the key column (in this case the order number), the next attempt to update would fail because the key column has changed. This technique is commonly used in single-user applications or when dealing with many-to-many tables where all columns make up the primary key. Where Clause Criteria – Key and Modified Columns The Key and Modified Columns criterion specifies that the key column(s) and the columns for which the values are being modified must match their original values for the update to be performed. Users 1 and 2 retrieve a row at the same time, both intending to modify the ship name. User 1 changes the ship name from Bates to Foy. The update is successful because both the key and the modified columns match their original values. However, when User 2 attempts www.PowerBuilderJournal.com longer matches its original value. This update technique has a distinct advantage. If a user has updated a value in the database, this value can’t be written over by another user. However, the technique also has a drawback (doesn’t everything?). Consider the following scenario. Users 1 and 2 retrieve the same row at the same time. User 1 changes the ship name from Bates to Foy, while User 2 changes the ship city from Boogers Holler to Wayne. Both updates would be successful, but neither user would be aware that the other had made changes. The result would be inconsistent data (see Figure 3A). Where Clause Criteria – Key and Updatable Columns The Key and Updatable Columns criterion specifies that the key column(s) and all updatable columns must match their original values for the update to be performed. In Figure 4, User 1 changes the last name from Bates to Foy. The change is successful. However, when User 2 attempts to change the ship city from Boogers Holler to Wayne, the update fails. Even though the particular change that User 2 made wouldn’t write over the change made by User 1, the update fails because User 1 changed an updatable column (ship_name). While this technique offers greater consistency in the database, it’s achieved at the cost of a greater potential for failed updates. Optimistic Locking Errors to change the ship name to Hendry, the update will fail (see Figure 3). Since the ship name is now Foy, while on User 2’s end it was expected to be Bates, User 2’s entry won’t overwrite the change by User 1. The database column ship_name no If any changes to the database fail because of how the programmer defined the DataWindow Update Properties, the DataWindow DBError event is fired and a PowerBuilder-generated message is displayed to the user (see Figure 5). USER 2 USER 1 Update orders Set ship_name =’Foy’ Set ship_name =’Foy’ Where order_id = 100 and ship_name = ’Bates’ ship_name = ’Bates’ Foy order_id 100 FIGURE 3A Update orders Set ship_city =’Wayne’ Where order_id y==’Wayne’ 100 and ship_city = ’Boogers Holler’ y = ’Boogers Holler’ Wayne ship_name Bates ship_city Boogers Holler Updates are successful, but data is inconsistent. PBDJ volume7 issue2 35 I N T R O D U C T I O N USER 1 USER 2 Update orders Set ship_name =’Foy’ Set ship_name Where order_id =’Foy’ = 100 and Where order_id = 100 and ship_name = ’Bates’ and ship_name = ’Bates’ Holler’ and ship_city = ’Boogers y = ’Boogers Holler’ Update orders Set ship_city =’Wayne’ y =’Wayne’ Where order_id = 100 and Where order_id = 100 ship_name = ’Bates’ andand ship_name = ’Bates’ Holler’ and ship_city = ’Boogers’ y = ’Boogers’ Holler’ Foy Update Fails order_id 100 FIGURE 4 ship_name Bates T O ship_city Boogers Holler Key and Updatable Update P O W E R B U I L D E R dow modify functions to change the DataWindow.Table.properties of the DataWindow. For those who actually want to try this, look up the UpdateTable, UpdateWhere and UpdateKeyInPlace properties. But believe me, trying to programmatically change the Update Properties of a DataWindow will make you want to throw your PC down the stairs. Multitable Update Service We can now move on to the focus of this month’s column: How can you use the PFC multitable service to update more than one database table? Let’s take the following scenario. A DataWindow contains columns from three tables. The user is allowed to update at least one column from each of the three (see Figure 6). The updatable columns are as follows: Column Table order date quantity ship date product description sales order sales order items sales order items product Adding the Code Just like any of the DataWindow services, the multitable service must be turned on. In the DataWindow constructor event, This.of_setMultiTable(TRUE) will turn on the service for the DataWindow. The next step is to determine what tables we need to update, what identifies a record in a table as a primary key, what columns we’ll allow the user to update, and what Update Properties (as discussed above) the DataWindow will use. When we’ve gathered all of that information, we’ll put the code behind the Update command button. The first table we want to update is the sales order table. The primary key of the sales order table is the sales_order_id column. Believe it or not, this is all you need to start coding. Put the following code behind the command button Clicked event: FIGURE 5 Update Error Message Box String String ls_table ls_key_cols[] FIGURE 6 The Multitable Update: Demo Window Table to Update This portion of the Update Properties dialog box specifies which table is updatable. Notice that only one table can be updatable. What if you want more than one table to be updated? Well, unless you want to write some pretty complicated 36 PBDJ volume7 issue2 PowerScript to modify the DataWindow’s Update Properties, you’re out of luck. Nowhere within the DataWindow painter can you specify that more than one table should be affected. Before the DataWindow multitable update service was available, programmers were forced to write many DataWin- // Update the sales_order table ls_table = "sales_order" ls_key_cols[1] = "sales_order_id" If IsValid(dw_1.inv_multitable) then dw_1.inv_multitable.of_register(ls_tab le,ls_key_cols[]) End If dw_1.Event pfc_update(TRUE,TRUE) www.PowerBuilderJournal.com The most important part of this block of code is the of_register function. This function informs the multitable service that any update properties specified in the DataWindow painter will be overridden with those supplied in our PowerScript. In the example above, we’re saying that we want to update the sales order table and the key is sales_order_id. The of_register function has three signatures. Type Value as_table String Table to be updated. as_key_cols String array Key columns. as_updatable_cols String array Columns that updates will be allowed on. Default is all columns. ab_key_inplace Boolean TRUE - use a SQL UPDATE. FALSE (default) - use a SQL DELETE then an INSERT. Signature 1: ai_where_option of_register(string as_table, string as_key_cols[]). In this signature, the programmer supplies the table to be updated and the key column(s) for the table. By default, all columns for that table are considered updatable. Also, the service assumes that you want to use Key and Updatable for the DataWindow Update Properties when building the Where clause. Finally, the service assumes that when performing the update, the row will first be deleted with a DELETE SQL statement, then reinserted with an INSERT SQL statement. In our examples we’ll use this signature. Argument Name Integer TABLE 2 will be updated. Table 2 provides a more detailed look at the syntax. The Rest of the Code Signature 2: Now that we’ve specified the code to update the sales_order table, we can add the code to update product and sales_order_items. See Listing 1 for the complete code. of_register(string as_table, string as_key_cols[], string as_updatable_cols[]). About the Code This signature is similar to the first. The only difference is that specific updatable columns are being passed to the service. Use this signature when you don’t want all of the columns in a table to be updatable. Signature 3: of_register(string as_table, string as_key_cols[], string as_updatable_cols[],boolean ab_key_inplace, integer ai_where_option). Use this signature when you want to supply every detail about how the table There are a couple of things worth noting in Listing 1. First of all, we have two key columns from the sales_order_ items table. This is because the sales_order_items_id and the sales_order_items_line_id uniquely identify each row. You may have noticed that the ID column in the sales_order_items_id table isn’t visible in the DataWindow. Even though we don’t display it to the user, it’s part of the DataWindow selection criteria because we need to use it as a key. When we’re updating the product table, the array that holds the key columns needs to be reset. That’s because the product table has only one key while the previous table has two. Listing 1 String String String ls_table ls_key_cols[] ls_reset_array[] // Update the sales_order table. ls_table = "sales_order" ls_key_cols[1] = "sales_order_id" If IsValid(dw_1.inv_multitable) then dw_1.inv_multitable.of_register(ls_table,ls_key_cols[]) End If // Update the sales_order_items table. ls_table = "sales_order_items" www.PowerBuilderJournal.com 0 – Key columns only. 1 – Key and updatable columns (default). 2 – Key and modified columns. Just before the event is over, changes are saved to the database and the pfc_ update event is used. When this event is fired, the update logic is redirected to the multitable update service. A regular DataWindow update function simply won’t work. While we’re talking about the PFC… remember, the use of most PFC services requires the entire application to be PFC-based. Mixing PFC components with non-PFC components, with few exceptions, is a recipe for disaster. To use the DataWindow multitable service, your entire application should be built on PFC components. Final Note I hope you’ve found this month’s column useful. Please remember that PowerBuilder Developer’s Journal is your magazine. Readers have requested every topic that has been presented thus far. If you’d like to suggest a topic, please e-mail me. Jim Burbank, who works at Texaco in Houston, first suggested this month’s topic. Hope this helped you, Jim! ▼ [email protected] AUTHOR BIO Bob Hendry is a PowerBuilder instructor for Envision Software Systems and a frequent speaker at national/international PowerBuilder conferences. He specializes in PFC development and has written two books on the subject. ls_key_cols[1] = "sales_order_items_id" ls_key_cols[2] = "sales_order_items_line_id" dw_1.inv_multitable.of_register(ls_table,ls_key_cols[]) // Update the product table. ls_key_cols[] = ls_reset_array[] ls_table = "product" ls_key_cols[1] = "product_id" dw_1.inv_multitable.of_register(ls_table,ls_key_cols[]) dw_1.Event pfc_update(TRUE,TRUE) ! d the Code a o l n Dow The code listing for this article can also be located at www.PowerBuilderJournal .com PBDJ volume7 issue2 37 slick tricks submit your slick tricks to Bernie at [email protected] Setting Column Attributes at Runtime edited by Bernie Metzger AUTHOR BIO Bernhard Metzger is a CPD professional and president of KEV Systems Inc., a Powersoft consulting partner in Newton, Massachusetts. Bernie can be reached at [email protected], or by voice at 800 376-5755. If you want your DataWindows to look consistent, you have two basic choices. The first one – always code the DataWindow expressions the same in all your DataWindows. However, there are drawbacks to this: you have to make sure your entire development team is aware of the standards, and you have to have methods to ensure their use and document the standards for future developers. In spite of such precautions, it’s likely your DataWindows will stray from the standards you’ve set up. The second and better approach is to put the standards in your base DataWindow. Randy Howie of the American Skiing Company has done this with a function to set the protection and background mode on DataWindow columns at runtime. His function takes two arguments, as_columnName and as_expressions. The expression is the same kind you would use in the DataWindow column. For example, if you wanted to prevent users from modifying the last name of an employee in department 100, you would put the following expression on the emp_lname column for protection: if (dept_id = 100 , 0, 1). Since it’s a good idea to give the user visual clues, you might also want to set the background.color to something like “silver” if the field isn’t editable. You’d then need an additional statement for the color attribute such as: if (dept_id = 100, 12632256,rgb[255,255,255]) which would make the column silver or white. The good thing about doing this in a function, of course, is that if you decide to change the color scheme later on, you have to change the code in only one place. Randy’s code sets the protection of the column and the background.mode Listing 1 /* FUNCTION: of_setProtectExpression Called From: constructor of any dw. Description: This function can be used as an alternative to setting conditional expressions in a dw for the protect and background.color properties. Arguments:as_columnName, as_expression Returns: 1 =Successful -1 =Unsuccessful */ string ls_modify, ls_modifyError integer li_rc=-1 ls_modify = as_columnName + & ".background.mode=~"0~t" + as_expression + '~"' ls_modifyError = this.modify (ls_modify) IF ls_modifyError = '' THEN ls_modify = as_columnName + & ".protect=~"0~t" + as_expression + '~"' ls_modifyError = this.modify (ls_modify) IF ls_modifyError = '' THEN li_rc = 1 END IF END IF 38 PBDJ volume7 issue2 (transparency) property. The function (see Listing 1) first attempts to set the background mode. If it succeeds, it then proceeds to set the protection property. Clearly you can modify this function to set other properties to create a consistent “object-oriented” look to your DataWindows. Listing 2 shows some sample calls to the function.▼ Randy Howie is a technical manager at American Skiing Company. He can be reached at [email protected] // If an error occurred on //either modify, display message IF li_rc = -1 THEN messageBox ("of_setProtectExpression", & "Developer: Invalid expression sent to"+& uo_dw.of_setProtectExpression.~r~n" + & ls_modify + "~r~nResponse: " +& ls_modifyError) END IF RETURN (li_rc) Listing 2 this.of_setProtectExpression ('emp_id', "if (isRowNew(), 0, 1)") this.of_setProtectExpression ('emp_lname', "if (dept_id = 100 or isNull(dept_id), 0, 1)") ! d the Code a o l n Dow The code listing for this article can also be located at www.PowerBuilderJournal .com www.PowerBuilderJournal.com Riverton www.riverton.com www.PowerBuilderJournal.com PBDJ volume7 issue2 39 SYBASE www.sybase.com/products/powerbuilder 40 PBDJ volume7 issue2 www.PowerBuilderJournal.com
Similar documents
PowerBuilder techniques
To order additional documents, U.S. and Canadian customers should call Customer Fulfillment at (800) 685-8225, fax (617) 229-9845. Customers in other countries with a U.S. license agreement may con...
More information