JDBC ドライバーの使用

このトピックでは、 JDBC ドライバーの使用方法に関する情報を提供します。

このトピックの内容:

Snowflake JDBC API 拡張機能

Snowflake JDBC ドライバーは、標準の JDBC 仕様を超える追加のメソッドをサポートしています。このセクションでは、ラップ解除を使用してSnowflake固有のメソッドにアクセスする方法を説明し、ラップ解除する必要がある状況の内の3つについて説明します。

Snowflake固有のクラスのラップ解除

Snowflake JDBCドライバーは、Snowflake固有のメソッドをサポートしています。これらのメソッドは、 SnowflakeConnection、 SnowflakeStatement、 SnowflakeResultSet などのSnowflake固有のJava言語インターフェイスで定義されています。たとえば、 SnowflakeStatement インターフェイスには、 JDBC ステートメントインターフェイスにはない getQueryID() メソッドが含まれています。

Snowflake JDBC ドライバーが JDBC オブジェクトを作成するように要求されると(例: Connection オブジェクトの createStatement() メソッドを呼び出して JDBC Statement オブジェクトを作成)、Snowflake JDBC ドライバーは、 JDBC 標準のメソッドだけでなく、Snowflakeインターフェイスの追加メソッドも実装するSnowflake固有のオブジェクトを実際に作成します。

これらのSnowflakeメソッドにアクセスするには、オブジェクト( Statement オブジェクトなど)を「ラップ解除」して、Snowflakeオブジェクトとそのメソッドを公開します。その後、追加のメソッドを呼び出すことができます。

次のコードは、 JDBC Statement オブジェクトをラップ解除して SnowflakeStatement インターフェイスのメソッドを公開し、これらのメソッドの1つ(この場合は setParameter)を呼び出す方法を示しています。

Statement statement1;
...
// Unwrap the statement1 object to expose the SnowflakeStatement object, and call the
// SnowflakeStatement object's setParameter() method.
statement1.unwrap(SnowflakeStatement.class).setParameter(...);

非同期クエリの実行

Snowflake JDBC ドライバーは、非同期クエリ(つまり、クエリが完了する前にユーザーに制御を返すクエリ)をサポートしています。ユーザーはクエリを開始し、ポーリングを使用してクエリがいつ完了したかを判断できます。クエリが完了すると、ユーザーは結果セットを読み取ることができます。

この機能により、クライアントプログラム自体はマルチスレッドを使用せずに、複数のクエリを並行して実行できます。

非同期クエリは、 SnowflakeConnection、 SnowflakeStatement、 SnowflakePreparedStatement、および SnowflakeResultSet クラスに追加されたメソッドを使用します。

同じセッションで同期クエリと非同期クエリを組み合わせて実行できます。

非同期クエリのベストプラクティス

  • クエリを並行して実行する前に、どのクエリが他のクエリに依存しているかを確認してください。一部のクエリは相互依存しており、順序に依存するため、並列化には適していません。たとえば、 INSERT ステートメントは、対応する CREATE TABLE ステートメントが終了するまで開始するべきではありません。

  • 使用可能なメモリに対して、実行するクエリが多すぎないことを確認してください。特に複数の ResultSet が同時にメモリに保存されている場合、複数のクエリを並行して実行すると通常は、より多くのメモリが消費されます。

  • ポーリング時に、クエリが成功しないまれなケースを処理します。たとえば、次の潜在的な無限ループを回避します。

    QueryStatus queryStatus = QueryStatus.RUNNING;
    while (queryStatus != QueryStatus.SUCCESS)  {     //  NOT RECOMMENDED
        Thread.sleep(2000);   // 2000 milliseconds.
        queryStatus = resultSet.unwrap(SnowflakeResultSet.class).getStatus();
        }
    

    代わりに、次のようなコードを使用します。

    // Assume that the query isn't done yet.
    QueryStatus queryStatus = QueryStatus.RUNNING;
    while (queryStatus == QueryStatus.RUNNING || queryStatus == QueryStatus.RESUMING_WAREHOUSE)  {
        Thread.sleep(2000);   // 2000 milliseconds.
        queryStatus = resultSet.unwrap(SnowflakeResultSet.class).getStatus();
        }
    
    if (queryStatus == QueryStatus.SUCCESS) {
        ...
        }
    
  • トランザクション制御ステートメント(BEGIN、 COMMIT、および ROLLBACK)が他のステートメントと並行して実行されないようにします。

非同期クエリの例

これらの例のほとんどでは、プログラムが以下に示すようにクラスをインポートする必要があります。

import java.sql.Connection;
import java.sql.ResultSet;
import java.sql.Statement;
import net.snowflake.client.core.QueryStatus;
import net.snowflake.client.jdbc.SnowflakeConnection;
import net.snowflake.client.jdbc.SnowflakeResultSet;
import net.snowflake.client.jdbc.SnowflakeStatement;

これは非常に単純な例です。

    String sql_command = "";
    ResultSet resultSet;

    System.out.println("Create JDBC statement.");
    Statement statement = connection.createStatement();
    sql_command = "SELECT PI()";
    System.out.println("Simple SELECT query: " + sql_command);
    resultSet = statement.unwrap(SnowflakeStatement.class).executeAsyncQuery(sql_command);

    // Assume that the query isn't done yet.
    QueryStatus queryStatus = QueryStatus.RUNNING;
    while (queryStatus == QueryStatus.RUNNING || queryStatus == QueryStatus.RESUMING_WAREHOUSE) {
      Thread.sleep(2000); // 2000 milliseconds.
      queryStatus = resultSet.unwrap(SnowflakeResultSet.class).getStatus();
    }

    if (queryStatus == QueryStatus.FAILED_WITH_ERROR) {
      // Print the error code to stdout
      System.out.format("Error code: %d%n", queryStatus.getErrorCode());
      System.out.format("Error message: %s%n", queryStatus.getErrorMessage());
    } else if (queryStatus != QueryStatus.SUCCESS) {
      System.out.println("ERROR: unexpected QueryStatus: " + queryStatus);
    } else {
      boolean result_exists = resultSet.next();
      if (!result_exists) {
        System.out.println("ERROR: No rows returned.");
      } else {
        float pi_result = resultSet.getFloat(1);
        System.out.println("pi = " + pi_result);
      }
    }

この例では、クエリ ID を保存し、接続を閉じ、接続を再度開き、クエリ ID を使用してデータを取得します。

    String sql_command = "";
    ResultSet resultSet;
    String queryID = "";

    System.out.println("Create JDBC statement.");
    Statement statement = connection.createStatement();
    sql_command = "SELECT PI() * 2";
    System.out.println("Simple SELECT query: " + sql_command);
    resultSet = statement.unwrap(SnowflakeStatement.class).executeAsyncQuery(sql_command);
    queryID = resultSet.unwrap(SnowflakeResultSet.class).getQueryID();
    System.out.println("INFO: Closing statement.");
    statement.close();
    System.out.println("INFO: Closing connection.");
    connection.close();

    System.out.println("INFO: Re-opening connection.");
    connection = create_connection(args);
    use_warehouse_db_and_schema(connection);
    resultSet = connection.unwrap(SnowflakeConnection.class).createResultSet(queryID);

    // Assume that the query isn't done yet.
    QueryStatus queryStatus = QueryStatus.RUNNING;
    while (queryStatus == QueryStatus.RUNNING) {
      Thread.sleep(2000); // 2000 milliseconds.
      queryStatus = resultSet.unwrap(SnowflakeResultSet.class).getStatus();
    }

    if (queryStatus == QueryStatus.FAILED_WITH_ERROR) {
      System.out.format(
          "ERROR %d: %s%n", queryStatus.getErrorMessage(), queryStatus.getErrorCode());
    } else if (queryStatus != QueryStatus.SUCCESS) {
      System.out.println("ERROR: unexpected QueryStatus: " + queryStatus);
    } else {
      boolean result_exists = resultSet.next();
      if (!result_exists) {
        System.out.println("ERROR: No rows returned.");
      } else {
        float pi_result = resultSet.getFloat(1);
        System.out.println("pi = " + pi_result);
      }
    }

データファイルをストリームから内部ステージに直接アップロードする

PUT コマンドを使用してデータファイルをアップロードできます。ただし、データをストリームから内部(つまり、Snowflake)ステージにファイルとして直接転送することが理にかなっている場合があります。 SnowflakeConnection クラスで公開されているメソッドは次のとおりです。

/**
 * Method to compress data from a stream and upload it at a stage location.
 * The data will be uploaded as one file. No splitting is done in this method.
 *
 * Caller is responsible for releasing the inputStream after the method is
 * called.
 *
 * @param stageName    stage name: e.g. ~ or table name or stage name
 * @param destPrefix   path / prefix under which the data should be uploaded on the stage
 * @param inputStream  input stream from which the data will be uploaded
 * @param destFileName destination file name to use
 * @param compressData compress data or not before uploading stream
 * @throws java.sql.SQLException failed to compress and put data from a stream at stage
 */
public void uploadStream(String stageName,
                         String destPrefix,
                         InputStream inputStream,
                         String destFileName,
                         boolean compressData)
    throws SQLException

サンプル使用法:

Connection connection = DriverManager.getConnection(url, prop);
File file = new File("/tmp/test.csv");
FileInputStream fileInputStream = new FileInputStream(file);

// upload file stream to user stage
connection.unwrap(SnowflakeConnection.class).uploadStream("MYSTAGE", "testUploadStream",
   fileInputStream, "destFile.csv", true);

3.9.2より前の JDBC ドライバーバージョン用に記述されたコードは、 SnowflakeConnection.class をラップ解除するのではなく、 SnowflakeConnectionV1 をキャストする場合があります。例:

...

// For versions prior to 3.9.2:
// upload file stream to user stage
((SnowflakeConnectionV1) connection.uploadStream("MYSTAGE", "testUploadStream",
   fileInputStream, "destFile.csv", true));

注釈

新しいバージョンのドライバーを使用しているお客様は、 unwrap を使用するようにコードを更新する必要があります。

内部ステージからストリームにデータファイルを直接ダウンロードする

GET コマンドを使用してデータファイルをダウンロードできます。ただし、内部(つまり、Snowflake)ステージのファイルからストリームにデータを直接転送することが理にかなっている場合があります。 SnowflakeConnection クラスで公開されているメソッドは次のとおりです。

/**
 * Download file from the given stage and return an input stream
 *
 * @param stageName      stage name
 * @param sourceFileName file path in stage
 * @param decompress     true if file compressed
 * @return an input stream
 * @throws SnowflakeSQLException if any SQL error occurs.
 */
InputStream downloadStream(String stageName,
                           String sourceFileName,
                           boolean decompress) throws SQLException;

サンプル使用法:

Connection connection = DriverManager.getConnection(url, prop);
InputStream out = connection.unwrap(SnowflakeConnection.class).downloadStream(
    "~",
    DEST_PREFIX + "/" + TEST_DATA_FILE + ".gz",
    true);

3.9.2より前の JDBC ドライバーバージョン用に記述されたコードは、 SnowflakeConnection.class をラップ解除するのではなく、 SnowflakeConnectionV1 をキャストする場合があります。例:

...

// For versions prior to 3.9.2:
// download file stream to user stage
((SnowflakeConnectionV1) connection.downloadStream(...));

複数ステートメントのサポート

このセクションでは、 JDBC ドライバー を使用して、単一のリクエストで複数のステートメントを実行する方法について説明します。

注釈

デフォルトでは、Snowflakeは、複数のステートメントで発行されたクエリに対してエラーを返します。この動作は、 SQL インジェクション から保護するために部分的に行われます。複数ステートメント機能を使用すると、 SQL インジェクションを受ける可能性があるため、慎重に使用する必要があります。 SnowflakeStatement クラスのsetParameter()メソッドを使用して実行するステートメントの数を指定することにより、ステートメントを追加して注入することがより困難になるため、リスクを軽減できます。詳細については、 インターフェイス: SnowflakeStatement をご参照ください。

複数のステートメントの送信と結果の処理

複数のステートメントを含むクエリは、クエリ文字列にセミコロンで区切られた複数のステートメントが含まれていることを除いて、単一ステートメントのクエリと同じ方法で実行できます。

複数のステートメントを許可するには、次の2つの方法があります。

  • Statement.setParameter(「MULTI_STATEMENT_COUNT」、n)を呼び出して、このStatementの実行を一度に許可するステートメントの数を指定します。詳細については、以下をご参照ください。

  • 次のコマンドの1つを実行して、セッションレベルまたはアカウントレベルで MULTI_STATEMENT_COUNT パラメーターを設定します。

    alter session set MULTI_STATEMENT_COUNT = 0;
    

    または、

    alter account set MULTI_STATEMENT_COUNT = 0;
    

    パラメーターを0に設定すると、ステートメントの数に制限がなくなります。パラメーターを1に設定すると、一度に1つのステートメントのみが許可されます。

SQL インジェクション攻撃をより困難にするために、ユーザーは setParameter メソッドを呼び出して、以下に示すように、1回の呼び出しで実行されるステートメントの数を指定できます。この例では、1回の呼び出しで実行するステートメントの数は3です。

// Specify the number of statements that we expect to execute.
statement.unwrap(SnowflakeStatement.class).setParameter(
        "MULTI_STATEMENT_COUNT", 3);

ステートメントのデフォルトの数は1です。つまり、複数ステートメントモードはオフです。

正確な数を指定せずに複数のステートメントを実行するには、値0を渡します。

MULTI_STATEMENT_COUNT パラメーターは JDBC 標準の一部ではありません。これはSnowflake拡張機能です。このパラメーターは、複数のSnowflakeドライバー/コネクタに影響します。

単一の execute() 呼び出しで複数のステートメントが実行される場合、最初のステートメントの結果は、標準の getResultSet() および getUpdateCount() メソッドを介して利用可能です。続くステートメントの結果にアクセスするには、 getMoreResults() メソッドを使用します。このメソッドでは、次の結果が結果セットである場合は true を返し、次の結果が更新カウントである場合(または結果がもうない場合)は false を返します。

以下の例では、 MULTI_STATEMENT_COUNT パラメーターを設定し、3つのステートメントを実行し、更新カウントと結果セットを取得します。

// Create a string that contains multiple SQL statements.
String command_string = "create table test(n int); " +
                        "insert into test values (1), (2); " +
                        "select * from test order by n";
Statement stmt = connection.createStatement();
// Specify the number of statements (3) that we expect to execute.
stmt.unwrap(SnowflakeStatement.class).setParameter(
        "MULTI_STATEMENT_COUNT", 3);

// Execute all of the statements.
stmt.execute(command_string);                       // false

// --- Get results. ---
// First statement (create table)
stmt.getUpdateCount();                              // -1 (DDL)

// Second statement (insert)
stmt.getMoreResults();                              // false
stmt.getUpdateCount();                              // 2

// Third statement (select)
stmt.getMoreResults();                              // true
ResultSet rs = stmt.getResultSet();
rs.next();                                          // true
rs.getInt(1);                                       // 1
rs.next();                                          // true
rs.getInt(1);                                       // 2
rs.next();                                          // false

// Past the last statement executed.
stmt.getMoreResults();                              // false
stmt.getUpdateCount();                              // -1 (no more results)

Snowflakeは、複数ステートメントのクエリには execute() を使用することをお勧めします。メソッド executeQuery() および executeUpdate() も複数のステートメントをサポートしますが、最初の結果が予期される結果タイプ(それぞれ結果セットと更新カウント)でない場合、例外をスローします。

失敗したステートメント

SQL ステートメントのいずれかがコンパイルまたは実行に失敗すると、実行は中止されます。以前に実行された以前のステートメントは影響を受けません。

例えば、以下のステートメントが単一の複数ステートメントクエリとして実行される場合、クエリは3番目のステートメントで失敗し、例外がスローされます。

CREATE OR REPLACE TABLE test(n int);
INSERT INTO TEST VALUES (1), (2);
INSERT INTO TEST VALUES ('not_an_int');  -- execution fails here
INSERT INTO TEST VALUES (3);

その後、テーブル test のコンテンツをクエリすると、値 1 および 2 が表示されます。

サポートされていない機能

PUT および GET ステートメントは複数ステートメントクエリではサポートされていません。

複数ステートメントクエリでは、ステートメントの準備とバインド変数の使用もサポートされていません。

変数のステートメントへのバインド

バインドにより、 SQL ステートメントはJava変数に保存されている値を使用できます。

簡単なバインド

SQL ステートメントは、バインドせず、ステートメント内のリテラルを指定することによって値を指定します。たとえば、次のステートメントは、 UPDATE ステートメントでリテラル値 42 を使用します。

stmt.execute("UPDATE table1 SET integer_column = 42 WHERE ID = 1000");

バインドを使用すると、変数内の値を使用する SQL ステートメントを実行できます。例:

int my_integer_variable = 42;
PreparedStatement pstmt = connection.prepareStatement("UPDATE table1 SET integer_colum = ? WHERE ID = 1000");
pstmt.setInt(1, my_integer_variable);
pstmt.executeUpdate();

VALUES 句内の ? は、 SQL ステートメントが変数からの値を使用することを指定します。 setInt() メソッドは、 SQL ステートメントにある最初の疑問符が、 my_integer_variable という名前の変数にある値に置き換えられる必要があることを指定します。 setInt() は0ベースの値ではなく、1ベースの値を使用することに注意してください。つまり、最初の疑問符は0ではなく1によって参照されます。

変数のタイムスタンプ列へのバインド

Snowflakeは、タイムスタンプ TIMESTAMP_LTZ , TIMESTAMP_NTZ , TIMESTAMP_TZ の3つの異なるバリエーションをサポートしています。 PreparedStatement.setTimestamp を呼び出して変数をタイムスタンプ列にバインドすると、 JDBC ドライバーはタイムスタンプ値をローカルタイムゾーン(TIMESTAMP_LTZ)、または引数として渡された Calendar オブジェクトのタイムゾーンの観点から解釈します。

// The following call interprets the timestamp in terms of the local time zone.
insertStmt.setTimestamp(1, myTimestamp);
// The following call interprets the timestamp in terms of the time zone of the Calendar object.
insertStmt.setTimestamp(1, myTimestamp, Calendar.getInstance(TimeZone.getTimeZone("America/New_York")));

ドライバーに別のバリエーション(例: TIMESTAMP_NTZ)を使用してタイムスタンプを解釈させたい場合は、次のいずれかの方法を使用します。

  • セッションパラメーター CLIENT_TIMESTAMP_TYPE_MAPPING をバリエーションに設定します。

    このパラメーターは、現在のセッションのバインディング操作すべてに影響することに注意してください。バリエーションを変更する必要がある場合(例: TIMESTAMP_LTZ に戻す場合)は、このセッションパラメーターを再度設定する必要があります。

  • (JDBC ドライバー3.13.3以降のバージョン) PreparedStatement.setObject メソッドを呼び出し、 targetSqlType パラメーターを使用して次のSnowflakeタイムスタンプバリエーションのいずれかを指定します。

    • SnowflakeUtil.EXTRA_TYPES_TIMESTAMP_LTZ

    • SnowflakeUtil.EXTRA_TYPES_TIMESTAMP_TZ

    • SnowflakeUtil.EXTRA_TYPES_TIMESTAMP_NTZ

    例:

    import net.snowflake.client.jdbc.SnowflakeUtil;
    ...
    insertStmt.setObject(1, myTimestamp, SnowflakeUtil.EXTRA_TYPES_TIMESTAMP_NTZ);
    

バッチ挿入

Javaアプリケーションコードでは、 INSERT ステートメントでパラメーターをバインドし、 addBatch() および executeBatch() を呼び出すことにより、単一のバッチに複数の行を挿入できます。

例として、次のコードは、 INTEGER 列と VARCHAR 列を含むテーブルに2つの行を挿入します。この例では、値を INSERT ステートメントのパラメーターにバインドし、 addBatch() および executeBatch() を呼び出してバッチ挿入を実行します。

Connection connection = DriverManager.getConnection(url, prop);
connection.setAutoCommit(false);

PreparedStatement pstmt = connection.prepareStatement("INSERT INTO t(c1, c2) VALUES(?, ?)");
pstmt.setInt(1, 101);
pstmt.setString(2, "test1");
pstmt.addBatch();

pstmt.setInt(1, 102);
pstmt.setString(2, "test2");
pstmt.addBatch();

int[] count = pstmt.executeBatch(); // After execution, count[0]=1, count[1]=1
connection.commit();

この手法を使用して多数の値を挿入する場合は、インジェストのためにデータを(ローカルマシン上でファイルを作成することなく)仮ステージにストリーミングすると、ドライバーのパフォーマンスを向上させることができます。値の数がしきい値を超えると、ドライバーはこれを自動的に実行します。

ドライバーによりデータを仮ステージに送信するには、ユーザーはスキーマに対して以下の権限を持っている必要があります。

  • CREATE STAGE

ユーザーにこの権限がない場合、ドライバーはフォールバックして、クエリを含むデータをSnowflakeデータベースに送信します。

さらに、セッションの現在のデータベースとスキーマを設定する必要があります。これらが設定されていない場合、ドライバーによって実行される CREATE TEMPORARY STAGE コマンドは、次のエラーにより失敗する可能性があります。

CREATE TEMPORARY STAGE SYSTEM$BIND file_format=(type=csv field_optionally_enclosed_by='"')
Cannot perform CREATE STAGE. This session does not have a current schema. Call 'USE SCHEMA', or use a qualified name.

注釈

データをSnowflakeデータベースにロードする別の方法( COPY コマンドを使用した一括ロードを含む)については、 Snowflakeへのデータのロード をご参照ください。

Javaサンプルプログラム

Javaで記述された作業サンプルの場合は、ファイルの名前、 SnowflakeJDBCExample.java を右クリックし、ローカルファイルシステムにリンク/ファイルを保存します。

トラブルシューティング

I / Oエラー: 接続のリセット

場合によっては、 JDBC ドライバーが失敗し、一定期間非アクティブになると次のエラーメッセージが表示されることがあります。

I/O error: Connection reset

接続に特定の「存続時間」を設定すると、問題を回避できます。接続が「存続時間」より長くアイドル状態の場合、 JDBC ドライバーは接続プールから接続を削除し、新しい接続を作成します。

存続時間を設定するには、 net.snowflake.jdbc.ttl という名前のJavaシステムプロパティに、接続が存続する秒数を設定します。

  • このプロパティをプログラムで設定するには、 System.setProperty を呼び出します。

    // Set the "time to live" to 60 seconds.
    System.setProperty("net.snowflake.jdbc.ttl", "60")
    
  • java コマンドの実行時にこのプロパティを設定するには、 -D フラグを使用します。

    # Set the "time to live" to 60 seconds.
    java -cp .:snowflake-jdbc-<version>.jar -Dnet.snowflake.jdbc.ttl=60 <ClassName>
    

net.snowflake.jdbc.ttl プロパティのデフォルト値は -1 です。これは、アイドル状態の接続が接続プールから削除されないことを意味します。