© Edward Sciore 2020
E. ScioreUnderstanding Oracle APEX 20 Application Developmenthttps://doi.org/10.1007/978-1-4842-6165-1_7

7. Processes

Edward Sciore1 
(1)
Newton Center, MA, USA
 

As you have seen, it is possible to write highly functional web pages using only items, reports, and charts. But to implement anything more complex, you have to be able to perform calculations and interact with the database more generally. The notion of a process addresses both issues. This chapter examines some typical situations that require the use of processes and introduces the basics of PL/SQL so that you can write your own code to implement these processes. It also examines the pitfalls you might encounter when writing PL/SQL code, such as the possibility of lost updates.

Stages of a Submit Action

Recall that the left panel of the page designer contains four tabs. This book so far has focused on the Rendering tab; it is now time to move to the Processing tab. To get to this tab, click the circular-arrows icon, third from the left at the top of the panel. Figure 7-1 shows the resulting screen.
../images/335103_3_En_7_Chapter/335103_3_En_7_Fig1_HTML.jpg
Figure 7-1

Processing tab of the page designer

The figure shows five nodes. The first four correspond to the four stages of activity that can occur during a submit action; APEX calls these stages execution points . These stages have the following purposes:
  • During the After Submit stage, the server computes additional session state values in preparation for the main processing activity. Such computations are occasionally useful and are briefly discussed in Chapter 10.

  • During the Validation stage, the server runs validation checks to ensure that the submit request has been called with appropriate data. The server will abort further processing if it detects a problem. Validation is the subject of Chapter 8.

  • During the Processing stage, the server executes the processes that have been associated with the submit request. The definition and use of these processes is the subject of this chapter and Chapter 10.

  • During the After Processing stage, the server uses branches to decide which page to send to the browser. Branching is the subject of Chapter 9.

The fifth node in Figure 7-1, named Ajax Callback, is not an execution point; instead, its purpose is to hold code that can be called from JavaScript functions. As the use of JavaScript in APEX is beyond the scope of this book, I shall not mention this node further.

Creating a Process

This section provides a quick introduction to APEX processes, using the Item Sampler page (page 8) of your employee demo application. Go to its page editor and click the Processing tab; you should see the screen of Figure 7-1. To create a new process, right-click the Processing node and select Create Process. The app builder will create a process, titled New, and add a node for it to the Processing subtree as shown in Figure 7-2. Note that the parent of the new node is the node Processes, which in turn is a child of Processing. The purpose of the Processes node is to group together all process components associated with the Processing execution point.
../images/335103_3_En_7_Chapter/335103_3_En_7_Fig2_HTML.jpg
Figure 7-2

The Processing component tree

Click the node for your new process and look at its properties. The Identification section specifies the name and type of the process. By default, a newly created process has the name New and the type PL/SQL Code. Figure 7-3 shows that I renamed my process SampleProcess and kept the default type value.
../images/335103_3_En_7_Chapter/335103_3_En_7_Fig3_HTML.jpg
Figure 7-3

Specifying the name and type of a process

Click the select list for the Type property and look at the other process types that are available. These types facilitate special purpose actions, such as modifying a table, sending email, or clearing the session state, and are the subject of Chapter 10. This chapter focuses exclusively on the PL/SQL Code process type, which enables you to write your own general-purpose actions.

The code for a PL/SQL process is specified in its Source section by the property PL/SQL Code. Look at the Source section for your new process—you will see that it has no code, which is why the node in Figure 7-2 is flagged as having an error. Let’s fix that.

In its most basic form, a PL/SQL code block consists of one or more statements surrounded by the keywords begin and end. For example, Figure 7-4 shows the Source section containing the simplest possible PL/SQL block. Performing it executes the null statement, which (as you might imagine) does absolutely nothing.
../images/335103_3_En_7_Chapter/335103_3_En_7_Fig4_HTML.jpg
Figure 7-4

A PL/SQL code block to do nothing

Note the semicolons. Each statement must be followed by a semicolon, as must the end keyword. If you forget to do this (and you probably will at some point), APEX will display an error message. When in doubt, check for forgotten semicolons.

Adding this code to your process will remove the error flag. When you run the page, the process will execute every time you click the Submit button. Of course, you have no way of knowing this because the code does nothing. The remainder of this chapter examines how you can add meaningful content to your processes.

PL/SQL to Access the Database

PL/SQL processes are commonly used to access the database. An SQL data modification command can be used as a PL/SQL statement, which means that a process that modifies the database can consist of just that one statement. For example, the following PL/SQL block increases the salary of everyone in department 10.
begin
  update EMP
  set Sal = Sal + 100
  where DeptNo = 10;
end;
For a more comprehensive example, consider the Employee Data Entry page shown in Figure 7-5, which is page 17 of the Employee Demo application. This page illustrates the ability of PL/SQL to modify the database. It has regions to delete a specified employee, insert a new employee, and update the columns of a specified employee. Above these regions is a report region that displays the employee table, so you can see the effect of your modifications.
../images/335103_3_En_7_Chapter/335103_3_En_7_Fig5_HTML.jpg
Figure 7-5

The Employee Data Entry page

To create this page, begin by creating a blank page and four regions. The Employees region is a classic report region whose source is the entire EMP table. The other three regions are static content regions, positioned as follows: Delete Employee begins a new row, and the remaining two regions begin a new column of that row. The following subsections discuss the content of these three regions.

Delete Employee Region

The Delete Employee region contains a select list and a button. The select list is named P17_DELETE_EMPNO, which performs no action on selection and whose values are defined by the query:
select EName, EmpNo
from EMP
order by EName

The button is named Delete, and its action is Submit. The idea is that a user chooses an employee from the list and clicks the Delete button to submit the page; during the submit, the APEX server will execute a process to delete that employee’s record. The following steps explain how to create this process and associate it with the button.

First, go to the Processing tab of the page designer, right-click the Processes node, and choose Create Process. Give the new process a name, such as DeleteEmp.

Second, go to the Source property section, and enter into its PL/SQL Code area the PL/SQL block shown in Listing 7-1. Note how the code uses bind variable syntax to reference the chosen employee number.
begin
    delete from EMP
    where EmpNo = :P17_DELETE_EMPNO;
end;
Listing 7-1

PL/SQL Code for the DeleteEmp Process

Third, change the When Button Pressed property in the process’s Server-side Condition section, as shown in Figure 7-6. Doing so specifies that the process will execute only when the Delete button is pressed. Otherwise, the process would execute each time a submit action occurs, regardless of how the submit occurred. This would be a problem for the Employee Data Entry page because it has four buttons, all doing a submit. You want the DeleteEmp process to execute only when the Delete button is pressed, which is the purpose of the When Button Pressed property.
../images/335103_3_En_7_Chapter/335103_3_En_7_Fig6_HTML.jpg
Figure 7-6

Setting the When Button Pressed property

Insert Employee Region

The Insert Employee region contains five items and a button. The items correspond to columns of the EMP table and have the names P17_INSERT_ENAME, P17_INSERT_JOB, and so on. Item P17_INSERT_ENAME is a text field and P17_INSERT_SAL is a number field. The other three items are select lists, whose values are defined via the following list-of-values queries.

The values of P17_INSERT_JOB are defined by the query:
select distinct Job as DisplayVal, Job as ResultVal
from EMP
The values of P17_INSERT_MGR are defined by the query:
select EName, EmpNo
from EMP
where Job = 'MANAGER'
The values of P17_INSERT_DEPTNO are defined by the query:
select DName, DeptNo
from DEPT
The button is named Insert, and its action is Submit. A user enters values for the items and clicks the Insert button. The process associated with the button will then insert a new record into the EMP table, using the specified item values for five of the columns and computing the following values for the other four columns:
  • EmpNo automatically gets the next value in the sequence determined by EMP’s built-in insertion trigger.

  • HireDate gets the current date.

  • Comm gets 0.

  • Offsite gets 'N'.

The steps to create this process are the same as for the DeleteEmp process. Call the process InsertEmp. Its When Button Pressed property should have the value Insert. Its PL/SQL code consists of the single SQL command shown in Listing 7-2.
begin
    insert into EMP (EName, Job, Mgr, Sal, DeptNo,
                     HireDate, Comm, Offsite)
    values (:P17_INSERT_ENAME,
            :P17_INSERT_JOB,
            :P17_INSERT_MGR,
            :P17_INSERT_SAL,
            :P17_INSERT_DEPTNO,
            current_date,  0,  'N');
end;
Listing 7-2

PL/SQL Code for the InsertEmp Process

At this point, you might want to test your page as it stands. Insert a record and make sure it appears correctly in the report. Then delete it, and watch the report return to its original state.

Update Employee Region

The Update Employee region has four items and two buttons. The items are named P17_UPDATE_EMPNO, P17_UPDATE_JOB, and so on. The select list values are defined the same as in the other regions. The buttons are named GetInfo and Update, and both have the action Submit.

Two steps are required to use this region. First, a user selects the desired employee from P17_UPDATE_EMPNO and clicks the GetInfo button. The process associated with this button populates the items P17_UPDATE_JOB, P17_UPDATE_SAL, and P17_UPDATE_DEPTNO with the corresponding values from that employee’s EMP record. The user then changes those items as desired. When the user clicks the Update button, the process associated with that button updates the record with the new values.

Each button will have its own process. The process for the Update button is called UpdateEmp. Its PL/SQL code is as shown in Listing 7-3, and its When Button Pressed property has the value Update.
begin
    update EMP
      set Job    =  :P17_UPDATE_JOB,
          Sal    =  :P17_UPDATE_SAL,
          DeptNo =  :P17_UPDATE_DEPTNO
    where EmpNo  =  :P17_UPDATE_EMPNO;
end;
Listing 7-3

PL/SQL Code for the UpdateEmp Process

The process for the GetInfo button is named GetEmpInfo. It differs from the other processes on the page in that it reads from the database instead of writing to it. The technique is to write an SQL query that places its output directly into items on the page. The PL/SQL code in Listing 7-4 does just that.
begin
    select Job, Sal, DeptNo
    into :P17_UPDATE_JOB, :P17_UPDATE_SAL, :P17_UPDATE_DEPTNO
    from EMP
    where EmpNo = :P17_UPDATE_EMPNO;
end;
Listing 7-4

PL/SQL Code for the GetEmpInfo Process

The critical part of the query is the clause beginning with the keyword into. This clause indicates that the query’s three output values will be placed into the three specified items. In other words, the into clause turns an SQL query into a PL/SQL statement that extracts values from the database and assigns them to items. Note that the into clause makes sense only if the SQL query returns exactly one row. A process that executes a multi-row SQL query needs to do something totally different with the query output, as you will see later in the next section.

PL/SQL to Compute Values

This section considers the programming-language aspects of PL/SQL. At its heart, PL/SQL is a traditional programming language with variables, assignment statements, conditionals, and loops. In this regard, it is not much different from C, Java, Python, and similar languages. If you have experience with any of these languages, then the most difficult part of writing PL/SQL code will probably be figuring out the appropriate syntax.

As an example, let’s build the Sales Commission page shown in Figure 7-7, which will be page 18 of the Employee Demo application.
../images/335103_3_En_7_Chapter/335103_3_En_7_Fig7_HTML.jpg
Figure 7-7

Page to calculate sales commissions

This page will get used each time a salesman makes a sale. Assume that salesmen receive 15 percent commission on the first $100 of a sale and 10 percent on the remaining amount; if several salesmen collaborate on a sale, they split the commission equally. The page has two outer regions. The top region, titled Solo Commission, is for entering an individual sale. The bottom region, titled Shared Commission, is for entering a collaborative sale.

The Solo Commission region works as follows. A user selects the salesman’s name and enters the amount of the sale into the Enter Sale region. Clicking the Submit button submits the page and causes a process to execute. This process does several things: It calculates the commission based on the sale amount, retrieves the salesman’s previous commission from the EMP table, calculates the new commission amount by summing the two values, and places those three values into the three corresponding items of the Commission Info region. It also updates the EMP table with the new commission value.

The Shared Commission region works similarly, except that several salesmen can be chosen from the select list. Its process calculates the total commission on the sale and the prorated amount, updates the EMP table, and writes the new commissions of the collaborating salesmen into the text area.

This section considers the implementation of the Solo Commission region. (The Shared Commission region will be examined in the section “Handling Multi-Row SQL Queries”.) The two items in the Enter Sale Info region are named P18_SOLO_SALESMAN and P18_SOLO_AMOUNT. Item P18_SOLO_AMOUNT is a number field, and P18_SOLO_SALESMAN is a select list with values defined by the following SQL query:
select EName, EmpNo
from EMP
where Job = 'SALESMAN'
The items in the Commission Info region are named P18_SOLO_PREV_COMMISSION, P18_SOLO_SALE_COMMISSION, and P18_SOLO_NEW_COMMISSION. Listing 7-5 contains the PL/SQL code that gets executed when the Submit button is clicked. This code illustrates several aspects of PL/SQL, which are described in the following subsections.
declare
    v_amount int := :P18_SOLO_AMOUNT;
    v_saleCommission number(7,2);
    v_prevCommission number(7,2);
    v_newCommission  number(7,2);
begin
    -- Step 1: Calculate the new commission as 15% of the first $100
    -- and then 10% of the remaining sale price.
    if v_amount > 100 then
            v_amount := v_amount - 100;
            v_saleCommission := 15 + (v_amount * 0.1);
    else
            v_saleCommission := v_amount * 0.15;
    end if;
    -- Step 2: Retrieve the previous commission from EMP
    -- and calculate the new commission.
    select Comm into v_prevCommission
    from EMP
    where EmpNo = :P18_SOLO_SALESMAN;
    v_newCommission := v_prevCommission + v_saleCommission;
    -- Step 3: Update the employee's record.
    update EMP
    set Comm = v_newCommission
    where EmpNo = :P18_SOLO_SALESMAN;
    -- Step 4: Compute the item values.
    :P18_SOLO_SALE_COMMISSION := v_saleCommission;
    :P18_SOLO_PREV_COMMISSION := v_prevCommission;
    :P18_SOLO_NEW_COMMISSION  := v_newCommission;
end;
Listing 7-5

PL/SQL Code to Handle a Solo Commission

Local Variables

A PL/SQL block can have a variable declaration section prior to the begin keyword. The declaration section starts with the keyword declare and is followed by a series of variable declarations. The code of Listing 7-5 declares four local variables: v_amount, v_saleCommission, v_prevCommission, and v_newCommission. The syntax for each declaration is similar to the syntax for declaring table columns in SQL, and the possible variable types are also the same. For example, the last three variables have the type number(7,2), which is the same type as the column Comm in the EMP table.

Local variables and item references can be used interchangeably in a PL/SQL block. The difference is that local variables are referenced as is, whereas an item reference requires a prepended colon. For example, the SQL update command in step 3 references both the local variable v_newCommission and the item P18_SOLO_SALESMAN. Because items are referenced using bind variable syntax, they are easy to spot. However, local variable references in SQL commands are hard to distinguish from column names or function calls. To avoid confusion, you should always name local variables so that people know they are variables. A common convention, which is adopted here, is to begin each local variable name with v_.

Comments

As in SQL, PL/SQL denotes comments with two consecutive minus signs. All characters following the two minus signs are ignored until the end of the line.

Assignment Statements

Assignment statements in PL/SQL use := as the assignment operator. Assignment can also be used in the declare section to assign an initial value to a local variable.

Conditional Statements

PL/SQL supports several forms of a conditional statement; the form appearing in step 1 of Listing 7-5 is good for general-purpose use. Note that the keywords act as section boundaries: the conditional expression appears between if and then, the statements to be executed when the condition is true appear between then and else, and the statements to be executed when the condition is false appear between else and end if. As usual, if there are no statements between the else and end if keywords, the else keyword can be omitted.

Handling Multi-Row SQL Queries

Now let’s see how to build the Shared Commission region of Figure 7-7. Its Enter Sale region is essentially the same as in the Solo Commission region. Its item names are P18_SHARED_SALESMAN and P18_SHARED_AMOUNT. The items in the Commission Info region are named P18_SHARED_SALE_COMMISSION, P18_SHARED_COMMISSION_EACH, and P18_SHARED_NEW_COMMISSIONS.

The select list P18_SHARED_SALESMAN has the same property values as P18_SOLO_SALESMAN; the difference is that its Allow Multi Selection property is set to Yes, with a Height value of 4. Recall from Chapter 5 that the value of such an item is a string containing the return values separated by colons. For example, in Figure 7-7, ALLEN and TURNER are chosen from the multi-value select list P18_SHARED_SALESMAN. Because their corresponding employee numbers are 7499 and 7844, the value of this item is the string "7499:7844".

It is not especially easy to extract the information you need from this string, so some fancy footwork is required. You will need to do two things: determine the number of selected employees, and extract their records from the EMP table.

Note that the number of selected employees is one more than the number of colons in the string. You can determine the number of colons by using the SQL replace function to create a new string in which the colons are replaced by the empty string (effectively removing the colons) and then comparing the lengths of the two strings. That is, if P18_SHARED_SALESMAN contains the string, the number of employees is given by this expression:
1 + length(:P18_SHARED_SALESMAN)
  - length(replace(:P18_SHARED_SALESMAN, ':', ''))
To extract the records for each selected employee, use the instr function, just as you did for the Filter by Possible Department page of Figure 6-6. As in that page, the following query will retrieve the EMP record of all selected employees:
select *
from EMP
where instr( ':'|| :P18_SHARED_SALESMAN||':',  ':'||EmpNo||':' ) > 0
With these issues taken care of, you can now write the process for the region's Submit button. The PL/SQL code appears in Listing 7-6.
declare
    v_saleCommission number(7,2);
    v_amount         int := :P18_SHARED_AMOUNT;
    v_empCount       int;
    v_commissionEach number(7,2);
    v_empInfo        varchar2(25);
    v_output         varchar2(250) := '';
begin
    -- Step 1: Calculate the new commission as 15% of the first $100
    -- and then 10% of the remaining sale price.
    if v_amount > 100 then
          v_amount := v_amount - 100;
          v_saleCommission := 15 + (v_amount * 0.1);
    else
          v_saleCommission := v_amount * 0.15;
    end if;
    -- Step 2: Determine the shared commission.
    v_empcount := 1 + length(:P18_SHARED_SALESMAN)
                    - length(replace(:P18_SHARED_SALESMAN, ':', '' ));
    v_commissionEach := v_saleCommission / v_empcount;
    -- Step 3: Update the EMP table.
    update EMP
    set Comm = Comm + v_commissionEach
    where instr( ':'||:P18_SHARED_SALESMAN||':',  ':'||EmpNo||':' ) > 0;
    -- Step 4: Use a loop to collect employee info.
    for row in (
        select EName, Comm
        from EMP
        where instr( ':'||:P18_EMPNO_SHARED||':',  ':'||EmpNo||':' ) > 0 )
    loop
          v_empInfo := row.EName || ': ' || row.Comm || chr(13);
          v_output  := v_output || v_empInfo;
    end loop;
    -- Step 5: Write the item values.
    :P18_SHARED_SALE_COMMISSION  := v_saleCommission;
    :P18_SHARED_COMMISSION_EACH  := v_commissionEach;
    :P18_SHARED_NEW_COMMISSIONS  := v_output;
end;
Listing 7-6

PL/SQL Code to Handle Shared Commissions

Step 1, which calculates the commission on the sale, is the same as before. Step 2 calculates the prorated commission for each employee, which you get from dividing the commission by the employee count. Step 3 updates the records for the selected employees. Step 5 writes a value to each item in the Commission region.

The most interesting code appears in step 4, in which you handle each selected employee. The issue is that the SQL query to retrieve the employee information returns multiple rows, so you cannot use the into clause the way that you did earlier. Instead, the technique is to loop through the records in the query, processing one row each time through the loop.

The loop has the following basic structure:
for r in ( <SQL query> )
loop
     -- statements to process row r
end loop;

Variable r need not be declared. Each time through the loop it will hold the next row of the query. If C is an output column of the query, the expression r.C will return the C-value of the current row. For example, in Listing 7-6, the row variable is named row, and the two output columns of the query are EName and Comm. The loop code therefore makes reference to row.EName and row.Comm.

The body of the loop processes each selected employee. Its task is to construct the string that summarizes the new commission of that employee. The code creates the summary string by concatenating the employee’s name with the new commission value. The expression chr(13) denotes a newline character.

Concurrent Database Updates

At any point in time, a web server such as APEX may be executing requests from multiple users concurrently. The users are not aware of this concurrency because the server gives each user the illusion that their application is the only one running on the system. However, application developers must see beyond the illusion. Applications that are unaware of this illusion can unintentionally interfere with each other, with disastrous consequences. This section discusses these issues.

Lost Update Problem

One frequently encountered situation is called the lost update problem . It arises when two users update the same database record at approximately the same time, and the timing of the updates is such that the database system loses one of them. Here are two scenarios that illustrate the problem.

For the first scenario, consider the Employee Data Entry page of Figure 7-5. Assume that two users use the Update Employee region to update Allen’s record—user A wants to change the salary to 1800, and user B wants to change the job to ANALYST. If both users click the GetInfo button at approximately the same time, then they both will retrieve the same record from the EMP table, which says that Allen is a salesman who has a salary of 1600. When user A changes the salary and clicks the Update button, that record will be replaced by one saying that Allen is a salesman who has a salary of 1800. User B, however, doesn’t see the updated salary. When user B changes the job and clicks the Update button, the record will be replaced by one saying that Allen is an analyst making 1600.

This scenario is problematic, regardless of who writes first. If user A modifies first, user B will overwrite the new salary with its previous value; if user B modifies first, user A will overwrite the new job with its previous value. No matter what happens, the database system will lose one of the updates.

You can verify this scenario for yourself. Open the Employee Data Entry page in two private browser windows (using private windows keeps APEX from realizing that they both belong to you). Select the same employee in each window and click the GetInfo button. Then do an update in each window and click their Update button. Now look at the modified record in the report—you will see that your first update was lost.

For the second scenario, consider the Sales Commission page of Figure 7-7. Suppose that Allen made solo sales of $100 and $200, earning commissions of $15 and $25, respectively. Users A and B each enter one of the sales into the Solo Commission region of the page and click its Submit button concurrently. Suppose that the execution of their PL/SQL processes both hit step 2 of Listing 7-5 at roughly the same time, meaning that both processes would see a current commission of $300. User A’s process would then calculate a new commission of $315 in step 3, and user B’s process would calculate a new commission of $325. Whichever process updates last would overwrite the commission of the process updating first. The overwritten update would be lost.

You can also verify this scenario for yourself. This time, open the Sales Commission page in your two private browser windows. In each window, select the same salesman, pick different sale amounts, and click the Submit button. Most likely, neither update will be lost. The reason is that the lost update scenario will only occur when the APEX server happens to postpone its execution of the first process immediately after step 2, which is possible but unlikely. You can improve the odds considerably by introducing a sleep statement into the process immediately after step 2, as follows:
...
v_newCommission := v_prevCommission + v_saleCommission;
dbms_session.sleep(10);
-- Step 3: Update the employee's record.
...

This statement causes the process to sleep for 10 seconds before beginning step 3. Now repeat the experiment. If you click the two Submit buttons within 10 seconds of each other, the problem scenario will occur and the first commission you submitted will be lost.

In both of these scenarios, the occurrence of a lost update depends entirely on the timing of the user requests. This dependence on timing means that lost updates tend to occur at seemingly random times. For example, it is quite possible for an application to run for months (or years) without a lost update and then have several in short succession.

Lost updates are clearly a very bad thing. A lost update might not be detected until long after it occurred, at which point it might be too late. The application developer must ensure that they never occur. Their unpredictable nature makes it hard to discover the problem via testing. Consequently, any PL/SQL process that updates the database must be written specifically to avoid lost updates.

Avoiding Lost Updates

Lost updates occur when two concurrent processes read the same data and then make conflicting updates to it. The way to avoid this problem is to force the conflicting processes to interact sequentially, so that the second process reads the data written by the first one.

The Oracle database system uses write locks to ensure this sequentiality. You specify write locks by adding the clause for update to an SQL query. When your process executes a query having that clause, the database system locks the query’s output records so that they cannot be read or modified by any other process. Your process can then modify these records without fear of conflict. The database system releases the locks when your process completes.

For example, consider the second scenario earlier, in which two users concurrently update the Solo Commission region. Step 2 of Listing 7-5 contains the problematic SQL query:
select Comm into v_prevCommission
from EMP
where EmpNo = :P18_EMPNO;
The output of this query is the Comm value of the specified EMP record, which will be modified later in step 3. To keep other processes from accessing this record, you lock it by changing the query to the following:
select Comm into v_prevCommission
from EMP
where EmpNo = :P18_EMPNO for update;

If another process tries to access this record while it is locked, the database system will delay that process until your process completes. Consequently, step 3 of your process can modify the record, secure in the knowledge that it will not overwrite the modification of another process.

Go to page 18 of your Employee Demo application and make this modification to the process for the Solo Commission region. Then use two private browser windows to verify that it works, as described in the previous section. The page should never lose an updated commission, even if you add the sleep statement before step 3 of the process.

Now consider the first scenario, in which two users concurrently update an employee record. In this scenario, the activity of updating a record requires two processes: the GetEmpInfo process reads values from a specified record, and the UpdateEmp process modifies that record. It is not sufficient for GetEmpInfo to lock its records, because the locks will be released when GetEmpInfo finishes (and therefore, before UpdateEmp begins). Instead, explicit cooperation between these two processes is needed. Here is a good strategy.

The GetEmpInfo process reads the values of the selected record, displays them on the page, and saves the original values for future reference. The UpdateEmp process rereads the values from the database and compares them with the saved values. If they match, then the process can perform the update. If not, then another process must have updated the record, which means that UpdateEmp must refuse to perform the update.

Page 19 of the Employee Demo application, named Revised Employee Data Entry, contains the implementation of this strategy. It looks the same as page 17 and has the same behavior. The only difference (for the moment) is that its GetEmpInfo and UpdateEmp processes have been revised so that they cannot lose updates.

Because this page is substantially the same as page 17, the best way to implement it is to begin with a copy of page 17. To do so, go to the page designer for any page and find the insert icon along its top row (the icon labeled +). Then choose its Page as Copy menu item as shown in Figure 7-8.
../images/335103_3_En_7_Chapter/335103_3_En_7_Fig8_HTML.jpg
Figure 7-8

Selecting the Page as Copy wizard

This item will bring up the APEX page copy wizard. Its first screen asks you where the to-be-copied page is; select Page in this application. The second screen asks you to specify the source and destination pages; Figure 7-9 shows how I filled it in. On the third screen, you should set up a navigation menu entry, in the usual way. The fourth screen tells you whether there were difficulties copying the page components; ignore that screen and click the Create button.
../images/335103_3_En_7_Chapter/335103_3_En_7_Fig9_HTML.jpg
Figure 7-9

Copying a page

Listing 7-7 shows a revision of the GetEmpInfo process of Listing 7-4 (bold lines are new). After retrieving the values for the specified employee, it saves them in the new hidden items P19_ORIGINAL_JOB, P19_ORIGINAL_SAL, and P19_ORIGINAL_DEPTNO (which you should create).
begin
    -- First retrieve the data.
    select Job, Sal, DeptNo
    into :P19_UPDATE_JOB, :P19_UPDATE_SAL, :P19_UPDATE_DEPTNO
    from EMP
    where EmpNo = :P19_UPDATE_EMPNO;
    -- Then use hidden items to save a copy of the data.
    :P19_ORIGINAL_JOB  := :P19_UPDATE_JOB;
    :P19_ORIGINAL_SAL  := :P19_UPDATE_SAL;
    :P19_ORIGINAL_DEPTNO := :P19_UPDATE_DEPTNO;
end;
Listing 7-7

Revising the GetEmpInfo Code of Listing 7-4

Listing 7-8 shows the corresponding revision of the UpdateEmp process of Listing 7-3 (bold lines are new). The code rereads the values from the database and compares them against the original values in the hidden items.
declare
    v_newjob  varchar2(9);
    v_newsal  number(7,2);
    v_newdept number(4,0);
begin
    -- First re-read the data.
    select Job, Sal, DeptNo
    into v_newjob, v_newsal, v_newdept
    from EMP
    where EmpNo = :P19_UPDATE_EMPNO for update;
    -- Then compare it with the original data.
    if :P19_ORIGINAL_JOB = v_newjob and
       :P19_ORIGINAL_SAL  = v_newsal and
       :P19_ORIGINAL_DEPTNO = v_newdept
    then
         -- The record hasn't changed, so update it.
         update EMP
         set Job    = :P19_UPDATE_JOB,
             Sal    = :P19_UPDATE_SAL,
             DeptNo = :P19_UPDATE_DEPTNO
         where EmpNo = :P19_UPDATE_EMPNO;
    else
         -- The record has changed, so abort.
         raise_application_error(-20000,
                      'The record is out of date. Get it again.');
    end if;
end;
Listing 7-8

Revising the UpdateEmp Code of Listing 7-3

The code to abort the process requires explanation. The raise_application_error function causes APEX to abort the process and display an error message on the page. The function takes two arguments: the first argument is the error code, which can be any number between –20,000 and –20,999; the second argument is the error message.

Note that the query contains the for update clause. The process needs to lock the specified EMP record until it can execute the update statement, to ensure that the record cannot be modified in the meantime.

You can test this code as you did in the previous section, by running the page from two private browser windows. In each window, select the same employee and click the GetInfo button. Then modify the item values in each and click the Update buttons. The second update operation will not work; instead, APEX will display the error message shown in Figure 7-10.
../images/335103_3_En_7_Chapter/335103_3_En_7_Fig10_HTML.jpg
Figure 7-10

Error message arising from an avoided lost update

Using a Hash Function

Although this revised code works, the technique does not scale well. The need to save and compare all the original values is tedious and will become more so as the number of saved values increases. An easier approach is to combine all the original values into a single value; you can then save and compare that one value. The preferred technique is to use a hash function , which transforms a collection of values into a single fixed-length value, called a checksum . A hash function also obfuscates its input, which makes it useful for encoding user passwords—as will be seen in Chapter 13.

APEX has a built-in hash function, named apex_util.get_hash. The input to this function is a collection that contains the values you want hashed. You should use the APEX built-in type apex_t_varchar2 to hold this collection. For an illustrative example, consider the following code:
v_vals   apex_t_varchar2 := apex_t_varchar2('CLERK', 20);
v_result varchar2(1000)  := apex_util.get_hash(v_vals);

The first line of the code declares a variable v_vals of type apex_t_varchar2 and assigns it the collection that gets created by the type’s constructor function (which is also called apex_t_varchar2). The arguments to the constructor function are the values 'CLERK' and 20. The second line of the code declares a variable v_result of type varchar2(1000) and assigns it the result of the apex_util.get_hash function. In other words, the variable v_result will contain the hash of the values ['CLERK', 20].

You should use this hash function to revise the code for the GetEmpInfo process so that the three updatable values are encoded into a single hash value. This hash value is then saved in the hidden item P19_HASH (which you will need to create). This code appears in Listing 7-9 (bold lines are new).
declare
    v_valuesToHash apex_t_varchar2;
begin
    -- First retrieve the data.
    select Job, Sal, DeptNo
    into :P19_UPDATE_JOB,
         :P19_UPDATE_SAL,
         :P19_UPDATE_DEPTNO
    from EMP
    where EmpNo = :P19_UPDATE_EMPNO;
    -- Then save the hash of these values.
    v_valuesToHash := apex_t_varchar2(:P19_UPDATE_JOB,
                                      :P19_UPDATE_SAL,  :P19_UPDATE_DEPTNO);
    :P19_HASH := apex_util.get_hash(v_valuesToHash);
end;
Listing 7-9

Second Revision of the GetEmpInfo Process of Listing 7-4

The code for the UpdateEmp process should also be re-revised so that it rereads the updatable values from the database, hashes them, and compares that hash value with the saved hash value. The code appears in Listing 7-10 (bold lines are new).
declare
    v_valuesToHash apex_t_varchar2;
begin
    -- First re-read the data.
    select apex_t_varchar2(Job, Sal, DeptNo)
    into v_valuesToHash
    from EMP
    where EmpNo = :P19_UPDATE_EMPNO for update;
    -- Then compare it with the original data.
    if :P19_HASH = apex_util.get_hash(v_valuesToHash)
    then
        -- The record hasn't changed, so update it.
        update EMP
        set Job    = :P19_UPDATE_JOB,
            Sal    = :P19_UPDATE_SAL,
            DeptNo = :P19_UPDATE_DEPTNO
        where EmpNo = :P19_UPDATE_EMPNO;
    else
        -- The record has changed, so abort.
        raise_application_error(-20000,
                     'The record is out of date. Get it again.');
    end if;
end;
Listing 7-10

Second Revision of the Update Code of Listing 7-3

Although this code does what you want, it is somewhat clunky. This code will be revisited at the end of Chapter 8, where you will see how to improve it using validations.

Success and Error Messages

A process that changes the database should display a message indicating what happened. Consider again the Revised Employee Data Entry page. If the employee report contains only a few records, a user can verify that the update was successful by seeing how the report changed. However, if the report contained thousands of records, it would be far more difficult to determine what happened. It would be better if the deletion process displayed a success message such as Your record was deleted. APEX processes have the Success Message and Error Message properties for exactly this purpose. Figure 7-11 shows these property values for the Delete process.
../images/335103_3_En_7_Chapter/335103_3_En_7_Fig11_HTML.jpg
Figure 7-11

Success and error message properties

Go to the property editor and add success and error messages to the processes in your Sales Commission and Revised Employee Data Entry pages. Run a page and make some changes. Your specified success messages will be displayed when the page is re-rendered. (If you are interested in seeing the error message in action, modify the process’s PL/SQL code so that it refers to a nonexistent table.)

Success and error messages can contain HTML code and item references. For example, suppose that you want the success message in Figure 7-11 to indicate the number of records affected, the way the SQL command window in the APEX SQL Workshop does. How do you get the number of affected records into the process’ success message?

The solution is to use the built-in PL/SQL function SQL%RowCount. If you call this function after executing an SQL command, it will return the number of affected records. Thus, you should modify the DeleteEmp process to assign that value to a hidden item (call it P19_DELETE_COUNT) and then refer to that item within the message body. The revised PL/SQL code for the DeleteEmp process is shown in Listing 7-11 (the bold line is new).
begin
    delete from EMP
    where EmpNo = :P19_DELETE_EMPNO;
    :P19_DELETE_COUNT := SQL%RowCount;
end;
Listing 7-11

Revising the DeleteEmp Process of Listing 7-1

You can then change the text of the success message in Figure 7-11 to this:
There were &P19_DELETE_COUNT. record(s) deleted.

Note that the reference to P19_DELETE_COUNT uses bind variable syntax in the PL/SQL process and substitution string syntax in the HTML message.

Conditional Processes

So far, each process you have written has had an associated button, and you used its When Button Pressed property to specify the association between the process and its button. This is a natural and typical way to use processes and usually all you need.

There are two situations in which the When Button Pressed property is not sufficient: when the submit action was not caused by a button and when the process is relevant to multiple buttons. The following subsections consider these situations.

Submitting via Items

Recall that items of certain types (i.e., radio groups, select lists, text fields, and password fields) can be configured to perform a submit action. Such items should be able to have an associated process. The problem is that the When Button Pressed property doesn’t give you the option of picking an item.

The solution to this conundrum is to forego the When Button Pressed property in the Server-side Condition section and instead use the property below it named Type (refer back to Figure 7-6). That property lets you explicitly specify a condition for when the process should execute. Its options are the same as for conditional rendering, described in Chapter 6.

For example, let’s change the Update Employee region of the Revised Employee Data Entry page so that it doesn’t use the GetInfo button. Figure 7-12 shows what the new region looks like. Now, simply choosing an employee from the select list is sufficient to cause the job, salary, and department of the selected employee to appear automatically.
../images/335103_3_En_7_Chapter/335103_3_En_7_Fig12_HTML.jpg
Figure 7-12

Using a select list to invoke the GetEmpInfo process

The way to implement this behavior is to first modify the select list so that it submits the page when changed and then modify the GetEmpInfo process to execute when that submit occurs. For the first task, modify the action of select list P19_UPDATE_EMPNO to be Submit Page, as shown in Figure 7-13.
../images/335103_3_En_7_Chapter/335103_3_En_7_Fig13_HTML.jpg
Figure 7-13

The select list now submits when changed

For the second task, change the Server-side Condition properties of the GetEmpInfo process as shown in Figure 7-14. Note that its SQL expression makes use of the APEX variable REQUEST, which holds the name of the item that performed the submit action. Because the expression is an SQL expression, it references the variable using bind variable notation. The expression compares the variable value to the string 'P19_UPDATE_EMPNO' (without a colon) because the expression is interested in the name of that item, not the value it holds.
../images/335103_3_En_7_Chapter/335103_3_En_7_Fig14_HTML.jpg
Figure 7-14

The revised condition for the GetEmpInfo process

When you are done, you can delete the GetInfo button from the page.

Multiple-Use Processes

Another situation that cannot make use of the When Button Pressed property is when a process is applicable to more than one button. This section contains two examples related to the Revised Employee Data Entry page.

For the first example, suppose that you want to log all changes made to the EMP table. In particular, each click of the Delete, Insert, or Update button should cause an appropriate record to be inserted into a table named EMPLOG.

The SQL statement to create EMPLOG appears in Listing 7-12. You should create this table by going to the SQL Workshop and submitting that statement. Note that the records in the table will describe the change request, who made it, and when.
create table EMPLOG(Request varchar2(20),
                    UserName varchar2(20),
                    RequestDate Date)
Listing 7-12

SQL Code to Create the EMPLOG Table

Let LogChanges be the name of the process that inserts records into EMPLOG. You should create this process in the Revised Employee Data Entry page, giving it the PL/SQL code of Listing 7-13.
begin
    insert into EMPLOG (Request, UserName, RequestDate)
    values (:REQUEST, :APP_USER, sysdate);
end;
Listing 7-13

PL/SQL Code for the LogChanges Process

This code refers to the built-in variables APP_USER and REQUEST, both of which you have seen before. The variable APP_USER holds the name of the logged in user, and REQUEST holds the name of the button or item that performed the submit action. Thus, the LogChanges process inserts a record into the EMPLOG table containing the source of the submit action, the user who performed it, and the date it occurred.

This process needs to execute each time the Insert, Delete, or Update button is clicked, but not when a value is chosen from the item P19_UPDATE_EMPNO. Clearly, the When Button Pressed property is of no use here because it lets you choose only a single button.

The solution is to set the server-side condition of the process to be the SQL expression shown in Figure 7-15. This expression ensures that the process will fire only when the button named Insert, Delete, or Update is clicked. Because each of these buttons also has its own dedicated process, clicking any of these buttons will cause two processes to execute. The processes will execute in order of their sequence number.
../images/335103_3_En_7_Chapter/335103_3_En_7_Fig15_HTML.jpg
Figure 7-15

Condition section for the LogChanges process

You might be thinking that you don’t need a separate process to do the logging; instead, you could append the logging code to each of the button-specific processes for these buttons. This strategy works, but is not advisable. The problem is that doing so would duplicate the logging code, which means that if you want to change the way the logging works (such as by adding a new column to the EMPLOG table or changing the RequestDate column to hold the time as well as the date), you will need to update it in three places. Moreover, if you decide to add a new button to the page, you must remember to extract this logging code from an existing button and add it to the process for the new button. It is much more elegant and practical to have a separate process that can be called by multiple buttons.

A second example of a process that cannot make use of the When Button Pressed property involves the sending of email. In particular, suppose that you want the Revised Employee Data Entry page to send an email to the database administrator when it detects a suspicious modification. Let’s define a suspicious modification as a deletion or update that occurs outside of normal 9 a.m. to 5 p.m. working hours. To implement this feature, you will need to do two things: create a process (call it SendSuspiciousEmail) to send the email, and specify a condition for when it will fire.

To create the process, you can use the APEX email-sending function called apex_mail.send. This function can take several arguments, denoting (among others) the sender and recipient addresses, the subject, and the message body. The code for the process simply has to calculate these argument values and call the function. Listing 7-14 shows my code. (Of course, if you want to try this code for yourself, you should change the recipient to be your own email address.)
declare
  v_action varchar2(40);
begin
  if :REQUEST = 'Delete' then
    v_action := ' deleted the record for emp '  || :P19_DELETE_EMPNO;
  else
    v_action := ' modified the record for emp ' || :P19_UPDATE_EMPNO;
  end if;
  apex_mail.send(
     p_from => '[email protected]',
     p_to   => '[email protected]',
     p_subj => 'Suspicious changes to EMP',
     p_body => :APP_USER || v_action);
end;
Listing 7-14

Code for the SendSuspiciousEmail process

The only notable part of this code is its use of the REQUEST variable to customize the message body so that it mentions the operation performed and the affected employee record. For example if the user [email protected] modified the contents of president KING’s record, the body of the email would say:
[email protected] modified the record for emp 7839
This code should execute when the Delete or Update button is pressed and the current time (as determined by the current_timestamp function) is earlier than 9 a.m. or later than 5 p.m. Figure 7-16 shows the SQL expression for this condition.
../images/335103_3_En_7_Chapter/335103_3_En_7_Fig16_HTML.jpg
Figure 7-16

Condition for the SendSuspiciousEmail process

Page Rendering Processes

So far, all the processes in this chapter have executed during submit. The remainder of the chapter considers processes that execute during page rendering. This situation often arises when a page uses redirection instead of submit, such as when linking from a row of a report.

For an example, recall from Chapter 3 how an interactive report can have a link column, such that clicking a row’s link column displays the values for that row in single row view (refer to Figures 3-66 and 3-67). The single row view has buttons to move to the previous and next rows of the table and another button to move back to report view.

Page 20 of the employee demo application is called Single Row View, and is shown in Figure 7-17. The page contains two regions that work similar to the single row view of an interactive report. The Employees region is a classic report of EMP records. A user can select a row by clicking the link in its first column; APEX responds by placing that row’s values into the items of the Emp Info region. The Emp Info region has three buttons: the arrow buttons at the bottom select the previous or next record, and the Report View button at the top transfers control back to the report.
../images/335103_3_En_7_Chapter/335103_3_En_7_Fig17_HTML.jpg
Figure 7-17

Single Row View page

Although Figure 7-17 shows the Employees and Emp Info regions together, they actually are never displayed this way. The Employees region is displayed only when there is no selected record, and the Emp Info region is displayed only when there is a selected record.

Several issues arise when you try to implement this page. How and when do the items in the Emp Info region get their values? How does the page know whether to display the Employees region or the Emp Info region? How are the arrow buttons implemented? These issues are discussed in the following sections.

Executing a Process Without a Submit

Let’s first consider how to build the Employees region . It is a classic report region having the following source query:
select EmpNo, EName, Job, Sal, DeptNo
from EMP
order by EName
You should set the type of the EmpNo column to be Link. To configure this link, click the column’s Target property to bring up the Link Builder wizard screen. Figure 7-18 shows the screen as I filled it in. It specifies that when a link is chosen, APEX will set an item named P20_EMPNO to the selected employee number and redirect to the same page. For this to be meaningful, you will of course need to create P20_EMPNO as a hidden item.
../images/335103_3_En_7_Chapter/335103_3_En_7_Fig18_HTML.jpg
Figure 7-18

Configuring the link for the EmpNo column

Now consider the Emp Info region . It should have four items and three buttons. The items are named P20_ENAME, P20_JOB, P20_SAL, and P20_DEPTNO. They all have the type Display Only so that they cannot be modified. The buttons are named ReportView, Previous, and Next. ReportView is positioned Above Region, and Previous and Next are positioned Below Region; all three buttons are aligned left.

The Previous and Next buttons use the same icons as the buttons used in the single row mode of an interactive report. I chose to use them in this region just to illustrate how it’s done. Figure 7-19 shows the Appearance properties for the Previous button.
../images/335103_3_En_7_Chapter/335103_3_En_7_Fig19_HTML.jpg
Figure 7-19

Appearance section for the previous button

To specify that the button should be displayed as an icon, you set the value of the Button Template property to Icon and the value of the Icon property to the name of the desired icon file. You can get this file name by clicking the quick-pick symbol to the right of the Icon property and choosing from the large number of icons displayed. Clicking the Template Options property lets you customize the button’s appearance. In particular, setting the Style property to Remove UI Decoration displays the icon without the rectangle that typically outlines the button. The property settings for the Next button are the same as for the Previous button, except that the icon is a right-facing arrow.

Once you have constructed the Emp Info region, the next step is to write a process to retrieve the values for the employee specified in P20_EMPNO and place them into the items of Emp Info. Go to the Processing tab of the page editor and create a process named FetchRow, having the PL/SQL code shown in Listing 7-15. This code is similar to Listing 7-4, which was the code associated with the GetEmpInfo button on the Employee Data Entry page.
begin
  if :P20_EMPNO is not null then
    select EName, Job, Sal, DeptNo
    into :P20_ENAME, :P20_JOB, :P20_SAL, :P20_DEPTNO
    from EMP
    where EmpNo = :P20_EMPNO;
  end if;
end;
Listing 7-15

PL/SQL Code for the FetchRow Process

Although this process has the correct code, it has the wrong execution point. The Processing execution point means that the process executes when a submit action occurs. But the Employees region does not perform a submit; instead, it invokes a link, and links perform a redirect. What you need is for the process to execute after the redirection occurs—that is, during page rendering.

Click the node for FetchRow in the page designer and look at its Execution Options section. The Point property specifies the execution point of the process. Figure 7-20 shows you the possible values.
../images/335103_3_En_7_Chapter/335103_3_En_7_Fig20_HTML.jpg
Figure 7-20

Possible process execution points

If a process has the execution point New Session, it will execute only when a session begins. The execution points from Before Header to After Footer denote the six stages of page rendering; a process assigned to one of these points will execute during the specified rendering stage. The execution points After Submit and Processing are two stages of a submit action. You assign the process to the After Submit point if you want it to execute before the validation stage and to Processing if you want it to execute after validation.

Although these options can be useful for advanced applications, the differences between many of the execution points are subtle and in most cases irrelevant. For our purposes, the only interesting distinction is between processes that execute during submit and those that execute during rendering. In particular, all processes in this book have the execution point After Header when they execute during page rendering, and Processing when they execute during submit.

Turn your attention back to the FetchRow process, and change its execution point to After Header. You should observe that the node for the process will vanish from the processing tree and reappear in the rendering tree, as shown in Figure 7-21. This makes sense, because the rendering tree contains everything that occurs during page rendering, which includes processes. This figure also shows that the After Header execution point is a pre-rendering point —that is, it occurs before the regions are rendered. This is important, as it ensures that FetchRow will assign values to the page items before they are rendered.
../images/335103_3_En_7_Chapter/335103_3_En_7_Fig21_HTML.jpg
Figure 7-21

FetchRow is now a rendering process

FetchRow will get called each time the page is rendered. But it doesn’t make sense for it to go to the database unless the user has selected a row—that is, it should execute its SQL statement only when P20_EMPNO is non-null. That is the purpose of the if-statement in Listing 7-15. (An equally good equivalent solution is to remove the if-statement and change the Server-side Condition properties of the process so that it executes only when P20_EMPNO is not null.)

Instead of creating FetchRow as a submit process and then changing its execution point, it is easier to create it directly as a rendering process. You simply need to right-click the After Header node in the rendering tree and select Create Process.

Moving Between Regions

At this point in your application, you should verify that clicking a link on the Employees report will correctly place the values for the selected employee into the items of Emp Info. The next issue is to work out a plan for how the regions can “take turns” rendering themselves.

The key idea is to realize that a non-null value for P20_EMPNO indicates that a row has been selected. So to “un-select” a row, it follows that you should put a null value into P20_EMPNO. The two regions can thus use the value of that item to determine their conditional rendering—the Employees region should be visible when the item value is null, and the Emp Info region should be visible when the value is not null. In other words, the value of P20_EMPNO determines whether the page is in report mode or single row mode.

This reasoning also leads to a natural way to implement the ReportView button. It should simply redirect to the same page, setting P20_EMPNO to null. Make these changes, and test your page again. Clicking a row of the report should display the Emp Info region for that employee, and clicking the Report View button should display the report again.

Finding Previous and Next Rows

The final issue is how to implement the Previous and Next buttons so that clicking them causes the region to display the previous (or next) row of the report. This section considers two different ways to implement this functionality. The first approach is easier to understand, but it has some limitations. The second approach is a bit harder to understand but is much more elegant.

Approach 1: Different Processes for Previous and Next

Consider the Previous button , which needs to select the previous row. Assume for the moment that “previous row” means the row having the next-lowest employee number. The PL/SQL code in Listing 7-16 does the job.
declare
    v_prevEmp number(4,0);
begin
    select max(EmpNo) into v_prevEmp
    from EMP
    where EmpNo < :P20_EMPNO;
    if v_prevEmp is not null then
          :P20_EMPNO := v_prevEmp;
    end if;
end;
Listing 7-16

Simplified Code for the Previous Button, Approach 1

The if-statement is necessary to handle the case when you try to get the previous row of the first employee. In this case, the query would return a null value. Without the if-statement, the button would put that null into P20_EMPNO, effectively deselecting the row and returning to report mode. By using the if-statement, clicking the Previous button from the first row will have no effect.

Now suppose that you want to use a different column to determine the previous row, such as EName. The preceding query does not extend easily to this case. Instead, you should use the SQL analytic function lag, which finds the previous value based on a specified sort order. Listing 7-17 gives the resulting PL/SQL code.
declare
    v_prevEmp number(4,0);
begin
    select PrevEmp into v_prevEmp
    from (select EmpNo, lag(EmpNo) over (order by EName) as PrevEmp
          from EMP)
    where EmpNo = :P20_EMPNO;
    if v_prevEmp is not null then
          :P20_EMPNO := v_prevEmp;
    end if;
end;
Listing 7-17

PL/SQL Code for the Previous Button, Approach 1

The inner query creates a two-column table of employee numbers, in which each employee is paired with the employee appearing immediately before in the sort order. The outer query then selects the row from that table whose EmpNo value equals P20_EMPNO and returns the PrevEmp value of that record.

In addition to the lag function, SQL also has the lead function that behaves the same way, except that it returns the next-highest value in the sort order. This function can be used in the implementation of the Next button, and its PL/SQL code is similar to Listing 7-17 (and thus is omitted).

Because the Previous and Next buttons each have their own process, you should set their actions to submit. You should also configure each process to execute conditionally on its button. Although this is not the best solution to the problem, it works well and is worth implementing just to see it in action.

Approach 2: A Unified Previous/Next Process

Approach 1 suffers from the fact the Previous and Next buttons don’t have any visible effect when they get to the first or last record; to the user, they seem to have stopped working. An alternative idea is to hide the buttons when they are not applicable. To do this, the page needs to “read ahead.” That is, in addition to keeping track of the current employee number, the page also needs to keep track of the previous and next values. This way, it can hide the Previous button if the previous value is null and similarly for the Next button.

To implement this approach, create the hidden items P20_PREV and P20_NEXT to hold the previous and next employee numbers. Then, whenever a record is selected, a process can “read ahead” and assign the appropriate values to these items. This process, called FindPreviousNext, appears in Listing 7-18.
begin
  if :P20_EMPNO is not null then
    select PrevEmp, NextEmp
    into :P20_PREV, :P20_NEXT
    from (select EmpNo, lag(EmpNo)  over (order by EName) as PrevEmp,
                        lead(EmpNo) over (order by EName) as NextEmp
          from EMP)
    where EmpNo = :P20_EMPNO;
  end if;
end;
Listing 7-18

PL/SQL Code to Find Previous and Next Values

This code is similar to the code in Listing 7-17, but differs in that it is not specific to any particular button. In fact, it must execute whenever a row gets selected, regardless of whether the selection is via a link from the report or via one of the Previous or Next buttons. In other words, the FindPreviousNext process should execute during page rendering, just like the FetchRow process.

As a consequence, the Previous and Next buttons can be configured much more simply. Consider the Previous button (the Next button is similar). Because P20_PREV holds the EmpNo of the previous row, you can configure the button to redirect when clicked, placing the value of P20_PREV into P20_EMPNO. See Figure 7-22.
../images/335103_3_En_7_Chapter/335103_3_En_7_Fig22_HTML.jpg
Figure 7-22

Configuring the action of the Previous button

You can also use P20_PREV and P20_NEXT to customize when the Previous and Next buttons get displayed. In particular, the Previous button should be displayed when P20_PREV is not null, and the Next button should be displayed when P20_NEXT is not null. These conditions can be expressed using conditional rendering. For example, Figure 7-23 shows the Server-side Condition section for the Previous button.
../images/335103_3_En_7_Chapter/335103_3_En_7_Fig23_HTML.jpg
Figure 7-23

Customizing when the Previous button is displayed

To summarize, approach 2 implements previous/next functionality as follows. You first create hidden items P20_PREV and P20_NEXT. You then create the process FindPreviousNext, which calculates the value of these two items using the code of Listing 7-18 and executes during page rendering.

Finally, you configure the Previous and Next buttons to redirect to the current page, assigning a value to P20_EMPNO. The Previous button uses the value of P20_PREV, and the Next button uses the value of P20_NEXT. Each button uses conditional rendering, so that it displays only when its associated item value is not null.

As a result, the entire page consists of two relatively small processes: one that fetches the values of the selected row and one that calculates the employee number of the previous and next records. Both processes execute during page rendering. A user will select a row by clicking either a report link or a Previous/Next button. In each case, the page will determine the employee number of the selected record, save it to the item P20_EMPNO, and redirect, thereby causing the two processes to execute. Everything works without any submit operations.

Single Row Update

Another use for the report mode/single row mode idea is to support data entry. The Single Row Update page illustrates this idea. It is page 21 of the demo application and appears in Figure 7-24. The three regions of the page are shown together in the figure, but in actuality only one region is visible at a time. The page will be in report mode, update mode, or insert mode, depending on which region is visible.
../images/335103_3_En_7_Chapter/335103_3_En_7_Fig24_HTML.jpg
Figure 7-24

Single Row Update page

Initially, the page displays the Employees region in report mode. From there, the user can click the Insert button to move to insert mode or click one of the Edit links to move to update mode.

The page displays the Insert Employee region when in insert mode. This region is essentially the same as the corresponding region in the Employee Data Entry page. Its Create button creates a new EMP record having the specified values and then clears the items in anticipation of another insertion. The Cancel button returns to report mode.

The page displays the Update/Delete Employee region when in update mode. The region’s items are initially populated with the chosen employee’s information. The user can change item values in the region and click the Update button, which updates the database. Alternatively, clicking the Delete button deletes the chosen record. Deleting a record also returns to report mode because there is no longer a record to display. Clicking the Cancel button simply returns to report mode.

Now that you know how the page should work, you can consider how to implement each region. The following subsections examine these issues.

Implementing the Report Region

The implementation of the report region is essentially the same as in the Single Row View page of Figure 7-17. One difference is that it also has a hidden item, called P21_MODE, to distinguish between the three modes. The item will have the values Report, Insert, and Update, as appropriate, with a null value indicating report mode.

The EmpNo column is implemented as a link. The link redirects to page 21 and sets values for two items when it redirects: P21_EMPNO is set to the chosen employee number, and P21_MODE is set to Update. The Insert button also redirects, setting P21_MODE to Insert. The region is conditionally rendered according to this SQL expression:
:P21_MODE = ‘Report’ or :P21_MODE is null

Implementing the Insert Region

A new EMP record has nine values, but only five need be specified by the user. The Insert Employee region has an item for each of these five values. The items are named P21_INSERT_ENAME, P21_INSERT_JOB, and so on. The region also has two buttons, named Create and CancelInsert. The region is conditionally rendered according to this SQL expression:
:P21_MODE = 'Insert'
The Create button performs a submit action. It has an associated process named InsertEmp that does two things: execute an SQL insert command and set its items to null. It uses default values for HireDate, Comm, and Office . Its code appears in Listing 7-19.
begin
    insert into EMP(EName, Job, Mgr, Sal, DeptNo, HireDate, Comm, Offsite)
    values (:P21_INSERT_ENAME,
            :P21_INSERT_JOB,
            :P21_INSERT_MGR,
            :P21_INSERT_SAL,
            :P21_INSERT_DEPTNO,
            current_date,  0,  'N');
    -- Clear the region's visible items.
    :P21_INSERT_ENAME   := null;
    :P21_INSERT_JOB     := null;
    :P21_INSERT_MGR     := null;
    :P21_INSERT_SAL     := null;
    :P21_INSERT_DEPTNO  := null;
    -- But do not change the mode.
end;
Listing 7-19

PL/SQL Code for the InsertEmp Process

The CancelInsert button redirects, setting P21_MODE to Report.

Implementing the Update/Delete Region

The items in the Update/Delete Employee region get populated by a process named FetchRowForUpdate, which executes during page rendering and is conditional on the SQL expression
:P21_MODE = 'Update'
The code for this process appears in Listing 7-20. The code is essentially the same as the FetchRow process in Listing 7-15, except that it also deals with the lost update problem. (This issue was not relevant with the Single Row View page because that page performed no updates.) It calculates the hash value of the three modifiable items and saves it in the hidden item P21_HASH.
declare
    v_valuesToHash apex_t_varchar2;
begin
  if :P21_EMPNO is not null then
    -- First, fetch the employee record and save its values.
    select EName, Job, Sal, DeptNo
    into :P21_UPDATE_ENAME,
         :P21_UPDATE_JOB,
         :P21_UPDATE_SAL,
         :P21_UPDATE_DEPTNO
    from EMP
    where EmpNo = :P21_EMPNO;
    -- Then save the hash of the updatable values.
    v_valuesToHash := apex_t_varchar2(:P21_UPDATE_JOB,
                      :P21_UPDATE_SAL, :P21_UPDATE_DEPTNO);
    :P21_HASH := apex_util.get_hash(v_valuesToHash);
  end if;
end;
Listing 7-20

PL/SQL Code for the FetchRowForUpdate Process

The PL/SQL code for the region’s Delete button is named DeleteEmp. It executes an SQL delete command, similar to Listing 7-1, and then returns to report mode. Its code appears in Listing 7-21.
begin
    -- First, delete the record.
    delete from EMP
    where EmpNo = :P21_EMPNO;
    -- Then return to report mode.
    :P21_EMPNO := null;
    :P21_MODE  := 'Report';
end;
Listing 7-21

Code for the DeleteEmp Process

The code for the Update button is named UpdateEmp. It executes an SQL update command after ensuring that there is no lost update. The code is essentially the same as the update code for the Revised Employee Data Entry page (refer to Listing 7-10). The code appears in Listing 7-22.
declare
    v_valuesToHash apex_t_varchar2;
begin
    -- First re-read the data.
    select apex_t_varchar2(Job, Sal, DeptNo)
    into v_valuesToHash
    from EMP
    where EmpNo = :P21_EMPNO for update;
    -- Then compare it with the original data.
    if :P21_HASH = apex_util.get_hash(v_valuesToHash) then
        -- The record hasn't changed, so update it.
        update EMP
        set Job    = :P21_UPDATE_JOB,
            Sal    = :P21_UPDATE_SAL,
            DeptNo = :P21_UPDATE_DEPTNO
        where EmpNo = :P21_EMPNO;
    else
        -- The record has changed, so abort.
        raise_application_error(-20000,
                     'The record is out of date. Get it again.');
    end if;
end;
Listing 7-22

Code for the UpdateEmp Process

To summarize, the Single Row Update page has four processes. Three of the processes execute during submit; their responsibilities are to run their respective insert, delete, and update SQL statements. The fourth process is responsible for running an SQL query to initialize a region’s items; it executes during page rendering.

Summary

A process is a page component that lets you access the database and perform calculations. This chapter examined the role that processes play in an application. For example, a button that performs a submit action typically has an associated process that will execute when the button is clicked. A process can also be configured to execute during page rendering and calculate initial values for items.

This chapter focused entirely on PL/SQL processes. To specify the behavior of a PL/SQL process, you write PL/SQL code. PL/SQL integrates SQL commands directly into a general-purpose programming language, which makes it easy to write database-aware code. Chapter 10 examines other process types.

Although PL/SQL processes can be very useful, they require careful coding. This chapter examined one common pitfall, known as the lost update problem. You learned how to recognize the problem and how to write the necessary PL/SQL code to avoid it.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.149.26.176