A few years of ADF experience means I see common mistakes made by different developers, some I regularly make myself. This post is designed to assist beginners to Oracle JDeveloper Application Development Framework (ADF) avoid a common ADF pitfall, the case of the phantom ADF developer [add Scooby-Doo music here].
ADF Business Components - triggers, default table values and instead of views.
Oracle's JDeveloper tutorials help with the A-B-Cs of ADF development, typically built on the nice 'n safe demo schema provided by with the Oracle database such as the HR demo schema. However it's not too long until ADF beginners, having built up some confidence from learning with the tutorials and vanilla demo schemas, start building ADF Business Components based upon their own existing database schema objects. This is where unexpected problems can sneak in.
The crime
Developers may encounter a surprising error at runtime when editing a record they just created or updated and committed to the database, based on their own existing tables, namely the error:
JBO-25014: Another user has changed the row with primary key oracle.jbo.Key[x]
...where X is the primary key value of the row at hand. In a production environment with multiple users this error may be legit, one of the other users has updated the row since you queried it. Yet in a development environment this error is just plain confusing. If developers are isolated in their own database, creating and editing records they know other users can't possibly be working with, or all the other developers have gone home for the day, how is this error possible? There are no other users? It must be the phantom ADF developer! [insert dramatic music here]
The following picture is what you'll see in the Business Component Browser, and you'll receive a similar error message via an ADF Faces page:
What can possibly cause this issue if it isn't our phantom ADF developer? Doesn't ADF BC implement record locking, locking database records when the row is modified in the ADF middle-tier by a user? How can our phantom ADF developer even take out a lock if this is the case? Maybe ADF has a bug, maybe ADF isn't implementing record locking at all? Shouldn't we see the error "JBO-26030: Failed to lock the record, another user holds the lock" as we attempt to modify the record, why do we see JBO-25014? :
Let's verify that ADF is in fact issuing the correct SQL LOCK-FOR-UPDATE statement to the database.
First we need to verify ADF's locking strategy. It is determined by the Application Module's jbo.locking.mode property. The default (as of JDev 11.1.1.4.0 if memory serves me correct) and recommended value is
optimistic, and the other valid value is
pessimistic.
Next we need a mechanism to check that ADF is issuing the LOCK statements to the database. We could ask DBAs to monitor locks with OEM, but optimally we'd rather not involve overworked DBAs in this process, so instead we can use the ADF runtime setting –Djbo.debugoutput=console. At runtime this options turns on instrumentation within the ADF BC layer, which among a lot of extra detail displayed in the log window, will show the actual SQL statement issued to the database, including the LOCK statement we're looking to confirm.
Setting our locking mode to pessimistic, opening the Business Components Browser of a JSF page allowing us to edit a record, say the CHARGEABLE field within a BOOKINGS record where BOOKING_NO = 1206, upon editing the record see among others the following log entries:
[421] Built select: 'SELECT BOOKING_NO, EVENT_NO, RESOURCE_CODE, CHARGEABLE, MADE_BY, QUANTITY, COST, STATUS, COMMENTS FROM BOOKINGS Bookings'
[422] Executing LOCK...SELECT BOOKING_NO, EVENT_NO, RESOURCE_CODE, CHARGEABLE, MADE_BY, QUANTITY, COST, STATUS, COMMENTS FROM BOOKINGS Bookings WHERE BOOKING_NO=:1 FOR UPDATE NOWAIT
[423] Where binding param 1: 1206
As can be seen on line 422, in fact a LOCK-FOR-UPDATE is indeed issued to the database. Later when we commit the record we see:
[441] OracleSQLBuilder: SAVEPOINT 'BO_SP'
[442] OracleSQLBuilder Executing, Lock 1 DML on: BOOKINGS (Update)
[443] UPDATE buf Bookings>#u SQLStmtBufLen: 210, actual=62
[444] UPDATE BOOKINGS Bookings SET CHARGEABLE=:1 WHERE BOOKING_NO=:2
[445] Update binding param 1: N
[446] Where binding param 2: 1206
[447] BookingsView1 notify COMMIT ...
[448] _LOCAL_VIEW_USAGE_model_Bookings_ResourceTypesView1 notify COMMIT ...
[449] EntityCache close prepared statement
....and as a result the changes are saved to the database, and the lock is released.
Let's see what happens when we use the optimistic locking mode, this time to change the same BOOKINGS record CHARGEABLE column again. As soon as we edit the record we see little activity in the logs, nothing to indicate any SQL statement, let alone a LOCK has been taken out on the row.
However when we save our records by issuing a commit, the following is recorded in the logs:
[509] OracleSQLBuilder: SAVEPOINT 'BO_SP'
[510] OracleSQLBuilder Executing doEntitySelect on: BOOKINGS (true)
[511] Built select: 'SELECT BOOKING_NO, EVENT_NO, RESOURCE_CODE, CHARGEABLE, MADE_BY, QUANTITY, COST, STATUS, COMMENTS FROM BOOKINGS Bookings'
[512] Executing LOCK...SELECT BOOKING_NO, EVENT_NO, RESOURCE_CODE, CHARGEABLE, MADE_BY, QUANTITY, COST, STATUS, COMMENTS FROM BOOKINGS Bookings WHERE BOOKING_NO=:1 FOR UPDATE NOWAIT
[513] Where binding param 1: 1205
[514] OracleSQLBuilder Executing, Lock 2 DML on: BOOKINGS (Update)
[515] UPDATE buf Bookings>#u SQLStmtBufLen: 210, actual=62
[516] UPDATE BOOKINGS Bookings SET CHARGEABLE=:1 WHERE BOOKING_NO=:2
[517] Update binding param 1: Y
[518] Where binding param 2: 1205
[519] BookingsView1 notify COMMIT ...
[520] _LOCAL_VIEW_USAGE_model_Bookings_ResourceTypesView1 notify COMMIT ...
[521] EntityCache close prepared statement
Again even though we're seeing the midtier delay the LOCK statement until commit time, it is in fact occurring on line 412, and released as part of the commit issued on line 419. Therefore with either optimistic or pessimistic locking a lock is indeed issued.
Our conclusion at this point must be, unless there's the unlikely cause the LOCK statement is never really hitting the database, or the even less likely cause the database has a bug, then ADF does in fact take out a lock on the record before allowing the current user to update it. So there's no way our phantom ADF developer could even modify the record if he tried without at least someone receiving a lock error.
Hmm, we can only conclude the locking mode is a red herring and not the true cause of our problem.
Who is the phantom?
At this point we'll need to conclude that the error message "JBO-25014: Another user has changed" is somehow legit, even though we don't understand yet what's causing it. This leads onto two further questions, how does ADF know another user has changed the row, and what's been changed anyway?
To answer the first question, how does ADF know another user has changed the row, the Fusion Guide's section4.10.11 How to Protect Against Losing Simultaneous Updated Data , that details the Entity Object Change-Indicator property, gives us the answer:
At runtime the framework provides automatic "lost update" detection for entity objects to ensure that a user cannot unknowingly modify data that another user has updated and committed in the meantime. Typically, this check is performed by comparing the original values of each persistent entity attribute against the corresponding current column values in the database at the time the underlying row is locked. Before updating a row, the entity object verifies that the row to be updated is still consistent with the current state of the database.
The guide further suggests to make this solution more efficient:
You can make the lost update detection more efficient by identifying any attributes of your entity whose values you know will be updated whenever the entity is modified. Typical candidates include a version number column or an updated date column in the row.....To detect whether the row has been modified since the user queried it in the most efficient way, select the Change Indicator option to compare only the change-indicator attribute values.
We now know that ADF BC doesn't use the locking mechanism at all to protect the current user against updates, but rather it keeps a copy of the original record fetched, separate to the user changed version of the record, and it compares the original record against the one in the database when the lock is taken out. If values don't match, be it the default compare-all-columns behaviour, or the more efficient Change Indicator mechanism, ADF BC will throw the JBO-25014 error.
This leaves one last question. Now we know the mechanism under which ADF identifies a changed row, what we don't know is what's changed and who changed it?
The real culprit
What's changed? We know the record in the mid-tier has been changed by the user, however ADF doesn't use the changed record in the mid-tier to compare to the database record, but rather a copy of the original record before it was changed. This leaves us to conclude the database record has changed, but how and by who?
There are three potential causes:
The database trigger among other uses, can be configured to fire PLSQL code on a database table insert, update or delete. In particular in an insert or update the trigger can override the value assigned to a particular column. The trigger execution is actioned by the database on behalf of the user initiating the insert or update action.
Why this causes the issue specific to our ADF use, is when we insert or update a record in the database via ADF, ADF keeps a copy of the record written to the database. However the cached record is instantly out of date as the database triggers have modified the record that was actually written to the database. Thus when we update the record we just inserted or updated for a second time to the database, ADF compares its original copy of the record to that in the database, and it detects the record has been changed – giving us JBO-25014.
This is probably the most common cause of this problem.
A second reason this issue can occur is another database feature, default column values. When creating a database table the schema designer can define default values for specific columns. For example a CREATED_BY column could be set to SYSDATE, or a flag column to Y or N. Default values are only used by the database when a user inserts a new record and the specific column is assigned NULL. The database in this case will overwrite the column with the default value.
As per the database trigger section, it then becomes apparent why ADF chokes on this feature, though it can only specifically occur in an insert-commit-update-commit scenario, not the update-commit-update-commit scenario.
I must admit I haven't double checked this scenario but it seems plausible, that of the Oracle database's instead of trigger view (sometimes referred to as instead of views). A view in the database is based on a query, and dependent on the queries complexity, may support insert, update and delete functionality to a limited degree. In order to support fully insertable, updateable and deletable views, Oracle introduced the instead of view, that gives the view designer the ability to not only define the view query, but a set of programmatic PLSQL triggers where the developer can define their own logic for inserts, updates and deletes.
While this provides the database programmer a very powerful feature, it can cause issues for our ADF application. On inserting or updating a record in the instead of view, the record and it's data that goes in is not necessarily the data that comes out when ADF compares the records, as the view developer has the option to practically do anything with the incoming data, including throwing it away or pushing it to tables which aren't used by the view underlying query for fetching the data.
Readers are at this point reminded that this article is specifically about how the JBO-25014 error occurs in the context of 1 developer on an isolated database. The article is not considering how the error occurs in a production environment where there are multiple users who can cause this error in a legitimate fashion. Assuming none of the above features are the cause of the problem, and optimistic locking is turned on (this error is not possible if pessimistic locking is the default mode *and* none of the previous causes are possible), JBO-25014 is quite feasible in a production ADF application if 2 users modify the same record.
At this point under project timelines pressure, the obvious fix for developers is to drop both database triggers and default values from the underlying tables. However we must be careful that these legacy constructs aren't used and assumed to be in place by other legacy systems. Dropping the database triggers or default value that the existing Oracle Forms applications assumes and requires to be in place could cause unexpected behaviour and bugs in the Forms application. Proficient software engineers would recognize such a change may require a partial or full regression test of the existing legacy system, a potentially costly and timely exercise, not ideal.
Solving the mystery once and for all
Luckily ADF has built in functionality to deal with this issue, though it's not a surprise, as Oracle as the author of ADF also built the database, and are fully aware of the Oracle database's feature set. At the Entity Object attribute level, the Refresh After Insert and Refresh After Update properties. Simply selecting these instructs ADF BC after inserting or updating a record to the database, to expect the database to modify the said attributes, and read a copy of the changed attributes back into its cached mid-tier record. Thus next time the developer modifies the current record, the comparison between the mid-tier record and the database record match, and JBO-25014: Another user has changed" is no longer an issue.
[Post edit - as per the comment from Oracle's Steven Davelaar below, as he correctly points out the above solution will not work for instead-of-triggers views as it relies on SQL RETURNING clause which is incompatible with this type of view]
Alternatively you can set the Change Indicator on one of the attributes. This will work as long as the relating column for the attribute in the database itself isn't inadvertently updated. In turn you're possibly just masking the issue rather than solving it, because if another developer turns the Change Indicator back on the original issue will return.