The accessible_by_clause
was introduced in Oracle Database 12 Release 1 and extended in Release 2. If you don‘t know this feature, I suggest having a look at the documentation or reading Steven Feuerstein’s blog post.
In this blog post, I talk about how to use this feature properly.
Consider you have a schema the_api
and there’s a package math
with the following signature:
CREATE OR REPLACE PACKAGE the_api.math AS
/**
* Calculates the sum of all integers found in a string.
*
* @param in_integers string containing integers to be summarized
* @returns sum, NULL if no integers are found
*/
FUNCTION get_sum(
in_integers IN VARCHAR2
) RETURN INTEGER DETERMINISTIC;
/**
* Calculates the digit sum of an integer.
*
* @param in_integer input integer to calculate cross sum from
* @returns cross sum, NULL if input is NULL
*/
FUNCTION get_cross_sum(
in_integer IN INTEGER
) RETURN INTEGER DETERMINISTIC;
END math;
/
The next query uses the provided functions get_sum
and get_cross_sum
:
SELECT the_api.math.get_sum('What is the sum of 5, 7, 13 and 17?') AS the_sum,
the_api.math.get_cross_sum(3456789) AS the_cross_sum
FROM dual;
THE_SUM THE_CROSS_SUM
---------- -------------
42 42
No accessible_by_clause
In an Oracle Database 11g the package body might be implemented like this:
CREATE OR REPLACE PACKAGE BODY the_api.math AS
FUNCTION get_sum(
in_integers IN sys.ora_mining_number_nt
) RETURN INTEGER DETERMINISTIC IS
l_result INTEGER;
BEGIN
SELECT sum(column_value)
INTO l_result
FROM table(in_integers);
RETURN l_result;
END get_sum;
FUNCTION to_int_table(
in_integers IN VARCHAR2,
in_pattern IN VARCHAR2 DEFAULT '[0-9]+'
) RETURN sys.ora_mining_number_nt DETERMINISTIC IS
l_result sys.ora_mining_number_nt := sys.ora_mining_number_nt();
l_pos INTEGER := 1;
l_int INTEGER;
BEGIN
<<integer_tokens>>
LOOP
l_int := to_number(regexp_substr(in_integers, in_pattern, 1, l_pos));
EXIT integer_tokens WHEN l_int IS NULL;
l_result.EXTEND;
l_result(l_pos) := l_int;
l_pos := l_pos + 1;
END LOOP integer_tokens;
RETURN l_result;
END to_int_table;
FUNCTION get_sum(
in_integers IN VARCHAR2
) RETURN INTEGER DETERMINISTIC IS
BEGIN
RETURN get_sum(to_int_table(in_integers));
END get_sum;
FUNCTION get_cross_sum(
in_integer IN INTEGER
) RETURN INTEGER DETERMINISTIC IS
BEGIN
RETURN get_sum(to_int_table(to_char(in_integer), '[0-9]'));
END get_cross_sum;
END math;
/
The private functions get_sum
and to_int_table
are doing the real work. Here are some issues with this code
1. Use of undocumented collection type sys.ora_mining_number_nt
The private functions avoid the use of their own type, something like CREATE TYPE t_integer_type IS TABLE OF INTEGER. This shortcut is hidden and not part of the API. It is not super elegant, but quite common and easy to fix when Oracle decides to remove this collection type in a future release or to protect it with an accessible_by_clause
. So I do not consider this a real problem and will not deal with it in this blog post.
2. Private function definitions must be ordered according its usage
The private functions are listed at the top of the package body, hence no forward declarations are necessary. Forward declarations lead to some confusion since IDEs do not distinguish between declarations and definitions in the outline window and you often end up selecting the wrong one. However, without forward declaration you have to order your private functions according its usage, which might break your domain-specific ordering logic.
3. Private functions are not documented
I usually document the signature of a PL/SQL unit in the package specification only, that’s supported by PLDoc. Hence the private functions are treated like second-class citizens and left undocumented.
4. Private functions cannot be unit-tested
I’m not really a testing advocate. But as a developer, I’d like to know if my code works. I have to run it somehow. Usually more than once to get a working result. Hence I create scripts or unit tests. It is not possible to unit test the private functions directly. They have to be tested through public functions get_sum
and get_cross_sum
. In this case I’d like to test the private function to_int_table
directly.
The accessible_by_clause
can address issues 2, 3 and 4 without implicitly extending the API.
Package-Level accessible_by_clause
In Oracle Database 12c Release 1, the accessible_by_clause was introduced on the package level. This allows us to move the private functions from the package math
into a dedicated package math_internal
with restricted access. Here’s the refactoring result:
CREATE OR REPLACE PACKAGE the_api.math_internal
ACCESSIBLE BY (PACKAGE the_api.math, PACKAGE the_api.test_math_internal)
AS
/**
* Calculates the sum of all integers in a collection.
*
* @param in_integers collection of integers to be summarized
* @returns sum, NULL if collection is empty
*/
FUNCTION get_sum(
in_integers IN sys.ora_mining_number_nt
) RETURN INTEGER DETERMINISTIC;
/**
* Finds integer tokens in string.
*
* @param in_integers string containing integers to be tokenized
* @param in_pattern regular expression for integers
* @returns table of integers
*/
FUNCTION to_int_table(
in_integers IN VARCHAR2,
in_pattern IN VARCHAR2 DEFAULT '[0-9]+'
) RETURN sys.ora_mining_number_nt DETERMINISTIC;
END math_internal;
/
CREATE OR REPLACE PACKAGE the_api.math AS
/**
* Calculates the sum of all integers found in a string.
*
* @param in_integers string containing integers to be summarized
* @returns sum, NULL if no integers are found
*/
FUNCTION get_sum(
in_integers IN VARCHAR2
) RETURN INTEGER DETERMINISTIC;
/**
* Calculates the digit sum of an integer.
*
* @param in_integer input integer to calculate cross sum from
* @returns cross sum, NULL if input is NULL
*/
FUNCTION get_cross_sum(
in_integer IN INTEGER
) RETURN INTEGER DETERMINISTIC;
END math;
/
The accessible_by_clause
defined on line 2 restricts access to the package math
and the package test_math_internal
. It is important to note that the units referenced in the accessible_by_clause
are not checked for existence when compiling PL/SQL definitions, hence it is perfectly fine to list PL/SQL units in the accessible_by_clause
which might not exist in a production environment, such as the utPLSQL unit test package test_math_internal
.
With this change, I address the previously mentioned issues 2, 3 and 4.
- There are no private functions anymore, hence the order in the code is irrelevant
- All functions are documented
- All functions can be unit-tested
Looks good, right? Yes and no. This change created some new issues.
5. Splitting code that belongs together
The original math
the package was reasonably small and contained the whole processing logic. Now the package is divided into two packages and the code is spread into 4 files in the VCS (2 package specification files and 2 package body files). This accessiblity_clause is driving my PL/SQL code structure. This might be good in some cases, but in this case, I do not like it.
6. Accessibility per package leads to more code splitting
Remember I just wanted to unit-test the function to_int_table. Now I can also unit-test the function get_sum
since it is defined in the same package. It would look incomplete, if my unit tests would not cover get_sum
, right? So, if I want to express that get_sum
does not need an explicit unit test, I have to split the package math_internal
further. For example into math_internal1
and math_internal2
and only the one containing the function to_int_table
will have an accessor for the test package. This clearly shows that the granularity of the accessible_by_clause
is too coarse-grained.
We can address these issues with an accessible_by_clause
on the unit level.
Unit-Level accessible_by_clause
Since Oracle Database 12c Release 2 the accessible_by_clause
can be defined per package suprogram. This allows us to keep all subprograms in one package while addressing all previously described issues. Here’s the refactoring result:
CREATE OR REPLACE PACKAGE the_api.math AS
/**
* Calculates the sum of all integers found in a string.
*
* @param in_integers string containing integers to be summarized
* @returns sum, NULL if no integers are found
*/
FUNCTION get_sum(
in_integers IN VARCHAR2
) RETURN INTEGER DETERMINISTIC;
/**
* Calculates the digit sum of an integer.
*
* @param in_integer input integer to calculate cross sum from
* @returns cross sum, NULL if input is NULL
*/
FUNCTION get_cross_sum(
in_integer IN INTEGER
) RETURN INTEGER DETERMINISTIC;
/**
* Calculates the sum of all integers in a collection.
*
* @param in_integers collection of integers to be summarized
* @returns sum, NULL if collection is empty
*/
FUNCTION get_sum(
in_integers IN sys.ora_mining_number_nt
) RETURN INTEGER DETERMINISTIC
ACCESSIBLE BY (PACKAGE the_api.math);
/**
* Finds integer tokens in string.
*
* @param in_integers string containing integers to be tokenized
* @param in_pattern regular expression for integers
* @returns table of integers
*/
FUNCTION to_int_table(
in_integers IN VARCHAR2,
in_pattern IN VARCHAR2 DEFAULT '[0-9]+'
) RETURN sys.ora_mining_number_nt DETERMINISTIC
ACCESSIBLE BY (PACKAGE the_api.math, PACKAGE the_api.test_math);
END math;
/
On line 31 the access to the overloaded function get_sum
is restricted to this package. It’s semantically clear that this function cannot be unit-tested. On line 44 access to the function to_int_table is restricted to this package and the package test_math. Hence it is possible to unit-test this function in the package test_math. The package math is not split up and the access to the original private functions is properly protected.
The package body looks quite similar to the original one. I’ve just put the access-restricted units at the bottom, to match the order and the signature in the specification.
CREATE OR REPLACE PACKAGE BODY the_api.math AS
FUNCTION get_sum(in_integers IN VARCHAR2) RETURN INTEGER DETERMINISTIC IS
BEGIN
RETURN math.get_sum(math.to_int_table(in_integers));
END get_sum;
FUNCTION get_cross_sum(in_integer IN INTEGER) RETURN INTEGER DETERMINISTIC IS
BEGIN
RETURN math.get_sum(math.to_int_table(to_char(in_integer), '[0-9]'));
END get_cross_sum;
FUNCTION get_sum(
in_integers IN sys.ora_mining_number_nt
) RETURN INTEGER DETERMINISTIC
ACCESSIBLE BY (PACKAGE the_api.math)
IS
l_result INTEGER;
BEGIN
SELECT sum(column_value)
INTO l_result
FROM table(in_integers);
RETURN l_result;
END get_sum;
FUNCTION to_int_table(
in_integers IN VARCHAR2,
in_pattern IN VARCHAR2 DEFAULT '[0-9]+'
) RETURN sys.ora_mining_number_nt DETERMINISTIC
ACCESSIBLE BY (PACKAGE the_api.math, PACKAGE the_api.test_math)
IS
l_result sys.ora_mining_number_nt := sys.ora_mining_number_nt();
l_pos INTEGER := 1;
l_int INTEGER;
BEGIN
<<integer_tokens>>
LOOP
l_int := to_number(regexp_substr(in_integers, in_pattern, 1, l_pos));
EXIT integer_tokens WHEN l_int IS NULL;
l_result.EXTEND;
l_result(l_pos) := l_int;
l_pos := l_pos + 1;
END LOOP integer_tokens;
RETURN l_result;
END to_int_table;
END math;
/
Taking about unit testing without showing a unit test is a bit inauthentic. So, here is the utPLSQL test package:
CREATE OR REPLACE PACKAGE the_api.test_math IS
--%suite
--%test
PROCEDURE get_sum_1;
--%test
PROCEDURE get_sum_2;
--%test
PROCEDURE get_cross_sum_1;
--%test
PROCEDURE get_cross_sum_2;
--%test
PROCEDURE to_int_table_1;
--%test
PROCEDURE to_int_table_2;
END test_math;
/
CREATE OR REPLACE PACKAGE BODY the_api.test_math IS
PROCEDURE get_sum_1 IS
BEGIN
ut.expect(42).to_equal(the_api.math.get_sum('What is the sum of 5, 7, 13 and 17?'));
END get_sum_1;
PROCEDURE get_sum_2 IS
BEGIN
ut.expect(CAST(NULL AS INTEGER)).to_equal(the_api.math.get_sum('What is the sum?'));
END get_sum_2;
PROCEDURE get_cross_sum_1 IS
BEGIN
ut.expect(42).to_equal(the_api.math.get_cross_sum(3456789));
END get_cross_sum_1;
PROCEDURE get_cross_sum_2 IS
BEGIN
ut.expect(CAST(NULL AS INTEGER)).to_equal(the_api.math.get_cross_sum(NULL));
END get_cross_sum_2;
PROCEDURE to_int_table_1 IS
l_expected sys.ora_mining_number_nt;
l_actual sys.ora_mining_number_nt;
BEGIN
l_expected := sys.ora_mining_number_nt(5, 7, 13, 17);
l_actual := math.to_int_table('What is the sum of 5, 7, 13 and 17?');
ut.expect(anydata.convertCollection(l_expected)).to_equal(anydata.convertCollection(l_actual));
END to_int_table_1;
PROCEDURE to_int_table_2 IS
l_expected sys.ora_mining_number_nt;
l_actual sys.ora_mining_number_nt;
BEGIN
l_expected := sys.ora_mining_number_nt();
l_actual := math.to_int_table(NULL);
ut.expect(anydata.convertCollection(l_expected)).to_equal(anydata.convertCollection(l_actual));
END to_int_table_2;
END test_math;
/
Running utPLSQL tests is easy, see:
SET SERVEROUTPUT ON SIZE UNLIMITED
EXECUTE ut.run('THE_API.TEST_MATH');
test_math
get_sum_1 [.004 sec]
get_sum_2 [.004 sec]
get_cross_sum_1 [.004 sec]
get_cross_sum_2 [.004 sec]
to_int_table_1 [.009 sec]
to_int_table_2 [.007 sec]
Finished in .034975 seconds
6 tests, 0 failed, 0 errored, 0 disabled, 0 warning(s)
PL/SQL procedure successfully completed.
Conclusion
Start using the accessible_by_clause
. But using the accessible_by_clause
should not drive the way you structure your PL/SQL code. It certainly should not lead to a code-splitting avalanche. Hence I favour the definition of the accessible_by_clause
on the subprogram level.
3 Comments
I’ve just read the Oracle Base article prior to coming here. I was thinking about the same usage to enable access to tests. It feels a bit clunky though; if I want to reorganise my tests, which might include changing package names, then I’ll need to change application code to update the object reference in the accessible by clause. In my current working environment, changes like described to test code would be fine, but application code changes get more controlled. A test refactoring that impacts application code would require an impact assessment. A nice feature though and I like the application.
As an aside, in your article you seem to dislike breaking out packages into two. To me smaller packages seems better, as it can lead to more reuse (provided guide lines like Single Responsibility, Cohesion, Loose Coupling are followed). I struggle though as my IDE of choice (SQL Developer) doesn’t make managing this style of development, or the associated refactoring steps, easy. Maybe one day …
Thanks for sharing!
Yes, you’re right. In this case the
accessible_by_clause
is not a good option. However, it should only be used for exceptional cases. Testing private units. Something to avoid anyway. ;-)No, that’s not what I wanted to say. Splitting packages to improve reusability or accountability or cohesion or decoupling or testability in general are good reasons. But if my only reason to split a package is to make a private unit visible and thus testable, then I think that’s wrong, and I certainly don’t like it. In those cases I consider adding an
accessible_by_clause
the lesser evil.P.S. Sorry, for the late answer, I missed this comment somehow.
Cheers for the follow up and clarifications. 👍🏼