CTS-KB
RPG 4 / 5

RPG × SQL 連携 (Embedded SQL)

#RPG #IBM i #SQL #DB2 for i

対象: RPG から SQL を使ってDB2 for i にアクセスする方法
基本文法は RPG 文法基礎 を参照
COBOL版は COBOL × SQL 連携 を参照


目次

  1. RPGとSQLの関係
  2. 埋め込みSQLの基本
  3. CRUD操作
  4. カーソル処理(複数行の取得)
  5. エラー処理(SQLCA / SQLSTATE)
  6. トランザクション制御
  7. ネイティブI/O vs SQL
  8. DB2 for i 固有の機能
  9. Cloud SQL(PostgreSQL)への移行
  10. 設計パターン

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一意制約違反(重複)
-811SELECT 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意味
*NONECOMMIT(*NONE)コミット制御なし
*CHGREAD UNCOMMITTEDダーティリード許可
*CSREAD COMMITTEDコミット済のみ読取
*ALLREPEATABLE READ繰返し読取り保証
*RRSERIALIZABLE完全分離

7. ネイティブI/O vs SQL

パフォーマンス比較

操作ネイティブI/OSQL勝者
キーで1件取得chain key FILESELECT WHERE key = :kネイティブ(わずかに速い)
全件順次読取read FILE ループDECLARE CURSOR + FETCH同等
条件付き全件read + if ループSELECT WHERE 条件SQL(DB2が最適化)
複数テーブル結合プログラム内で chainJOINSQL(圧倒的に速い)
集計プログラム内でループ加算SUM/COUNT/AVGSQL(圧倒的に速い)
バルク更新read + update ループUPDATE WHERESQL(圧倒的に速い)

モダナイゼーションへの示唆

ネイティブ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 iPostgreSQL備考
CURRENT_DATECURRENT_DATE同じ
CURRENT_TIMESTAMPCURRENT_TIMESTAMP同じ
CURRENT_USERCURRENT_USER同じ
NEXT VALUE FOR seqnextval('seq')構文が異なる
CONCAT(a, b)a || bPostgreSQLは || が標準
SUBSTR(s, p, l)SUBSTRING(s FROM p FOR l)
DECIMAL(v, m, n)CAST(v AS NUMERIC(m,n))
DATE(ts)ts::DATEPostgreSQLはキャスト構文
LIBRARY/FILEschema.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/INTEGERINTEGER
L 日付DATEDATE
T 時刻TIMETIME
Z タイムスタンプTIMESTAMPTIMESTAMP

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に移行すれば、さらに高度な分析(時系列予測、異常検知)が可能。


関連記事