The case of the phantom ADF developer (and other yarns)
Posted
by Chris Muir
on Oracle Blogs
See other posts from Oracle Blogs
or by Chris Muir
Published on Wed, 21 Mar 2012 07:57:17 +0000
Indexed on
2012/03/21
23:36 UTC
Read the original article
Hit count: 513
/Oracle
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:
A false conclusion
Let's verify that ADF is in fact issuing the correct SQL LOCK-FOR-UPDATE statement to the database.
[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:
[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
[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
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?
To answer the first question, how does ADF know another user has changed the row, the Fusion Guide's section 4.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:
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
- Database triggers
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.
- Default values
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.
- Instead of trigger views
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.
Solving the mystery once and for all
[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]
© Oracle Blogs or respective owner