Go database/sql チュートリアル 11 - 想定外・アンチパターン・制限

"Surprises, Antipatterns and Limitations" from database/sql tutorial

Go database/sqlチュートリアルの想定外・アンチパターン・制限の日本語意訳になります。


想定外・アンチパターン・制限

database/sql は一度慣れるとシンプルですが、サポートしているユースケースの細かい点に驚くかもしれません。
これはGoのコアライブラリに共通しています。

リソースの枯渇

このサイト全体で説明しているように、意図した通りに database/sql を使わないと、リソースの消費や効果的に再利用されないといったようなトラブルの原因となります。

  • データベースのコネクションを開いたり閉じたりするとリソース枯渇の原因となる可能性があります。
  • 全ての行の読み込みやに失敗したり、row.Close() を使用すると、プール内のコネクションを予約します。
  • 行を返却しないSQLに対して Query() を使用すると、プール内のコネクションを予約するでしょう。
  • プリペアドステートメントがどのように動いているか認識できないと、データベースで大量の無駄な動作が発生する可能性があります。


uint64 の大きな数値

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

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

これはエラーが発生します。
uint64を使う場合、最初は小さな数値でエラーが発生しないかもしれませんが、時間が経つにつれて数値が増えていくとエラーが発生するので気をつけてください。

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

コネクションの状態は変更されることがあり、2つの理由によって問題が発生する可能性があります。

  • トランザクション中のように、いくつかのコネクションの状態については代わりにGoの型で処理する必要があります。
  • 単一の接続で実行されていると思っていても、実際には複数の接続で実行されている場合があります。

例えば、多くの人々が USE 文で現在のデータベースをセットします。
しかしGoではそれを実行したコネクションのみに効果があるでしょう。
トランザクション中でない限り、他のステートメントはそのコネクションで実行されると思っていても、実際にはプール内の違うコネクションで実行されるかもしれません。
違うコネクションでは USE の変更を受けないでしょう。

またコネクションを変更すると、そのコネクションはプールに戻り、他のコードの状態に悪影響を及ぼす危険性があります。
これが BEGINCOMMIT ステートメントをSQLコマンドとして直接発行してはいけない理由の一つです。

データベース固有のシンタックス

database/sql APIは行指向データベースの抽象化を提供しています。
しかし特定のデータベースとドライバは、プリペアドステートメントのプレースホルダのように、振る舞いやシンタックスが異なる可能性があります。

複数の結果セット

Goのドライバは、単一のクエリによる複数の結果セットをサポートしていません。
バルクコピーのような一括操作をサポートして欲しいという機能リクエストは存在していますが、今後もサポートする予定無さそうです。

これは特に、複数の結果セットを返却するようなストアドプロシージャがうまく動作しなくなることを意味します。

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

ストアドプロシージャの呼び出しはドライバ固有のものですが、MySQLドライバでは現在実行できません。
以下のようなやり方で、単一の結果セットを返却する簡単なプロシージャを呼び出すことができそうに見えます。

err := db.QueryRow("CALL mydb.myprocedure").Scan(&result) // エラー

実際には、これは動作しません。
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") // エラー 結果は予測不可能

サーバーはこのSQLを自由に解釈することが許されているため、エラーを返したり、最初のステートメントだけを実行したり、両方を実行するかもしれません。

同様に、トランザクション中でステートメントをバッチ実行する方法はありません。
トランザクション内で各ステートメントは順番に実行されなければならず、
実行結果行のリソースはスキャンかクローズされなければなりません。
そのため実際のコネクションは次のステートメントの使用のために解放されます。
これはトランザクションを使わない通常の動作とは異なっています。
このシナリオでは、クエリを実行、結果行のループ、ループ内でのクエリの作成(新しい接続でのクエリ)ができます。

rows, err := db.Query("select * from tbl1") // コネクション1を使用
for rows.Next() {
	err = rows.Scan(&myvariable)
	// これは「使用中」になっているコネクション1を使いません
	db.Query("select * from tbl2 where id = ?", myvariable)
}

しかし、トランザクションは単一のコネクションに紐付いているため、
このやり方はトランザクションでは不可能です。

tx, err := db.Begin()
rows, err := tx.Query("select * from tbl1") // txのコネクションを使用
for rows.Next() {
	err = rows.Scan(&myvariable)
	// エラー! txのコネクションは既に「ビジー状態」です
	tx.Query("select * from tbl2 where id = ?", myvariable)
}

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


前章: コネクションプール

 
comments powered by Disqus