2016/11/06

Recent entries from same category

  1. Go 言語プログラミングエッセンスという本を書きました。
  2. errors.Join が入った。
  3. unsafe.StringData、unsafe.String、unsafe.SliceData が入った。
  4. Re: Go言語で画像ファイルか確認してみる
  5. net/url に JoinPath が入った。

golang 1.8 では database/sql に幾らかの新機能が追加されます。
  • キャンセル可能なクエリ
  • データベースの型の可視化
  • 複数の結果セット
  • サーバへのping
  • トランザクション分離レベル
  • 名前付きパラメータ
database/sql changes - Google ドキュメント
https://docs.google.com/document/d/1F778e7ZSNiSmbju3jsEWzShcb8lIO4kDyfKDNm4PNd8/edit#

本記事では Golang 1.8 で追加される database/sql の変更内容と、go-sqlite3 での対応状況、利用する上での注意点等を書いていきます。

キャンセル可能なクエリ

実行が長いクエリがキャンセルできるようになります。各 API に Context のサフィックスが付いた物が提供されます。具体的には Query であれば QueryContext、Exec であれば ExecContext が追加されます。第一引数に context.Context が渡され、外部からクエリのキャンセルが行えます。context.WithCancel でキャンセル用の関数を得て、キャンセルしたいタイミングで呼び出します。単純にタイムアウト目的であれば context.WithTimeout が使えます。

ctx, cancel := context.WithCancel(context.Background())
go func() {
    // 1秒待ってからキャンセル
    time.Sleep(1 * time.Second)
    cancel()
}()

stmt, err := db.PrepareContext(ctx, "SELECT name FROM test where id = ?")
if err != nil {
    panic(err)
}
defer stmt.Close()

rows, err := stmt.QueryContext(ctx, id)
if err != nil {
    log.Fatal(err)
}

ただしこの動作はデータベースのドライバにより異なります。MySQL の場合はクエリ毎にキャンセルできる仕組みがデータベースに備わっていますが、SQLite3 はクエリ毎ではありません。おそらく MySQL 向けのドライバはクエリ毎にキャンセル出来る実装を作ってくると思いますが、SQLite3 ドライバではデータベース全体でのキャンセルになります。もし個別にキャンセルしたいのであれば接続ごと作り直し別のコネクションでクエリを発行して下さい。(この動作は現状、暫定的です)

データベースの型の可視化

これまでは結果のカラム名を得る事は出来ましたが、型を得る事が出来ませんでした。しかし go1.8 からはドライバ側から型を得られる様になります。以下の情報が得られる様になります。
  • データベース上の型名
  • reflect.Type で表す型
  • 長さ
  • 厳密な数値のスケール
  • Null 可否

これまでも interface{} のポインタを使って Scan を呼び出す事でダイナミックな値をスキャンする事が出来ましたが、reflect.Type を使う事で、データベース側から予め型を提供できる様になる為、よりシームレスな値の取得が出来る様になります。また Null 可否が得られる様になるので例えばデータベースオーサリングツールを作る際には便利だと思います。実際には、Scan を使う限りそれほど便利にはなりません。しかしこれは今後、golang の database/sql として Blob をサポートしたり Blob を io.Reader/io.Writer で読み書き出来る様にする為の布石だと僕は思っています。

go-sqlite3 は上記の API を全てサポート済みです。

複数の結果セット

幾らかのデータベースでは複数の結果セットをサポートしています。複数のクエリを一度に発行し、結果セットを一度に得ます。

for {
    for rows.Next() {
        if err := rows.Scan(...); err != nil {
            log.Fatal(err)
        }
    }
    if !rows.NextResultSet() {
        break
    }
}

go-sqlite3 もサポートする予定でしたが、sqlite3 の複数クエリ実行は golang の複数結果セットが期待する物と異なる為、現状は実装を見送りました。

サーバへのPing

データベースが接続可能かどうかは、実際に接続してみないと分からない仕様であった為、今ある Ping の実装も実際に Open を呼び出しているだけでした。golang 1.8 ではこの実装をドライバ側に委ねられる様になりました。もちろん context.Context が引き渡されるため、データベースがサポートしているのであれば Ping の中断も行えます。go-sqlite3 でもサポートしています。ただし SQLite3 の場合はコネクションが閉じたかどうかでしか判断していませんが。

トランザクション分離レベル

トランザクション分離レベルをユーザ側から指定できる様になります。具体的には以下の種別を BeginContext で与えた context.Context に IsolationContext でレベル指定できる様になります。
  • LevelDefault
  • LevelReadUncommitted
  • LevelReadCommitted
  • LevelWriteCommitted
  • LevelRepeatableRead
  • LevelSnapshot
  • LevelSerializable
  • LevelLinearizable

ただしこれらはデータベースドライバによりサポート状況が異なるはずです。例えば SQLite3 であればデータベース全体でのロックしか粒度が設定できません。これについては現在実装を検討中です。それまではデフォルト以外の IsolationLevel は未サポートとなります。

名前付きパラメータ

これが今回、ユーザにとっては一番大きな変更点だと思います。これまでは Exec や Query 時に引数で値を受け渡す API であった為、名前付き引数が実現できませんでした。例えば PostgreSQL の場合はプレースホルダが $1$2 という指定であった為、引数の順番をプレースホルダ側から指定できるのですがこの形式を取らないデータベースでは引数の順番だけでプレースホルダの位置が決められません。今回の変更では以下の様に名前付き引数を値として渡せる様になりました。

row := db.QueryRow(`
    select id, extra from foo where id = :id and extra = :extra
`, sql.Param(":id"1), sql.Param(":extra""foo"))

SQL 内で値を使いまわせるのでとても便利になります。go-sqlite3 で対応済みです。

各ドライバの対応状況

現状僕が知る限り go-sqlite3 以外のドライバ全てではまだ実装が進んでいません(というか始まってすらいないかも)。go-sqlite3 がこの仕様に乗っかり人柱になろうという目論見です。名前付きパラメータはおそらく問題なく使えるかと思いますが、IsolateLevel やクエリキャンセルについては今後動作を変えるかもしれません。

Posted at by