Fork me on GitHub

驚き/アンチパターン/制限


database/sql は慣れれば簡単ですが、サポートするユースケースの微妙さに驚くかもしれません。これは Go のコアライブラリに共通です。

リソースの枯渇

このサイト全体で言及したように、意図したとおりに database/sql を使用しない場合、通常はリソースを消費するか、リソースの効果的な再利用を妨げることにより、問題を引き起こす可能性があります。

  • データベースを開いたり閉じたりすると、リソースが枯渇する可能性があります。

  • すべての行の読み取りに失敗したり rows.Close() を使用しないと、プールからの接続が確保されたままになります。

  • 行を返さないステートメントに Query を使用すると、プールからの接続が確保されたままになります。

  • プリペアードステートメント がどのように動作するかを認識しないと、データベースで大量の無駄な動作が発生する可能性があります。

uint64の大きな値

これは驚くべきエラーです。大きな unit64 で上位ビットが設定されている場合、ステートメントにパラメーターとして渡すことはできません。

_, err := db.Exec("INSERT INTO users(id) VALUES", math.MaxUint64) // Error

これはエラーが発生します。 uint64 値を使用する場合は注意してください。小さな値から始まり、エラーなしで機能しますが、時間の経過とともに値が増加するとエラーが発生し始める可能性があります。

コネクション状態の不一致

コネクションの状態を変更できるいくつかの操作がありますが、それは次の2つの理由で問題を引き起こす可能性があります。

  1. トランザクション内かどうかなどの、一部のコネクションの状態は、Goの型を介して処理する必要があります。

  2. 実際にはクエリは複数のコネクションで実行されていたとしても、クエリが単一の接続で実行されていると思う場合があります。

たとえば、USE ステートメントを使用して現在のデータベースを設定することは、一般的なことです。 ただし、Goでは、実行中のコネクションのみに影響します。トランザクション中でない限り、そのコネクションで実行されると思っていたが、プールから取得した別のコネクションで実際に実行される場合、そのような変更の影響を受けません。

また、コネクションを変更すると、プールに戻り、他のコードの状態を潜在的に汚染します。これは BEGIN または COMMIT ステートメントをSQLコマンドとして直接発行してはいけない理由の1つです。

データベース固有の構文

database/sql のAPIは行指向データベースの抽象化を提供しますが、特定のデータベースとドライバーは、プリペアドステートメントのプレースホルダー などの動作や構文が異なる場合があります。

複数のリザルトセット

Goドライバーは、単一のクエリからの複数のリザルトセットをサポートしていません。バルクコピーなどの一括操作をサポートするための 機能要求 はありますが、機能に追加する予定はないようです。

これは、特に、複数のリザルトセットを返すストアドプロシージャが正しく機能しないことを意味します。

ストアドプロシージャの呼び出し

ストアドプロシージャの呼び出しはドライバー固有ですが、MySQLドライバーでは現在実行できません。 次のようなものを実行することで、単一のリザルトセットを返す簡単なプロシージャを呼び出すことができるように思えるかもしれません。

err := db.QueryRow("CALL mydb.myprocedure").Scan(result) // Error

実際、これは機能しません。 次のエラーが表示されます。「 Error 1312: PROCEDURE mydb.myprocedure can’t return a result set in the given context. 」 これは、MySQLは、単一の結果であっても、接続がマルチステートメントモードに設定されることを期待しており、ドライバーが現在サポートしていないです(この問題 を参照してください)。

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

database/sql は複数のステートメントを明示的にサポートしていません。つまり、この動作はバックエンドのデータベースに依存しています。

_, err := db.Exec("DELETE FROM tbl1; DELETE FROM tbl2") // Error/unpredictable result

サーバーは、これを必要に応じて解釈できます。これには、エラーを返す、最初のステートメントのみを実行する、または両方を実行することが含まれます。

同様に、トランザクション内のステートメントをバッチ処理する方法はありません。 トランザクション内の各ステートメントはシリアルで実行する必要があり、単一の行や複数の行などの結果のリソースをスキャンまたは閉じる必要があります。そのため、コネクションは次のステートメントで使用できます。 これは、トランザクションを処理していないときの通常の動作とは異なります。 そのシナリオでは、クエリを実行し、行をループし、ループ内でデータベースへのクエリを作成することが完全に可能です(新しいコネクションで発生します)。

rows, err := db.Query("select * from tbl1") // Uses connection 1
for rows.Next() {
    err = rows.Scan(&myvariable)
    // The following line will NOT use connection 1, which is already in-use
    db.Query("select * from tbl2 where id = ?", myvariable)
}

ただし、トランザクションは1つのコネクションのみにバインドされるため、トランザクションではこれは不可能です。

tx, err := db.Begin()
rows, err := tx.Query("select * from tbl1") // Uses tx's connection
for rows.Next() {
    err = rows.Scan(&myvariable)
    // ERROR! tx's connection is already busy!
    tx.Query("select * from tbl2 where id = ?", myvariable)
}

ただし、Goは試すことを止めるわけではありません。 そのため、最初のステートメントがリソースを解放してからクリーンアップする前に別のステートメントを実行しようとすると、コネクションが破損する可能性があります。 これは、トランザクション内の各ステートメントが、データベースへの別々のネットワーク通信となることを意味します。