Wednesday, 10 January 2018

Concurrency Issues in Oracle

Avoiding duplicates in multi-user environments 

Avoiding concurrency issues in stored procedures can be tricky. I'm writing some functionality to add a document ID to a table in Oracle. The requirement is as follows:

The document ID must consist of a string identifying the entity a string identifying the month and a string identifying the year and an increment.

[entity]_[mm]_[yyyy]_[nnn] which might look like 9999_01_2017_1

At first glance this looks like it should be quite easy. query the data by entity, month, year. count and add one. Cast the numeric values to a varchar and Concatenate. This, however doesn't take into account the fact that two concurrent inserts might not see a unique count. In this case you'll end up with the, possibility disastrous, duplicate. This is the scenario that DBMS_LOCK was designed to resolve.

For me the solution was to employ the example here
This very good article clearly shows how to lock a portion of pl/sql while an update is carried out and then release the lock such that the process remains isolated and gets an unimpeded view of the data it's updating and can remain confident that the process will function without concurrency issues.

   v_doc_id varchar2(20);
   v_call_status INTEGER;
   v_lock_handle varchar2(128);

   BEGIN
      -- Calculate the document id
     DBMS_LOCK.allocate_unique( 'SET_APPROVAL_DOCUMENT_ID', v_lock_handle );
     v_call_status := DBMS_LOCK.REQUEST(v_lock_handle, DBMS_LOCK.X_MODE, 5, TRUE);
     IF v_call_status = 0 THEN
      Select ( select to_char(je.legal_entity_cd)||'_'||je.financial_period
                 from me_journal_entry je
                  where je.CFME_ID = p_cfme_id) ||'_'||
               ( select to_char(count(*)+1)
                   from V_ME_JOURNAL_APPROVAL_LOG al,
                        (select je.legal_entity_cd, je.financial_period
                           from me_journal_entry je
                          where je.CFME_ID = p_cfme_id) j
                  where al.is_approved = 'Y'
                    and j.legal_entity_cd = al.legal_entity_cd
                    and j.financial_period = al.financial_period)
        into v_doc_id
        from dual;
       
    -- Insert the approval row
   
    Insert into G79_CFM.ME_APPROVAL_LOG (IS_APPROVED, CFME_ID, APPROVAL_NOTES,USER_ID, APPROVAL_TYPE, CFME_DOCUMENT_ID, APPROVAL_DATE_TIME)
                                 Values (p_is_approved, p_cfme_id,  p_approval_notes, p_user_id, p_approval_type, v_doc_id, sysdate());
   
        COMMIT;
    ELSE