2025年2月14日金曜日

Oracle Database 23ai 23.6のセッションレス・トランザクションを使ってみる

Oracle Databaseの開発ツールのPMであるJeff Smithさんのブログにアクセスしたところ、最近の記事として以下が公開されていました。

Sessionless Transactions in Database 23ai & your REST APIs
https://www.thatjeffsmith.com/archive/2025/02/sessionless-transactions-in-database-23ai-your-rest-apis/

セッションレス・トランザクション!? なんだろうと思ったので上記の記事を読んで、自分でも動作を確認してみました。

Oracle Databaseのトランザクションは基本的にデータベースのセッションに紐づいていますが、セッションレス・トランザクションはセッションと切り離すことができます。

セッションレス・トランザクションを使うと、Oracle REST Data Servicesの連続したREST API呼び出しをひとつのデータベース・トランザクションに含めることができます。

テストに使用したAPEXアプリケーションは以下のように動作します。

ボタンStart, Update, Commit, Rollbackはそれぞれ、1回のREST API呼び出しに対応しています。

ボタンStartをクリックしセッションレス・トランザクションを開始します。呼び出したブラウザにXID - トランザクションIDを返します。ボタンUpdateではそのXIDTextに入力した文字列を引数として、REST API経由で表TEST_SESSIONLESS_TXに文字列を書き込みます。

連続した文字列の書き込みは、ボタンCommitのクリックで確定します。これもREST API呼び出しです。ボタンRollbackをクリックすると、それまで書き込まれた文字列はロールバックされます。


REST APIやSQLclのセッションからはDBMS_TRANSACTION.SUSPEND_TRANSACTIONを呼び出すことができますが、Oracle APEXのプロセスからDBMS_TRANSACTION.SUSPEND_TRANSACTIONを呼び出すと、処理がハングします。Oracle APEXに埋め込まれたSQLはDBMS_SQL(正確にはDBMS_SYS_SQL)パッケージを呼び出して実行されるので、一般的なセッションとは扱いが異なることがハングする理由だと思われます。そのため、セッションレス・トランザクションは概ねOracle REST Data ServicesのREST APIとともに使用することになると思われます。

以下より、上記のサンプル・アプリケーションの実装について簡単に紹介します。

ボタンUpdateをクリックしたときにデータを挿入する表をTEST_SESSIONLESS_TXとして作成しています。画面下に配置されたクラシック・レポートのソースです。

-- create tables
create table test_sessionless_tx (
id number generated by default on null as identity
constraint test_sessionless_tx_id_pk primary key,
text varchar2(80 char) not null,
created date not null,
created_by varchar2(255 char) not null,
updated date not null,
updated_by varchar2(255 char) not null
);
-- triggers
create or replace trigger test_sessionless_tx_biu
before insert or update
on test_sessionless_tx
for each row
begin
if inserting then
:new.created := sysdate;
:new.created_by := coalesce(sys_context('APEX$SESSION','APP_USER'),user);
end if;
:new.updated := sysdate;
:new.updated_by := coalesce(sys_context('APEX$SESSION','APP_USER'),user);
end test_sessionless_tx_biu;
/
セッションレス・トランザクションを開始、再開するには、DBMS_TRANSACTION.START_TRANSACTIONを呼び出します。

ボタンStartUpdateCommitRollbackの処理に対応するファンクションやプロシージャを組み込んだパッケージTEST_SESSIONLESS_TX_PKGを作成します。

ファンクションSTART_TXがボタンStart、プロシージャUPDATE_TXがボタンUpdate、プロシージャCOMMIT_TXがボタンCommit、プロシージャROLLBACK_TXがボタンRollbackの処理になります。

パッケージTEST_SESSIONLESS_TX_PKGの定義部です。

create or replace package test_sessionless_tx_pkg as
/**
* セッションレス・トランザクションを開始する。
* システムが生成したトランザクションIDを返す。
*/
function start_tx(
p_timeout in number default 60
)
return varchar2;
/**
* 表TEST_SESSIONLESS_TXにp_textに与えられた文字列を1行挿入する。
* p_xidで与えられたセッションレス・トランザクションで実行する。
*/
procedure update_tx(
p_xid in varchar2
,p_text in varchar2
);
/**
* p_xidで与えられたセッションレス・トランザクションをコミットする。
*/
procedure commit_tx(
p_xid in varchar2
);
/**
* p_xidで与えられたセッションレス・トランザクションをロールバックする。
*/
procedure rollback_tx(
p_xid in varchar2
);
end test_sessionless_tx_pkg;
/

パッケージTEST_SESSIONLESS_TX_PKGの本体部です。ほとんどはDBMS_TRANSACTION.START_TRANSACTIONを呼び出しセッションレス・トランザクションの開始または再開をした後に、1行データベースの操作を行っているだけです。

create or replace package body test_sessionless_tx_pkg as
/**
* 新たにセッションレス・トランザクションを開始し、サスペンドする。
* システムが生成したトランザクションIDを呼び出し元に返す。
*
* dbms_transaction.start_transactionの戻り値の方はVARCHAR2だが、
* 引数xidが受け取るのはRAW型なので、dbms_transaction.start_transactionの
* 戻り値をxidに与える際にはhextorawでRAW型にする必要がある。
*/
function start_tx(
p_timeout in number default 60
)
return varchar2
as
l_xid_raw raw(16);
l_xid_hex varchar2(32);
begin
l_xid_hex := dbms_transaction.start_transaction(
-- xid is null to generate id by system.
transaction_type => dbms_transaction.TRANSACTION_TYPE_SESSIONLESS
,timeout => p_timeout
,flag => dbms_transaction.TRANSACTION_NEW
);
dbms_transaction.suspend_transaction;
return l_xid_hex;
end start_tx;
/**
* 指定されたセッションレス・トランザクションを再開したのち、
* p_textをtest_sessionless_txに挿入する。
* 挿入後、セッションレス・トランザクションをサスペンドする。
*/
procedure update_tx(
p_xid in varchar2
,p_text in varchar2
)
as
l_xid_raw raw(16);
l_xid_hex varchar2(32);
begin
l_xid_raw := hextoraw(p_xid);
l_xid_hex := dbms_transaction.start_transaction(
xid => l_xid_raw
,transaction_type => dbms_transaction.TRANSACTION_TYPE_SESSIONLESS
,flag => dbms_transaction.TRANSACTION_RESUME
);
insert into test_sessionless_tx(text) values(p_text);
dbms_transaction.suspend_transaction;
end update_tx;
/**
* 指定されたセッションレス・トランザクションを再開してコミットする。
*/
procedure commit_tx(
p_xid in varchar2
)
as
l_xid_raw raw(16);
l_xid_hex varchar2(32);
begin
l_xid_raw := hextoraw(p_xid);
l_xid_hex := dbms_transaction.start_transaction(
xid => l_xid_raw
,transaction_type => dbms_transaction.TRANSACTION_TYPE_SESSIONLESS
,flag => dbms_transaction.TRANSACTION_RESUME
);
commit;
end commit_tx;
/**
* 指定されたセッションレス・トランザクションを再開してロールバックする。
*/
procedure rollback_tx(
p_xid in varchar2
)
as
l_xid_raw raw(16);
l_xid_hex varchar2(32);
begin
l_xid_raw := hextoraw(p_xid);
l_xid_hex := dbms_transaction.start_transaction(
xid => l_xid_raw
,transaction_type => dbms_transaction.TRANSACTION_TYPE_SESSIONLESS
,flag => dbms_transaction.TRANSACTION_RESUME
);
rollback;
end rollback_tx;
end test_sessionless_tx_pkg;
/

これらの処理を呼び出すRESTサービスを作成します。RESTサービス・モジュールとしてsessionlessを作成し、そのテンプレートとしてstartupdatecommitrollbackを作成します。テンプレートstartにはGETハンドラを作成し、それ以外はPOSTハンドラを作成します。ハンドラの処理はすべてPL/SQLで記述します。

処理内容自体は、パッケージTEST_SESSIONLESS_TX_PKGの対応するファンクションまたはプロシージャの呼び出しです。

-- Generated by ORDS REST Data Services 24.3.2.r3121009
-- Schema: WKSP_APEXDEV Date: 金 2月 14 05:46:05 2025
--
BEGIN
ORDS.DEFINE_MODULE(
p_module_name => 'sessionless',
p_base_path => '/sessionless/',
p_items_per_page => 25,
p_status => 'PUBLISHED',
p_comments => NULL);
ORDS.DEFINE_TEMPLATE(
p_module_name => 'sessionless',
p_pattern => 'start',
p_priority => 0,
p_etag_type => 'HASH',
p_etag_query => NULL,
p_comments => NULL);
ORDS.DEFINE_HANDLER(
p_module_name => 'sessionless',
p_pattern => 'start',
p_method => 'GET',
p_source_type => 'plsql/block',
p_mimes_allowed => NULL,
p_comments => NULL,
p_source =>
'declare
l_xid varchar2(32);
l_response clob;
l_response_json json_object_t;
begin
l_xid := test_sessionless_tx_pkg.start_tx(p_timeout => 180);
-- レスポンスの送信。
l_response_json := json_object_t();
l_response_json.put(''id'', l_xid);
l_response_json.put(''success'', true);
l_response := l_response_json.to_clob();
htp.p(l_response);
end;');
ORDS.DEFINE_TEMPLATE(
p_module_name => 'sessionless',
p_pattern => 'update',
p_priority => 0,
p_etag_type => 'HASH',
p_etag_query => NULL,
p_comments => NULL);
ORDS.DEFINE_HANDLER(
p_module_name => 'sessionless',
p_pattern => 'update',
p_method => 'POST',
p_source_type => 'plsql/block',
p_mimes_allowed => 'application/json',
p_comments => NULL,
p_source =>
'declare
l_request json_object_t;
l_xid varchar2(32);
l_text varchar2(4000);
l_response clob;
l_response_json json_object_t;
begin
l_request := json_object_t(:body_json);
l_xid := l_request.get_string(''id'');
l_text := l_request.get_string(''text'');
test_sessionless_tx_pkg.update_tx(l_xid, l_text);
-- レスポンスの送信。
l_response_json := json_object_t();
l_response_json.put(''success'', true);
l_response := l_response_json.to_clob();
htp.p(l_response);
end;');
ORDS.DEFINE_TEMPLATE(
p_module_name => 'sessionless',
p_pattern => 'commit',
p_priority => 0,
p_etag_type => 'HASH',
p_etag_query => NULL,
p_comments => NULL);
ORDS.DEFINE_HANDLER(
p_module_name => 'sessionless',
p_pattern => 'commit',
p_method => 'POST',
p_source_type => 'plsql/block',
p_mimes_allowed => 'application/json',
p_comments => NULL,
p_source =>
'declare
l_request json_object_t;
l_xid varchar2(32);
l_response clob;
l_response_json json_object_t;
begin
l_request := json_object_t(:body_json);
l_xid := l_request.get_string(''id'');
test_sessionless_tx_pkg.commit_tx(l_xid);
-- レスポンスの送信。
l_response_json := json_object_t();
l_response_json.put(''success'', true);
l_response := l_response_json.to_clob();
htp.p(l_response);
end;');
ORDS.DEFINE_TEMPLATE(
p_module_name => 'sessionless',
p_pattern => 'rollback',
p_priority => 0,
p_etag_type => 'HASH',
p_etag_query => NULL,
p_comments => NULL);
ORDS.DEFINE_HANDLER(
p_module_name => 'sessionless',
p_pattern => 'rollback',
p_method => 'POST',
p_source_type => 'plsql/block',
p_mimes_allowed => 'application/json',
p_comments => NULL,
p_source =>
'declare
l_request json_object_t;
l_xid varchar2(32);
l_response clob;
l_response_json json_object_t;
begin
l_request := json_object_t(:body_json);
l_xid := l_request.get_string(''id'');
test_sessionless_tx_pkg.rollback_tx(l_xid);
-- レスポンスの送信。
l_response_json := json_object_t();
l_response_json.put(''success'', true);
l_response := l_response_json.to_clob();
htp.p(l_response);
end;');
COMMIT;
END;

RESTモジュールsessionlessとして作成したREST APIのブラウザからの呼び出しは、APEXアクションとして実装します。

共有コンポーネントの静的アプリケーション・ファイルとしてtest.jsを作成します。

/*
+ ボタンクリックにより、サーバーサイドでセッションレスなトランザクションを開始する
*/
const REST_PATH = '/ords/apexdev'; // 環境に合わせて変更が必要。
const a_START = {
name: "START",
action: (event, element, args) => {
fetch(REST_PATH + '/sessionless/start')
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json(); // Parse the JSON response
})
.then(data => {
console.log(data);
apex.item("P1_XID").setValue(data.id);
})
.catch(error => {
console.error('Error fetching data:', error);
});
}
};
const a_UPDATE = {
name: "UPDATE",
action: (event, element, args) => {
fetch(REST_PATH + '/sessionless/update', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: apex.item("P1_XID").getValue(),
text: apex.item("P1_TEXT").getValue()
})
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json(); // Parse the JSON response
})
.then(data => {
console.log(data);
})
.catch(error => {
console.error('Error fetching data:', error);
});
}
};
const a_COMMIT = {
name: "COMMIT",
action: (event, element, args) => {
fetch(REST_PATH + '/sessionless/commit', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: apex.item("P1_XID").getValue()
})
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json(); // Parse the JSON response
})
.then(data => {
console.log(data);
})
.catch(error => {
console.error('Error fetching data:', error);
});
}
};
const a_ROLLBACK = {
name: "ROLLBACK",
action: (event, element, args) => {
fetch(REST_PATH + '/sessionless/rollback', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
id: apex.item("P1_XID").getValue()
})
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! Status: ${response.status}`);
}
return response.json(); // Parse the JSON response
})
.then(data => {
console.log(data);
})
.catch(error => {
console.error('Error fetching data:', error);
});
}
};
const controlsCtx = apex.actions.createContext("controls", document.getElementById("CONTROLS"));
controlsCtx.add([a_START, a_UPDATE, a_COMMIT, a_ROLLBACK]);
view raw test.js hosted with ❤ by GitHub

作成したファイルtest.jsは、ページ・プロパティJavaScriptファイルURLに記述します。

[defer]#APP_FILES#test#MIN#.js


アプリケーションの画面を作成します。

ボタンに紐づけるAPEXアクションのコンテキストを作成するリージョンとしてControlsを作成しています。静的IDCONTROLSを設定しています。REST API呼び出しに関わるボタンはすべてのこのリージョンに含めます。


セッションレス・トランザクションのトランザクションIDを保持するページ・アイテムとして、P1_XIDを作成しています。セッション・ステートストレージリクエストごと(メモリーのみ)を選択します。


ボタンSTARTUPDATECOMMITROLLBACK動作アクションはすべて動的アクションで定義とします。詳細カスタム属性data-action=を設定し、呼び出すAPEXアクションを指定します。


ボタンUpdateをクリックしたときに表TEST_SESSIONLESS_TXに書き込む文字列を入力するページ・アイテムをP1_TEXTとして作成しています。セッション・ステートストレージリクエストごと(メモリーのみ)です。


TEST_SESSIONLESS_TXを一覧するリージョンをクラシック・レポートとして作成しています。ソース表名TEST_SESSIONLESS_TXを指定しています。


ボタンREFRESHを作成し、動的アクションでクラシック・レポートのリージョンをリフレッシュしています。

ボタンUpdateをクリックして表TEST_SESSIONLESS_TXを更新しても、レポートに更新した行は現れません。ボタンCommitをクリックしてレポートをリフレッシュすると、それまでにUpdateした行が表示されます。


ボタンRESETはハウスキーピングに使用します。クリックするとプロセスRESETを呼び出し、表TEST_SESSIONLESS_TXの内容を全削除します。


プロセスRESETソースPL/SQLコードとして以下を記述します。

delete from test_sessionless_tx;

ボタンRESETをクリックしたときに実行されるように、サーバー側の条件ボタン押下時RESETを指定します。


確認用のアプリケーションについての説明は以上です。

今回作成したAPEXアプリケーションのエクスポートを以下に置きました。
https://github.com/ujnak/apexapps/blob/master/exports/test-sessionless-tx.zip

Oracle APEXのアプリケーション作成の参考になれば幸いです。