Skip to content

golang Database programing

foolmacky edited this page May 12, 2023 · 10 revisions

■database/sqlパッケージの特性とpooling

  • database/sqlパッケージにはpooling機能が実装されていて、意識しなくても利用される
  • なので1つのデータベース上で、2つの連続したステートメントを実行すると、2つのコネクションをオープンし、別々に実行する可能性がある。
  • コネクションに制限はないが、アイドル状態のコネクションを制御することは可能。長い時間アイドル状態になり、エラーが発生する場合はdb.SetMaxIdleConns(0)を試してみる。
  • データベースのコネクションを開いたり閉じたりすると、リソース枯渇の原因となる可能性がある。
  • Failing to read all rows or user rows.Close() reserves connections from the pool.
  • Using Query() for a statement that doesn't return rows will reserve a connection from the pool.
  • Failing to be aware of how prepared statements work can lead to a lot of extra database activity.
  • 単一の接続で実行されていると思っていても、実際には複数の接続で実行されている場合がある。
  • コネクションを変更すると、そのコネクションはpoolに戻り、他のコードの状態に悪影響を及ぼす可能性がある。
  • begin, commitを直接発行してはいけない。
  • コネクションでエラーが発生すると、Goは新しいコネクションのオープンを最大10回リトライする。
  • 大きなUnit64で上位ビットが設定されている場合、ステートメントのパラメータとして渡せない。uint64を使う場合、最初は小さな数値でエラーが発生しないかもしれないが、時間が立つにつれて数値が増えていくと、エラーが発生する。
  • どうしてもNULLがリターンされる場合は、NULL許可のbool, string, int, floatの型を使う。
  • database/sqlでのほぼすべての操作では、最後の返り値としてエラーが返却される。それらのエラーをチェックし、無視しない。
  • 各Databaseの特定のエラーは、定義されているエラーの番号の定数を使って判定する。

■参考になるページ

■Goのドライバ

  • ドライバ一覧 https://github.com/golang/go/wiki/SQLDrivers
  • Goのドライバは、単一のクエリによる複数の結果セットをサポートしていないので、複数の結果セットを返却するようなストアドプロシージャはうまく動作しない。
  • 明示的には複数ステートメントサポートをしていない。
DELETE FROM tbl1; DELETE FROM tbl2
  • Mysqlドライバでは、ストアドが実行できない。MySQLがコネクションが複数ステートメントモードに設定されることを期待するため。
  • 通常はドライバーパッケージを直接使わないで、database/sqlで定義された型のみを使う。そうすることで、特定のドライバに依存しないようになる。

■DBについて

  • 直感に反しているかもしれないが、sql.Open()はデータベースへの接続を確立しないし、ドライバーの接続パラメータの検証もしない。後で使用されるabstructされたデータベースの準備をするだけ。
  • 実際のデータベースへのコネクションは、必要になったときに初めて、遅延して確立される。
  • この仕様とdriverの実装の整合性が原因なのか、Openしたあとのconnectionを複数のgoroutineで並行処理しようとすると、エラーが発生する。Openしたあとに、Pingを一度発行すると、問題ない。
  • データベースが利用可能でアクセス可能かどうか、すぐに確認したい場合は、sql.Ping()を使う。
  • sql.DBオブジェクトは長期間使われるように設計されているので、Opne()とClose()を頻繁には行わない。
  • それぞれのデータストアごとに単一のsql.DBオブジェクトを作り、必要に応じてそのsql.DBを引き回すか、あるいはグローバルに利用可能にしたり、関数に引数として渡す。

■Prepared Statement

  • prepared statementのときの動き
    • SQL文をプリペアすると、pool内のコネクション上でprepare
    • Stmtオブジェクトはどのコネクションが使われたかを記憶
    • そのStmtを実行すると、そのコネクションをしようとするが、もしもクローズされていたり他の処理をしていて使用できない場合は、プールから他のコネクションを取得し、データベースの他のコネクションで再度SQL文をプリペア
    • もとのコネクションがビジー状態のとき、SQL文は必要に応じて再度prepareされるため、大量のprepared Stmtを生成し、ステートメントのリークに結びつく可能性がある。
  • もしprepared statementを使いたくない場合は、fmt.Sprint()などを使ってSQLを組み立て、そのSQLをdb.Query(), db.QueryRow()へ唯一の引数として渡す必要があるが、これはドライバのサポートに依存している。
  • Txで作成されたprepared statementはそのTx専用に紐付けられているため、Txオブジェクトの操作をするとき、アクションはTxとそれに紐付けられた単一のコネクションに直接マッピングされる。
  • Tx外部でprepareされたstatementをTx内で使うには、Tx.Stmt()を使うことができるが、実装が良くないので基本使わない。
  • prepared statementでのプレースホルダーパラメータの構文は、データベースにより異なる。

■更新系とトランザクションについて

  • Goでは基本的にトランザクションはデータストアへのコネクションを予約するオブジェクト
  • Begin()やCommit()等のトランザクション関連の関数を使い、begin, commitのようなSQL文を組み合わせて使わない。
  • 下手に使うとコネクションが開放されなくなったり、更新の一部が勝手に確定されたりする。
  • トランザクション中で、ステートメントをバッチ実行する方法はない。
  • トランザクション内で処理をしている間は、DB変数を呼び出してはいけない。それを呼び出すと、他のコネクションで処理される。
  • insert文、update文、delete文、そして行を返却しないSQLステートメントではExec()を使う。
err := db.Query("DELETE FROM users")

などとしてしまうと、sql.Rowsがクローズされるまでコネクションを閉じないため、リソース枯渇につながる。

■結果セットの取得について

  • Goで結果セットを取得するには、唯一以下の手順しかない。正しい型の変数を作成しそのポインタを渡す必要がある。
var {
    id int
    name string
)
rows, err := db.Query("SELECT id, name FROM users WHERE id = ?", 1)
if err != nil {
    log.Fatal(err)
}
defer rows.Close()
for rows.Next() {
    err := rows.Scan(&id, &name)
    if err != nil {
        log.Ftal(err)
    }
    log.Println(id, name)
}
err = rows.Err()
if err != nil {
        log.Fatal(err)
}
  • for rows.Next() loopの終了後、必ずエラーのチェックをする。loop内でのエラーの発生を把握する必要があり、すべての行が処理されるまでloopが継続するはずだと決めつけてはいけない。
  • rowsで表現されるオープン中の結果セットがある限り、実際のコネクションはビジー状態になっていて、他のクエリで使うことはできない。
  • rows.Next()ですべての行を繰り返し処理すると、最終的に最後の行が読み込まれ、rows.Next()内部でEOFエラーが発生するので、rows.Close()を呼び出します。
  • もしも途中でloopを抜けてしまうと、rowsはクローズされないし、コネクションもオープン状態のままになってしまう。(ただし、rows.Next()がエラーによってfalseが返却されるときには、自動的にクローズされます)こうなるとリソースが簡単に使い尽くされてしまう。
  • rows.Close()はすでにクロースされている場合には何もしない安全な操作なので、何回呼び出してもOK
  • ただし、実行時パニックを避けるため、エラーチェックを最初に行ってから、エラーがない場合のみ、rows.Close()を呼び出す。
  • loop終了時に明示的にrows.Close()していたとしても、defer rows.Close()をするのは悪いことじゃない。
  • loop内でdeferはしない。loopを含む関数が終了するまでdeferは実行されないため、長時間実行される関数で使うべきではないし、使ってしまうと徐々にメモリーが浪費される。
  • loop内で繰り返しクエリを実行し、結果セットを取得している場合には、deferを使わずに毎回結果の利用が終わった際に、明示的にrows.Close()を呼び出す。
  • Go ver1.8からContextを渡すことにより、SQLの実行をキャンセルできるようになった。
  • 結果が1行もない場合は、sql.ErrorRowsがリターンされる。一般的には他の言語ではエラーにならないことが多いが、Goではそれを意識する。
  • クエリのエラーはScan()が呼び出されるまで延期され、Scan()によって返却される。

Clone this wiki locally