最近は新たに表を作る時は、もっぱらクイックSQLを使っています。こういう使い方をした方が便利だな、と感じたことをまとめてみます。
いきなり仕様を読むのもつらいので、その前段階の使い方の紹介になります。仕様そのものについては、オンライン・ヘルプを参照してください。
クイックSQLが使えるサイト
Oracle APEXがインストールされていれば、どこでも。SQLワークショップに含まれるユーティリティを開いて、クイックSQLを実行します。
LiveSQL - https://livesql.oracle.com - でも、クイックSQLを実行できます。
コメントによるインライン設定
モデルの保存
表定義の基本
# date: timestamp with local time zone
customers
name vc80
register_date
age num
# date: timestamp with local time zone
列の制約の指定
is_enabled vc1 /check Y,N /default N /nn
is_enabled varchar2(1) default 'N' constraint customers_is_enabled_cc
check (is_enabled in ('Y','N')) not null,
is_enabled vc1 /nn /default N /check Y,N
is_enabled varchar2(1) default 'N' constraint customers_is_enabled_vc_cc
check (is_enabled_vc1__/default_n in ('Y','N')) not null,
マスター・ディテイル関係の表の定義
# semantics: default
orders
customer_name
order_date
order_items
product_name
price num
quantity num
-- create tables
create table orders (
id number generated by default on null as identity
constraint orders_id_pk primary key,
customer_name varchar2(255),
order_date date
)
;
create table order_items (
id number generated by default on null as identity
constraint order_items_id_pk primary key,
order_id number
constraint order_items_order_id_fk
references orders on delete cascade,
product_name varchar2(255),
price number,
quantity number
)
;
-- table index
create index order_items_i1 on order_items (order_id);
主キー定義
customers
customer_id num /pk
name vc80
register_date
age num
# prefixPKwithTname: true
# PK: identity
# DB: 18c
customers
name vc80
register_date
age num
customer_id number default on null to_number(sys_guid(), 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX')
constraint customers_id_pk primary key,
customer_id number default on null customers_seq.NEXTVAL
constraint customers_id_pk primary key,
customer_id number generated by default on null as identity
constraint customers_id_pk primary key,
外部キー制約
# semantics: default
orders
customer_name
order_date
order_items
order_id /fk orders
product_name
price num
quantity num
監査列について
created date not null,
created_by varchar2(255) not null,
updated date not null,
updated_by varchar2(255) not null
-- triggers
create or replace trigger orders_biu
before insert or update
on orders
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 orders_biu;
/
-- triggers
create or replace trigger orders_biu
before insert or update
on orders
for each row
begin
if inserting then
:new.created := sysdate;
:new.created_by := user;
end if;
:new.updated := sysdate;
:new.updated_by := user;
end orders_biu;
/
表と列の名前について
# prefix: tst
# semantics: default
emploees
name vc80
sal num
com num
-- create tables
create table tst_emploees (
id number generated by default on null as identity
constraint tst_emploees_id_pk primary key,
name varchar2(80),
sal number,
com number
)
;
# prefixPKwithTname: true
# prefix: tst
# semantics: default
emploees
employee_name vc80
sal num
-- create tables
create table tst_emploees (
tst_emploee_id number generated by default on null as identity
constraint tst_emploees_id_pk primary key,
employee_name varchar2(80),
sal number
)
;
行キーと行バージョン番号
# prefix: tst
# semantics: default
# rowVersion: true
emploees
employee_name vc80
sal num
列ROW_VERSIONが追加され、その列に値を設定するトリガーが生成されます。
-- create tables
create table tst_emploees (
id number generated by default on null as identity
constraint tst_emploees_id_pk primary key,
row_version integer not null,
employee_name varchar2(80),
sal number
)
;
-- triggers
create or replace trigger tst_emploees_biu
before insert or update
on tst_emploees
for each row
begin
if inserting then
:new.row_version := 1;
elsif updating then
:new.row_version := nvl(:old.row_version,0) + 1;
end if;
end tst_emploees_biu;
/
行バージョン番号は、その行の更新が発生する度に1づつインクリメントされます。この列の使いどころの説明として、REST APIでの利用を考えてみます。
最初に、表TST_EMPLOYEESよりIDを渡して、EMPLOYEE_NAME, SALを取得します。このときROW_VERSIONも同時に取得します。
1. クライアントAがID 300のデータを取得しました。
Client A: https://xxxxxx/xxx/employee/300 (GET)
取得した値は以下です。
{ "id": 300, "employee_name": "山田太郎", "sal": 300, "row_version": 4 }
2. クライアントBも同じデータを取得しました。
Client B: https://xxxxxx/xxx/employee/300 (GET)
{ "id": 300, "employee_name": "山田太郎", "sal": 300, "row_version": 4 }
表に保存されている行のROW_VERSIONはすでに5になっており、送信されたデータとは異なるため更新は失敗します。
以上のように行バージョン番号は、異なるクライアントによって、アップデートが不用意に上書きされないことを保証するために使用されます。
HTTPのPUTを受け付けて列SALを更新する前に、ROW_VERSIONの値を参照する必要があります。そのため、以下のような問い合わせを発行します。
select row_version from tst_employees where id = 300 for update;
受信したrow_versionと比較して、一致していたら、
update tst_employees set sal = 300 where id = 300;
といった手順を踏むことになります。ここで主キー列の名称が表毎に異なっていると、row_versionを取り出す手順が煩雑になります。一意列が同じ列名ROW_KEYとして全ての表に定義されていることにより、少々扱いが容易になります。これが行キーの使い道です。
私の嗜好としては、主キーに表名を接頭辞として付けますがOFFで、主キー列がつねにIDという名前になっていれば、列ROW_KEYを定義する理由はないので、定義しないです。
Oracle APEXでは行バージョン番号の代わりにチェックサムを使うことで、上記の制御を実装しています。以下のような形式でチェックサムを計算し、保存されている行のチェックサムと受信したチェックサムに違いが無ければ更新処理を行います。
SELECT APEX_ITEM.MD5_CHECKSUM(ID, EMPLOYEE_NAME, SAL) FROM TST_EMPLOYEES WHERE ID = 300
FOR UPDATE NOWAIT;
行バージョン番号も使用できますが、デフォルトではチェックサムを使用します。そのため、Oracle APEXで表を扱う範囲では、列キーと行バージョン列の双方とも必要としていません。
チェックサムでは扱いが難しい、例えば表に列が大量に定義されている、Oracle APEXの外でデータが変更されることがある、といった場合に利用を検討します。
表と列のコメント
employees -- 従業員の情報
employee_name vc80 -- 従業員名
sal num -- 月給
または
employees [従業員の情報]
employee_name vc80 [従業員名]
sal num [月給]
-- comments
comment on table emploees is '従業員の情報';
comment on column emploees.employee_name is '従業員名';
comment on column emploees.sal is '月給';
テスト・データの生成
# language: "JA"
履歴表の作成
# prefix: tst
employees /history
name vc80
sal num
デフォルトのセマンティクス
# semantics: default
無指定の場合は列NAMEの型は varchar2(80 char)として定義されますが、上記のセマンティックスのインライン設定を含めると varchar2(80) となります。