Building and documenting REST APIs with ORDS

Introduction

In the first part of the article we will provide an overview of how you can use Oracle REST Data Services for providing APIs directly from your PL/SQL code . The second part covers how to document our Web services using Swagger. Lets begin with a couple of technical concepts:

  • ORDS: Oracle REST Data Services ORDS enables developers with SQL and database skills to develop REST APIs for the Oracle Database. In a few words ORDS is:
    • A mid-tier Java application.
    • Runs in a Java application server like WebLogic or Tomcat.
    • Maps standard http(s) RESTful requests to database transactions.
      • Access to Relational data over HTTP(s) without installing JDBC/ODBC drivers
    • Can declaratively returns results in JSON format.
    • Can connect to Oracle NoSQL and Oracle container databases in Cloud.
    • Supports Swagger based Open API integration.
    • It was formally known as Oracle APEX Listener.

Figure 1: Relational to JSON with ORDS

Figure 1: Relational to JSON with ORDS

The picture above shows the ORDS architecture and how does it work. It basically acts as a middleman between clients (applications) and the database, mapping incoming HTTP(S) requests to resource handlers for a specific URI pattern. Resource handlers can have different source types, such as query and PL/SQL. With the query source type, ORDS executes the query, converts the results into JSON, and returns the JSON to the client.

  • ORDS - URL Structure. The URL for an ORDS Web Service consists of five elements: https://<host>:<port>/ords/<schema>/<module>/<template>
    • Host: Name of the host.
    • Port: Port Number.
    • Schema: Pattern defined for the schema.
    • Module: Base path for the module.
    • Template: Pattern defined for the template.
  • Swagger: Swagger is an open-source software framework supported by a large tool ecosystem that helps developers design, build, document, and consume RESTful web services. While most users use the Swagger UI tool to identify Swagger, the Swagger tool set includes automated documentation support, code generation and test case generation.

It is out of the scope how to install and configure ORDS. For that purpose you can check this fantastic article from Tim Hall: https://oracle-base.com/articles/linux/docker-oracle-rest-data-services-...

REST services, how to.

This example will show us how to create a Web Services within a new schema called ORDSEXAMPLE.

Create a new user

Log as SYSDBA and create a new user with the necessary privileges using the following SQL:

ALTER SESSION SET CONTAINER =pdb1;
CREATE USER ORDSEXAMPLE IDENTIFIED BY ordsexample_1995 DEFAULT TABLESPACE USERS QUOTA UNLIMITED ON USERS;
GRANT CREATE TABLE TO ORDSEXAMPLE;
GRANT CREATE SESSION TO ORDSEXAMPLE

Prepare your schema

Create a copy of the good old classic Oracle EMP and DEPT tables with sample data using the following SQL snippets:

CREATE TABLE dept (
    deptno   NUMBER(2, 0),
    dname    VARCHAR2(14),
    loc      VARCHAR2(13),
    CONSTRAINT pk_dept PRIMARY KEY ( deptno )
);

CREATE TABLE emp (
    empno      NUMBER(4, 0),
    ename      VARCHAR2(10),
    job        VARCHAR2(9),
    mgr        NUMBER(4, 0),
    hiredate   DATE,
    sal        NUMBER(7, 2),
    comm       NUMBER(7, 2),
    deptno     NUMBER(2, 0),
    CONSTRAINT pk_emp PRIMARY KEY ( empno ),
    CONSTRAINT fk_deptno FOREIGN KEY ( deptno )
        REFERENCES dept ( deptno )
);

Populate them:

INSERT INTO DEPT VALUES(10,'ACCOUNTING','NEW YORK');
INSERT INTO DEPT VALUES(20,'RESEARCH','DALLAS');
INSERT INTO DEPT VALUES(30,'SALES','CHICAGO');
INSERT INTO DEPT VALUES(40,'OPERATIONS','BOSTON');
INSERT INTO DEPT VALUES(50,'PURCHASING','WASHINGTON');
INSERT INTO DEPT VALUES(60,'HR','NEW YORK');

INSERT INTO EMP (EMPNO,ENAME,JOB,MGR,HIREDATE,SAL,COMM,DEPTNO)
VALUES (7788,'SCOTT','ANALYST',7566,TO_DATE('19-APR-87','DD-MON-RR'),3000,NULL,20);
INSERT INTO EMP (EMPNO,ENAME,JOB,MGR,HIREDATE,SAL,COMM,DEPTNO)
VALUES (7698,'BLAKE','MANAGER',7839,TO_DATE('01-MAY-81','DD-MON-RR'),2850,NULL,30);
INSERT INTO EMP (EMPNO,ENAME,JOB,MGR,HIREDATE,SAL,COMM,DEPTNO)
VALUES (7839,'KING','PRESIDENT',NULL,TO_DATE('17-NOV-81','DD-MON-RR'),5000,NULL,10);
INSERT INTO EMP (EMPNO,ENAME,JOB,MGR,HIREDATE,SAL,COMM,DEPTNO)
VALUES (7566,'JONES','MANAGER',7839,TO_DATE('02-APR-81','DD-MON-RR'),2975,NULL,20);
INSERT INTO EMP (EMPNO,ENAME,JOB,MGR,HIREDATE,SAL,COMM,DEPTNO)
VALUES (7782,'CLARK','MANAGER',7839,TO_DATE('09-JUN-81','DD-MON-RR'),2450,NULL,10);
INSERT INTO EMP (EMPNO,ENAME,JOB,MGR,HIREDATE,SAL,COMM,DEPTNO)
VALUES (7902,'FORD','ANALYST',7566,TO_DATE('03-DEC-81','DD-MON-RR'),3000,NULL,20);
INSERT INTO EMP (EMPNO,ENAME,JOB,MGR,HIREDATE,SAL,COMM,DEPTNO)
VALUES (7876,'ADAMS','CLERK',7788,TO_DATE('23-MAY-87','DD-MON-RR'),1100,NULL,20);
INSERT INTO EMP (EMPNO,ENAME,JOB,MGR,HIREDATE,SAL,COMM,DEPTNO)
VALUES (7499,'ALLEN','SALESMAN',7698,TO_DATE('20-FEB-81','DD-MON-RR'),1600,300,30);
INSERT INTO EMP (EMPNO,ENAME,JOB,MGR,HIREDATE,SAL,COMM,DEPTNO)
VALUES (7900,'JAMES','CLERK',7698,TO_DATE('03-DEC-81','DD-MON-RR'),950,NULL,50);
INSERT INTO EMP (EMPNO,ENAME,JOB,MGR,HIREDATE,SAL,COMM,DEPTNO)
VALUES (7654,'MARTIN','SALESMAN',7698,TO_DATE('28-SEP-81','DD-MON-RR'),1250,1400,30);
INSERT INTO EMP (EMPNO,ENAME,JOB,MGR,HIREDATE,SAL,COMM,DEPTNO)
VALUES (7934,'MILLER','CLERK',7782,TO_DATE('23-JAN-82','DD-MON-RR'),1300,NULL,10);
INSERT INTO EMP (EMPNO,ENAME,JOB,MGR,HIREDATE,SAL,COMM,DEPTNO)
VALUES (7369,'SMITH','CLERK',7902,TO_DATE('17-DEC-80','DD-MON-RR'),800,NULL,60);
INSERT INTO EMP (EMPNO,ENAME,JOB,MGR,HIREDATE,SAL,COMM,DEPTNO)
VALUES (7844,'TURNER','SALESMAN',7698,TO_DATE('08-SEP-81','DD-MON-RR'),1500,0,30);
INSERT INTO EMP (EMPNO,ENAME,JOB,MGR,HIREDATE,SAL,COMM,DEPTNO)
VALUES (7521,'WARD','SALESMAN',7698,TO_DATE('22-FEB-81','DD-MON-RR'),1250,500,40);
COMMIT;

We can use check that the tables are correctly filled and include the default collection of six divisions and fourteen employees:

SELECT
    ename,
    dname,
    job,
    empno,
    hiredate,
    loc
FROM
    emp,
    dept
WHERE
    emp.deptno = dept.deptno
ORDER BY
    emp.ename;

Enable the ORDS schema

Before creating any Web Services, we have to enable REST data services for the ORDSEXAMPLE schema:

BEGIN
    ords.enable_schema(p_enabled => TRUE,
                                    p_schema => 'ORDSEXAMPLE',
                                    p_url_mapping_type => 'BASE_PATH',
                                    p_url_mapping_pattern => 'api',
                                    p_auto_rest_auth => FALSE);
    COMMIT;
END;

Web services from the schema can now be referenced using the following base URL: http://localhost:8080/ords/ORDSEXAMPLE/

Define Module

We name the resource module hr.v1. A Web Service name should always include the version number; this will allow us to publish updated versions of a Web Service that follow the same URL patterns:

BEGIN
    ords.define_module(p_module_name => 'hr.v1',
                                   p_base_path => 'hr/v1/',
                                   p_items_per_page => 10,
                                   p_status => 'PUBLISHED',
                                   p_comments => 'Sample HR Module');
    COMMIT;
END;

Define Template

BEGIN
    ords.define_template(p_module_name => 'hr.v1',
                                     p_pattern => 'departments',
                                     p_comments => 'Departments Resource');
                                     ords.define_template(p_module_name => 'hr.v1',
                                     p_pattern => 'employees',
                                     p_comments => 'Employees Resource');
    COMMIT;
END;

Define the handlers

One for each HTTP method: GET, POST,PUT and DELETE

-- The GET Handler method on the departments template
BEGIN
  ords.define_handler(p_module_name    => 'hr.v1',
                      p_pattern        => 'departments',
                      p_method         => 'GET',
                      p_source_type    => ords.source_type_query,
                      p_source         => 'SELECT deptno, dname, loc
                                           FROM dept ORDER BY deptno',
                      p_items_per_page => 5,
                      p_comments       => 'List departments');
  COMMIT;
END;

-- The POST Handler method on the departments template
BEGIN
    ords.define_handler(p_module_name => 'hr.v1', p_pattern => 'departments', p_method => 'POST', p_source_type => ords.source_type_plsql
    , p_source => 'BEGIN
                                          INSERT INTO dept
                                            (deptno, dname, loc)
                                          VALUES
                                            (:pn_dept_no, :pv_dept_name, :pv_location);
                                          :pn_status := 200;
                                          :pv_result := ''Department Added'';
                                        EXCEPTION
                                          WHEN OTHERS THEN
                                            :pn_status := 400;
                                            :pv_result := ''Unable to add department: ''
                                                          || SQLERRM;
                                        END;'
    , p_comments => 'Create a Department');
END;

-- The PUT Handler method on the departments template

BEGIN
    ords.define_handler(p_module_name => 'hr.v1', p_pattern => 'departments', p_method => 'PUT', p_source_type => ords.source_type_plsql
    , p_source => 'BEGIN
                                          UPDATE dept
                                          SET dname = :pv_dept_name, loc = :pv_location
                                          WHERE deptno = :pn_dept_no;
                                          IF SQL%ROWCOUNT = 0 THEN
                                            :pn_status := 400;
                                            :pv_result := ''Invalid department number'';
                                          ELSE
                                            :pn_status := 200;
                                            :pv_result := ''Department Updated'';
                                          END IF;
                                        EXCEPTION
                                          WHEN OTHERS THEN
                                            :pn_status := 400;
                                            :pv_result := ''Unable to update department:''
                                                          || SQLERRM;
                                        END;'
    , p_comments => 'Create a Department');
END;

-- The DELETE Handler method on the departments template
BEGIN
    ords.define_handler(p_module_name => 'hr.v1', p_pattern => 'departments', p_method => 'DELETE', p_source_type => ords.source_type_plsql
    , p_source => 'BEGIN
                                          DELETE FROM dept
                                          WHERE deptno = :pn_dept_no;
                                          IF SQL%ROWCOUNT = 0 THEN
                                            :pn_status := 400;
                                            :pv_result := ''Invalid department number'';
                                          ELSE
                                            :pn_status := 200;
                                            :pv_result := ''Department Deleted'';
                                          END IF;
                                        EXCEPTION
                                          WHEN OTHERS THEN
                                            :pn_status := 400;
                                            :pv_result := ''Unable to delete department: ''
                                                          || SQLERRM;  
                                        END;'
    , p_comments => 'Delete a Department');

    COMMIT;
END
-- The GET Handler method on the employees template
BEGIN
    ords.define_handler(p_module_name => 'hr.v1', p_pattern => 'employees', p_method => 'GET', p_source_type => ords.source_type_query
    , p_source => 'SELECT d.dname, e.ename, e.job,
                                               e.empno, e.hiredate, d.loc
                                        FROM emp e, dept d
                                        WHERE e.deptno = d.deptno
                                        AND (:pn_deptno IS NULL OR d.deptno = :pn_deptno)
                                        ORDER BY d.dname, e.ename'
    , p_comments => 'List employees');

    COMMIT;
END;

Define Parameters

The following scripts will define all parameters for each handler:

--Departments PUT

BEGIN
  ords.define_parameter(p_module_name        => 'hr.v1',
                        p_pattern            => 'departments',
                        p_method             => 'PUT',
                        p_name               => 'department_number',
                        p_bind_variable_name => 'pn_dept_no',
                        p_source_type        => 'HEADER',
                        p_param_type         => 'INT',
                        p_access_method      => 'IN',
                        p_comments           => 'Department Number');
  ords.define_parameter(p_module_name        => 'hr.v1',
                        p_pattern            => 'departments',
                        p_method             => 'PUT',
                        p_name               => 'department_name',
                        p_bind_variable_name => 'pv_dept_name',
                        p_source_type        => 'HEADER',
                        p_param_type         => 'STRING',
                        p_access_method      => 'IN',
                        p_comments           => 'Department Name');
  ords.define_parameter(p_module_name        => 'hr.v1',
                        p_pattern            => 'departments',
                        p_method             => 'PUT',
                        p_name               => 'location_name',
                        p_bind_variable_name => 'pv_location',
                        p_source_type        => 'HEADER',
                        p_param_type         => 'STRING',
                        p_access_method      => 'IN',
                        p_comments           => 'Location Name');
  ords.define_parameter(p_module_name        => 'hr.v1',
                        p_pattern            => 'departments',
                        p_method             => 'PUT',
                        p_name               => 'X-APEX-STATUS-CODE',
                        p_bind_variable_name => 'pn_status',
                        p_source_type        => 'HEADER',
                        p_param_type         => 'INT',
                        p_access_method      => 'OUT',
                        p_comments           => 'Response status');
  ords.define_parameter(p_module_name        => 'hr.v1',
                        p_pattern            => 'departments',
                        p_method             => 'PUT',
                        p_name               => 'result_message',
                        p_bind_variable_name => 'pv_result',
                        p_source_type        => 'RESPONSE',
                        p_param_type         => 'STRING',
                        p_access_method      => 'OUT',
                        p_comments           => 'Result message');
  COMMIT;
END;

--Department DELETE

BEGIN
  ords.define_parameter(p_module_name        => 'hr.v1',
                        p_pattern            => 'departments',
                        p_method             => 'DELETE',
                        p_name               => 'department_number',
                        p_bind_variable_name => 'pn_dept_no',
                        p_source_type        => 'HEADER',
                        p_param_type         => 'INT',
                        p_access_method      => 'IN',
                        p_comments           => 'Department Number');
  ords.define_parameter(p_module_name        => 'hr.v1',
                        p_pattern            => 'departments',
                        p_method             => 'DELETE',
                        p_name               => 'X-APEX-STATUS-CODE',
                        p_bind_variable_name => 'pn_status',
                        p_source_type        => 'HEADER',
                        p_param_type         => 'INT',
                        p_access_method      => 'OUT',
                        p_comments           => 'Response status');
  ords.define_parameter(p_module_name        => 'hr.v1',
                        p_pattern            => 'departments',
                        p_method             => 'DELETE',
                        p_name               => 'result_message',
                        p_bind_variable_name => 'pv_result',
                        p_source_type        => 'RESPONSE',
                        p_param_type         => 'STRING',
                        p_access_method      => 'OUT',
                        p_comments           => 'Result message');
  COMMIT;
END;

--Employee GET

BEGIN
  ords.define_parameter(p_module_name        => 'hr.v1',
                        p_pattern            => 'employees',
                        p_method             => 'GET',
                        p_name               => 'department_number',
                        p_bind_variable_name => 'pn_deptno',
                        p_source_type        => 'URI',
                        p_param_type         => 'INT',
                        p_access_method      => 'IN',
                        p_comments           => 'Restrict employees by department');  
  COMMIT;
END;

We can use the ORDS_METADATA views to query them. The following SQL query will return all of the Web Services defined for the ORDSEXAMPLE schema.

--Reviewing the Web Services
SELECT
    uom.comments   module_desc,
    uot.comments   template_desc,
    uoh.comments   handler_desc,
    uoh.method,
    uoh.source_type,
    '<host_ref>'
    || uos.pattern
    || uom.uri_prefix
    || uot.uri_template url,
    (
        SELECT
            COUNT(id)
        FROM
            user_ords_parameters
        WHERE
            handler_id = uoh.id
    ) parameter_count
FROM
    user_ords_schemas     uos,
    user_ords_modules     uom,
    user_ords_templates   uot,
    user_ords_handlers    uoh
WHERE
    uot.module_id = uom.id
    AND uom.schema_id = uos.id
    AND uoh.template_id = uot.id
    AND uos.parsing_schema = 'ORDSEXAMPLE'
ORDER BY
    uom.comments,
    uot.uri_template
--Example  <host_ref> = http://localhost:8080/ords/

Testing Web Services

Just choose your preferred HTTP client!

Generating a Swagger document through ORDS

1- Using the open-api-catalog (Swagger Editor)

The open-api-catalog output is in Open API (Swagger) 2.0 format, which makes it really simple to generate documentation and an example of the calling code in many programming languages.

An overview of the contents of an ORDS enabled schema can be displayed in a browser (GET HTTP method) using the following type of URL. The output is a JSON document:

Go to the online Swagger Editor (https://editor.swagger.io/), paste the output JSON text. It will be converted to YAML and display the available endpoints.

2- Using the swagger-UI server

In the previous version of APEX was not obvious to generate this documentation. The new setting takes a URL which points to a SWAGGER UI 2.0 server. If the instance setting is set, the URL that generates the web service’s swagger JSON document will be sent to the Swagger UI server. If there is no URL specified, raw JSON will be produced.

 

Figure 2: Setting the Swagger URL

 

Setting up the Swagger server is actually quite easy. It is a simple set of HTML, Javascript and CSS files that can be downloaded from here (https://swagger.io/tools/swagger-ui/) and unzipped into a directory into the web root of our local web server. During my test I pulled a pre-built docker image of the swagger-ui directly from Docker Hub. and I ran it in a separate container http://localhost:8888/. Then, from ORDS REST Workshop, navigate to the module definition level and click the Generate Swagger Doc button.

If the Swagger UI URL is set correctly at the Instance level, APEX will forward the URL of the documentation to the Swagger UI Server. The server must be able to reach back to the documentation URL, but as long as it can, we will not only get documentation but be able to test the services as well.

Figure 3: APEX REST Workshop for generating swagger documents

 

Figure 4: Documentation generated by Swagger UI

 

Acknowledgements and Reference links

My greatest thanks go to my supervisor Luis Rodriguez Fernandez who has been guiding me throughout the program. I think him for all the time devoted to me, for his total availability, for his kindness and especially for his remarks and constructive criticism. I am also thankful to all the IT-DB group members for their warm welcome, their help during this project and for making this experience very pleasant and rewarding.

 

Add new comment