はじめに
この記事では認証認可に関する情報は記載しておりません。
Okta の Auth0 との連携について後日記事を投稿する予定ですが、REST API を外部に公開する際はネットワーク制限やセキュリティを強固にするための設定などを考慮した上でご検討ください。
前回の記事では、Oracle Autonomous AI Database に標準で用意されている Oracle REST Data Services (ORDS) を使って、Database 上のテーブルを REST API として公開する方法を紹介しました。
ORDS の基本的な考え方や、Module、Template、Handler の作り方については前回の記事を参考にしてください。
今回はその続編として、ORDS の REST API 経由で Select AI を実行する方法を紹介します。
Select AI を使うと、自然言語のプロンプトから Database 上のデータに対する問い合わせや要約を実行できます。
ただし、Select AI は LLM を呼び出すため、通常の SELECT API と比べて処理に時間がかかる可能性があります。
そのため、この記事では REST API のリクエスト中に Select AI の完了を待つのではなく、以下のように非同期ジョブとして実行します。
-
POSTで Select AI の実行ジョブを登録する - ORDS から
DBMS_SCHEDULERのジョブを起動する - Scheduler Job の中で
DBMS_CLOUD_AI.GENERATEを実行する - ジョブ管理テーブルにステータスと結果を保存する
- クライアントは
GETでジョブの状態を定期的に確認する - ステータスが
SUCCEEDEDになったら結果を取得する
今回作るもの
今回は、以下の REST API を作成します。
| Method | Path | 内容 |
|---|---|---|
| POST | /ords/<schema>/ai/v1/jobs/ |
Select AI の非同期ジョブを登録する |
| GET | /ords/<schema>/ai/v1/jobs/:job_id |
ジョブの状態と実行結果を取得する |
POST /jobs/ には、以下のような JSON を送信します。
{
"prompt": "問い合わせる内容 (例 : 2026年4月の売上傾向を要約してください)",
"action": "narrate",
"profile_name": "<事前に作成したProfile名>",
"conversation_id": "demo-session-001"
}
レスポンスは以下のようになります。
{
"job_id": "9A1F2C3D4E5F67890123456789ABCDEF",
"job_type": "SELECTAI",
"status": "QUEUED",
"profile_name": "<事前に作成したProfile名>",
"action": "narrate",
"status_url": "./jobs/9A1F2C3D4E5F67890123456789ABCDEF"
}
この時点では Select AI の結果はまだ返しません。
job_id を使って、後続の GET /jobs/:job_id で進捗と結果を確認します。
なぜ非同期にするのか
通常の SQL を返すだけの REST API であれば、Handler の中で SELECT を実行し、その結果をそのまま返せば十分です。
しかし Select AI は、内部で LLM に問い合わせます。
プロンプトの内容、参照するデータ量、利用する AI Provider、ネットワーク状況によって、応答時間が変わります。
そのため、フロントエンドや外部システムから呼び出す REST API としては、以下のような構成にしておくと扱いやすくなります。
- API 呼び出しはすぐに
202 Acceptedを返す - 実際の Select AI 実行はバックグラウンドで行う
- 実行状態をテーブルで管理する
- クライアントは一定間隔でステータスを確認する
- 成功時は
result、失敗時はerror_messageを確認する
今回の実装では、ジョブの状態を以下のように管理します。
| status | 意味 |
|---|---|
QUEUED |
ジョブ登録済み |
RUNNING |
Select AI 実行中 |
SUCCEEDED |
正常終了 |
FAILED |
エラー終了 |
CANCEL_REQUESTED |
キャンセル要求済み |
CANCELLED |
キャンセル済み |
EXPIRED |
期限切れ |
TIMEOUT |
タイムアウト |
今回のサンプルでは、実際に使う主なステータスは QUEUED、RUNNING、SUCCEEDED、FAILED です。
全体構成
今回の構成は以下のようになります。
ORDS の Handler で直接 DBMS_CLOUD_AI.GENERATE を呼び出すのではなく、ai_rest_worker という PL/SQL パッケージに処理を閉じ込めています。
このようにしておくと、REST API 側の定義はシンプルになり、ジョブ登録、ステータス更新、Select AI 実行、エラー処理を PL/SQL パッケージ側で管理できます。
前提
この記事では、以下が準備済みである前提で進めます。
- Autonomous AI Database を利用できること
- ORDS が有効なスキーマで作業すること
- Select AI profile が作成済みであること
-
DBMS_CLOUD_AI.GENERATEを実行できること - Scheduler Job を作成するための
CREATE JOB権限があること
手順
Select AI を実行するための AI Profile を作成する
この記事では省略します。
作成方法については、弊社の下記の公開情報をご参考ください。
ちなみに私は、下記のような感じでクレデンシャル情報と AI Profile を作成しました。
BEGIN
DBMS_CLOUD.CREATE_CREDENTIAL(
credential_name => 'OCI_CRED',
user_ocid => 'ocid1.user.oc1..xxxxxxxxxxxx',
tenancy_ocid => 'ocid1.tenancy.oc1..xxxxxxxxxxxxxxxx',
private_key => '-----BEGIN PRIVATE KEY-----
MIIEvxxx
EQzy8=
-----END PRIVATE KEY-----',
fingerprint => '5d:xx:xx:.....:d9:3a'
);
END;
/
BEGIN
DBMS_CLOUD_AI.CREATE_PROFILE(
profile_name => 'GENAI_PROFILE',
attributes => '{
"provider" : "oci",
"credential_name" : "OCI_CRED",
"oci_compartment_id" : "ocid1.compartment.oc1..xxxxxx",
"conversation" : "TRUE",
"annotations" : "TRUE",
"model" : "openai.gpt-4o-mini",
"region" : "ap-osaka-1",
"object_list": [
{ "owner": "TEMP_USER", "name": "DEMO_CUSTOMER_MASTER" },
{ "owner": "TEMP_USER", "name": "DEMO_CUSTOMER_VOICES" },
{ "owner": "TEMP_USER", "name": "DEMO_PURCHASE_HISTORY" }
]
}'
);
END;
/
必要なテーブルや PL/SQL パッケージ、ORDS の定義を作成する
以下の SQL を、Autonomous AI Database の対象のスキーマで実行します。
実行前に最低限変更するのは、冒頭の SELECT_AI_PROFILE です。
ブラウザのフロントエンドから呼び出す場合は、CORS_ALLOWED_ORIGINS も環境に合わせて変更してください。
-- Select AI を ORDS REST API から非同期実行するためのセットアップ SQL です。
-- SQLcl / SQL Developer で対象スキーマに接続して実行してください。
SET DEFINE ON
DEFINE SELECT_AI_PROFILE = '<事前に作成したProfile名>'
DEFINE CORS_ALLOWED_ORIGINS = 'http://localhost:5173,http://127.0.0.1:5173,http://localhost:5174,http://127.0.0.1:5174'
-- 新規スキーマで ORDS が未有効の場合だけ使用してください。
-- 既に ORDS が有効なスキーマで実行すると ORA-20049 になる場合があります。
--
-- BEGIN
-- ORDS.ENABLE_SCHEMA(
-- p_enabled => TRUE,
-- p_schema => USER,
-- p_url_mapping_type => 'BASE_PATH',
-- p_url_mapping_pattern => LOWER(USER),
-- p_auto_rest_auth => FALSE
-- );
-- END;
-- /
BEGIN
EXECUTE IMMEDIATE q'~
CREATE TABLE ai_rest_jobs (
job_id VARCHAR2(32) DEFAULT RAWTOHEX(SYS_GUID()) NOT NULL,
job_type VARCHAR2(30) DEFAULT 'SELECTAI' NOT NULL,
status VARCHAR2(30) DEFAULT 'QUEUED' NOT NULL,
profile_name VARCHAR2(128) DEFAULT '&&SELECT_AI_PROFILE' NOT NULL,
prompt CLOB NOT NULL,
action VARCHAR2(30) DEFAULT 'narrate' NOT NULL,
conversation_id VARCHAR2(128),
result CLOB,
error_message CLOB,
created_at TIMESTAMP WITH LOCAL TIME ZONE DEFAULT SYSTIMESTAMP NOT NULL,
started_at TIMESTAMP WITH LOCAL TIME ZONE,
completed_at TIMESTAMP WITH LOCAL TIME ZONE,
created_by VARCHAR2(128) DEFAULT SYS_CONTEXT('USERENV', 'SESSION_USER') NOT NULL,
CONSTRAINT ai_rest_jobs_pk PRIMARY KEY (job_id),
CONSTRAINT ai_rest_jobs_type_ck CHECK (job_type IN ('SELECTAI')),
CONSTRAINT ai_rest_jobs_status_ck CHECK (
status IN ('QUEUED', 'RUNNING', 'SUCCEEDED', 'FAILED', 'CANCEL_REQUESTED', 'CANCELLED', 'EXPIRED', 'TIMEOUT')
),
CONSTRAINT ai_rest_jobs_action_ck CHECK (action IN ('narrate', 'chat', 'showsql', 'runsql'))
)
~';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -955 THEN
RAISE;
END IF;
END;
/
BEGIN
EXECUTE IMMEDIATE 'CREATE INDEX ai_rest_jobs_status_ix ON ai_rest_jobs (status, created_at)';
EXCEPTION
WHEN OTHERS THEN
IF SQLCODE != -955 THEN
RAISE;
END IF;
END;
/
CREATE OR REPLACE PACKAGE ai_rest_worker AS
PROCEDURE submit_job(
p_prompt IN CLOB,
p_action IN VARCHAR2 DEFAULT 'narrate',
p_profile_name IN VARCHAR2 DEFAULT '&&SELECT_AI_PROFILE',
p_conversation_id IN VARCHAR2 DEFAULT NULL,
p_job_id OUT VARCHAR2,
p_status OUT VARCHAR2
);
PROCEDURE run_job(
p_job_id IN VARCHAR2
);
END ai_rest_worker;
/
CREATE OR REPLACE PACKAGE BODY ai_rest_worker AS
FUNCTION normalize_action(p_action IN VARCHAR2) RETURN VARCHAR2 IS
l_action VARCHAR2(30) := LOWER(TRIM(NVL(p_action, 'narrate')));
BEGIN
IF l_action NOT IN ('narrate', 'chat', 'showsql', 'runsql') THEN
RETURN 'narrate';
END IF;
RETURN l_action;
END normalize_action;
PROCEDURE submit_job(
p_prompt IN CLOB,
p_action IN VARCHAR2 DEFAULT 'narrate',
p_profile_name IN VARCHAR2 DEFAULT '&&SELECT_AI_PROFILE',
p_conversation_id IN VARCHAR2 DEFAULT NULL,
p_job_id OUT VARCHAR2,
p_status OUT VARCHAR2
) IS
l_job_id VARCHAR2(32) := RAWTOHEX(SYS_GUID());
l_profile_name VARCHAR2(128) := TRIM(NVL(p_profile_name, '&&SELECT_AI_PROFILE'));
l_action VARCHAR2(30) := normalize_action(p_action);
l_job_name VARCHAR2(128) := 'AI_REST_JOB_' || l_job_id;
BEGIN
IF p_prompt IS NULL OR DBMS_LOB.GETLENGTH(p_prompt) = 0 THEN
RAISE_APPLICATION_ERROR(-20001, 'prompt は必須です。');
END IF;
INSERT INTO ai_rest_jobs (
job_id,
job_type,
status,
profile_name,
prompt,
action,
conversation_id,
created_by
) VALUES (
l_job_id,
'SELECTAI',
'QUEUED',
l_profile_name,
p_prompt,
l_action,
p_conversation_id,
COALESCE(SYS_CONTEXT('USERENV', 'CLIENT_IDENTIFIER'), SYS_CONTEXT('USERENV', 'SESSION_USER'))
);
DBMS_SCHEDULER.CREATE_JOB(
job_name => l_job_name,
job_type => 'PLSQL_BLOCK',
job_action => 'BEGIN ai_rest_worker.run_job(''' || l_job_id || '''); END;',
enabled => TRUE,
auto_drop => TRUE,
comments => 'Select AI 非同期実行ジョブ'
);
p_job_id := l_job_id;
p_status := 'QUEUED';
COMMIT;
END submit_job;
PROCEDURE run_job(
p_job_id IN VARCHAR2
) IS
l_prompt CLOB;
l_profile_name ai_rest_jobs.profile_name%TYPE;
l_action ai_rest_jobs.action%TYPE;
l_result CLOB;
l_error_message CLOB;
BEGIN
SELECT prompt, profile_name, action
INTO l_prompt, l_profile_name, l_action
FROM ai_rest_jobs
WHERE job_id = p_job_id
FOR UPDATE;
UPDATE ai_rest_jobs
SET status = 'RUNNING',
started_at = SYSTIMESTAMP,
error_message = NULL
WHERE job_id = p_job_id;
COMMIT;
BEGIN
l_result := DBMS_CLOUD_AI.GENERATE(
prompt => l_prompt,
profile_name => l_profile_name,
action => l_action
);
UPDATE ai_rest_jobs
SET status = 'SUCCEEDED',
result = l_result,
completed_at = SYSTIMESTAMP
WHERE job_id = p_job_id;
EXCEPTION
WHEN OTHERS THEN
l_error_message := SQLERRM || CHR(10) || DBMS_UTILITY.FORMAT_ERROR_BACKTRACE;
UPDATE ai_rest_jobs
SET status = 'FAILED',
error_message = l_error_message,
completed_at = SYSTIMESTAMP
WHERE job_id = p_job_id;
END;
COMMIT;
EXCEPTION
WHEN NO_DATA_FOUND THEN
NULL;
END run_job;
END ai_rest_worker;
/
BEGIN
ORDS.DEFINE_MODULE(
p_module_name => 'select_ai_simple.ai.v1',
p_base_path => '/ai/v1/',
p_items_per_page => 25,
p_status => 'PUBLISHED',
p_comments => 'Select AI asynchronous job API'
);
ORDS.SET_MODULE_ORIGINS_ALLOWED(
p_module_name => 'select_ai_simple.ai.v1',
p_origins_allowed => '&&CORS_ALLOWED_ORIGINS'
);
ORDS.DEFINE_TEMPLATE(
p_module_name => 'select_ai_simple.ai.v1',
p_pattern => 'jobs/',
p_comments => 'Select AI job collection'
);
ORDS.DEFINE_TEMPLATE(
p_module_name => 'select_ai_simple.ai.v1',
p_pattern => 'jobs/:job_id',
p_comments => 'Select AI job status'
);
END;
/
BEGIN
ORDS.DEFINE_HANDLER(
p_module_name => 'select_ai_simple.ai.v1',
p_pattern => 'jobs/',
p_method => 'POST',
p_source_type => ORDS.source_type_plsql,
p_mimes_allowed => 'application/json',
p_comments => 'Submit Select AI job',
p_source => q'~
DECLARE
l_job_id VARCHAR2(32);
l_status VARCHAR2(30);
l_profile_name VARCHAR2(128) := NVL(:profile_name, '&&SELECT_AI_PROFILE');
l_action VARCHAR2(30) := NVL(:action, 'narrate');
BEGIN
ai_rest_worker.submit_job(
p_prompt => TO_CLOB(:prompt),
p_action => l_action,
p_profile_name => l_profile_name,
p_conversation_id => :conversation_id,
p_job_id => l_job_id,
p_status => l_status
);
:status_code := 202;
OWA_UTIL.MIME_HEADER('application/json', FALSE);
HTP.P('Cache-Control: no-store');
OWA_UTIL.HTTP_HEADER_CLOSE;
APEX_JSON.OPEN_OBJECT;
APEX_JSON.WRITE('job_id', l_job_id);
APEX_JSON.WRITE('job_type', 'SELECTAI');
APEX_JSON.WRITE('status', l_status);
APEX_JSON.WRITE('profile_name', l_profile_name);
APEX_JSON.WRITE('action', l_action);
APEX_JSON.WRITE('status_url', './jobs/' || l_job_id);
APEX_JSON.CLOSE_OBJECT;
EXCEPTION
WHEN OTHERS THEN
:status_code := 400;
OWA_UTIL.MIME_HEADER('application/json', FALSE);
HTP.P('Cache-Control: no-store');
OWA_UTIL.HTTP_HEADER_CLOSE;
APEX_JSON.OPEN_OBJECT;
APEX_JSON.WRITE('error', SQLERRM);
APEX_JSON.CLOSE_OBJECT;
END;
~'
);
ORDS.DEFINE_HANDLER(
p_module_name => 'select_ai_simple.ai.v1',
p_pattern => 'jobs/:job_id',
p_method => 'GET',
p_source_type => ORDS.source_type_collection_item,
p_items_per_page => 1,
p_comments => 'Get Select AI job status and result',
p_source => q'~
SELECT job_id,
job_type,
status,
profile_name,
DBMS_LOB.SUBSTR(prompt, 32767, 1) AS prompt,
action,
conversation_id,
created_at,
started_at,
completed_at,
result,
error_message
FROM ai_rest_jobs
WHERE job_id = :job_id
~'
);
END;
/
COMMIT;
この SQL を実行すると、以下がまとめて作成されます。
| 作成されるもの | 内容 |
|---|---|
AI_REST_JOBS |
Select AI のジョブ状態、プロンプト、結果、エラーを保存するテーブル |
AI_REST_JOBS_STATUS_IX |
ステータス確認用のインデックス |
AI_REST_WORKER |
ジョブ登録と Select AI 実行を行う PL/SQL パッケージ |
select_ai_simple.ai.v1 |
ORDS の REST API Module |
jobs/ |
POST でジョブを登録する Template と Handler |
jobs/:job_id |
GET でジョブ状態と結果を取得する Template と Handler |
この SQL の中で具体的に何をしているのかは、Appendix としてこの記事の最後に記載しておきます。
実際に試す
1. curl でジョブを登録する
まず POST /jobs/ を呼び出して Select AI ジョブを登録します。
curl -i -X POST \
"https://<adb-host>/ords/<schema>/ai/v1/jobs/" \
-H "Content-Type: application/json" \
-d '{
"prompt": "<適当なプロンプトを入力>",
"action": "narrate",
"profile_name": "<事前に作成したProfile名>",
"conversation_id": "demo-session-001"
}'
正常に登録できると、HTTP ステータス 202 Accepted で job_id が返ります。
{
"job_id": "9A1F2C3D4E5F67890123456789ABCDEF",
"job_type": "SELECTAI",
"status": "QUEUED",
"profile_name": "<事前に作成したProfile名>",
"action": "narrate",
"status_url": "./jobs/9A1F2C3D4E5F67890123456789ABCDEF"
}
もし ORA-27486: insufficient privilegesのエラーが発生した場合は、ジョブを作成する権限が無い可能性があります。
その場合は、対象ユーザーに対して GRANT CREATE JOB TO <ユーザー名>;を実行してください。
2. curl でステータスを確認する
返ってきた job_id を使って、GET /jobs/:job_id を呼び出します。
curl -s \
"https://<adb-host>/ords/<schema>/ai/v1/jobs/9A1F2C3D4E5F67890123456789ABCDEF"
実行中であれば、以下のように RUNNING が返ります。
{
"job_id": "9A1F2C3D4E5F67890123456789ABCDEF",
"job_type": "SELECTAI",
"status": "RUNNING",
"profile_name": "<事前に作成したProfile名>",
"prompt": "2026年4月の売上傾向を要約してください",
"action": "narrate",
"conversation_id": "demo-session-001",
"result": null,
"error_message": null
}
完了すると、status が SUCCEEDED になり、result に Select AI の結果が入ります。
{
"job_id": "9A1F2C3D4E5F67890123456789ABCDEF",
"job_type": "SELECTAI",
"status": "SUCCEEDED",
"profile_name": "<事前に作成したProfile名>",
"prompt": "2026年4月の売上傾向を要約してください",
"action": "narrate",
"conversation_id": "demo-session-001",
"result": "2026年4月の売上は、月前半にオンライン経由の購入が増加し...",
"error_message": null
}
エラーになった場合は、status が FAILED になり、error_message に DB 側で記録したエラーが入ります。
今回のポイント
今回の実装で重要なのは、ORDS の Handler はあくまで REST API の入口にして、時間がかかる Select AI 実行は非同期ジョブに逃がしている点です。
構成としては以下の役割分担になります。
| 要素 | 役割 |
|---|---|
AI_REST_JOBS |
ジョブの状態、プロンプト、結果、エラーを保存する |
ai_rest_worker.submit_job |
ジョブを登録し、Scheduler Job を作成する |
ai_rest_worker.run_job |
Select AI を実行し、結果を保存する |
POST /ai/v1/jobs/ |
REST API からジョブを受け付ける |
GET /ai/v1/jobs/:job_id |
ジョブの状態と結果を返す |
この形にしておくと、Select AI の処理時間が多少長くなっても、REST API の呼び出し元はタイムアウトに巻き込まれにくくなります。
また、ジョブ管理テーブルに状態が残るため、失敗時の確認や再実行の設計もしやすくなります。
まとめ
ORDS を使うと、Database 上の SQL や PL/SQL を REST API として公開できます。
前回の記事では、テーブルの SELECT 結果を REST API として返すシンプルな例を紹介しました。
今回はその応用として、ORDS から Select AI を非同期で実行する REST API を作成しました。
ポイントは以下です。
- Select AI は LLM を呼び出すため、REST API では非同期実行にしておくと扱いやすい
-
POST /jobs/ではジョブを登録し、202 Acceptedとjob_idを返す - 実際の Select AI 実行は
DBMS_SCHEDULERから PL/SQL パッケージを呼び出して行う - 実行状態と結果は
AI_REST_JOBSテーブルで管理する - クライアントは
GET /jobs/:job_idを定期的に呼び出して、SUCCEEDEDになったら結果を表示する
これで、フロントエンドや外部アプリケーションから、ORDS の REST API 経由で Select AI を呼び出せるようになります。
Appendix
"必要なテーブルや PL/SQL パッケージ、ORDS の定義を作成する" にて記載したクエリの内容について解説します。
ジョブ管理テーブルを作成するブロック
Select AI の実行状態と結果を保存するテーブルです。
CREATE TABLE ai_rest_jobs (
job_id VARCHAR2(32) DEFAULT RAWTOHEX(SYS_GUID()) NOT NULL,
job_type VARCHAR2(30) DEFAULT 'SELECTAI' NOT NULL,
status VARCHAR2(30) DEFAULT 'QUEUED' NOT NULL,
profile_name VARCHAR2(128) DEFAULT '&&SELECT_AI_PROFILE' NOT NULL,
prompt CLOB NOT NULL,
action VARCHAR2(30) DEFAULT 'narrate' NOT NULL,
conversation_id VARCHAR2(128),
result CLOB,
error_message CLOB,
created_at TIMESTAMP WITH LOCAL TIME ZONE DEFAULT SYSTIMESTAMP NOT NULL,
started_at TIMESTAMP WITH LOCAL TIME ZONE,
completed_at TIMESTAMP WITH LOCAL TIME ZONE,
created_by VARCHAR2(128) DEFAULT SYS_CONTEXT('USERENV', 'SESSION_USER') NOT NULL,
CONSTRAINT ai_rest_jobs_pk PRIMARY KEY (job_id),
CONSTRAINT ai_rest_jobs_type_ck CHECK (job_type IN ('SELECTAI')),
CONSTRAINT ai_rest_jobs_status_ck CHECK (
status IN ('QUEUED', 'RUNNING', 'SUCCEEDED', 'FAILED', 'CANCEL_REQUESTED', 'CANCELLED', 'EXPIRED', 'TIMEOUT')
),
CONSTRAINT ai_rest_jobs_action_ck CHECK (action IN ('narrate', 'chat', 'showsql', 'runsql'))
);
このテーブルに、プロンプト、Select AI profile 名、実行ステータス、実行結果、エラーメッセージを保存します。
result と error_message は長くなる可能性があるため、CLOB にしています。
また、進捗確認 API ではステータスと作成日時で検索することが多いため、以下のインデックスも作成しています。
CREATE INDEX ai_rest_jobs_status_ix ON ai_rest_jobs (status, created_at);
Select AI 実行用の PL/SQL パッケージを作成するブロック
次に、Select AI のジョブ登録と実行処理をまとめた PL/SQL パッケージです。
パッケージ仕様は以下のようにしています。
CREATE OR REPLACE PACKAGE ai_rest_worker AS
PROCEDURE submit_job(
p_prompt IN CLOB,
p_action IN VARCHAR2 DEFAULT 'narrate',
p_profile_name IN VARCHAR2 DEFAULT '&&SELECT_AI_PROFILE',
p_conversation_id IN VARCHAR2 DEFAULT NULL,
p_job_id OUT VARCHAR2,
p_status OUT VARCHAR2
);
PROCEDURE run_job(
p_job_id IN VARCHAR2
);
END ai_rest_worker;
/
submit_job は ORDS の POST Handler から呼び出します。
ここでは AI_REST_JOBS にレコードを登録し、DBMS_SCHEDULER.CREATE_JOB でバックグラウンドジョブを作成します。
run_job は Scheduler Job から呼び出します。
ここで実際に DBMS_CLOUD_AI.GENERATE を実行し、結果を AI_REST_JOBS に保存します。
submit_job のポイント
submit_job では、まずプロンプトが空でないことを確認します。
IF p_prompt IS NULL OR DBMS_LOB.GETLENGTH(p_prompt) = 0 THEN
RAISE_APPLICATION_ERROR(-20001, 'prompt は必須です。');
END IF;
次に、ジョブ管理テーブルへ QUEUED の状態で登録します。
INSERT INTO ai_rest_jobs (
job_id,
job_type,
status,
profile_name,
prompt,
action,
conversation_id,
created_by
) VALUES (
l_job_id,
'SELECTAI',
'QUEUED',
l_profile_name,
p_prompt,
l_action,
p_conversation_id,
COALESCE(SYS_CONTEXT('USERENV', 'CLIENT_IDENTIFIER'), SYS_CONTEXT('USERENV', 'SESSION_USER'))
);
その後、DBMS_SCHEDULER.CREATE_JOB で run_job を非同期に実行します。
DBMS_SCHEDULER.CREATE_JOB(
job_name => l_job_name,
job_type => 'PLSQL_BLOCK',
job_action => 'BEGIN ai_rest_worker.run_job(''' || l_job_id || '''); END;',
enabled => TRUE,
auto_drop => TRUE,
comments => 'Select AI Simple 非同期実行ジョブ'
);
auto_drop => TRUE にしているため、Scheduler Job 自体は実行後に削除されます。
実行履歴や結果は AI_REST_JOBS テーブルに残します。
run_job のポイント
run_job では、対象のジョブを取得してステータスを RUNNING に更新します。
UPDATE ai_rest_jobs
SET status = 'RUNNING',
started_at = SYSTIMESTAMP,
error_message = NULL
WHERE job_id = p_job_id;
その後、DBMS_CLOUD_AI.GENERATE で Select AI を実行します。
l_result := DBMS_CLOUD_AI.GENERATE(
prompt => l_prompt,
profile_name => l_profile_name,
action => l_action
);
成功した場合は SUCCEEDED にして結果を保存します。
UPDATE ai_rest_jobs
SET status = 'SUCCEEDED',
result = l_result,
completed_at = SYSTIMESTAMP
WHERE job_id = p_job_id;
失敗した場合は FAILED にして、SQLERRM とバックトレースを error_message に保存します。
UPDATE ai_rest_jobs
SET status = 'FAILED',
error_message = l_error_message,
completed_at = SYSTIMESTAMP
WHERE job_id = p_job_id;
REST API 側ではなく DB 側にエラー内容を残しておくことで、フロントエンドからも運用確認からも原因を追いやすくなります。
ORDS の REST API を定義するブロック
次に、ORDS の Module、Template、Handler 作成するブロックです。
今回の Module は select_ai_simple.ai.v1、base path は /ai/v1/ としています。
BEGIN
ORDS.DEFINE_MODULE(
p_module_name => 'select_ai_simple.ai.v1',
p_base_path => '/ai/v1/',
p_items_per_page => 25,
p_status => 'PUBLISHED',
p_comments => 'Select AI Simple asynchronous job API'
);
ORDS.DEFINE_TEMPLATE(
p_module_name => 'select_ai_simple.ai.v1',
p_pattern => 'jobs/',
p_comments => 'Select AI job collection'
);
ORDS.DEFINE_TEMPLATE(
p_module_name => 'select_ai_simple.ai.v1',
p_pattern => 'jobs/:job_id',
p_comments => 'Select AI job status'
);
END;
/
これで以下の URL に対応する Template が作成されます。
https://<adb-host>/ords/<schema>/ai/v1/jobs/
https://<adb-host>/ords/<schema>/ai/v1/jobs/:job_id
POST /jobs/ の Handler
POST /jobs/ は、Select AI ジョブを登録する API です。
Handler の p_source_type は ORDS.source_type_plsql にします。
ORDS.DEFINE_HANDLER(
p_module_name => 'select_ai_simple.ai.v1',
p_pattern => 'jobs/',
p_method => 'POST',
p_source_type => ORDS.source_type_plsql,
p_mimes_allowed => 'application/json',
p_comments => 'Submit Select AI job',
p_source => q'~
DECLARE
l_job_id VARCHAR2(32);
l_status VARCHAR2(30);
l_profile_name VARCHAR2(128) := NVL(:profile_name, '&&SELECT_AI_PROFILE');
l_action VARCHAR2(30) := NVL(:action, 'narrate');
BEGIN
ai_rest_worker.submit_job(
p_prompt => TO_CLOB(:prompt),
p_action => l_action,
p_profile_name => l_profile_name,
p_conversation_id => :conversation_id,
p_job_id => l_job_id,
p_status => l_status
);
:status_code := 202;
OWA_UTIL.MIME_HEADER('application/json', FALSE);
HTP.P('Cache-Control: no-store');
OWA_UTIL.HTTP_HEADER_CLOSE;
APEX_JSON.OPEN_OBJECT;
APEX_JSON.WRITE('job_id', l_job_id);
APEX_JSON.WRITE('job_type', 'SELECTAI');
APEX_JSON.WRITE('status', l_status);
APEX_JSON.WRITE('profile_name', l_profile_name);
APEX_JSON.WRITE('action', l_action);
APEX_JSON.WRITE('status_url', './jobs/' || l_job_id);
APEX_JSON.CLOSE_OBJECT;
EXCEPTION
WHEN OTHERS THEN
:status_code := 400;
OWA_UTIL.MIME_HEADER('application/json', FALSE);
HTP.P('Cache-Control: no-store');
OWA_UTIL.HTTP_HEADER_CLOSE;
APEX_JSON.OPEN_OBJECT;
APEX_JSON.WRITE('error', SQLERRM);
APEX_JSON.CLOSE_OBJECT;
END;
~'
);
ここでは、リクエスト JSON の prompt、action、profile_name、conversation_id を ORDS のバインド変数として受け取っています。
正常にジョブ登録できた場合は、HTTP ステータスを 202 にしています。
これは「リクエストは受け付けたが、処理はまだ完了していない」という意味です。
:status_code := 202;
レスポンスには Select AI の結果ではなく、ジョブ ID とステータス確認用の URL を返します。
GET /jobs/:job_id の Handler
GET /jobs/:job_id は、ジョブの状態と結果を取得する API です。
ORDS.DEFINE_HANDLER(
p_module_name => 'select_ai_simple.ai.v1',
p_pattern => 'jobs/:job_id',
p_method => 'GET',
p_source_type => ORDS.source_type_collection_item,
p_items_per_page => 1,
p_comments => 'Get Select AI job status and result',
p_source => q'~
SELECT job_id,
job_type,
status,
profile_name,
DBMS_LOB.SUBSTR(prompt, 32767, 1) AS prompt,
action,
conversation_id,
created_at,
started_at,
completed_at,
result,
error_message
FROM ai_rest_jobs
WHERE job_id = :job_id
~'
);
1 件のジョブ情報を返す API なので、p_source_type は ORDS.source_type_collection_item にしています。
レスポンスの例は以下のようになります。
{
"job_id": "9A1F2C3D4E5F67890123456789ABCDEF",
"job_type": "SELECTAI",
"status": "SUCCEEDED",
"profile_name": "<事前に作成したProfile名>",
"prompt": "2026年4月の売上傾向を要約してください",
"action": "narrate",
"conversation_id": "demo-session-001",
"created_at": "2026-05-01T10:00:00+09:00",
"started_at": "2026-05-01T10:00:01+09:00",
"completed_at": "2026-05-01T10:00:09+09:00",
"result": "2026年4月の売上は...",
"error_message": null
}
status が QUEUED または RUNNING の間は、クライアント側で数秒おきにこの API を呼び出します。
SUCCEEDED になったら result を表示し、FAILED になったら error_message を表示する、という流れです。

