OpenEdge Development: Progress 4GL Handbook
![]() ![]() ![]()
|
Using QueriesThe programming syntax you have learned so far uses blocks of 4GL code to define and iterate through a set of records. Progress defines an alternative to this form of data access called a query. You’ve seen queries in action already, in the chapter where you looked at the syntax the AppBuilder generates to define a default result set for a window and to display its records in a browse. This chapter discusses why these different forms exist and how you can use queries in ways that are distinct from how you would use a result set in a
FOR EACHblock.This chapter includes the following sections:
Why you use queries in your application
The first question to answer about queries is why Progress has them at all. For many years, the Progress 4GL did not support queries, and developers wrote very powerful and complete applications without them. So, before you go into the details of how to define and use queries, you’ll learn the differences between queries and the block-oriented data access language you’ve used so far.
Queries versus block-oriented data access
The first and most obvious characteristic of queries is precisely that they are not block-oriented. The language statements you’ve used in the last few chapters are all tied to blocks of 4GL code. You define a result set inside a block beginning with
DO,FOR, orREPEAT. The result set is generally scoped to the block where it is defined. The term result set denotes the set of rows that satisfy a query.You learned which of the block types iterate through the result set automatically and which require you to explicitly find the next record.
Even the
FINDstatement itself, although it does not define a block, is subject to the same rules of record scoping. You’ve learned how the presence of record-oriented blocks like aFOR EACHblock andFINDstatements together define the scope of a record buffer within a procedure.These are among the most powerful features in the Progress 4GL. They help give it its unique flexibility and strength for defining complex business logic in a way that ties data access statements closely to the logic that uses the data.
However, there are times when you don’t want your data access tied to the nested blocks of 4GL logic in your procedures. Earlier chapters briefly discussed the notion of event-driven applications. The examples you built in the chapter on the AppBuilder are a small but representative example of this. Think about the procedure
h-CustOrderWin1.w, for instance. When you run this, a Customer and its Orders are displayed for you, as shown in Figure 11–1.Figure 11–1: Customers and Orders sample window
![]()
Iterating through the data, for example through all the Customers in New Hampshire, isn’t done inside a block of 4GL logic. It’s done entirely under user control. Defining the Customer result set and its Order result set using queries is essential to this. When the user chooses the Next button, the code retrieves and displays the next record in the result set:
This happens independently of the block of code where the query is defined, or where it is opened. This gives you great flexibility as a developer to let user interface events, or other programmatic events, determine the flow of the application and the processing of data. By contrast, the examples you built in "Record Buffers and Record Scope,"were chunks of logic that executed independently of the user interface, such as in the procedure
h-BinCheck.p, which does some calculations and returns the results to another procedure.This is the essence of the difference between queries and block-oriented data access. You use queries when you need to keep the data access separate from the structure of the procedure, and instead control it by events. You use block-oriented data access when you need to define business logic for data within a defined scope.
Thus queries give your data access language these important characteristics:
- Scope independence — You can refer to the records in the query anywhere in your procedure.
- Block independence — You aren’t required to do all your data access within a given block.
- Record retrieval independence — You can move through the result set under complete control of either program logic or user events.
- Repositioning flexibility — You can position to any record in the result set at any time.
Using queries to share data between procedures
A query is also a true object in Progress. It has a definition and a name, and you can use the name to access it anywhere in your procedure. You have already learned a little about handles, which give you a reference to an object that you can pass from procedure to procedure. Using a query’s handle, you can access the query and its result set from anywhere in your application session. This gives you the ability to modularize your application in ways that can’t be done with block-oriented result sets, which are not named objects and which have no meaning or visibility outside their defined scope. You’ll learn a lot more about how to use a query’s handle to access its data in later chapters that discuss dynamic data retrieval language.
Using queries to populate a browse
Another thing you observed in the chapters on the AppBuilder and the code it generates is that a query is the basis for the data definition for a browse object. You can’t populate a browse with the data from a
FOR EACHblock, only from a query. The browse gives you a viewport into the query. The repositioning you can do by selecting a record in the browse or scrolling through its contents reflects the repositioning that you can do programmatically through the query. In fact, selecting a row in a browse automatically repositions the query to that row, making it the current row in the buffers the query uses.The chapters on graphical objects didn’t cover the browse control for this very reason: you need to know more about defining and using queries before you can use the browse. You’ll look at some of the many capabilities of the browse after you know how to define the queries they use.
Defining and using queries
There is a
DEFINEstatement for a query just as there is for other Progress objects. This is the general syntax:
The statement gives the query a name and, in turn, names the buffer or buffers that the query uses. In the simplest case, the buffers are database table names. But you can also use other buffers you’ve defined as alternatives to table names or, as you’ll learn later in this chapter, buffers for temporary tables.
If you want to reposition within the result set without using the
GET FIRST,NEXT,PREV, andLASTstatements, you need to define the query asSCROLLING. You’ll learn later in this section how to reposition within the result set of a scrolling query. You must also define a query that is going to be associated with a browse asSCROLLING. There is a slight performance cost to using theSCROLLINGoption, so you should leave it off if you are not using the capabilities it enables.You don’t actually specify the exact data selection statement for the buffers and the tables they represent until you open the query. At that time, you can describe the joins between tables and any other parts of a
WHEREclause that filter the data from the tables. As with otherDEFINEstatements, nothing actually happens when Progress encounters theDEFINE QUERYstatement. No data is retrieved. Progress simply registers the query name and sets up storage and a handle for the query itself as an object.OPEN and CLOSE QUERY statements
To get a query to retrieve data, you need to open it. When you open it, you specify the name of the query and a
FOR EACHstatement that references the buffers you named in the query definition, in the same order. If the query is already open, Progress closes the current open query and then reopens it. This is the general syntax:
The syntax of the
record-phraseis generally the same as the syntax forFOR EACHstatements. If you use thePRESELECT EACHphrase instead of theFOR EACHphrase, then all the records that satisfy the query are selected and their row identifiers pre-cached, just as for aPRESELECTphrase in an ordinary data retrieval block. However, there are a few special cases for the record phrase in a query:
- The first record phrase must specify
EACH, and notFIRST, because the query is intended to retrieve a set of records. It is, however, valid to specify aWHEREclause in therecord-phrasefor the table that resulted in only a single record being selected, so a query can certainly have only one record in its result set. Therecord-phrasefor any other buffers in the query can use theFIRSTkeyword instead ofEACHif that is appropriate.- You cannot use the
CAN-FINDkeyword in a query definition. Doing so results in a compile-time error.- Queries support the use of an outer join between tables, using the
OUTER-JOINkeyword, as explained below.FOR EACHstatements outside of a query do not support the use ofOUTER-JOIN.Using an outer join in a query
An outer join between tables is a join that does not discard records in the first table that have no corresponding record in the second table. For example, consider this query definition:
DEFINE QUERY CustOrd FOR Customer, Order.OPEN QUERY CustOrd FOR EACH Customer, EACH Order OF Customer.
As Progress retrieves records to satisfy this query, it first retrieves a Customer record and then the first Order record with the same CustNum field. When you do a
NEXToperation on the query, Progress locates the next Order for that Customer (if there is one), and replaces the contents of the Order buffer with the new Order. If there are no more Orders for the Customer, then Progress retrieves the next Customer and its first Order.The question is: What happens to a Customer that has no Orders at all? The Customer does not appear in the result set for the query. The same is true for a
FOR EACHblock with the same record phrase. This is simply because the record phrase asks for Customers and the Orders that match them, and if there is no matching Order, then the Customer by itself does not satisfy the record phrase.In many cases this is not the behavior you want. You want to see the Customer data regardless of whether it has any Orders or not. In this case, you can include the
OUTER-JOINkeyword in theOPEN QUERYstatement:
DEFINE QUERY CustOrd FOR Customer, Order.OPEN QUERY CustOrd FOR EACH Customer, EACH Order OF CustomerOUTER-JOIN.
Now Progress retrieves Customers even if they have no Orders. When the Customer has no Orders, the values for all fields in the Order buffer have the unknown value.
Sorting the query results
You can specify a
BYphrase on yourOPEN QUERYstatement just as you can in aFOR EACHblock. In this case, Progress either uses an index to satisfy the sort order if possible or, if no index can allow Progress to retrieve the data in the proper order, preselects and sorts all the query results before any data is made available to the application.GET statements
You use a form of the
GETstatement to change the current position within the record set that theOPEN QUERYstatement defines. You’ve already seen these statements on the button triggers in theh-CustOrderWin1.wprocedure you built in "Introducing the OpenEdge AppBuilder." This is the syntax for theGETstatement:
The query must be open before you can use a
GETstatement. If the query involves a join, Progress populates all the buffers used in the query on eachGETstatement. As noted earlier, a record can remain current through multipleGETstatements if a second table in the query has multiple records matching the record for the first table. This would be the case for a Customer and its Orders, for example. A series ofGET NEXTstatements leaves the same Customer record in its buffer as long as there is another matching Order record to read into the Order buffer.When you first open a query, it is positioned in effect before the first record in the result set. Either a
GET FIRSTor aGET NEXTstatement positions to the first record in the result set.If you execute a
GET NEXTstatement when the query is already positioned on the last record, then the query is positioned effectively beyond the end of the result set. AGET PREVstatement then repositions to the last record in the result set. Likewise, aGET PREVstatement executed when already on the first record results in the query being positioned before the first record, and aGET FIRSTorGET NEXTstatement repositions to the first record. When the query is repositioned off the beginning or off the end of the result set, no error results. You can use theAVAILABLEfunction that you’re already familiar with to check whether you have positioned beyond the result set. For example, this code opens a query and cycles through all the records in its result set, simply counting them as it goes:
Figure 11–2 shows the result.
Figure 11–2: Result of GET statement example
![]()
The
GET FIRSTstatement is needed to make a record available before theIF AVAILABLEstatement is first encountered. Otherwise, theAVAILABLEtest would fail before the code ever entered the loop.Note also that the
AVAILABLEfunction must take a buffer name as its argument, not the name of the query. If the query involves an outer join, then you should be careful about which buffer you use in theAVAILABLEfunction. If you name a buffer that could be empty because of an outer join (such as an empty Order buffer for a Customer with no Orders), then your loop could terminate prematurely. On the other hand, you might want your application logic to test specifically for the presence of one buffer or another in order to take special action when one of the buffers has no record.Using the QUERY-OFF-END function
There is a built-in Progress function that you can use for the same purpose as the
AVAILABLEstatement:
QUERY-OFF-ENDis a logical function that returnstrueif the query is positioned either before the first result set row or after the last row, andfalseif it is positioned directly on any row in the result set. Thequery-nameparameter must be either a quoted literal string with the name of the query or a variable name that has been set to the name of the query. In this way, you can use the statement programmatically to test potentially multiple different active queries in your procedure. For example, here is the same procedure used above, this time with theQUERY-OFF-ENDfunction in place ofAVAILABLE:
The difference between
QUERY-OFF-ENDandAVAILABLEis simply thatAVAILABLErequires a buffer name as a parameter, whereasQUERY-OFF-ENDrequires a query name. If you use theAVAILABLEfunction with the name of the first buffer in the query, it is equivalent to usingQUERY-OFF-ENDwith the query name. Just for stylistic reasons, it is more appropriate to use theQUERY-OFF-ENDfunction in most cases, since it is the position of the query and not the presence of a record in a particular buffer that you’re really interested in. By contrast, if you really want to test for the presence of a record, especially when your query does an outer join that might not always retrieve a record into every buffer, then use theAVAILABLEfunction.Closing a query
When you are done with a query, you should close it using this statement:
An
OPEN QUERYstatement automatically closes a query if it was previously open. For this reason, it isn’t essential to execute aCLOSE QUERYstatement just before reopening a query. However, you should explicitly close a query when you are done with it and you are not immediately reopening it. This frees the system resources used by the query. After you close the query you cannot reference it again (with aGETstatement, for instance). However, if there are records still in the buffer or buffers used by the query, they are still available after the query is closed unless your application has specifically released them.Determining the current number of rows in a query
You can use the
NUM-RESULTSfunction to determine how many rows there are in the current results list:
This
INTEGERfunction returns the number of rows currently in the query’s results list. As with theQUERY-OFF-ENDfunction, thequery-nameis an expression, which can be either the quoted name of the query or a variable containing the name.The phrase “currently in the query’s results list” requires some explanation. The results list is a list of the row identifiers of all rows that satisfy the query, and that have already been retrieved.
Progress normally builds up the results list as you walk through the query using the
GETstatement. Therefore, when you first open a query with aFOR EACHclause in theOPEN QUERYstatement, the results list is empty and theNUM-RESULTSfunction returns zero.As you move through the query using the
GET NEXTstatement, Progress adds each new row’s identifier to the results list and increments the value returned byNUM-RESULTS. For example, this example retrieves all the Customers in the state of Louisiana using a query. For each row, it displays the Customer Number, Name, and the value ofNUM-RESULTS:
When you run the procedure, you see the value of
NUM-RESULTSchange as each new row is retrieved, as shown in Figure 11–3.Figure 11–3: Result of NUM-RESULTS example
![]()
Using a DOWN frame and the DOWN WITH statement
As a small digression, it is necessary to explain a couple of statements in this example that you haven’t seen before. They illustrate one of the key characteristics about queries: there’s no built-in block scoping or iteration in a query. First, here’s the new phrase on the
DISPLAYstatement:
You learned a little about down frames in "Using Basic 4GL Constructs," and "Examining the Code the AppBuilder Generates." A down frame is a frame that can display more than one row of data, each showing the same fields for a different record in a report-like format. In the examples you wrote in earlier chapters, you didn’t have to specify the
DOWNphrase to indicate how many rows the frame should have. Progress gave you a down frame with a default number of rows automatically.Why doesn’t it do that in this case? Because a query is not associated with a particular block, and doesn’t have any automatic iteration, Progress doesn’t know how the data is going to be displayed. So by default, it just gives you a one down frame that displays a single record.
The second new piece of syntax is this statement at the end of the block:
No, this is not a political protest slogan! Rather, it tells Progress to display a row in the frame CustFrame, and then to position down a row in the frame before displaying the next row. If you don’t use this statement, then even if you define the frame to be
15 DOWN, all the rows are displayed on top of each other on the first row of the frame. Once again, this is because Progress doesn’t know how you’re going to display the data. It does not associate iteration through a result set with yourDOblock automatically, as it would with aFOR EACHblock. Therefore, you have to tell it what to do.Retrieving query results in advance
The value of
NUM-RESULTSdoes not always increment as you executeGET NEXTstatements and operate on each row, however. There are various factors that force Progress to retrieve all the results in advance of presenting you with any data.One of these is under your direct control: the
PRESELECToption on theOPEN QUERYstatement. When you use aPRESELECT EACHrather than aFOR EACHstatement to define the data selection, you are telling Progress to retrieve all the records that satisfy the query in advance and to save off their record identifiers in temporary storage. Then Progress again retrieves the records using their identifiers as you need them. As discussed in "OPEN and CLOSE QUERY statements" , you typically use thePRESELECToption to make sure that the set of records is not disturbed by changes that you make as you work your way through the list, such as changing a key value in such a way as to change a record’s position in the list.To see visible evidence of the effect of the
PRESELECTkeyword in yourOPEN QUERYstatement:All the records are pre-retrieved. Therefore, the value of
NUM-RESULTSis the same no matter what record you are positioned to. This means that you could use thePRESELECToption to display, or otherwise make use of, the total number of records in the results list before displaying or processing all the data.Another factor that can force Progress to pre-retrieve all the data is a sort that cannot be satisfied using an index.
To see an example of this:
- Change the
OPEN QUERYstatement back to use aFOR EACHblock and then try sorting the data in the query by the Customer Name:
- Run the query:
The Name field is indexed, so Progress can satisfy theBYphrase and present the data in the sort order you want by using the index to traverse the database and retrieve the records.- By contrast, try sorting on the City field:
- Add the City field to the
DISPLAYlist and rerun the procedure to see the result:
There is no index on the City field, so Progress has to retrieve all 13 of the records for Customers in Louisiana in advance to sort them by the City field before presenting them to your procedure. Therefore,NUM-RESULTSis equal to the total number of records from the beginning, as soon as the query is opened.Identifying the current row in the query
As you move through the results list, Progress keeps track of the current row number, that is, the sequence of the row in the results list. You can retrieve this value using the
CURRENT-RESULT-ROWfunction:
The function returns an
INTEGERvalue with the sequence of the current row. Thequery-nameis an expression, either a quoted query name or a variable reference.For
CURRENT-RESULT-ROWto work properly, you must define the query to beSCROLLING. If you don’t define the query asSCROLLING, theCURRENT-RESULT-ROWfunction returns a value, but that value is not reliable.To use
CURRENT-RESULT-ROW, make these changes to your sample procedure:
When you run the procedure, you see that the value of
CURRENT-RESULT-ROWkeeps pace withNUM-RESULTS, as shown in Figure 11–4.Figure 11–4: Result of CURRENT-RESULT-ROW example
![]()
This is not always the case, of course. If you use the
PRESELECToption or a nonindexed sort to retrieve the data, thenNUM-RESULTSis always 13, as you have seen. But the value ofCURRENT-RESULT-ROWchanges from 1 to 13 just as it does above.You can use the
CURRENT-RESULT-ROWfunction to save off a pointer to reposition to a specific row. See the "Using a RowID to identify a record" for information on how to identify a record.Here are a few special cases for
CURRENT-RESULT-ROW:
- If the query is empty, the function returns the unknown value (?).
- If the query is explicitly positioned before the first row, for example by executing a
GET FIRSTfollowed by aGET PREV, then the function returns the value 1.- If the query is explicitly positioned after the last row, for example by executing a
GET LASTfollowed by aGET NEXT, then the function returns the value one more than the number of rows in the results list.Using INDEXED-REPOSITION to improve query performance
If you anticipate jumping around in the result set using statements such as
GET LAST, you should add another option to the end of yourOPEN QUERYstatement: theINDEXED-REPOSITIONkeyword. If you do this, yourDEFINE QUERYstatement must also specify theSCROLLINGkeyword.If you don’t open the query with
INDEXED-REPOSITION, then Progress retrieves all records in sequence in order to satisfy a request such asGET LAST. This can be very costly. If you do useINDEXED-REPOSITION, Progress uses indexes, if possible, to jump directly to a requested row, greatly improving performance in some cases. There are side effects to doing this, however, in terms of the integrity of the results list, as discussed next.Factors that invalidate CURRENT-RESULT-ROW and NUM-RESULTS
Under some circumstances, when you open your query with the
INDEXED-REPOSITIONkeyword, the value ofCURRENT-RESULT-ROWorNUM-RESULTSbecomes invalid. As explained earlier, the results list holds the row identifiers for those rows that satisfy the query and that have already been retrieved.Thirteen rows satisfy the query for Customers in Louisiana, so the value of these two functions goes as high as 13 for that query. When you do a
PRESELECTor a nonindexed sort, all the rows have already been retrieved before any data is presented to you, soNUM-RESULTSis 13 at the beginning of theDISPLAYloop. Normally, Progress adds the identifiers for all the rows it retrieves to the results list, but there are circumstances where this is not the case. If you execute aGET LASTstatement on a query, and yourOPEN QUERYstatement does not use aPRESELECTor a sort that forces records to be pre-retrieved, Progress jumps directly to the last record using a database index, without cycling through all the records in between. In this case, it has no way of knowing how many records would have been retrieved between the first one and the last one, and it cannot maintain a contiguous results list of all rows that satisfy the query. For this reason, Progress flushes and reinitializes the results list when you jump forward or backward in the query. So after aGET LASTstatement,NUM-RESULTSreturns 1 (because theGET LASTstatement has retrieved one row) andCURRENT-RESULT-ROWis unknown (because there is no way to know where that row would fit into the full results list).Repositioning a query
Often you need to reposition a query other than to the first, last, next, or previous row. You might need to jump to a row based on data the user entered or return to a row that you previously saved off. Or you might want to jump forward or backward a specific number of rows to simulate paging through the query. You can do all these things with the
REPOSITIONstatement, which has this syntax:
REPOSITIONquery-name{ |TO ROWrow-number|FORWARDSn|BACKWARDSn|TO ROWIDbuffer-1-rowid[, . . .] [ NO-ERROR ]}
The
query-namein this case is not an expression. It can only be an unquoted query name, not a variable.If you specify the
TO ROWoption followed by an integer expression, the query repositions to that sequential position within the results list. If you have previously saved off that position using theCURRENT-RESULT-ROWfunction, you can use the value that function returned as the value in theTO ROWphrase to reposition to that row.If you use the
FORWARDSorBACKWARDSphrase, you can jump forward or backward any number of rows, specified by theninteger expression. You can use theFORWARDorBACKWARDkeywords instead ofFORWARDSorBACKWARDS.The last of the
REPOSITIONoptions requires an explanation of a Progress data construct you haven’t seen before.Using a RowID to identify a record
Every record in every table of a database has a unique row identifier. (Technically, the row identifier is only unique within a single storage area of a database. Since an entire database table must be allocated to a single storage area, this effectively makes the identifier unique at least within that table. A discussion of database constructs such as storage areas is beyond the scope of this book.)
The identifier is called a RowID. There is both a
ROWIDdata type that allows you to store a row identifier in a procedure variable and aROWIDfunction to return the identifier of a record from its record buffer.Generally, you should consider a RowID to be a special data type without being concerned about its storage format. The RowID is (among other things) designed to be valid, not just for the OpenEdge database, but for all the different databases you can access from the 4GL using OpenEdge DataServers, which provide access from the 4GL to database types such as Oracle and Microsoft SQLServer.
In fact, you can’t display a RowID directly in a Progress 4GL procedure. If you try to, you get an error. You can see a RowID by converting it to a
CHARACTERtype using theSTRINGfunction. For instance, here is a procedure that shows you the RowIDs of the rows that satisfy the sample query you’ve been working with:
Figure 11–5 shows the result.
Figure 11–5: Result of RowID example
![]()
The RowID is displayed as a hexadecimal value. The values you would see in your own copy of the Sports2000 database might be different from these, and certainly they would be different if you modified the data, dumped it, and reloaded it into the database, because the RowID reflects the actual storage location of the data for the record, and this is not in any way predictable or necessarily repeatable. You should never count on a RowID as a permanent identifier for a record. However, you can use a RowID if you need to relocate a record you have previously retrieved within a procedure and whose RowID you saved off. This is what the
TOROWIDphrase in theREPOSITIONstatement is for.Even in this case, you must be aware that in the event of a record deletion, it is possible that a new record could be created that has the same RowID as the record that was deleted. So, even within a single session a RowID is not an absolutely reliable pointer to a record. In addition, RowIDs are unique only within a single database storage area. Therefore, the same RowID might occur for records in different tables that happen to be in different storage areas.
With these conditions in mind, you can use the
TO ROWIDphrase to reposition to a record in a query. Note that the RowID is for a particular database record, not an entire query row, so you need to save off the RowID of the record buffer, not of the query name, to reuse it. And in the case of a query with a join between tables, you need to save the RowID of each record buffer in order to reposition to it later and restore each of the records in the join.The
NO-ERRORoption in this phrase lets you suppress an error message if the RowID turns out to be invalid for any reason. You could then use theAVAILABLEfunction or theERROR-STATUShandle (see "Managing Transactions") to determine whether the query was successfully repositioned.There is another similar identifier in Progress called a
RECID. This identifier was used in earlier versions of Progress to identify records in the same way as RowIDs do now. TheRECIDis essentially an integer identifier for a record, though it has its own data type. It is still supported but for several reasons (including, but not limited to, portability of code between database types that you can access with DataServers), it is strongly recommended that you use only the RowID form in new application code. The Progress 4GL continues to supportRECIDs mainly for backward compatibility with older applications that still use them.Positioning details with the REPOSITION statement
So if you execute a
REPOSITIONstatement that repositionsTO ROW 5orFORWARDS 10orTO ROWID rRow, then your procedure is positioned to that row so that you can display it or otherwise use it, right? Well, not exactly. When you use theREPOSITIONstatement, Progress positions the query in between records, so that you then have to execute aGET NEXTorGET PREVstatement to position on a row so that you can actually use it. Here are some of the specifics:
- When you execute a
REPOSITIONstatement that uses theTO ROWIDorTO ROWphrase, the query is positioned before the record requested. You then need to execute aGET NEXTstatement to make the row you want available.- When you execute a
REPOSITIONquery-nameBACKWARDSstatement, the query is positioned between records, and you need to execute aGET NEXTstatement to make the row you intend available. For example, if the query is positioned to row 6 of a query results list and you execute aREPOSITIONquery-nameBACKWARDS 2, then the query is positioned effectively between results list rows 3 and 4, and aGET NEXTstatement makes row 4 available. AGET PREVstatement makes row 3 available.- When you execute a
REPOSITIONstatement with theFORWARDSphrase, you are actually positioned beyond the record you might expect. For example, if you are on row 6 and issue a statement withFORWARDS 2, then the query is positioned between row 8 and row 9, and you need to then execute aGET PREVstatement to make row 8 available or aGET NEXTstatement to make row 9 available.Note as well that there is only one way for Progress to know which row is five rows ahead of the current row, or five rows behind it, or the fifth row of the result set, and that is to walk through the query row by row until it gets to the one you want. If the row in question has already been retrieved and is in the results list, then Progress can reposition to the row you want very quickly using the results list. Therefore, the
REPOSITIONstatement with any of the optionsTO ROW,FORWARDS, orBACKWARDSmaintains the integrity of the results list and the functions that use it. TheREPOSITION TO ROWIDstatement also maintains the integrity of the list if the row you want has already been retrieved and the results list has not been flushed since you retrieved it, because Progress can scan the results list to locate the RowID.Extending the sample window to use the queries
Now you can try out some of the things you’ve learned about queries.
To extend the sample application window to use some query statements:
- Open the procedure
h-CustOrderWin1.w.This gives you the test procedure with Customer fields and the Order browse but without some of the other graphical objects you added later.- Extend the window somewhat to the right, then drop two buttons onto the window.
- Name the first button PosButton and give it a label of Save Position.
- Name the second button ReposButton and give it a label of Restore Position.
- Open the Section Editor and select the Definitions section.
- Define a variable to hold a value for the Customer query’s
CURRENT-RESULT-ROW:
Remember that the Definitions section is scoped to the entire procedure file, so anything you define here is available to any trigger or internal procedure inside it.- Go into the Triggers section for the
PosButtonand give it thisCHOOSEtrigger:
- Define this
CHOOSEtrigger for theReposButton:
When you choose the Save Position button, your code saves off the current row number from the results list. You can then move around in the query. When you press the Restore Position button, the Customer query is repositioned to the row you saved, and the Order query is opened for that Customer.Why did you need theAPPLY “CHOOSE” TO BtnNext? statement. Remember that when you use theREPOSITIONstatement Progress positions before the record you want, so you need to execute aGET NEXTto make it available. At the same time, in this case, your code needs to reopen the dependent Order query for the Customer as well. All this is done by the trigger code on the Next button.- Run the procedure and test saving off and restoring the current row.
- Add a fill-in field to give you a reason to test reopening the query with a different
WHEREclause:
- Drop a fill-in on the window. Call it cState and give it a label of New State.
- Write this
LEAVEtrigger for the fill-in field in the Section Editor:
DO:OPEN QUERY CustQuery FOR EACH Customer WHERE State =cState:SCREEN-VALUE.APPLY "CHOOSE" TO BtnFirst.END.Each time you enter a state abbreviation in the field and tab out of it, the trigger fires and the query is reopened with that new state. Remember that it isn’t necessary to close a query explicitly if you are immediately going to reopen it, so aCLOSE QUERY CustQuerystatement here is optional. Also remember that unless you specifically assign it using theASSIGNstatement, the value the user types into the fill-in exists only in the frame’s screen buffer, so you can retrieve it using theSCREEN-VALUEattribute of the field.Similarly for theCHOOSEtrigger that repositions to the previously saved row, you need to applyCHOOSEto the First button trigger to get the first row for the new query and reopen the Order query.- Run your procedure. You can enter a state name and tab out of the New State field and see the Customers in that state along with their Orders:
If you enter an invalid state name, the procedure doesn’t give you any feedback to confirm this. In the next section, you make a few more changes to the procedure to check whether there were any results for the state that is entered. This makes use of theNUM-RESULTSfunction and also introduces you to another new and useful 4GL statement.Using NUM-RESULTS to check the validity of the query
The query is sorted by the City, which is a nonindexed field. Therefore, the
NUM-RESULTSfunction returns the total number of records that satisfy the query as soon as it is opened, because they all have to be retrieved before they can be sorted.Next, you add another field to the screen to display that number, and then check the same value in the trigger for the New State field to make sure that the state entered was valid
To add another field to the screen:
- Drop another fill-in field onto the window. Name it iMatches and give it a Label of Number of Matches.
To display initial values for the New State and Number of Matches fields, you’ll put some code into the Main Block of the procedure, to be executed right after the query is initially opened.- In the Section Editor, select the Main Block and add this code after the
RUN enable_UIstatement:
This displays the initial value of the Customer State (“NH”) along with the number of Customers in New Hampshire. Remember that, in this case,CustQueryis both the name of the query and also the name of the frame the AppBuilder created for you.- Edit the
LEAVEtrigger for the New State field again. Add a statement before theOPEN QUERYstatement to save off the current State in thecStatefield, and a statement following theOPEN QUERYstatement to retrieve theNUM-RESULTSvalue for the new query:
Remember that the value you assign to
cStateis not overwritten by theSCREEN-VALUEthat you type into the field on the screen, so you can open the query based on theSCREEN-VALUEand still keep the old value around if you need it later (which you will).Using the MESSAGE statement
Next you need to display a warning message if there are no results, which means that no Customer records match the state value that was entered. Progress has a
MESSAGEstatement to display a message to the screen or to a message area at the bottom of the window if there is one. AMESSAGEstatement can contain one or more character expressions (quoted literals orCHARACTERvariables) that make up the message. If there are multiple expressions in the statement, Progress simply concatenates them all, with a single space between them. You can also insert theSKIPkeyword anywhere in the message to skip a line, orSKIP(n)to skipnlines.If you want the message to appear in its own alert box, you can include the phrase
VIEW-AS ALERT-BOXin the statement. This is the default if the message is displayed from a GUI window. If you want to give the message alert box a special format, you can include one of theQUESTION,INFORMATION,ERROR, orWARNINGkeywords after theVIEW-AS ALERT-BOXphrase. TheMESSAGEstatement has other features, including the ability to display different sets of buttons, beyond just an OK button to acknowledge the message. It can also capture an answer from the message and store it in a variable. You can see all the syntax details in OpenEdge Development: Progress 4GL Reference or in the online help topic for theMESSAGEstatement.To display a warning message that tells you that there are no matching Customers:
- In the New State trigger, add this code:
IF iMatches = 0 THENDO:MESSAGE "There are no Customers that match the State"cState:SCREEN-VALUE "." SKIP"Restoring the previous State."VIEW-AS ALERT-BOX WARNING.- Reopen the query using the value of the State field that you saved off in the
cStatevariable and redisplay the previous valid New State value:
This ends theDOblock that is executed ifNUM-RESULTSis zero.- To display the number of matching Customers if
NUM-RESULTSis not zero, add the following code:
- Add the following code so that, regardless of whether the New State was valid, the statement executes
APPLY “CHOOSE”to the First button to do aGET FIRSTand opens the Order query:
- Save the procedure as
h-CustOrderWin4.w.- Run the procedure to see the number of matching records for any State you enter:
![]()
- To see your warning message, enter an invalid State value, such as QQ:
![]()
Summary
This ends the discussion of using queries in your application. In this chapter, you’ve learned how to define and use queries and what the differences are between queries and other Progress 4GL data access statements.
The next chapter discusses how to define and use temporary tables. At the end of the next chapter, you’ll be using queries and temp-tables together.
|
Copyright © 2005 Progress Software Corporation www.progress.com Voice: (781) 280-4000 Fax: (781) 280-4095 |
![]() ![]() ![]()
|