Part 1 looked at the “obvious” way for an Application Engine to call a Component Interface and why that can result in fragile and abend-prone batch programs.
Here’s a simple Application Class that handles most of the low-level technical details around the generally recommended approach: passing a Rowset (in this particular case, to be exact, an in-memory Record instance) to the CI instead. You just have to plug in your actual business in one method, ci_business_logic.
The base classes, along with more explanations and sample code, are on my AE2CI repository on Github.
I didn’t originally intend to write it as an Application Class, but while adapting some earlier code I had written for a client, I realized that most of the repetitive and tricky code could be moved to a reusable class.
Most of the “technical” code is in the base class. Business logic goes in your subclasses’ ci_business_logic method
Reliable error handling
Catch errors and throw exceptions automatically. Distinguish between data errors (expected) and coding errors. Display errors in message fields.
Commit row by row. Allow the user to correct the data for rejected rows. If multiple CIs are being called, support all-or-nothing writes
Application Engine peoplecode
/* */ /*this is from Github */ import AE2CI:*; /* The 2 subclasses that contain your CI business logic */ import TCI:*; Component TCI:Wrap_CI_JOB_DATA &ci_job; Component TCI:Wrap_CI_PERSONAL_DATA &ci_personal; Local boolean &saved = False; /* this is the state record we use to save status/error messages on */ Local Record &rec_comm = GetRecord(Record.AE2CIAET); If (&ci_job = Null) Then &ci_job = create TCI:Wrap_CI_JOB_DATA(); End-If; If (&ci_personal = Null) Then &ci_personal = create TCI:Wrap_CI_PERSONAL_DATA(); End-If; /* All you need for the Rowset are the Fill query and the Record being used */ Local Rowset &rs_data = CreateRowset(Record.TCI_SOURCE); &rs_data.Fill("WHERE EMPLID = :1", TCI_AET.EMPLID); Local Record &rec_data = &rs_data.GetRow(1).GetRecord(Record.TCI_SOURCE); /* call first CI, handle exceptions */ try &saved = &ci_job.callci(&rec_comm, &rec_data); catch AE2CI:NoDataException &e_missing_job /* we're treating missing data slightly differently, because we can differentiate based on the exception class */ Exit (0); catch Exception &e_any_job SQLExec("ROLLBACK"); Exit (0); end-try; /* call second CI, handle exceptions */ try &saved = &ci_personal.callci(&rec_comm, &rec_data); catch Exception &e_ci_personal SQLExec("ROLLBACK"); Exit (0); end-try;
The CI wrapper subclasses doing the real work
Component TCI:Wrap_CI_JOB_DATA &ci_job; Component TCI:Wrap_CI_PERSONAL_DATA &ci_personal;
Prepping the data as a Rowset Record
Local Rowset &rs_data = CreateRowset(Record.TCI_SOURCE); &rs_data.Fill("WHERE EMPLID = :1", TCI_AET.EMPLID); Local Record &rec_data = &rs_data.GetRow(1).GetRecord(Record.TCI_SOURCE);
Calling the CI wrappers
try &saved = &ci_job.callci(&rec_comm, &rec_data); catch AE2CI:NoDataException &e_missing_job /* we're treating missing data slightly differently, because we can differentiate based on the exception class */ Exit (0); catch Exception &e_any_job SQLExec("ROLLBACK"); Exit (0); end-try;
The AE really doesn’t know much, it just calls the CI with both &rec_comm, the communication record for the wrapper and &rec_data holding the data to be loaded. The wrapper’s callci method does all the work and returns a boolean to indicate whether anything was saved or not. It can also throw an exception.Having the AE initiate rollbacks here allows one CI to save successfully first, but then rollback in case another CI has any problems. The individual CIs don’t have to care whether they are operating solo or not.
Not bad, 40 lines of code to call 2 CIs and manage a transaction around them…
This is one of the CI wrapper subclasses, which you would have to write. The other one looks much the same.
import AE2CI:*; class Wrap_CI_PERSONAL_DATA extends AE2CI:CiWrapper method Wrap_CI_PERSONAL_DATA(); method callci(&rec_comm As Record, &rec_data As Record) Returns boolean; method ci_business_logic(&rec_comm As Record, &data As Record) Returns boolean; end-class; method Wrap_CI_PERSONAL_DATA %Super = create AE2CI:CiWrapper(CompIntfc.CI_PERSONAL_DATA); end-method;
This method mostly contains regular Component Interface type logic. You can need to return a boolean indicating whether or not
/* all of the following is strictly business-specific logic and depends on the CI, the data record as well as the business requirements */ method ci_business_logic /+ &rec_comm as Record, +/ /+ &data as Record +/ /+ Returns Boolean +/ /* sample minimal update-only implementation*/ Local boolean &needs_saving; %Super.myCI.KEYPROP_EMPLID = &data.EMPLID.Value; &needs_saving = %Super.myCI.Get(); If Not &needs_saving Then rem assign user/developer feedback to the message-holding field; rem %Super.fld_message.Value = "no PERSONAL_DATA for EMPLID." | &data.EMPLID.Value; /* indicate you don't need a save */ Return False; rem or throw an Exception..., which will take care of message updating...; &msg = "no data for EMPLID." | &data.EMPLID.Value; throw create AE2CI:NoDataException(&msg, %This); End-If; &needs_saving = %This.check_business_logic_ok(&rec_comm); If Not &needs_saving Then Return False; End-If; If All(&data.BIRTHCOUNTRY.Value) Then %Super.myCI.PROP_BIRTHCOUNTRY = &data.BIRTHCOUNTRY.Value; &needs_saving = True; End-If; Return &needs_saving; end-method;
This method is where you map your incoming data to the CI being used. You will need to return a boolean indicating if saving is required. Notice also that when you see some expected business problem with the data, you populate the %Super.fld_message.Value and return False. The rest of the code is much of the same and is typical of standard PeopleSoft Component Interface code.
You can also call check_business_logic_ok() on the base class at any time – it checks the CI’s specialized attributes/methods like ErrorPending on your behalf. The wrapper class will automatically call it once again before attempting calling the Component Interface save().
Note: I did not show method callci because it is always exactly the same, but needs to be re-implemented on each subclass, at least on PT 8.51, otherwise it would call the super’s ci_business_logic.
The CI will operate as normal, but any errors and rollbacks it throws will be caught and notified to the Application Engine rather than causing an abend.
I’ve experimented with fairly “hard” errors, like division by zeros or purposefully referencing wrong fieldnames in the data record or the CI. Even a compilation error, by declaring a function and then subsequently deleting that function. The exception details will be communicated to the Notification and Message fields in &rec_comm.
Try/catch in PeopleSoft is actually fairly reliable and robust, it’s only really the unexpected rollbacks that are causing troubles with Application Engine.
There is one last, separate, Application Engine step required, which is to update the database with the values of the notification and message fields – those have been in-memory only so far, to allow them to survive rollbacks. For more details that and how to set up the Application Engine loop, please refer to my Github sample code.
This is an overall diagram of the process: