# SqlAgentFactory

SQL実行を行うクラスであるSqlAgentを生成するファクトリクラスです。SQL実行時の挙動を変更するための初期値の設定が行えます。

設定例

SqlConfig config = UroboroSQL.builder(...)
  // SqlAgentFactoryの設定
  .setSqlAgentFactory(new SqlAgentFactoryImpl()
    // JDBCフェッチサイズ
    .setFetchSize(1000)
    // Statementオブジェクトの検索タイムアウト時間(s)
    .setQueryTimeout(10)
    // 例外発生時のログ出力を行うかどうか
    .setOutputExceptionLog(true)
    // SQL_IDの置換文字列
    .setSqlIdKeyName("_SQL_ID_")
    // 検索結果を格納するMapのキー変換に使用するCaseFormatの初期値
    .setDefaultMapKeyCaseFormat(CaseFormat.UPPER_SNAKE_CASE)
    // 複数件挿入時の挿入方法の初期値
    .setDefaultInsertsType(InsertsType.BATCH)
    // アプリケーション全体のリトライ設定
    // SQLエラーコードが54,30006のいずれか(Oracleのリソース・ビジー)の場合
    .setSqlRetryCodeList(Arrays.asList("54", "30006"))
    // 最大リトライ回数
    .setDefaultMaxRetryCount(3)
    // リトライ間隔
    .setDefaultSqlRetryWaitTime(10)
    // トランザクション内での更新を強制するかどうか
    .setForceUpdateWithinTransaction(true)
    // 明示的な行ロック時の待機時間(s)デフォルト値
    .setDefaultForUpdateWaitSeconds(10)
    )
  ).build();
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28

# フェッチサイズと検索タイムアウト設定 ( SqlAgentFactory#setFetchSize /#setQueryTimeout )

SqlAgentで検索処理を行う際、データベースから一度に取得する行数(fetchSize)や 検索タイムアウト時間(秒)(queryTimeout)の初期値を指定することが出来ます。 指定しない場合fetchSize, queryTimeoutともに-1が設定されます。

SqlConfig config = UroboroSQL.builder(...)
  // SqlAgentFactoryの設定
  .setSqlAgentFactory(new SqlAgentFactoryImpl()
    // JDBCフェッチサイズ
    .setFetchSize(1000)
    // Statementオブジェクトの検索タイムアウト時間(s)
    .setQueryTimeout(10)
  ).build();
1
2
3
4
5
6
7
8

補足

fetchSizeは、Statement.setFetchSize (opens new window)に渡される値で、パフォーマンスに影響します。
JDBCクライアント(uroborosqlを使用しているJavaアプリケーション)ではDBサーバ側で実行されたSELECTの結果セットをfetchサイズで指定された行数ずつ分割して取得します。 そのため結果行数に対してfetchSizeが小さいと、JDBCクライアント <-> DBサーバ間の通信回数が増大してパフォーマンスに悪影響を及ぼします。
(例:select結果が10,000件、fetchSizeが100の場合、JDBCクライアント⇔DBサーバ間の通信は10,000÷100 = 100回行われる)

注意

fetchSizeはcollect/foreachメソッドで返却される結果セットの行数を制限する設定ではありません。

# 例外発生時のログ出力を行うかどうかを設定 ( SqlAgentFactory#setOutputExceptionLog )

SQL実行時にSQL例外が発生した場合に、発生した例外と実行したSQLの詳細情報を出力するかどうかを指定できます。 指定しない場合falseになります。

SqlConfig config = UroboroSQL.builder(...)
  // SqlAgentFactoryの設定
  .setSqlAgentFactory(new SqlAgentFactoryImpl()
    // 例外発生時のログ出力を行うかどうか
    .setOutputExceptionLog(true)
  ).build();
1
2
3
4
5
6

# SQL_IDの置換文字列設定 ( SqlAgentFactory#setSqlIdKeyName )

SQL文に特定の置換文字列をSQLコメントとして記述することで、SQL実行時に実行したSQLの元となるSQLファイルを特定するための 情報(SQL_ID)を埋め込むことが出来ます。SQL_IDを埋め込むことでSQLログやDBのSQL履歴で実行されたSQLの元となるファイルを 特定しやすくなります。
必要に応じてこの置換文字列は変更することが出来ます。 指定しない場合_SQL_ID_になります。

設定例

SqlConfig config = UroboroSQL.builder(...)
  // SqlAgentFactoryの設定
  .setSqlAgentFactory(new SqlAgentFactoryImpl()
    // SQL_IDの置換文字列
    .setSqlIdKeyName("_SQL_ID_")
  ).build();
1
2
3
4
5
6

department/select_department.sql

select /* _SQL_ID_ */  -- _SQL_ID_ がSQLファイルを特定するための情報の埋め込み先となる
  dept.dept_no      as  dept_no
, dept.dept_name    as  dept_name
, dept.lock_version as  lock_version
from
  department  dept
/*BEGIN*/
where
/*IF SF.isNotEmpty(deptNo)*/
and dept.dept_no  = /*deptNo*/1
/*END*/
/*IF SF.isNotEmpty(deptName)*/
and dept.dept_name  = /*deptName*/'sample'
/*END*/
/*END*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

SQL実行処理

agent.query("department/select_department")
  .param("deptNo", 1)
  .collect();
1
2
3

実行されるSQL

select /* department/select_department */  -- _SQL_ID_ にSQL名(department/select_department)が設定される
  dept.dept_no      as  dept_no
, dept.dept_name    as  dept_name
, dept.lock_version as  lock_version
from
  department  dept
where
  dept.dept_no  = 1/*deptNo*/
1
2
3
4
5
6
7
8

# CaseFormatの初期値設定 ( SqlAgentFactory#setDefaultMapKeyCaseFormat )

SQLによる検索で、以下のメソッドを使用してList<Map<String, Object>>Map<String, Object>を取得する際、 取得したMapのキー名に対する書式の初期値を指定することが出来ます。 指定しない場合CaseFormat.UPPER_SNAKE_CASEになります。

対象メソッド 戻り値の型
SqlQuery#collect() List<Map<String, Object>>
SqlQuery#findFirst() Optional<Map<String, Object>>
SqlQuery#first() Map<String, Object>
SqlQuery#stream() Stream<Map<String, Object>>

指定しない場合(初期設定:CaseFormat.UPPER_SNAKE_CASE

agent.query("department/select_department").collect();

// 結果(departments) キーがUPPER_SNAKE_CASEとなっている
[
 {"DEPT_NO"=1, "DEPT_NAME"="sales"},
 {"DEPT_NO"=2, "DEPT_NAME"="export"},
 {"DEPT_NO"=3, "DEPT_NAME"="accounting"},
 {"DEPT_NO"=4, "DEPT_NAME"="personnel"}
]
1
2
3
4
5
6
7
8
9

CaseFormat.CAMEL_CASEを初期値として設定

SqlConfig config = UroboroSQL.builder(...)
  // SqlAgentFactoryの設定
  .setSqlAgentFactory(new SqlAgentFactoryImpl()
    // 検索結果を格納するMapのキー変換に使用するCaseFormatの初期値
    .setDefaultMapKeyCaseFormat(CaseFormat.CAMEL_CASE)
  ).build();
1
2
3
4
5
6
agent.query("department/select_department").collect();

// 結果(departments) キーがCAMEL_CASEとなっている
[
 {"deptNo"=1, "deptName"="sales"},
 {"deptNo"=2, "deptName"="export"},
 {"deptNo"=3, "deptName"="accounting"},
 {"deptNo"=4, "deptName"="personnel"}
]
1
2
3
4
5
6
7
8
9

# 複数件挿入時の挿入方法の初期値設定 ( SqlAgentFactory#setDefaultInsertsType )

SqlAgent#inserts()メソッドで使用するInsertsTypeの初期値を設定することが出来ます。 指定しない場合InsertsType.BATCHになります。

SqlConfig config = UroboroSQL.builder(...)
  // SqlAgentFactoryの設定
  .setSqlAgentFactory(new SqlAgentFactoryImpl()
    // 複数件挿入時の挿入方法の初期値
    .setDefaultInsertsType(InsertsType.BATCH)
  ).build();
1
2
3
4
5
6

# SQL実行のリトライ ( SqlAgentFactory#setSqlRetryCodeList /#setDefaultMaxRetryCount /#setDefaultSqlRetryWaitTime )

SQLを実行した際、タイミングによって発生する例外(テーブルロックエラーなど)の場合はリトライを行い、 できるだけ正常に処理を終了させたい場合があります。
通常、このようなケースでは以下のような実装を行います。

String MAX_RETRY_COUNT = 3; // MAX_RETRY_COUNT はアプリケーションで定義された最大リトライ回数の定数とする
SqlConfig config = UroboroSQL.builder(...).build();

int retryCount = 0;
for(;;) {
  try (SqlAgent agent = config.agent()) {
    // INSERT文の実行
    // insert into product (product_id) values (/*product_id*/0);
    agent.update("example/insert_product").param("product_id", 1).count();
    break;
  } catch (UroborosqlSQLException ex) {
    // SQLExceptionが発生した際に行う処理を実装
    int errorCode = ex.getErrorCode();
    if (errorCode == 30006 || errorCode == 54) {// リソース・ビジー(Oracleの場合)
      // リトライ対象エラーコードの場合はリトライカウントをカウントアップしてリトライする
      retryCount++;
      if (retryCount == MAX_RETRY_COUNT) {
        // 最大リトライ回数に達した場合は例外をスローする
        throw ex;
      } else {
        try {
          // 10ms 待機
          Thread.sleep(10);
        } catch (InterruptedException iex) {
          // do nothing
        }
      }
    } else {
      // リトライ対象エラーコード以外はすぐに例外をスローする
      throw ex;
    }
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

しかし、上記のようなリトライ処理を個々の実装で行うと、 実装漏れや実装ミス、実装方法の差異(for()の代わりにwhile()を使用するなど)により不具合が発生しやすくなります。
uroboroSQLでは、アプリケーション全体のリトライ設定と、全体設定より優先される個別処理でのリトライ用APIの 2種類のAPIを提供することで、より簡潔で確実なリトライ処理が行えるよう工夫されています。
アプリケーション全体のリトライ設定はSqlAgentFactory生成時に行います。

SqlConfig config = UroboroSQL.builder(...)
  // SqlAgentFactoryの設定
  .setSqlAgentFactory(new SqlAgentFactoryImpl()
    // アプリケーション全体のリトライ設定
    // SQLエラーコードが54,30006のいずれか(Oracleのリソース・ビジー)の場合
    .setSqlRetryCodeList(Arrays.asList("54", "30006"))
    // 最大リトライ回数(3回)リトライ
    .setDefaultMaxRetryCount(3)
    // リトライ間隔10ms待機
    .setDefaultSqlRetryWaitTime(10)
  ).build();
1
2
3
4
5
6
7
8
9
10
11

リトライAPIを用いた実装は次のようになります。

// アプリケーション全体のリトライ設定に従ってリトライを行う。(個別のリトライ指定なし)
try (SqlAgent agent = config.agent()) {
  // INSERT文の実行
  // insert into product (product_id) values (/*product_id*/0);
  agent.update("example/insert_product")
    .param("product_id", 1)
    .count();
}

// 個別にリトライ設定を上書きする(retry()を利用)
try (SqlAgent agent = config.agent()) {
  // INSERT文の実行
  // insert into product (product_id) values (/*product_id*/0);
  // リトライ対象エラーコードの場合、5回のリトライを20ms間隔で行う
  agent.update("example/insert_product")
    .param("product_id", 1)
    .retry(5, 20)
    .count();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# DB更新処理をトランザクション内のみに強制 ( SqlAgentFactory#setForceUpdateWithinTransaction ) 0.14.0+

複数のDB更新処理をまとめて行う際、途中で例外が発生するとDBデータが不整合な状態になる場合があります。このようなデータ不整合を防ぐためにはトランザクションを利用します。
しかし、通常の設定ではトランザクションを開始しない状態でもDB更新処理を行うことが可能になっているため不具合に気付きにくいという問題があります。
uroboroSQLではトランザクションを開始していない状態でDB更新処理が行なわれた場合に例外をスローするオプションを提供しています。このオプションを使用することでDBデータの整合性を維持しやすくなります。

SqlConfig config = UroboroSQL.builder(...)
  // SqlAgentFactoryの設定
  .setSqlAgentFactory(new SqlAgentFactoryImpl()
    // トランザクション内での更新を強制するかどうか
    .setForceUpdateWithinTransaction(true)
    )
  ).build();
1
2
3
4
5
6
7

SqlAgentFactory#setForceUpdateWithinTransaction()trueを指定することでトランザクションを開始していない状態でDB更新処理が行なわれた場合にUroborosqlTransactionExceptionがスローされます。

agent.required(() -> { // トランザクション開始
  // トランザクション内でのDB更新なのでOK
  agent.updateWith("insert into employee (emp_no) values (/*emp_no*/1001)")
    .param("emp_no", 1)
    .count();
  });
}); // トランザクション終了

// トランザクション外でのDB更新なので UroborosqlTransactionException がスローされる
agent.updateWith("insert into department (dept_no, dept_name) values (/*dept_no*/1111, /*dept_name*/'Sales')")
  .param("dept_no", 2)
  .param("dept_name", "export")
  .count();
1
2
3
4
5
6
7
8
9
10
11
12
13

# 明示的な行ロック時の待機時間(s)のデフォルト値設定 ( SqlAgentFactory#setDefaultForUpdateWaitSeconds ) 0.14.0+

SqlEntityQuery#forUpdateWait()による明示的な行ロックをおこなう際の待機時間を指定することができます。

SqlConfig config = UroboroSQL.builder(...)
  // SqlAgentFactoryの設定
  .setSqlAgentFactory(new SqlAgentFactoryImpl()
    // 明示的な行ロック時の待機時間(s)デフォルト値
    .setDefaultForUpdateWaitSeconds(10)
    )
  ).build();
1
2
3
4
5
6
7

待機時間の初期値を設定することでSqlEntityQuery#forUpdateWait()を発行する際に適用され、 待機時間を都度指定する必要がなくなります。
SqlEntityQuery#forUpdateWait(int)を使って個別に待機時間を指定した場合は個別設定が優先されます。