views:

167

answers:

2

Friends,

This Ask Tom thread which I found via another SO question, mentions Table and Transactional API's and I'm trying to understand the difference between them.

A Table API (TAPI) is where there is no access to the underlying tables and there are "getters" & "setters" to obtain information.

For example to select an address I would:

   the_address := get_address(address_id);

Instead of:

   select the_address
   from some_table
   where identifier = address_id

And then to change the address I would invoke another TAPI which takes care of the change:

   ...
   change_address(address_id, new_address);
   ...

A Transactional API (XAPI) is again where there is no direct access to modify the information in the table but I can select from it? (this is where my understanding is kind of hazy)

To select an address I would:

   select the_address
   from some_table
   where identifier = address_id

and then to change it I would call

   ...
   change_address(address_id, new_address);
   ...

So the only difference I can see between a TAPI and a XAPI is the method in which a record is retrieved from the database, i.e. a Select Versus a PL/SQL call?

Is that it? or have I missed the point entirely?

+3  A: 

A table API (TAPI) is a simple API that provides the basic CRUD operations for a table. For example, if we have a tableR MYTABLE (id INTEGER PRIMARY KEY, text VACHAR2(30)), then the TAPI would be something like:

package mytable_tapi is
    procedure create_rec (p_id integer, p_text varchar2);
    procedure update_rec (p_id integer, p_text varchar2);
    procedure delete_rec (p_id integer);
    function get_rec (p_id integer) returns mytable%rowtype;
end;

When you use TAPIs, every table has a TAPI, and every insert, update and delete goes through the TAPI.

A transaction API (XAPI) is an API that works at the transaction level rather than at the individual CRUD level (though in some cases this will be the same thing). For example, a XAPI to handle a banking transactions might look something like this:

package banking_xapi is
    procedure make_transfer (p_from_account integer, p_to_account integer,
                             p_amount number);
    ... -- other XAPI procs
end;

The make_transfer procedure may not perform a single insert, update or delete. It may do something like this:

procedure make_transfer (p_from_account integer, p_to_account integer,
                         p_amount number)
is
begin
    insert into transfer_details (from_account, to_account, amount)
       values (p_from_account, p_to_account, p_amount);

    update accounts set balance = balance-p_amount
    where account_no = p_from_account;

    update accounts set balance = balance+p_amount
    where account_no = p_to_account;
end;

i.e. it performs an entire transaction, which may consist of 1 or several DML statements.

A TAPI proponent would say that this is coded wrong and should contain no DML, but instead call TAPI code like:

procedure make_transfer (p_from_account integer, p_to_account integer,
                         p_amount number)
is
begin
    transactions_tapi.insert_rec (p_from_account, p_to_account, p_amount);

    accounts_tapi.update_rec (p_from_account, -p_amount);

    accounts_tapi.update_rec (p_to_account, p_amount);
end;

Others (like Tom Kyte and myself) would see this as overkill, adding no real value.

So you can have XAPIs alone (Tom Kyte's way), or XAPIs that call TAPIs (Steve Feuerstein's way). But some systems have TAPIs alone, which is really poor - i.e. they leave it to writers of the user interface to string together the necessary TAPI calls to make up a transaction. See my blog for the implications of that approach.

Tony Andrews
+1 Thanks Tony for response
carpenteri
+7  A: 

Let's start with the Table API. This is the practice of mediating access to tables through a PL/SQL API. So, we have a package per table, which should be generated from the data dictionary. The package presents a standard set of procedures for issuing DML against the table and some functions for retrieving data.

By comparison a Transactional API represents a Unit Of Work. It doesn't expose any information about the underlying database objects at all. Transactional APIs offer better encapsulation, and a cleaner interface.

The contrast is like this. Consider these business rules for creating a new Department:

  1. The new Department must have a Name and Location
  2. The new Department must have a manager, who must be an existing Employee
  3. Other existing Employees may be transferred to the new Department
  4. New employees may be assigned to the new Department
  5. The new Department must have at least two Employees assigned (including the manager)

Using Table APIs the transaction might look something like this:

DECLARE
    dno pls_integer;
    emp_count pls_integer;
BEGIN
    dept_utils.insert_one_rec(:new_name, :new_loc, dno);
    emp_utils.update_one_rec(:new_mgr_no ,p_job=>'MGR’ ,p_deptno=>dno);
    emp_utils.update_multi_recs(:transfer_emp_array, p_deptno=>dno);
    FOR idx IN :new_hires_array.FIRST..:new_hires_array.LAST LOOP
        :new_hires_array(idx).deptno := dno;
    END LOOP;
    emp_utils.insert_multi_recs(:new_hires_array);
    emp_count := emp_utils.get_count(p_deptno=>dno); 
    IF emp_count < 2 THEN
        raise_application_error(-20000, ‘Not enough employees’);
    END IF;
END;
/

Whereas with a Transactional API it is much simpler:

DECLARE
    dno subtype_pkg.deptno;
BEGIN
    dept_txns.create_new_dept(:new_name
                                , :new_loc
                                , :new_mgr_no
                                , :transfer_emps_array
                                , :new_hires_array
                                , dno);
END;
/

So why the difference in retrieving data? Because the Transactional API approach discourages generic get() functions in order to avoid the mindless use of inefficient SELECT statements.

For example, if you just want the salary and commission for an Employee, querying this ...

select sal, comm
into l_sal, l_comm
from emp
where empno = p_eno;

... is better than executing this ...

l_emprec := emp_utils.get_whole_row(p_eno);

...especially if the Employee record has LOB columns.

It is also more efficient than:

l_sql := emp_utils.get_sal(p_eno);
l_comm := emp_utils.get_comm(p_eno);

... if each of those getters executes a separate SELECT statement. Which is not unknown: it's a bad OO practice that leads to horrible database performance.

The proponents of Table APIs argue for them on the basis that they shield the developer from needing to think about SQL. The people who deprecate them dislike Table APIs for the very same reason. Even the best Table APIs tend to encourage RBAR processing. If we write our own SQL each time we're more likely to choose a set-based approach.

Using Transactional APis doesn't necessarily rule out the use of get_resultset() functions. There is still a lot of value in a querying API. But it's more likely to be built out of views and functions implementing joins than SELECTs on individual tables.

Incidentally, I think building Transactional APIs on top of Table APIs is not a good idea: we still have siloed SQL statements instead of carefully written joins.

As an illustration, here are to different implemntations of a transactional API to update the salary of every Employee in a Region (REgion being a large scale section of the organisation; Departments are assigned to Regions).

The first version has no pure SQL just Table API calls, I don't think this is a straw man: it uses the sort of functionality I have seen in Table API packages (although some use dynamic SQL rather than named SET_XXX() procedures).

create or replace procedure adjust_sal_by_region
    (p_region in dept.region%type
           , p_sal_adjustment in number )
as
    emps_rc sys_refcursor;
    emp_rec emp%rowtype;
    depts_rc sys_refcursor;
    dept_rec dept%rowtype;
begin
    depts_rc := dept_utils.get_depts_by_region(p_region);

    << depts >>
    loop
        fetch depts_rc into dept_rec;
        exit when depts_rc%notfound;
        emps_rc := emp_utils.get_emps_by_dept(dept_rec.deptno);

        << emps >>
        loop
            fetch emps_rc into emp_rec;
            exit when emps_rc%notfound;
            emp_rec.sal := emp_rec.sal * p_sal_adjustment;
            emp_utils.set_sal(emp_rec.empno, emp_rec.sal);
        end loop emps;

    end loop depts;

end adjust_sal_by_region;
/

The equivalent implementation in SQL:

create or replace procedure adjust_sal_by_region
    (p_region in dept.region%type
           , p_sal_adjustment in number )
as
begin
    update emp e
    set e.sal = e.sal * p_sal_adjustment
    where e.deptno in ( select d.deptno 
                        from dept d
                        where d.region = p_region );
end adjust_sal_by_region;
/

This is much nicer than the nested cursor loops and single row update of the previous version. This is because in SQL it is a cinch to write the join we need to select Employees by Region. It is a lot harder using a Table API, because Region is not a key of Employees.

To be fair, if we have a Table API which supports dynamic SQL, things are better but still not ideal:

create or replace procedure adjust_sal_by_region
    (p_region in dept.region%type
           , p_sal_adjustment in number )
as
    emps_rc sys_refcursor;
    emp_rec emp%rowtype;
begin
    emps_rc := emp_utils.get_all_emps(
                    p_where_clause=>'deptno in ( select d.deptno 
                        from dept d where d.region = '||p_region||' )' );

    << emps >>
    loop
        fetch emps_rc into emp_rec;
        exit when emps_rc%notfound;
        emp_rec.sal := emp_rec.sal * p_sal_adjustment;
        emp_utils.set_sal(emp_rec.empno, emp_rec.sal);
    end loop emps;

end adjust_sal_by_region;
/

last word

Having said all that, there are scenarios where Table APIs can be useful, situations when we only want to interact with single tables in fairly standard ways. An obvious case might be producing or consuming data feeds from other systems e.g. ETL.

If you want to investigate the use of Table APIs, the best place to start is Steven Feuerstein's Quest CodeGen Utility (formerly QNXO). This is about as good as TAPI generators get, and it's free.

APC
+1 Excellently put!
Tony Andrews
@APC Thank you for the comprehensive and lucid response. Can I ask you to expand on your last paragraph; "Incidentally, I think building Transactional APIs over Table APIs is not a good idea: we still have siloed SQL statements instead of carefully written joins." I have read this twice and come to two different conclusions (which is my fault and not yours) I just want to be crystal clear about this!
carpenteri
+1 Excellent answer
Rob van Wijk
+100 very nice answer :)
Jeffrey Kemp
I'm not at all sure a TAPI guy like SF would approve of allowing you to pass in a dynamic WHERE clause as per your last example, since it means writing (fragments of) SQL.
Tony Andrews
If I could upvote twice I would - great answer with good examples.
Paul James
@TonyAndrews - on the contrary, Steven's QCGU package generates precisely that sort of procedure.
APC
Really? That sounds insane...?
Tony Andrews
@TonyAndrews - Steve's a crazy kinda guy!
APC
So I could use p_where_clause like this: '1=0 UNION select * from emp where empno=1234"?!
Tony Andrews
@TonyAndrews - the default behaviour is to use strongly typed cursors but there are ways of finagling the projection. QCGU comes with mechanisms for customising the generated packages. So, if we feel strongly about the risks of SQL injection we use a template which doesn't generate support for dynamic SQL.
APC