Writing Reusable Service Programs

Article ID: 56298

There's a lot of material out there that explains ILE concepts to the RPG programmer, from articles, books, and IBM manuals to classes and user group presentations. They teach you what the components of ILE are, and the syntax of using them -- great stuff.

Once you've learned that, then what? Now that you've learned ILE, you know how it can be used, but do you know how it should be used?

This article discusses how I've learned to use ILE, and how to design ILE tools so that they can be called not only by ILE applications, but also from SQL, Java, PHP, and other environments.

Key Concepts

The intent of ILE is to let you design your software as a set of reusable components. To design your software as a series of components requires a different way of thinking about your software. You have to give thought to how to break your code up into smaller routines: Where do you draw the line between one routine and the next? You also have to think about how the routines will communicate with each other.

There are several key concepts that I use to determine where to break apart my routines. Here's a list of them:

  • Model View Controller (MVC) -- Your user interface should always be kept separate from your business rules.
  • Service Oriented Architecture (SOA) -- Design your business rules as "services" to be utilized by other routines.
  • Encapsulation -- Routines should be completely separate. One routine should not know or care how another routine works.
  • Statelessness -- Routines should not depend on previous calls, or calls to different routines, to leave data in a particular state.

I discuss these concepts in more detail below.

The Sample Application

In the April 2008 issue of System iNEWS magazine, we will demonstrate how different web technologies (PHP, Java, Groovy, and CGIDEV2) can interface with RPG business logic. I volunteered to coordinate this project, and to write the RPG logic that they'd interface with. I decided that an order entry application would make a good example, so I designed an RPG service program with a green screen front end. I then explained to the PHP, Java, and CGIDEV2 authors how to interface with this service program.

I use that same RPG service program for my examples in this article. At the end of the article, I provide a link so you can download the code and try it for yourself. Since I've included the entire program as something downloadable for you to try, I will keep the code snippets and screen shots in this article to a minimum.

Naming Convention

In my shop, we use service programs extensively. When we first started using them, we started running into name collisions. By that, I mean that two developers created subprocedures with the same name, but stored in different service programs. To eliminate those conflicts, we created the following naming convention:

  • A "base prefix" name is created for each service program. In the example presented today, that name is ORDER because the service program pertains to working with orders.
  • The primary module of the service program starts with the base prefix followed by an identifier of the language it was written in. In this case, ORDERR4 is the primary module, where the R4 stands for RPG IV.
  • The service program is named after the primary module.
  • If there are additional modules, a number is inserted between the base prefix and the language ID. In this example code, there's only one module, but if there were more, they'd be named ORDER2R4, ORDER3R4, and so forth. This makes it easy to relate modules to the service program that they're a part of.
  • Each service program has a corresponding copy book containing all of the definitions needed to use it. This includes prototypes, named constants and data structure definitions.
  • The copy book is named with the base prefix followed by _H, so in this example, the copy book is called ORDER_H. The H at the end stands for "header". We use this name because C programs typically work the same way -- their prototypes are kept in files that end in .h for "header".
  • All of the definitions in the copy book, as well as all of the procedures that can be called from outside of the service program are prefixed with the base prefix. So a routine that might've been called "loadHeader" will be called ORDER_loadHeader(). That way, it won't conflict with any other service program that happens to have a loadHeader subprocdure.

We've also found the base prefix (ORDER_) on our subprocedures makes it easier to read and troubleshoot our code. In some programs, we call routines from 10-15 different service programs, and it's immediately obvious which service program we're calling by the prefix.

Model - View - Controller (MVC)

MVC involves separating your code into three discrete pieces. The model is the business logic behind your application. The view is the user interface, and the controller is the logic that drives the program. The controller binds it all together, it calls the routines from the model and the view in the correct sequence to make an application.

The view is the user interface -- it's the part that a user will see. In the case of an interactive program, the view might be logic that loads a screen, displays it to the user, checks which function keys were pressed, and so forth. In a traditional green-screen RPG application, the view is partially coded in the DDS for the display file, and partially coded in the RPG code. The EXFMT that I use to display the screen, the IF statements that check the indicators for F3, F12, and so forth are all part of the view, as the program logic is directly related to the user interface. In a report program, the view might consist of O specs or DDS code that prints stuff on paper, as well as the RPG code that checks for page overflow. Anything that involves reading input from a user, or sending output to a user is part of the view.

The controller is usually the smallest, simplest part of an MVC program. It controls the flow of the application. It typically calls a routine from the view to find out what the user wants to do. It then calls a routine from the model to carry out that action, and calls another routine from the view to display the results.

My experience has been that the code in the controller is very closely tied to the logic in the view. This is especially true in an interactive program, since the flow of the program is controlled by the user's actions. Whenever you change the view, you always end up changing the controller as well. Consequently, I often "cheat" and put the controller and the view in the same module.

The model is where the business rules are implemented. I always keep the model separate from the view or controller, and I put a lot of effort into making the model as versatile as follows. The code in the model is usually the code you want to reuse. Think about it: You might have a green-screen application today, but might want a web interface tomorrow. Or perhaps you want to have a GUI Windows application instead of a web one. Or maybe you can utilize the same business rules in a batch program that runs the business logic over and over again and totals up the results. The model is where you implement your company's (or your client's) business strategy, and therefore you want it to be used throughout the system. By keeping the model separate from the view and controller, you can keep the business rules consistent across all interfaces. Conversely, if the business rules change, you only have to change the model and all of the front ends (Web, GUI, report, green screen) will all be updated.

A common mistake I've seen in the industry is when folks don't keep the display and business logic separate. For example, I saw someone who really wanted to modularize an application create a program that would display an order. It reads the order from the database, does all the calculations and loads it into a subfile. "You can call this program from any other program, any time you want to display an order!" the programmer told me. Trouble is, since the subfile logic is deeply integrated with the business logic, it can only be used to display an order. The same basic calculations could've been used to let cusotmer's view their orders on the web, except that the display file logic makes that impossible. The code would've also worked nicely for a report that show order statistics, since that report needs to go through all of the orders and calculate the details -- but in this case, those calculations were well-written and well-tested, but couldn't be reused because the display logic was so deeply integrated into the business logic.

So, that's the point behind MVC -- keep the display and business logic completely separate. In the sample program included with this article, I've done that simply by putting my business logic into a service program that does no user interfacing -- all it does is communicate with the calling program through parameters.

Service Oriented Architecture (SOA)

Please don't confuse SOA with web services. There's a lot of marketing hype and a lot of buzzwords surrounding SOA these days, and it's easy to get confused about what SOA means. SOA means that your business rules are organized into re-usable services.

Think about the conveniences that we who live in a city have. Various institutions provide electricity, running water, telephone service, health care, and more to us. We don't have to implement all of these things ourselves, do we? We don't have to set up our own telephones, or be our own doctors. We simply utilize the existing services. Anyone in our community can use the same services -- assuming they can afford them, of course. The services aren't designed with any particular customer in mind, but are designed to work for everyone.

SOA is based on the same concept. Write your business rules as a series of services that can be used by other pieces of software. Let any program access these services. In an ILE service program, this is implemented as a bunch of procedures that any other program can call. If you design your code well enough, any program can utilize these services. Don't design your service with a particular program in mind!

That concept is pretty much all there is to SOA -- SOA is just an architecture revolving around the concept of offering reusable services. Certainly web services and all the technologies that go with them are one method of implementing SOA. But, a simple ILE service program can also be considered SOA when designed as a series of services.

In the case of my order entry application, I designed a service program named ORDERR4. Based on the requirements of taking orders in my business, I came up with the following procedures that I need to have in my application:

  • ORDER_New(): Creates a new (empty) order for a given customer. Returns the default header values for that customer's orders.
  • ORDER_LoadHeader(): Returns the header information for an existing order. I defined "header information" as any information that applies to the order as a whole.
  • ORDER_LoadItems(): Returns the items that have been entered on a given order.
  • ORDER_SaveHeader(): Validates and saves the header information back to disk.
  • ORDER_SaveItem(): Validates a particular line item, and saves it's details back to disk.
  • ORDER_ChkItem(): Checks whether a particular item number is valid. If so, return a description of the item.
  • ORDER_ChkPrice(): Checks whether a keyed price is legitimate for a given item.
  • ORDER_ChkQuantity(): Check whether a keyed quantity is legitimate for a given item.

From the above services, I can write my order entry application. My controller logic will call these routines as needed to produce a valid order. When I need an additional function, such as calculating the total cost of an order, the estimated ship date for an order, etc., I can simply add additional services to my existing service program.

Encapsulation

It's my opinion that business logic should be designed to be easy to update as business needs change. Since the time spent testing and maintaining a computer application typically dwarfs the time spent writing the initial code, the more you can reduce maintenance time, the better!

Encapsulation is often referred to as "information hiding." The idea is to hide the implementation of a given routine. In other words, a given subprocedure should be a "black box". You provide it with the parameters it needs to do it's job, and it does it. You don't care how the work gets done, and it doesn't usually know or care what you'll do with the result.

Let's take a step back and look at our history. Historically, RPG code was broken into subroutines, and the way subroutines communicate with the rest of the program is through "global variables" (variables that exist, and are accessible to the entire program.) It's not possible for a subroutine to be truly independent of the rest of the code in a program, is it? If you change the size or data type of a variable in a subroutine, you also have to scan the code to see where else that variable is used, and ensure that it won't break the rest of the code. Even if a variable is only used as a temporary loop index, or to hold a temporary value that's used within a calculation, you still have to make sure that any changes you make to the variable won't break something in the rest of the program. If you add a new variable to the subroutine, you have to scan the rest of the program to verify that the name isn't already in use. Even if all you change is the output value of the variable, you have to do some sort of analysis of the rest of the code to verify that it won't cause something else to break -- without searching the code, you can't be sure. Even once you've decided that it's safe to change something, you still have to re-test the code -- all of it, not just the subroutine -- to verify that you didn't miss something.

Historically, if you wanted to write a routine that could be called independently from many programs, and you wanted to be sure that changes to that routine didn't break other programs, a subroutine wouldn't do the job. You'd have to resort to creating a separate program. Putting it in a separate program provides better "encapsulation" than calling a subroutine. However, program calls can be cumbersome. CALL/PARM is not as simple to use as EXSR, doesn't perform as well, and requires you to use a separate source member for each individual routine, which can make it cumbersome to maintain. Plus, flipping between lots and lots of members can make the code hard to follow. To add to all of that, it's still not very well encapsulated -- CALL/PARM doesn't provide a mechanism to force you to pass the same data types or lengths for it's parameters, and it doesn't provide a mechanism to specify which parameters are for input, which are for output, which are option, etc. In even older implementations of RPG, they didn't have parameters, and had to use the LDA, which really makes encapsulation difficult since it doesn't specify which data is located where, and the parameter space is shared by all of the programs running a job stream. Any time you made a change to data stored in the LDA, you had to do an analysis of all of the programs in the job stream to verify that it wouldn't break anything -- that's poor encapsulation.

In ILE RPG encapsulation is provided by creating a prototype (PR) and a matching procedure interface (PI) for your program or subprocedure. Since an ILE service program can have many subprocedures that can be called independently, you don't necessarily have to have a separate source member for each. When you have multiple subprocedures in the same module, they can share global variables with one another without sharing those variables with the routines that call them. You can also provide subprocedures that are callable by the routines inside a service program but aren't callable from outside the service program, which provides the ability to break your routines up into smaller pieces without sacrificing encapsulation.

The basic idea behind implementing encapsulation is to create a stable interface for your routine via the PR/PI. For example:

     D ORDER_new       PI             1n
     D   CustNo                       6p 0 const
     D   Header                            likeds(ORDER_Header_t)

This interface is the only means of calling my ORDER_new() procedure. Even if I change the code that runs behind the ORDER_new() routine, I don't have to change any of it's callers, as long as I don't change the preceding interface.

I keep my interfaces defined as narrowly as possible. The CONST keyword ensures that my CustNo parameter is not changed by the subprocedure. Callers don't have to worry about this procedure changing the value of the field. Even if I accidentally added code to change it, the compiler would stop that code from complling because I declared the parameter as CONST.

As long as I don't change the procedure interface, I won't break compatibility with any of the routines that call my procedure. Of course, ORDER_new() really just loads data from a database, and isn't too likely to change, so let's look at a different example, the ORDER_ChkPrice() procedure interface:

     D ORDER_chkPrice...
     D                 PI             1n
     D  ItemNo                        8a   const
     D  Price                         9p 2 const

This routine checks a price to see if it's valid. In my first revision of the service program, which is the one that you can download, the code simply looks up the item in a database, and checks that the price within the range of valid prices defined in that database. The record has fields named LowPrice and HighPrice -- as long as the keyed price is in their range, it's okay.

However, tomorrow I might want to implement this completely differently. For example, perhaps I might want to look up the current market value for the raw materials that make up the item I'm selling. For example, I work for a sausage company, and in our business it doesn't make sense to sell sausage for less than the current price of beef or pork. So, I might check to see what the current market price for one of those ingredients is.

Or perhaps I'm using RPG's native I/O today, and tomorrow I want to use SQL. Or maybe I want to look up the price ranges in an Oracle database located on a different server. Or perhaps I want to get the price range by calling a web service.

The point is, as long as I don't change the procedure interface (and corresponding prototype) it doesn't matter how I go about implementing my ORDER_ChkPrice() routine. Since the interface didn't change, I know the existing routines that call my routine will continue to work. I only have to test my changes to ORDER_ChkPrice(), I don't have to re-test all of the routines that call it.

In fact, as long as I'm been careful not to change my interface, I don't even have to know which routines call my ORDER_ChkPrice() subprocedure! I don't even have to do any analysis to search through the system to find out who the callers are, since I know I didn't break compatibility. So not only did I save time by eliminating the need to change calling routines and re-test them, but I also saved time by eliminating the need for analysis of where those routines are.

Encapsulation becomes even more important if you work for a software vendor. If you sell your software to thousands of customers, you don't necessarily know how your customers will use your routines. You certainly can't do an analysis of all of the callers, since you don't have access to their code! If you provide proper encapsulation, you won't need to!

Statelessness

The idea of statelessness is to treat every procedure call as if it's completely unrelated to the other procedure calls. For example, one procedure shouldn't expect that a previously called procedure has already loaded a record that it needs. It shouldn't assume that a previous procedure call has left a given record locked. It shouldn't expect that the database cursor is in a given position.

For example, in my order entry application, an order can contain many different line items. It might be tempting for the ORDER_LoadHeader() to perform a SETLL on the database file that contains the items, and to have an ORDER_LoadItem() that reads the next item from the file using READE. That way, I could call ORDER_LoadItem() in a loop to read the entire order's contents. This is a bad idea because it's "stateful". Each call to ORDER_LoadItem() expects the database file to be in a particular state that was set by a previous call to ORDER_loadHeader() or ORDER_loadItem(). In a traditional interactive application, this stateful situation wouldn't cause a problem as long as I coded the calling routine carefully. However, what if the job was running two programs that both call ORDER_LoadItem()? Now one program would interfere with the other, because the first program might change the file cursor, causing the second program to read the wrong record.

Even worse, what if I wanted to call my service program from a web application? In a web application, every time the user clicks a button or link in his web browser, my controller program is called again. My controller might be running in a separate job every time, so the file position would not necessarily be in the same place.

To solve the problem, I make my routine stateless. Instead of performing SETLL and READE in separate calls, I load all of the data for a given order into an array, and return the entire array at once. That way, the position in the file doesn't have to be maintained from call to call. There's no "state" that has to be maintained between calls.

Although I believe statelessness to be an important concept, it can sometimes make an application unwieldy to write, so sometimes I cheat a little bit. In particular, I find that saving error information in global variables that are encapsulated inside my business model makes error handling more elegant. For more information about how I make stateful error handling work in a stateless environment, see the sections on "Error Handling" and "Stored Procedure Wrappers" below.

Using RPG File Access

Keeping the database access stateless is important when using RPG's native I/O in a service program, as discussed in the preceding section. That way, you don't have to worry about how many different programs within a given job will utilize your service program.

Another question that is common among programmers who are new to service programs is that of opening and closing the files. How do you know when to open the files in a service program? How do you know when all of the callers are done, so you can close the files?

I tend to open the files the first time my service program is called. Then, I just leave them open until the activation group ends.

In order to make sure they're opened on the first call, I write an OpenFiles() subprocedure and call it at the start of every exported routine. For example, at the start of the ORDER_new(), I call OpenFiles() as follows:

     P ORDER_new       B                   export
     D ORDER_new       PI             1n
     D   CustNo                       6p 0 const
     D   Header                            likeds(ORDER_Header_t)

     D CUSTFILE1       ds                  likerec(CUSTFILEF:*INPUT)
      /free
         openFiles();
          .
          .

Every single routine that can be called from outside of my service program calls OpenFiles() as it's first statement. As you can see, it's just one line of code to call it, so it's not too big of a deal.

Here's what the OpenFiles() routine looks like:

      *****************************************************
      * openFiles(): Open files used by this srvpgm
      *****************************************************
     P openFiles       B
     D openFiles       PI
      /free
        if (FilesAreOpen);
           return;
        endif;

        open ORDHEAD;
        open ORDITEM;
        open CUSTFILE;
        open ITEMFILE;
        open CTRLFILE;

        FilesAreOpen=*ON;
        return;

        begsr *pssr;
           close *all;
           FilesAreOpen=*OFF;
        endsr;

      /end-free
     P                 E

In this example, the FilesAreOpen variable is a variable that's global to the module. When the OpenFiles() procedure is called, it first verifies that this variable hasn't already been set -- and if it hasn't, it uses the OPEN op-code to open each file. If the FilesAreOpen variable is already set to *ON, it exits the routine without doing anything, that way I don't have to worry about accidentally opening the files twice. My files are defined as USROPN on the F-spec so that that won't be opened automatically, I have to open them manually with the OPEN op-code.

Did you know that you can code a *PSSR that's part of a subprocedure? If any errors occur in that subprocedure, the *PSSR is called to handle the error. In this example, if any of the files fail to open, the *PSSR routine is called. It uses CLOSE *ALL to close any of the previous files that might've been opened, and then sets FilesAreOpen off. That way, if something is wrong, the service program will try again on the next call.

In this example program, I didn't bother coding a routine to close the files. They'll be automatically close when the activation group ends, otherwise they'll be left open and active in memory which speeds up subsequent calls to my service program.

In a more sophisticated environment, I might use the CEE4RAGE API to register a subprocedure to be called when the activation group ends. That way, I can add additional code to clean up temporary objects, etc., when my routine ends.

Error Handling

Some people recommend having parameters on each subprocedure that can be used to communicate error information to the calling procedure. This is very similar to the way IBM's APIs handle errors, they provide a data structure in which they can store the error message ID and the corresponding message data.

Personally, I find it easier to use a global variable in the service program to store the last error message that occurred in the service program. Then, I use a subprocedure to provide access to the error message, and optionally the error number.

In the code for the business model, I'll write something like this:

         chain CustNo CUSTFILEF CUSTFILE1;
         if not %found;
            SetError( ORDER_ERROR_CUST_NOT_FOUND
                    : 'Customer ' + %editc(CustNo:'X') + ' not found!');
            return *OFF;
         endif;

In this example, I use the CHAIN op-code to read a record from the customer file. If the customer number isn't found in that file, I set an error message that says "Customer XXXX not found". ORDER_ERROR_CUST_NOT_FOUND is merely a named constant that corresponds to the error message.

After setting the error message, the procedure returns *OFF, which the caller can use to determine that the routine failed.

The SetError() procedure is not exported from the module, so it can't be called by the calling application, it can only be called by code within the business model. It looks like this:

     P setError        B
     D setError        PI
     D   ErrId                        7a   const
     D   ErrMsg                      80a   varying const
      /free
         Error.MsgId = ErrId;
         Error.Msg   = ErrMsg;
      /end-free
     P                 E

As you can see, there's not much to it. It simply receives the error number and error message as parameters and saves them off to the Error.MsgId and Error.Msg variables, which are global variables in the module.

Another exported subprocedure named ORDER_error() will return the error message and can optionally return the error number as well. This makes it easy to check for errors in a calling ILE program:

    if (ORDER_new(xxxxx: xxxxx) = *OFF);
         errMsg = ORDER_Error();
         // show errMsg to user.
    endif;

In this example, if ORDER_new() fails, it returns *OFF -- so the system gets the return value from ORDER_error() which contains the error message and assigns it to the errMsg variable. The calling application would then use some means of displaying errMsg to the user to let them know that something went wrong.

If ORDER_New() is successful, it won't return *OFF, so the call to ORDER_error() will never happen.

The problem with this method of error handling is that it's not stateless -- in the above example, when ORDER_new() fails, it sets a condition in the service program that's retrieved by calling the service program a second time. That's not usually a problem with error handling, because you typically will want to call ORDER_error() immediately after ORDER_new(). You wouldn't want to do something in-between (such as calling another program, or returning control to the browser). As long as they're called back-to-back with nothing in between, it doesn't matter that it's stateful.

The exception is when the service program routines are called independently from a non-ILE environment. For example, if they're called as web services, the statefulness of the error handling would be a problematic. I solve that problem by creating a wrapper procedure (which one might call a "facade") that calls the ORDER_New() routine, and the ORDER_error() routine back-to-back, and returns the results of both in a single parameter list. That way, I can have the elegant results that I want when calling my routine from an ILE language, and still have statelessness when calling it from somewhere else. Best of both worlds!

Stored Procedure Wrappers

I prefer to let non-ILE languages call my code via a stored procedure interface, so I typically design my wrapper routines with stored procedures in mind.

Stored procedures have the ability to return a result set which can contain many different records, and each record can contain many different fields. One of the really neat aspects of result sets is that they contain meta-data that a caller can read to get the size, data type and names of the returned fields. The caller can use this meta-data to interpret the response in much the same way that RPG programs can use externally definitions to get a record layout with field definitions. It makes it possible for the caller to avoid hard-coding these details.

Because of this behavior, I always code stored procedure wrappers for my subprocedures. I never attempt to use the same routines for both ILE and non-ILE access, unless I go through a wrapper. My stored procedure wrappers are all suffixed with "_sp" to separate them from the other procedures with the same names. For example:

     P ORDER_new_sp    B                   export
     D ORDER_new_sp    PI
     D   CustNo                       6p 0 const
     D Hdr             ds                  likeds(Order_Header_t)

     D RESULT1         ds                  qualified occurs(1)
     D  OrderNo                      10a   inz
     D  CustNo                        6p 0 inz
     D  SCAC                          4a   inz
     D  ShipName                     30a   inz
     D  ShipAddr1                    30a   inz
     D  ShipAddr2                    30a   inz
     D  ShipAddr3                    30a   inz
     D  BillName                     30a   inz
     D  BillAddr1                    30a   inz
     D  BillAddr2                    30a   inz
     D  BillAddr3                    30a   inz
     D  ShipDate                     10a
     D  MsgId                         7a   inz
     D  Msg                          80a   varying inz

     C/exec SQL set option naming=*SYS,commit=*NONE
     C/end-exec

      /free
        %occur(RESULT1) = 1;

        if (ORDER_new(CustNo: hdr));
           Result1.OrderNo   = hdr.OrderNo;
           Result1.SCAC      = hdr.SCAC;
           Result1.CustNo    = hdr.CustNo;
           Result1.ShipName  = hdr.ShipTo.Name;
           Result1.ShipAddr1 = hdr.ShipTo.Addr1;
           Result1.ShipAddr2 = hdr.ShipTo.Addr2;
           Result1.ShipAddr3 = hdr.ShipTo.Addr3;
           Result1.BillName  = hdr.BillTo.Name;
           Result1.BillAddr1 = hdr.BillTo.Addr1;
           Result1.BillAddr2 = hdr.BillTo.Addr2;
           Result1.BillAddr3 = hdr.BillTo.Addr3;
           Result1.ShipDate  = %char(hdr.ShipDate:*ISO);
        else;
           Result1.Msg = ORDER_error(RESULT1.MsgID);
        endif;

      /end-free
     C/exec SQL set Result sets Array :RESULT1 for 1 Rows
     C/end-exec
     P                 E

In this example, ORDER_new_sp() is the stored procedure wrapper. It calls the ordinary ORDER_new() ILE routine, and if that routine is successful it copies all of the returned data into a Result1 data structure which is returned via the "Set Result sets" SQL code at the bottom. If the call to ORDER_new() fails, it sets the error message instead. In any case, all of the output fields are returned as part of the result set so that the caller can take advantage of the result set's meta-data.

This wrapper also solves the problem of error information not being stateless because it calls ORDER_error() immediately upon failure. Since the caller only calls one routine (ORDER_error_sp) the result is effectively stateless.

Once I've written the RPG code for my stored procedure wrappers, I run the SQL CREATE PROCEDURE command to identify the stored procedure interface to SQL.

CREATE PROCEDURE ORDER_NEW(
          IN CustNo DECIMAL(6,0)
       )
       LANGUAGE RPGLE
       NOT DETERMINISTIC
       CONTAINS SQL
       EXTERNAL NAME 'ISNMAG/ORDERR4(ORDER_NEW_SP)'
       PARAMETER STYLE GENERAL;

In this example, the SQL CALL statement that calls the stored procedure will call a procedure named 'ORDER_NEW'. The ORDER_NEW SQL procedure calls the ORDER_NEW_SP routine in the ORDERR4 service program that's located in the ISNMAG library.

That way, when I call these routines from Java, PHP, .NET, etc, they can use the name "ORDER_NEW" which looks the same as the main ORDER_New() procedure that I'd call from the main code. It creates the illusion that both the ILE and non-ILE methods are calling the same routine. Pretty cool, eh?

Once I've created my stored procedure wrappers and created the procedure interfaces in SQL, I can run my stored procedure from pretty much anywhere that can use SQL. Plus, can test it from the Run SQL Scripts option of iSeries Navigator. Since Java, .NET and PHP can all use SQL databases, this makes it easy to interface with them. A GUI application in Windows would also be able to call the stored procedure the same way. It provides a lot of versatility!

Conclusion

Using service programs properly in ILE RPG can significantly reduce maintenance time, and provide some very versatile, reusable code! Learning how to do this stuff does involve a learning curve, but hopefully the concepts I've presented in this article will help lessen that curve for you.

If you find my methods interesting, please download the the source code to use as an example for your own projects.

Code Download

You can download the source code for this article from the following link. Be sure to read the README file included in the download for instructions of how to install the code on your system:
http://www.pentontech.com/IBMContent/Documents/article/56298_465_Code.zip

ProVIP Sponsors

ProVIP Sponsors