対象: RPG から SQL を使ってDB2 for i にアクセスする方法
基本文法は RPG 文法基礎 を参照
COBOL版は COBOL × SQL 連携 を参照
目次
- RPGとSQLの関係
- 埋め込みSQLの基本
- CRUD操作
- カーソル処理(複数行の取得)
- エラー処理(SQLCA / SQLSTATE)
- トランザクション制御
- ネイティブI/O vs SQL
- DB2 for i 固有の機能
- Cloud SQL(PostgreSQL)への移行
- 設計パターン
1. RPGとSQLの関係
RPGには2つのDB2アクセス方法がある:
ネイティブI/O(RPG固有) 埋め込みSQL
───────────────────── ─────────────────────
dcl-f CUSTPF disk keyed; exec sql
read CUSTFILE; SELECT * FROM CUSTPF
chain custId CUSTFILE; WHERE CUSTID = :custId;
write CUSTFMT; exec sql
update CUSTFMT; INSERT INTO CUSTPF ...;
exec sql
UPDATE CUSTPF SET ...;
特徴: 特徴:
・RPG固有の構文 ・標準SQL(移植性高い)
・レコード単位の操作 ・集合操作が得意
・AS/400で高速 ・JOIN/GROUP BY が使える
・学習コスト低い ・モダナイゼーション時に有利
いつどちらを使うか
| ケース | 推奨 | 理由 |
|---|---|---|
| キーでの1件取得 | ネイティブ(chain) | シンプル&高速 |
| 順次処理(全件ループ) | ネイティブ(read) | 定番パターン |
| 複数テーブル結合 | SQL(JOIN) | ネイティブでは困難 |
| 集計(合計、平均、件数) | SQL(GROUP BY) | RPGでループ集計不要 |
| 条件検索(複雑な条件) | SQL(WHERE) | 柔軟 |
| バルク更新 | SQL(UPDATE WHERE) | 1文で完了 |
2. 埋め込みSQLの基本
RPG内にSQLを埋め込む。exec sql で始まるブロックで記述。
**free
ctl-opt dftactgrp(*no) actgrp(*caller) option(*nodebugio);
dcl-s custId char(8);
dcl-s custName char(30);
dcl-s balance packed(11:2);
custId = 'C0001';
// 埋め込みSQL(1件取得)
exec sql
SELECT CUSTNM, BALANCE
INTO :custName, :balance
FROM CUSTPF
WHERE CUSTID = :custId;
if sqlcode = 0;
dsply ('Name: ' + %trim(custName));
dsply ('Balance: ' + %char(balance));
else;
dsply ('Not found: SQLCODE=' + %char(sqlcode));
endif;
*inlr = *on;
ホスト変数
RPGの変数をSQLで使うには、変数名の前に : を付ける。
dcl-s custId char(8);
dcl-s custName char(30);
// :custId = ホスト変数(RPGの変数をSQLに渡す)
exec sql
SELECT CUSTNM INTO :custName
FROM CUSTPF
WHERE CUSTID = :custId;
COBOL との対比
COBOL: RPG:
EXEC SQL exec sql
SELECT CUSTNM SELECT CUSTNM
INTO :CUST-NAME INTO :custName
FROM CUSTPF FROM CUSTPF
WHERE CUSTID = :CUST-ID WHERE CUSTID = :custId;
END-EXEC
ほぼ同じ構文。違いは END-EXEC が不要(RPGはセミコロンで終端)な点のみ。
3. CRUD操作
SELECT(1件取得)
exec sql
SELECT CUSTNM, ADDR, BALANCE
INTO :custName, :custAddr, :balance
FROM CUSTPF
WHERE CUSTID = :custId;
INSERT
exec sql
INSERT INTO ORDHDR (ORDNO, CUSTID, ORDDATE, STATUS, AMOUNT)
VALUES (:ordNo, :custId, CURRENT_DATE, 'N', :amount);
UPDATE
exec sql
UPDATE CUSTPF
SET BALANCE = BALANCE + :paymentAmt,
UPDDATE = CURRENT_DATE
WHERE CUSTID = :custId;
DELETE
exec sql
DELETE FROM ORDDTL
WHERE ORDNO = :ordNo
AND STATUS = 'X';
4. カーソル処理(複数行の取得)
SQLが複数行を返す場合、カーソルを使って1行ずつ取得する。COBOLと同じパターン。
**free
ctl-opt dftactgrp(*no) actgrp(*caller);
dcl-s custId char(8);
dcl-s custName char(30);
dcl-s balance packed(11:2);
dcl-s count int(10) inz(0);
// カーソル宣言
exec sql
DECLARE custCursor CURSOR FOR
SELECT CUSTID, CUSTNM, BALANCE
FROM CUSTPF
WHERE BALANCE > 100000
ORDER BY BALANCE DESC;
// カーソルオープン
exec sql OPEN custCursor;
// フェッチループ
exec sql FETCH custCursor INTO :custId, :custName, :balance;
dow sqlcode = 0;
count += 1;
dsply (%trim(custId) + ': ' + %trim(custName) + ' ' + %char(balance));
exec sql FETCH custCursor INTO :custId, :custName, :balance;
enddo;
// カーソルクローズ
exec sql CLOSE custCursor;
dsply ('Total: ' + %char(count) + ' customers');
*inlr = *on;
ネイティブI/Oとの対比
ネイティブI/O: SQL カーソル:
read CUSTFILE; exec sql OPEN custCursor;
dow not %eof(CUSTFILE); exec sql FETCH custCursor INTO ...;
// 処理 dow sqlcode = 0;
read CUSTFILE; // 処理
enddo; exec sql FETCH custCursor INTO ...;
enddo;
exec sql CLOSE custCursor;
5. エラー処理(SQLCA / SQLSTATE)
SQLCODE
| SQLCODE | 意味 |
|---|---|
0 | 正常終了 |
100 | データなし(NOT FOUND) |
< 0 | エラー |
-803 | 一意制約違反(重複) |
-811 | SELECT INTO で複数行 |
-530 | 外部キー制約違反 |
エラーハンドリング
exec sql
INSERT INTO ORDHDR (ORDNO, CUSTID, AMOUNT)
VALUES (:ordNo, :custId, :amount);
select;
when sqlcode = 0;
dsply 'Insert successful';
when sqlcode = -803;
dsply 'Duplicate order number';
other;
dsply ('SQL Error: ' + %char(sqlcode));
endsl;
SQLSTATE(5桁のステータスコード)
dcl-s sqlState char(5);
exec sql
SELECT CUSTNM INTO :custName FROM CUSTPF WHERE CUSTID = :custId;
sqlState = sqlstt; // RPGの特殊変数
select;
when sqlState = '00000'; // 正常
when sqlState = '02000'; // NOT FOUND
other; // エラー
endsl;
6. トランザクション制御
COMMIT / ROLLBACK
// 受注登録(ヘッダ + 明細をアトミックに)
exec sql SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
exec sql
INSERT INTO ORDHDR (ORDNO, CUSTID, AMOUNT)
VALUES (:ordNo, :custId, :totalAmt);
if sqlcode <> 0;
exec sql ROLLBACK;
dsply 'Header insert failed';
return;
endif;
for i = 1 to lineCount;
exec sql
INSERT INTO ORDDTL (ORDNO, LINENO, ITEMCD, QTY, PRICE)
VALUES (:ordNo, :i, :detail(i).itemCd, :detail(i).qty, :detail(i).price);
if sqlcode <> 0;
exec sql ROLLBACK;
dsply 'Detail insert failed';
return;
endif;
endfor;
exec sql COMMIT;
dsply 'Order created successfully';
分離レベル
| レベル | RPG/SQL | 意味 |
|---|---|---|
| *NONE | COMMIT(*NONE) | コミット制御なし |
| *CHG | READ UNCOMMITTED | ダーティリード許可 |
| *CS | READ COMMITTED | コミット済のみ読取 |
| *ALL | REPEATABLE READ | 繰返し読取り保証 |
| *RR | SERIALIZABLE | 完全分離 |
7. ネイティブI/O vs SQL
パフォーマンス比較
| 操作 | ネイティブI/O | SQL | 勝者 |
|---|---|---|---|
| キーで1件取得 | chain key FILE | SELECT WHERE key = :k | ネイティブ(わずかに速い) |
| 全件順次読取 | read FILE ループ | DECLARE CURSOR + FETCH | 同等 |
| 条件付き全件 | read + if ループ | SELECT WHERE 条件 | SQL(DB2が最適化) |
| 複数テーブル結合 | プログラム内で chain | JOIN | SQL(圧倒的に速い) |
| 集計 | プログラム内でループ加算 | SUM/COUNT/AVG | SQL(圧倒的に速い) |
| バルク更新 | read + update ループ | UPDATE WHERE | SQL(圧倒的に速い) |
モダナイゼーションへの示唆
ネイティブI/O(read/chain/write/update)で書かれた処理は、移行時にSQLに変換する。特にループ内で chain を繰り返すパターンは JOIN に置き換えることで劇的にパフォーマンスが改善する場合がある。
8. DB2 for i 固有の機能
システム値の取得
exec sql
SET :currentUser = CURRENT_USER;
exec sql
SET :currentDate = CURRENT_DATE;
exec sql
SET :currentTs = CURRENT_TIMESTAMP;
ライブラリ修飾
// AS/400ではスキーマ = ライブラリ
exec sql
SELECT * FROM PRODLIB/CUSTPF WHERE CUSTID = :custId;
// または SET SCHEMA
exec sql SET SCHEMA PRODLIB;
exec sql
SELECT * FROM CUSTPF WHERE CUSTID = :custId;
シーケンス(採番)
// シーケンス作成
exec sql
CREATE SEQUENCE ORDSEQ START WITH 1 INCREMENT BY 1;
// 次の値を取得
exec sql
SET :nextNo = NEXT VALUE FOR ORDSEQ;
9. Cloud SQL(PostgreSQL)への移行
SQL構文の対応表
| DB2 for i | PostgreSQL | 備考 |
|---|---|---|
CURRENT_DATE | CURRENT_DATE | 同じ |
CURRENT_TIMESTAMP | CURRENT_TIMESTAMP | 同じ |
CURRENT_USER | CURRENT_USER | 同じ |
NEXT VALUE FOR seq | nextval('seq') | 構文が異なる |
CONCAT(a, b) | a || b | PostgreSQLは || が標準 |
SUBSTR(s, p, l) | SUBSTRING(s FROM p FOR l) | |
DECIMAL(v, m, n) | CAST(v AS NUMERIC(m,n)) | |
DATE(ts) | ts::DATE | PostgreSQLはキャスト構文 |
LIBRARY/FILE | schema.table | ライブラリ→スキーマ |
*LIBL(ライブラリリスト) | search_path |
データ型の対応表
| DB2 for i (DDS) | DB2 for i (SQL) | PostgreSQL |
|---|---|---|
A(n) 文字 | CHAR(n) | CHAR(n) / VARCHAR(n) |
P(m,n) パック10進 | DECIMAL(m,n) | NUMERIC(m,n) |
S(m,n) ゾーン10進 | NUMERIC(m,n) | NUMERIC(m,n) |
B(4) 2進 | SMALLINT/INTEGER | INTEGER |
L 日付 | DATE | DATE |
T 時刻 | TIME | TIME |
Z タイムスタンプ | TIMESTAMP | TIMESTAMP |
10. 設計パターン
パターン1: 検索API(SELECT + パラメータ)
// RPG(AS/400)
dcl-proc searchOrders export;
dcl-pi *n;
custId char(8) const;
fromDate date const;
toDate date const;
end-pi;
exec sql
DECLARE ordCsr CURSOR FOR
SELECT ORDNO, ORDDATE, AMOUNT
FROM ORDHDR
WHERE CUSTID = :custId
AND ORDDATE BETWEEN :fromDate AND :toDate
ORDER BY ORDDATE DESC;
exec sql OPEN ordCsr;
// ... FETCH ループ
exec sql CLOSE ordCsr;
end-proc;
→ モダナイゼーション後(.NET):
app.MapGet("/orders", async (string custId, DateOnly from, DateOnly to, NpgsqlConnection db) =>
{
var orders = await db.QueryAsync<Order>(
"SELECT ord_no, ord_date, amount FROM orders " +
"WHERE cust_id = @custId AND ord_date BETWEEN @from AND @to " +
"ORDER BY ord_date DESC",
new { custId, from, to });
return Results.Ok(orders);
});
パターン2: 集計ダッシュボード(GROUP BY)
// RPG(AS/400)
exec sql
DECLARE salesCsr CURSOR FOR
SELECT CUSTID, SUM(AMOUNT) AS TOTAL, COUNT(*) AS CNT
FROM ORDHDR
WHERE ORDDATE >= :startDate
GROUP BY CUSTID
ORDER BY TOTAL DESC;
→ これがAI経営ダッシュボードの基礎データになる。BigQueryに移行すれば、さらに高度な分析(時系列予測、異常検知)が可能。
関連記事
- COBOL × SQL 連携 — COBOL の対応する SQL 連携ガイド
- RPG 文法基礎 — RPG IV の基本文法リファレンス
- RPG 業務ロジック実装 — 業務システムの設計パターン
- RPG 本番運用 — IBM i での本番運用ガイド