I have a Tabular Report with an editable Amount item. When the page loads, the total amount should be shown below the report; and if the user updates any amount on any row, the total amount should be updated automatically.
Note: this method does not work if you have a tabular report that might have a very large number of records (as it relies on all records being rendered in the page at one time).
1. Make sure the report always shows all the records. To do this, set the Number of Rows and the Maximum Row Count to a large number (e.g. 1000).
2. Add an item to show the total, e.g. P1_TOTAL_AMOUNT. I use a Number field, and add “disabled=true” to the HTML Form Element Attributes so that the user won’t change it.
3. Examine the generated HTML to see what ID is given to the amount fields in the tabular report. In my case, the amount field is rendered with input items with name “f04” and id “f04_0001”, “f04_0002”, etc.
4. Add the following code to the page’s Function and Global Variable Declaration:
function UpdateTotal () {
var t = 0;
$("input[name='f04']").each(function() {
t += parseFloat($(this).val().replace(/,/g,''))||0;
});
$s("P1_TOTAL_AMOUNT",t.formatMoney());
}
This strips out any commas from the amounts before parsing them as Floats and adding them to a running total; it finally formats the total using my formatMoney function and updates the total amount item.
5. Add the following to the page’s Execute when Page Loads:
$("input[name='f04']").change(function(){UpdateTotal();});
To prime the total amount field when the page is loaded, I have a Before Header process that calculates the total based on a simple query on the table.
Now, in my case I want to have two running totals: one for “Cash” lines and another for “Salary” lines. My tabular report renders a radio button on each record which the user can select “Cash” or “Salary”. So instead of just the one total amount field, I have two: P1_TOTAL_CASH and P1_TOTAL_SALARY. The radio buttons have hidden input items with the value, rendered with id “f05_nnnn” (where nnnn is the row number).
My UpdateTotal function therefore looks like this:
function UpdateTotals () {
var sal = 0, cash = 0, amt, rownum, linetype;
$("input[name='f04']").each(function() {
amt = parseFloat($(this).val().replace(/,/g,''))||0;
// determine if this is a Cash or Salary line
rownum = $(this).prop("id").split("_")[1];
linetype = $("input[id='f05_"+rownum+"']").val();
if (linetype == "SALARY") {
sal += amt;
} else if (linetype == "CASH") {
cash += amt;
}
});
$s("P52_TOTAL_SALARY",sal.formatMoney());
$s("P52_TOTAL_CASH",cash.formatMoney());
}
And my Execute when Page Loads has an additional call:
$("input[name='f05']").change(function(){UpdateTotals();});
Now, when the user changes the amounts or changes the line type, the totals are updated dynamically.
EDIT: simplified jquery selectors based on Tom’s feedback (see comments) and use the hidden field for the radio buttons instead of querying for “checked”
UPDATE: If the tabular form has an “Add Row” button, the above code won’t work on the newly added rows. In this case, the Execute when Page Load should be this instead:
$(document).on("change", "input[name='f05']", function(){UpdateTotals();});
I have a standard tabular report with checkboxes on each row, and a multi-record delete button called MULTI_ROW_DELETE.
If the user clicks the button before selecting any records (or if there are no records), they get an error message. Instead, I’d rather hide the button and only show it when they have selected one or more records.
To do this:
1. Edit the MULTI_ROW_DELETE button to have a Static ID (e.g. “MULTI_ROW_DELETE”).
2. Add this function to the page’s Function and Global Variable Declaration:
function ShowHideMultiRowDelete () {
if ($("input[id^='f01_']:checked").length==0) {
$x_Hide("MULTI_ROW_DELETE");
} else {
$x_Show("MULTI_ROW_DELETE");
}
}
This looks to see if there are any checkboxes selected, if none are found it hides the delete button, otherwise it shows it.
3. Add this code to the page’s Execute when Page Loads:
ShowHideMultiRowDelete();
$("input[id^='f01_']").change(function(){ShowHideMultiRowDelete();});
$x_Hide("check-all-rows");
This does the initial check on form load (i.e. it initially hides the button, since none of the checkboxes will be selected yet), and adds a listener to the checkboxes so that if any of them are changed, the function is re-run to show or hide the button as needed.
Unfortunately this doesn’t work with the “all rows” checkbox that was generated by the tabular report, so I’ve added a step to hide that checkbox (“check-all-rows”) until I can find a solution for that.
If you’re building a “finance-ey” application you probably have plenty of fields that should show and accept monetary amounts – and quite possibly these items may be implemented in a variety of ways – ordinary apex number items, edit fields in tabular reports, or even dynamically generated items using APEX_ITEM.text.
In my case I had all three, scattered throughout the application. Our users routinely deal with multi-million dollar amounts and they had trouble checking the amounts visually, especially when there are a lot of zeros, e.g. “10000010.00” – so they asked for them all to be formatted with commas, e.g. “10,000,010.00”.
Step 1. SQL number format
So in my first release of the apex application I applied the “FM999G999G999G999G990D00” format to all the money amount items, including in reports etc. The users were reasonably happy with this, but thought it wasn’t working in all cases – e.g. they’d type in a new amount, and the item wouldn’t get formatted until after they Saved the record. This is because the format is only applied when the APEX rendering engine is formatting the page for display – it doesn’t apply it dynamically as the items are changed.
Step 2. Dynamic Actions using SQL
So I started adding dynamic actions to all the apex items which would call the database to format the amount every time the item was changed. This was ok, but performance wasn’t that great – there was a visible sub-second delay while the page did an ajax call to the database just to do the formatting.
Step 3. Dynamic Actions using Javascript
So then I found a Javascript money formatter and modified my dynamic actions to call that instead. The only downside is that it is not internationally-aware. In my case this application’s target users are all here in Australia, are in the education industry, and they haven’t complained about the lack of international money-formatting support (yet).
Number.prototype.formatMoney = function(decPlaces, thouSep, decSep) {
var n = this
,decPlaces = isNaN(decPlaces = Math.abs(decPlaces)) ? 2 : decPlaces
,decSep = decSep == undefined ? "." : decSep
,thouSep = thouSep == undefined ? "," : thouSep
,sign = n < 0 ? "-" : ""
,i = parseInt(n = Math.abs(+n || 0).toFixed(decPlaces)) + ""
,j = (j = i.length) > 3 ? j % 3 : 0;
return sign
+ (j ? i.substr(0, j) + thouSep : "")
+ i.substr(j).replace(/(\d{3})(?=\d)/g, "$1" + thouSep)
+ (decPlaces ? decSep + Math.abs(n - i).toFixed(decPlaces).slice(2) : "");
};
That worked really well, there was no visible delay, and the users were pleased. But I wasn’t satisfied – this trick doesn’t work on the tabular reports or on my APEX_ITEM-generated items.
Step 4. jQuery to the rescue!
So I’ve gone back to the drawing table and decided that I don’t want to have to add Dynamic Actions to each and every item that needs it, which doesn’t work for the items that are generated dynamically (e.g. when the user adds a record to a tabular report). This formatting should be applied automatically to each item, and the only thing I’m going to add to each item is a CSS class. I needed to use some jquery to dynamically bind some javascript to every item that has a particular class, even if the item is added after the page has loaded.
This stackoverflow question came in useful. I added the following to my global javascript file:
$(document).ready(function() {
$( document ).on('change', '.edit_money', function(){
var i = "#"+$(this).attr("id")
,v = parseFloat($(i).val().replace(/,/g,''))||0;
$(i).val( v.formatMoney() );
});
});
All I have to do is add the “edit_money” class to all my money items. For ordinary Apex items, you put the class in the HTML Form Element CSS Classes attribute. For items in a tabular report, the same attribute is under Column Attributes, called Element CSS Classes.
For items generated using APEX_ITEM, I just had to add some extra parameters (p_attributes and p_item_id), e.g.
SELECT APEX_ITEM.text
(p_idx => 2
,p_size => 16
,p_maxlength => 22
,p_attributes => 'class="edit_money" style="text-align:right"'
,p_item_id => 'f02_'||TO_CHAR(ROWNUM,'fm0000')
)
...
So, that was a reasonably good couple of hour’s work, I think. I’m not the world’s expect on javascript or jquery by any stretch of the imagination, but I’m quite happy with the result so far. I’m sure there are even better ways of doing this, so if you know of a better way please comment.
One of my clients reported an issue – they were seeing “Waiting for 1.2.3.4” and a blank screen when they tried to access the Apex web site I’d built for them. They were using Mozilla on a Windows PC, connecting via Vodaphone 3G – the problem was consistent, and it went away when they used their ADSL connection.
My initial response was “don’t use Vodaphone 3G” because the problem seemed to be outside of my area. It appears to be a common issue, something that some mobile operators do to reduce image sizes – c.f. http://support.mozilla.org/en-US/questions/791180 and http://www.geekstogo.com/forum/topic/277895-suspected-issue-waiting-for-1234-in-firefox-on-at/
My client did a little more digging (he’s a techie as well) and found this: http://stackoverflow.com/questions/4113268/how-to-stop-javascript-injection-from-vodafone-proxy
After reading that I said I’d give it another go and see what could be done. As far as I could see, the only really viable solution is to add the “Cache-Control: no-transform” header to the responses. Since I’m using Apache, to do this I added the following to my apache config as per http://httpd.apache.org/docs/current/mod/mod_headers.html:
Header merge Cache-Control no-transform
That seemed to fix the problem. What this header does is instruct all intermediaries to not modify the content in any way – i.e. don’t try to recompress the images, don’t inject any extra CSS or javascript into the page, nothing. Adding this header does carry the risk that performance on some mobile networks may suffer (because they will no longer do the image compression), so it’s now up to me to make sure my pages and images are as small as possible.
I recently was working on an application in APEX 4.2.1.00.08, where the application had several pages with Interactive Reports.
On all these pages, the IR worked fine – except for one crucial page, where the IR’s action menu didn’t work (Select Columns, for example, showed a little circle instead of the expected shuttle region; all the column headings menus would freeze the page; and other issues).
In Console I could see the following errors get raised (depending on which IR widget I tried):
Uncaught SyntaxError: Unexpected token ) desktop_all.min.js?v=4.2.1.00.08:14
$u_evaldesktop_all.min.js?v=4.2.1.00.08:14
_Return widget.interactiveReport.min.js?v=4.2.1.00.08:1
b.onreadystatechange desktop_all.min.js?v=4.2.1.00.08:15
Uncaught TypeError: Object #<error> has no method 'cloneNode' desktop_all.min.js?v=4.2.1.00.08:14
dhtml_ShuttleObject desktop_all.min.js?v=4.2.1.00.08:14
_Return widget.interactiveReport.min.js?v=4.2.1.00.08:1
b.onreadystatechange desktop_all.min.js?v=4.2.1.00.08:15
Uncaught TypeError: Cannot read property 'undefined' of undefined widget.interactiveReport.min.js?v=4.2.1.00.08:1
dialog.column_check widget.interactiveReport.min.js?v=4.2.1.00.08:1
_Return widget.interactiveReport.min.js?v=4.2.1.00.08:1
b.onreadystatechange desktop_all.min.js?v=4.2.1.00.08:15
After a lot of head scratching and some investigative work from the resident javascript guru (“it looks like ajax is not getting the expected results from the server”), I found the following:
http://forums.oracle.com/message/10496937
The one thing in common was that my IR also had a Display Condition on it. In my case, the condition was based on an application item, not REQUEST. I removed the condition, and the problem went away.
I’ve tried to make a reproducible test case with a fresh application, but unfortunately with no success – which means I haven’t yet isolated the actual cause of the issue. A PL/SQL condition like “1=1” doesn’t reproduce the problem. If I have a PL/SQL Expression like “:P1_SHOW = ‘Y'”, or a Value of Item / Column in Expression 1 = Expression 2 with a similar effect, the problem is reproduced – but only in this application.
As a workaround I’ve used a Dynamic Action to hide the IR on page load if required.
Here is a short story about a little problem that caused me a bit of grief; but in the end had a simple cause and a simple fix.
I had a dynamic action in my APEX 4.1 app that had to run some PL/SQL – which was working fine, except the PL/SQL kept on getting longer and longer and more complex; so quite naturally I wanted it to be encapsulated in a database procedure.
I did so, but it didn’t work: the page ran without error, but it seemed like the dynamic action wasn’t firing. It was supposed to change the value of some display items on the page in response to the change of a radio button item, but now they weren’t changing! There was no debug warnings or logs to give a hint either. I tried using Chrome’s developer tools to trace it but that just showed me a very high-level view of what the client was doing, and didn’t report any errors or warnings.
I reverted to my original code, and it worked fine. Ok, so that means it’s probably a problem with my procedure.
I checked and rechecked my procedure. Didn’t seem to be anything wrong with it. I added a line into the procedure to raise an exception. The APEX page dutifully reported the PL/SQL error in the Ajax call – which means that my procedure was being called successfully. Also, I included the return values in the exception message, and this proved that my procedure was correctly determining the values. They just weren’t being returned to the items on the page.
I tried raising an exception in the apex dynamic action’s PL/SQL Code. That worked. The exception message correctly showed the new values were being returned; they still weren’t being populated on the page.
I tried removing all the items from the Page Items to Return setting; then I gradually added them back in, one by one. I narrowed it down to just one item. If I included that item, none of the items were being updated when the procedure returned. If I excluded that item, all the other items were correctly being updated when the procedure returned. Of course, that wasn’t a solution, because there was a cascade of other dynamic actions that were dependent on that particular item, so it has to be updated.
After lunch and a short walk, it occurred to me: unlike the other parameters, that particular parameter was anchored to a database column defined as CHAR(1). Could that be a problem?
Sure enough, when I changed the parameter’s data type from column%TYPE (which mapped to a CHAR) to just a plain VARCHAR2, everything worked.
Yet another reason to avoid CHAR, I guess.
I want to visually enable/disable certain items in each row of a tabular form depending on the value of another item in that row. I’m using APEX 4.1.
My tabular form has a number of editable fields representing budgets. Each line might be an Annual budget (AMOUNT_TYPE = ‘YEAR’) with a single amount for the year, or a Monthly budget (AMOUNT_TYPE = ‘MONTH’) with separate amounts for each of the twelve months.
The first editable item (internal id f02) is AMOUNT_TYPE which is a Select List with an LOV. The second editable item (internal id f03) is the Annual Amount and should only be enabled if AMOUNT_TYPE = ‘YEAR’. The 3rd, 4th … 14th items (internal ids f04..f15) are the Monthly Amounts and should only be enabled if AMOUNT_TYPE = ‘MONTH’.
To do this:
1. Define a visual style to be applied to items that are disabled.
Add this to the Page’s “HTML Header” attribute:
<style>
.textinputdisabled {
color:grey;
background-color:lightgrey;
text-decoration:line-through;
}
</style>
In this instance, I’ve set the background color to a light grey, the text color to darker grey, and I’ve added a strikethrough effect.
2. Set the class on the AMOUNT_TYPE item
Edit the Column Attributes on the AMOUNT_TYPE column, set Element Attributes to:
class="typeselect"
3. Define the Dynamic Action
Event = Change
Selection Type = jQuery Selector
jQuery Selector = .typeselect
Condition = – No Condition –
True Action = Execute JavaScript Code
Fire On Page Load = yes
Code =
row_id = $(this.triggeringElement ).attr('id').substr(4);
if( $(this.triggeringElement ).val() == 'MONTH')
{
$( "#f03_" + row_id ).prop( 'readOnly', 'readonly');
$( "#f03_" + row_id ).prop( 'class', 'textinputdisabled');
for (var i=4;i<16;i++)
{
column_id = ("0" + i).slice(-2);
$( "#f" + column_id + "_" + row_id ).prop( 'readOnly', false);
$( "#f" + column_id + "_" + row_id ).prop( 'class', false);
}
}
else
{
$( "#f03_" + row_id ).prop( 'readOnly', false);
$( "#f03_" + row_id ).prop( 'class', false);
for (var i=4;i<16;i++)
{
column_id = ("0" + i).slice(-2);
$( "#f" + column_id + "_" + row_id ).prop( 'readOnly', 'readonly');
$( "#f" + column_id + "_" + row_id ).prop( 'class', 'textinputdisabled');
}
}
The above code first determines the id for the row; $(this.triggeringElement).attr(‘id’) returns ‘f02_nnnn’ where nnnn is the row number left-padded with zeroes. For Oracle peeps, substr(4) is equivalent to SUBSTR(x,5).
If the value of the triggering item is MONTH, we want to disable the Annual amount item and re-enable (in case they were previously disabled) the Month amount items. And vice-versa.
To disable an item, we set the readOnly property (note the capital O: this is case sensitive!) to the value “readonly” (all lowercase); this makes it so that the user cannot modify the value in the field. Note that we don’t set the “disabled” property because that would stop the item being posted to the database, which will break the tabular form processing.
Side Note: at first, I was using the .attr and .removeAttr jquery functions to set/unset readOnly as per some notes I’d found on the web; this worked for Chrome, but it made all the items permanently read-only in IE7; after some googling I found this is a feature, not a bug; and that .prop is the correct function to use in this instance.
We also set the class to the CSS style we defined earlier.
Because I have 12 items in a row to modify, I use a Javascript loop. The expression to generate the column id (“0” + i).slice(-2) does the same job as the Oracle expression TO_CHAR(i, ‘fm00’).
Next, I want to enhance this page further, so that when the user is entering monthly amounts, the Total field automatically calculates the sum of all the months (while still saving the original annual amount, if any, on the database). I had to get outside help [stackoverflow] to get this working.
UPDATE (31/7/2015): to make this work if the tabular form has an “Add Row” button, you need to use a jquery “on” event handler instead of using the Dynamic Action, and refer to the item using just “this” instead of “this.triggeringElement”, e.g. put this into the forms Execute when Page Loads:
$(document).on("change", ".typeselect", function(){
row_id = $(this).attr('id').substr(4);
... etc. ...
});
When you deploy a procedure, function or package that has a compilation error, the object is still created, and you can still apply grants on them. This is convenient when deploying a large number of objects, meaning you don’t have to get them all in the right order. After deploying your schema, you can just recompile the invalid objects.
Unfortunately, this doesn’t work for views. Now, normally if you create a view with compilation errors, the view will not be created at all; for a deployment script, however, you could use CREATE FORCE VIEW that means the view will be created (but marked invalid).
Let’s say you have a view that depends on a table that doesn’t exist yet – and won’t exist until much later in your deployment scripts. So you create the view with the FORCE option – success. Then, you apply the GRANTs for the view, and get this:
ORA-04063: view "x" has errors
Why? If you try to grant on a procedure, function or package that has errors, it works fine. For views, apparently, this is not allowed.
Obviously, to solve this you might do the hard work and reorder your deployment scripts so that they create every object in the perfectly correct order, avoiding compilation errors entirely. If you have a large number of objects to deploy, this might be more trouble than you want. Well, there is a workaround:
1. Create the view, minus the bit that causes a compilation error.
2. Apply the grant.
3. Recreate the view, compilation error and all. The grant will remain.
Why might this be useful? In my case, we have two databases connected by database links (on both sides); and we need to deploy a large number of objects to both instances. They are managed by different teams, so we want to be able to deploy the changes to each independently. For the most part, the objects on “our side” compile fine, except for some views that refer to objects on the other side of the database link; but they won’t exist until the other team deploys their changes. We could even have a chicken-and-egg problem, when their views refer to objects on our instance; either way, some of the objects cannot be created error-free until both deployments have been completed.
As it stands, we have two options: deploy everything as best we can, then afterwards (when both deployments have completed), recompile the invalid objects and apply the view grants. An alternative is to use this workaround.
Demonstration
TEST CASE #1: cannot grant on a view with errors
SQL> create or replace force view testview as
select 1 as col from bla;
Warning: View created with compilation errors.
SQL> grant select on testview to someone;
ORA-04063: view "USER.TESTVIEW" has errors
SQL> select grantee, privilege from user_tab_privs
where table_name = 'TESTVIEW';
no rows selected
TEST CASE #2: grant on a view with errors
SQL> create or replace view testview as
select 1 as col from dual;
View created.
SQL> grant select on testview to someone;
Grant succeeded.
SQL> create or replace force view testview as
select 1 as col from bla;
Warning: View created with compilation errors.
SQL> select grantee, privilege from user_tab_privs
where table_name = 'TESTVIEW';
GRANTEE PRIVILEGE
======= =========
SOMEONE SELECT
The above has been tested on Oracle 10gR2 and 11gR2. Should this mean that Oracle should not really raise ORA-04063 in this case? I think so.
If you have a table that represents time-varying info, e.g. with From and To date/time columns, you have a few options with regards to the problem of overlapping dates:
1. Check for overlapping dates in the application layer.
2. Use an off-the-shelf product to generate the appropriate triggers, e.g. Oracle CDM*RuleFrame or Toon Koppelaars’ RuleGen.
3. Roll your own, in the database.
4. Use a different data model that can use a unique constraint.
5. Forget about it.
One reason it’s difficult is that this is an example of a cross-row constraint, i.e. one that cannot merely be checked for the current row by itself. Oracle supports a few cross-row constraints, i.e. Primary Key, Unique and Foreign Key constraints; but it doesn’t natively support arbitrary assertions, which would allow us to easily declare this sort of constraint.
The real challenge comes from the fact that Oracle is a multi-user system and concurrent sessions cannot see the uncommitted data from other sessions; so some form of serialization will be required to ensure that when one session wishes to insert/update the data for a particular entity, no other session is allowed to start working on the same entity until the first session commits (or issues a rollback).
The problem is not new; it’s been around for a long time, and tripped many a new (and old) programmer.
There are two problems with option #1 (code in the application layer): firstly, you have to repeat the code for each different type of client (e.g. you might have a Java UI on one side, as well as some batch script somewhere else); secondly, usually the programmer involved will not understand the concurrency problem mentioned above and will not take it into account.
Option #2 is probably the best, most of the time. The solution is implemented at the database level, and is more likely to work correctly and efficiently.
Option #4 (change the data model) involves not storing the From and To dates, but instead dividing up all time ranges into discrete chunks, and each row represents a single chunk of time. This solution is valid if the desired booking ranges are at predictable discrete ranges, e.g. daily. You can then use an ordinary unique constraint to ensure that each chunk of time is only booked by one entity at any one time. This is the solution described here.
Option #5 (forget about it) is also a viable option, in my opinion. Basically it entails designing the rest of the application around the fact that overlapping date ranges might exist in the table – e.g. a report might simply merge the date ranges together prior to output.
Option #3, where you implement the triggers yourself on the database, has the same advantage as Option #2, where it doesn’t matter which application the data is coming from, the constraint will hold true. However, you have to be really careful because it’s much easier to get it wrong than it is to get right, due to concurrency.
I hear you scoffing, “Triggers?!?”. I won’t comment except to refer you to this opinion, which I couldn’t say it better myself: The fourth use-case for Triggers.
There is another Option #3 using a materialized view instead of triggers; I’ll describe this alternative at the end of this post.
So, here is a small example showing how an overlapping date constraint may be implemented. It is intentionally simple to illustrate the approach: it assumes that the From and To dates cannot be NULL, and its rule for detecting overlapping dates requires that the dates not overlap at all, to the nearest second.
- Create the tables
CREATE TABLE room
(room_no NUMBER NOT NULL
,CONSTRAINT room_pk PRIMARY KEY (room_no)
);
CREATE TABLE room_booking
(room_no NUMBER NOT NULL
,booked_from DATE NOT NULL
,booked_to DATE NOT NULL
,CONSTRAINT room_booking_pk
PRIMARY KEY (room_no, booked_from)
,CONSTRAINT room_booking_fk
FOREIGN KEY (room_no) REFERENCES room (room_no)
);
- Create the validation trigger (note – I’ve used an Oracle 11g compound trigger here, but it could easily be rewritten for earlier versions by using two triggers + a database package):
CREATE OR REPLACE TRIGGER room_booking_trg
FOR INSERT OR UPDATE OF room_no, booked_from, booked_to
ON room_booking
COMPOUND TRIGGER
TYPE room_no_array IS TABLE OF CHAR(1)
INDEX BY BINARY_INTEGER;
room_nos room_no_array;
PROCEDURE lock_room (room_no IN room.room_no%TYPE) IS
dummy room.room_no%TYPE;
BEGIN
SELECT r.room_no
INTO dummy
FROM room r
WHERE r.room_no = lock_room.room_no
FOR UPDATE;
END lock_room;
PROCEDURE validate_room (room_no IN room.room_no%TYPE) IS
overlapping_booking EXCEPTION;
dummy CHAR(1);
BEGIN
-- check for overlapping date/time ranges
BEGIN
SELECT 'X' INTO dummy
FROM room_booking rb1
,room_booking rb2
WHERE rb1.room_no = validate_room.room_no
AND rb2.room_no = validate_room.room_no
AND rb1.booked_from != rb2.booked_from
AND (
rb1.booked_from BETWEEN rb2.booked_from
AND rb2.booked_to
OR
rb1.booked_to BETWEEN rb2.booked_from
AND rb2.booked_to
)
AND ROWNUM = 1;
RAISE overlapping_booking;
EXCEPTION
WHEN NO_DATA_FOUND THEN
-- good, no constraint violations
NULL;
END;
EXCEPTION
WHEN overlapping_booking THEN
RAISE_APPLICATION_ERROR(-20000,
'Overlapping booking for room #' || room_no);
END validate_room;
PROCEDURE validate_rooms IS
room_no room.room_no%TYPE;
BEGIN
room_no := room_nos.FIRST;
LOOP
EXIT WHEN room_no IS NULL;
validate_room (room_no);
room_no := room_nos.NEXT(room_no);
END LOOP;
room_nos.DELETE;
EXCEPTION
WHEN OTHERS THEN
room_nos.DELETE;
RAISE;
END validate_rooms;
BEFORE EACH ROW IS
BEGIN
-- lock the header record (so other sessions
-- can't modify the bookings for this room
-- at the same time)
lock_room(:NEW.room_no);
-- remember the room_no to validate later
room_nos(:NEW.room_no) := 'Y';
END BEFORE EACH ROW;
AFTER STATEMENT IS
BEGIN
validate_rooms;
END AFTER STATEMENT;
END room_booking_trg;
/
That’s all you need. The trigger locks the header record for the room, so only one session can modify the bookings for a particular room at any one time. If you don’t have a table like “room” in your database that you can use for this purpose, you could use DBMS_LOCK instead (similarly to that proposed in the OTN forum discussion here).
It would not be difficult to adapt this example for alternative requirements, e.g. where the From and To dates may be NULL, or where the overlapping criteria should allow date/time ranges that coincide at their endpoints (e.g. so that the date ranges (1-Feb-2000 to 2-Feb-2000) and (2-Feb-2000 to 3-Feb-2000) would not be considered to overlap). You’d just need to modify the comparison in the query in validate_room to take these requirements into account.
Test case #1
INSERT INTO room (room_no) VALUES (101);
INSERT INTO room (room_no) VALUES (201);
INSERT INTO room (room_no) VALUES (301);
INSERT INTO room_booking (room_no, booked_from, booked_to)
VALUES (101, DATE '2000-01-01', DATE '2000-01-02' - 0.00001);
INSERT INTO room_booking (room_no, booked_from, booked_to)
VALUES (101, DATE '2000-01-02', DATE '2000-01-03' - 0.00001);
INSERT INTO room_booking (room_no, booked_from, booked_to)
VALUES (201, DATE '2000-02-01', DATE '2000-02-05' - 0.00001);
Expected: no errors
Test case #2
INSERT INTO room_booking (room_no, booked_from, booked_to)
VALUES (201, DATE '2000-02-01', DATE '2000-02-02' - 0.00001);
INSERT INTO room_booking (room_no, booked_from, booked_to)
VALUES (201, DATE '2000-02-02', DATE '2000-02-04' - 0.00001);
INSERT INTO room_booking (room_no, booked_from, booked_to)
VALUES (201, DATE '2000-02-03', DATE '2000-02-05' - 0.00001);
INSERT INTO room_booking (room_no, booked_from, booked_to)
VALUES (201, DATE '2000-02-03', DATE '2000-02-06' - 0.00001);
INSERT INTO room_booking (room_no, booked_from, booked_to)
VALUES (201, DATE '2000-01-31', DATE '2000-02-01');
INSERT INTO room_booking (room_no, booked_from, booked_to)
VALUES (201, DATE '2000-01-31', DATE '2000-02-02' - 0.00001);
INSERT INTO room_booking (room_no, booked_from, booked_to)
VALUES (201, DATE '2000-01-31', DATE '2000-02-06' - 0.00001);
INSERT INTO room_booking (room_no, booked_from, booked_to)
VALUES (201, DATE '2000-02-05' - 0.00001, DATE '2000-02-06' - 0.00001);
UPDATE room_booking SET booked_to = '2000-01-02' - 0.00001
WHERE room_no = 101 AND booked_from = DATE '2000-01-02';
Expected: constraint violation on each statement
Test case #3
in session #1:
INSERT INTO room_booking (room_no, booked_from, booked_to)
VALUES (301, DATE '2000-01-01', DATE '2000-02-01' - 0.00001);
in session #2:
INSERT INTO room_booking (room_no, booked_from, booked_to)
VALUES (301, DATE '2000-01-15', DATE '2000-01-16' - 0.00001);
Expected: session #2 will wait until session #1 issues a COMMIT or ROLLBACK. If session #1 COMMITs, session #2 will then report a constraint violation. If session #2 rolls back, session #2 will complete without error.
The No-Trigger option #3: Materialized View
This is similar to a solution proposed by Rob Van Wijk. It uses a constraint on a materialized view to stop overlapping date ranges.
So, instead of the trigger, you would do something like this:
CREATE MATERIALIZED VIEW LOG ON room_booking WITH ROWID;
CREATE MATERIALIZED VIEW room_booking_date_ranges
REFRESH FORCE ON COMMIT
AS SELECT 'X' AS dummy
FROM room_booking rb1
,room_booking rb2
WHERE rb1.room_no = rb2.room_no
AND rb1.booked_from != rb2.booked_from
AND (
rb1.booked_from BETWEEN rb2.booked_from
AND rb2.booked_to
OR
rb1.booked_to BETWEEN rb2.booked_from
AND rb2.booked_to
);
ALTER TABLE room_booking_date_ranges
ADD CONSTRAINT no_overlapping_dates_ck
CHECK ( dummy = 'Z' );
The nice thing about this solution is that it is simpler to code, and seems more “declarative” in nature. Also, you don’t have to worry about concurrency at all.
The constraint is checked at COMMIT-time when the materialized view is refreshed; so it behaves like a deferred constraint, which may be an advantage for some scenarios.
I believe it may perform better than the trigger-based option when large volumes of data are inserted or updated; however it may perform worse than the trigger-based option when you have lots of small transactions. This is because, unfortunately, the query here cannot be a “REFRESH FAST ON COMMIT” (if you know how this could be changed into a REFRESH FAST MV, please let me know!).
What do you think? If you see any potential issues with the above solutions please feel free to comment.
EDIT 30/8: added some more test cases
I recently saw this question on StackOverflow (“Is there any way to determine if a package has state in Oracle?”) which caught my attention.
You’re probably already aware that when a package is recompiled, any sessions that were using that package won’t even notice the change; unless that package has “state” – i.e. if the package has one or more package-level variables or constants. The current value of these variables and constants is kept in the PGA for each session; but if you recompile the package (or modify something on which the package depends), Oracle cannot know for certain whether the new version of the package needs to reset the values of the variables or not, so it errs on the side of caution and discards them. The next time the session tries to access the package in any way, Oracle will raise ORA-04068, and reset the package state. After that, the session can try again and it will work fine.
Side Note: There are a number of approaches to solving the ORA-04068 problem, some of which are given as answers to this question here. Not all of them are appropriate for every situation. Another approach not mentioned there is to avoid or minimize it – move all the package variables to a separate package, which hopefully will be invalidated less often.
It’s quite straightforward to tell whether a given package has “state” and thus has the potential for causing ORA-04068: look for any variables or constants declared in the package specification or body. If you have a lot of packages, however, you might want to get a listing of all of them. To do this, you can use the new PL/Scope feature introduced in Oracle 11g.
select object_name AS package,
type,
name AS variable_name
from user_identifiers
where object_type IN ('PACKAGE','PACKAGE BODY')
and usage = 'DECLARATION'
and type in ('VARIABLE','CONSTANT')
and usage_context_id in (
select usage_id
from user_identifiers
where type = 'PACKAGE'
);
If you have compiled the packages in the schema with PL/Scope on (i.e. alter session set plscope_settings='IDENTIFIERS:ALL';
), this query will list all the packages and the variables that mean they will potentially have state.
Before this question was raised, I hadn’t used PL/Scope for real; it was quite pleasing to see how easy it was to use to answer this particular question. This also illustrates a good reason why I like to hang out on Stackoverflow – it’s a great way to learn something new every day.