Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

[インデックス 18019] ファイルの概要

このコミットは、Go言語の標準ライブラリ database/sql パッケージにおける、プリペアドステートメント使用時の自動再接続の不具合と、それに伴う複数のコネクションリークを修正するものです。特に、データベース接続が切断された際に driver.ErrBadConn エラーが発生した場合のハンドリングを改善し、堅牢性を高めています。

コミット

database/sql: プリペアドステートメントにおける自動再接続の修正

これにより、いくつかのコネクションリークも修正されます。 Fixes #5718

R=bradfitz, adg CC=alberto.garcia.hierro, golang-dev https://golang.org/cl/14920046

GitHub上でのコミットページへのリンク

https://github.com/golang/go/commit/762a9d934eab267418595df7a220eec50919b77d

元コミット内容

commit 762a9d934eab267418595df7a220eec50919b77d
Author: Julien Schmidt <google@julienschmidt.com>
Date:   Tue Dec 17 11:57:30 2013 -0800

    database/sql: fix auto-reconnect in prepared statements
    
    This also fixes several connection leaks.
    Fixes #5718
    
    R=bradfitz, adg
    CC=alberto.garcia.hierro, golang-dev
    https://golang.org/cl/14920046

変更の背景

このコミットの主な背景は、Goの database/sql パッケージが、データベースとの接続が予期せず切断された際に、特にプリペアドステートメントを使用している場合に、適切に自動再接続を試みない、または再接続の試行中にコネクションリークが発生するという問題に対処するためです。

具体的には、Goの database/sql パッケージは、データベースドライバが driver.ErrBadConn を返した場合に、内部的に接続を再確立し、操作をリトライするメカニズムを持っています。しかし、このメカニズムがプリペアドステートメント (*sql.Stmt) のコンテキストで十分に機能していなかったようです。プリペアドステートメントは、一度準備されると特定のデータベースコネクションに紐付けられることが多く、そのコネクションが切断された場合に、ステートメント自体が新しいコネクションで再準備される必要があります。この再準備のロジックに不備があったため、driver.ErrBadConn が返された際に、無限ループに陥ったり、古いコネクションが適切にクローズされずにリークしたりする問題が発生していました。

GitHub Issue #5718 (golang.org/issue/5718) は、この問題の具体的な報告であり、このコミットはその問題を解決するために作成されました。コネクションリークは、アプリケーションのメモリ使用量が増加し、最終的にはデータベースへの接続が枯渇する原因となるため、非常に重要な修正です。

前提知識の解説

database/sql パッケージ

database/sql はGo言語の標準ライブラリで、SQLデータベースへの汎用的なインターフェースを提供します。このパッケージ自体は特定のデータベースドライバを含まず、データベース固有の操作は database/sql/driver インターフェースを実装した外部ドライバに委譲します。

driver.ErrBadConn

database/sql/driver パッケージで定義されているエラーで、データベースドライバが「コネクションが壊れている、または使用できない」と判断した場合に返されます。このエラーが返された場合、database/sql パッケージは通常、内部的に新しいコネクションを取得し、失敗した操作をリトライしようとします。これは、一時的なネットワークの問題やデータベースサーバーの再起動などによってコネクションが切断された場合に、アプリケーションが自動的に回復できるようにするための重要なメカニズムです。

プリペアドステートメント (*sql.Stmt)

プリペアドステートメントは、SQLクエリを事前にデータベースサーバーに送信して準備(コンパイル)しておくことで、繰り返し実行する際のパフォーマンスを向上させるための機能です。SQLインジェクション攻撃を防ぐためにも推奨されます。database/sql パッケージでは、DB.Prepare() メソッドや Tx.Prepare() メソッドで *sql.Stmt オブジェクトを作成します。この *sql.Stmt オブジェクトは、通常、それを準備した特定のデータベースコネクションに紐付けられます。

コネクションプール

database/sql パッケージは、データベースコネクションのプールを内部的に管理しています。これにより、アプリケーションがデータベースにアクセスするたびに新しいコネクションを確立するオーバーヘッドを避けることができます。コネクションは使用後にプールに戻され、再利用されます。しかし、コネクションが適切にクローズされずにプールに戻されない場合、コネクションリークが発生し、プール内の利用可能なコネクションが枯渇する可能性があります。

自動再接続メカニズム

database/sql パッケージは、driver.ErrBadConn が返された場合に、透過的に操作をリトライし、必要に応じて新しいコネクションを確立しようとします。これは、アプリケーション開発者が手動でエラーハンドリングとリトライロジックを記述する手間を省くためのものです。しかし、このメカニズムが正しく機能しないと、アプリケーションの信頼性やパフォーマンスに悪影響を及ぼします。

技術的詳細

このコミットの技術的詳細は、主に database/sql/sql.godatabase/sql/fakedb_test.go の変更に集約されます。

sql.go の変更点

  1. maxBadConnRetries 定数の導入: const maxBadConnRetries = 10 が導入されました。これは、driver.ErrBadConn が返された場合に操作をリトライする最大回数を定義します。以前はハードコードされた 10 が使われていましたが、これを定数として明示することで、コードの意図が明確になり、将来的な調整が容易になります。

  2. DB メソッドのリトライロジックの改善: DB.Prepare, DB.Exec, DB.Query, DB.Begin の各メソッドにおいて、driver.ErrBadConn が返された場合に maxBadConnRetries 回までリトライするループが明示的に記述されています。これにより、一時的なコネクションの問題が発生した場合でも、これらの操作が自動的に回復を試みるようになります。

  3. Stmt.Exec および Stmt.Query のリトライロジックの追加: これがこのコミットの最も重要な変更点の一つです。以前の Stmt.ExecStmt.Query は、s.connStmt() でコネクションとステートメントを取得し、エラーが発生した場合はすぐに返していました。しかし、この変更では、DB メソッドと同様に maxBadConnRetries 回のリトライループが導入されました。

    • s.connStmt()driver.ErrBadConn を返した場合、ループを継続して再試行します。
    • resultFromStatement または rowsiFromStatement の実行中に driver.ErrBadConn が返された場合も、releaseConn(err) を呼び出してコネクションを適切に解放し、ループを継続して再試行します。
    • これにより、プリペアドステートメントが紐付けられているコネクションが切断された場合でも、新しいコネクションでステートメントを再準備し、操作をリトライできるようになります。
  4. Stmt.connStmt() の変更: Stmt.connStmt() は、プリペアドステートメントが使用するコネクションと、そのコネクション上のドライバステートメント (driver.Stmt) を取得する内部メソッドです。

    • 以前は、新しいコネクションが必要な場合に for i := 0; ; i++ の無限ループでコネクションを取得し、driver.ErrBadConn の場合に continue していましたが、このループは Stmtcss (cached statements) に新しい connStmt を追加するロジックと混在しており、複雑でした。
    • 変更後、Stmt.connStmt()driver.ErrBadConn のリトライロジックを直接持たず、単一のコネクション取得と準備の試行を行います。エラーが発生した場合は、そのエラーを呼び出し元 (Stmt.ExecStmt.Query) に返します。これにより、Stmt.ExecStmt.Query のリトライループが driver.ErrBadConn を適切に処理できるようになります。
    • また、dc.prepareLocked でエラーが発生した場合に s.db.putConn(dc, err) を呼び出してコネクションをプールに戻すことで、コネクションリークを防いでいます。

fakedb_test.go の変更点

このファイルは、database/sql パッケージのテストのために、データベースドライバの振る舞いをシミュレートする fakeConnfakeStmt を定義しています。

  1. hookPrepareBadConn, hookExecBadConn, hookQueryBadConn の追加: これらのグローバル変数は、テスト中に fakeConn.Prepare, fakeStmt.Exec, fakeStmt.Query メソッドが意図的に driver.ErrBadConn を返すようにするためのフックです。これにより、実際のデータベース接続が切断された状況をシミュレートし、database/sql パッケージの再接続ロジックが正しく機能するかを検証できるようになります。

sql_test.go の変更点

  1. TestErrBadConnReconnect テストケースの追加: この新しいテストケースは、fakedb_test.go で追加されたフックを利用して、DB.Exec, DB.Query, DB.Prepare, Stmt.Exec, Stmt.Query の各操作が driver.ErrBadConn を受け取った際に、適切に再接続とリトライが行われることを検証します。
    • simulateBadConn ヘルパー関数が定義されており、指定された操作が一度 driver.ErrBadConn を発生させ、その後成功することを確認します。
    • 特に重要なのは、numOpen をチェックしてコネクションリークが発生していないことを検証している点です。これは、このコミットがコネクションリークの修正も目的としているため、非常に重要なテストです。
    • stmt1.cssstmt2.cssdc.inUse = true を設定することで、プリペアドステートメントがキャッシュされたコネクションを使用している状態をシミュレートし、そのコネクションが壊れた場合の挙動をテストしています。

これらの変更により、database/sql パッケージは、データベース接続が不安定な環境下でも、より堅牢に動作し、コネクションリークのリスクを低減するようになります。

コアとなるコードの変更箇所

src/pkg/database/sql/sql.go

--- a/src/pkg/database/sql/sql.go
+++ b/src/pkg/database/sql/sql.go
@@ -798,13 +798,17 @@ func (db *DB) putConnDBLocked(dc *driverConn, err error) bool {
 	return false
 }
 
+// maxBadConnRetries is the number of maximum retries if the driver returns
+// driver.ErrBadConn to signal a broken connection.
+const maxBadConnRetries = 10
+
 // Prepare creates a prepared statement for later queries or executions.
 // Multiple queries or executions may be run concurrently from the
 // returned statement.
 func (db *DB) Prepare(query string) (*Stmt, error) {
 	var stmt *Stmt
 	var err error
-	for i := 0; i < 10; i++ {
+	for i := 0; i < maxBadConnRetries; i++ {
 		stmt, err = db.prepare(query)
 		if err != driver.ErrBadConn {
 			break
@@ -846,7 +850,7 @@ func (db *DB) prepare(query string) (*Stmt, error) {
 func (db *DB) Exec(query string, args ...interface{}) (Result, error) {
 	var res Result
 	var err error
-	for i := 0; i < 10; i++ {
+	for i := 0; i < maxBadConnRetries; i++ {
 		res, err = db.exec(query, args)
 		if err != driver.ErrBadConn {
 			break
@@ -895,7 +899,7 @@ func (db *DB) exec(query string, args []interface{}) (res Result, err error) {
 func (db *DB) Query(query string, args ...interface{}) (*Rows, error) {
 	var rows *Rows
 	var err error
-	for i := 0; i < 10; i++ {
+	for i := 0; i < maxBadConnRetries; i++ {
 		rows, err = db.query(query, args)
 		if err != driver.ErrBadConn {
 			break
@@ -983,7 +987,7 @@ func (db *DB) QueryRow(query string, args ...interface{}) *Row {
 func (db *DB) Begin() (*Tx, error) {
 	var tx *Tx
 	var err error
-	for i := 0; i < 10; i++ {
+	for i := 0; i < maxBadConnRetries; i++ {
 		tx, err = db.begin()
 		if err != driver.ErrBadConn {
 			break
@@ -1245,13 +1249,24 @@ type Stmt struct {
 func (s *Stmt) Exec(args ...interface{}) (Result, error) {
 	s.closemu.RLock()
 	defer s.closemu.RUnlock()
-\tdc, releaseConn, si, err := s.connStmt()\n-\tif err != nil {\n-\t\treturn nil, err\n-\t}\n-\tdefer releaseConn(nil)\n \n-\treturn resultFromStatement(driverStmt{dc, si}, args...)\n+\tvar res Result\n+\tfor i := 0; i < maxBadConnRetries; i++ {\n+\t\tdc, releaseConn, si, err := s.connStmt()\n+\t\tif err != nil {\n+\t\t\tif err == driver.ErrBadConn {\n+\t\t\t\tcontinue\n+\t\t\t}\n+\t\t\treturn nil, err\n+\t\t}\n+\n+\t\tres, err = resultFromStatement(driverStmt{dc, si}, args...)\n+\t\treleaseConn(err)\n+\t\tif err != driver.ErrBadConn {\n+\t\t\treturn res, err\n+\t\t}\n+\t}\n+\treturn nil, driver.ErrBadConn
 }
 
 func resultFromStatement(ds driverStmt, args ...interface{}) (Result, error) {
@@ -1329,26 +1344,21 @@ func (s *Stmt) connStmt() (ci *driverConn, releaseConn func(error), si driver.St
 	// Make a new conn if all are busy.
 	// TODO(bradfitz): or wait for one? make configurable later?
 	if !match {
-\t\tfor i := 0; ; i++ {\n-\t\t\tdc, err := s.db.conn()\n-\t\t\tif err != nil {\n-\t\t\t\treturn nil, nil, nil, err\n-\t\t\t}\n-\t\t\tdc.Lock()\n-\t\t\tsi, err := dc.prepareLocked(s.query)\n-\t\t\tdc.Unlock()\n-\t\t\tif err == driver.ErrBadConn && i < 10 {\n-\t\t\t\tcontinue\n-\t\t\t}\n-\t\t\tif err != nil {\n-\t\t\t\treturn nil, nil, nil, err\n-\t\t\t}\n-\t\t\ts.mu.Lock()\n-\t\t\tcs = connStmt{dc, si}\n-\t\t\ts.css = append(s.css, cs)\n-\t\t\ts.mu.Unlock()\n-\t\t\tbreak\n+\t\tdc, err := s.db.conn()\n+\t\tif err != nil {\n+\t\t\treturn nil, nil, nil, err\n \t\t}\n+\t\tdc.Lock()\n+\t\tsi, err := dc.prepareLocked(s.query)\n+\t\tdc.Unlock()\n+\t\tif err != nil {\n+\t\t\ts.db.putConn(dc, err)\n+\t\t\treturn nil, nil, nil, err\n+\t\t}\n+\t\ts.mu.Lock()\n+\t\tcs = connStmt{dc, si}\n+\t\ts.css = append(s.css, cs)\n+\t\ts.mu.Unlock()\n 	}\
 
 	conn := cs.dc
@@ -1361,31 +1371,39 @@ func (s *Stmt) Query(args ...interface{}) (*Rows, error) {
 	s.closemu.RLock()
 	defer s.closemu.RUnlock()
 
-\tdc, releaseConn, si, err := s.connStmt()\n-\tif err != nil {\n-\t\treturn nil, err\n-\t}\n+\tvar rowsi driver.Rows\n+\tfor i := 0; i < maxBadConnRetries; i++ {\n+\t\tdc, releaseConn, si, err := s.connStmt()\n+\t\tif err != nil {\n+\t\t\tif err == driver.ErrBadConn {\n+\t\t\t\tcontinue\n+\t\t\t}\n+\t\t\treturn nil, err\n+\t\t}\n \n-\tds := driverStmt{dc, si}\n-\trowsi, err := rowsiFromStatement(ds, args...)\n-\tif err != nil {\n-\t\treleaseConn(err)\n-\t\treturn nil, err\n-\t}\n+\t\trowsi, err = rowsiFromStatement(driverStmt{dc, si}, args...)\n+\t\tif err == nil {\n+\t\t\t// Note: ownership of ci passes to the *Rows, to be freed\n+\t\t\t// with releaseConn.\n+\t\t\trows := &Rows{\n+\t\t\t\tdc:    dc,\n+\t\t\t\trowsi: rowsi,\n+\t\t\t\t// releaseConn set below\n+\t\t\t}\n+\t\t\ts.db.addDep(s, rows)\n+\t\t\trows.releaseConn = func(err error) {\n+\t\t\t\treleaseConn(err)\n+\t\t\t\ts.db.removeDep(s, rows)\n+\t\t\t}\n+\t\t\treturn rows, nil\n+\t\t}\n \n-\t// Note: ownership of ci passes to the *Rows, to be freed\n-\t// with releaseConn.\n-\trows := &Rows{\n-\t\tdc:    dc,\n-\t\trowsi: rowsi,\n-\t\t// releaseConn set below\n-\t}\n-\ts.db.addDep(s, rows)\n-\trows.releaseConn = func(err error) {\n \t\treleaseConn(err)\n-\t\ts.db.removeDep(s, rows)\n+\t\tif err != driver.ErrBadConn {\n+\t\t\treturn nil, err\n+\t\t}\n \t}\n-\treturn rows, nil\n+\treturn nil, driver.ErrBadConn
 }

src/pkg/database/sql/fakedb_test.go

--- a/src/pkg/database/sql/fakedb_test.go
+++ b/src/pkg/database/sql/fakedb_test.go
@@ -433,11 +433,19 @@ func (c *fakeConn) prepareInsert(stmt *fakeStmt, parts []string) (driver.Stmt, e
 	return stmt, nil
 }
 
+// hook to simulate broken connections
+var hookPrepareBadConn func() bool
+
 func (c *fakeConn) Prepare(query string) (driver.Stmt, error) {
 	c.numPrepare++
 	if c.db == nil {
 		panic(\"nil c.db; conn = \" + fmt.Sprintf(\"%#v\", c))\
 	}\
+\n+\tif hookPrepareBadConn != nil && hookPrepareBadConn() {\n+\t\treturn nil, driver.ErrBadConn\n+\t}\n+\n 	parts := strings.Split(query, \"|\")
 	if len(parts) < 1 {
 		return nil, errf(\"empty query\")
@@ -489,10 +497,18 @@ func (s *fakeStmt) Close() error {
 
 var errClosed = errors.New(\"fakedb: statement has been closed\")
 
+// hook to simulate broken connections
+var hookExecBadConn func() bool
+
 func (s *fakeStmt) Exec(args []driver.Value) (driver.Result, error) {
 	if s.closed {
 		return nil, errClosed
 	}\
+\n+\tif hookExecBadConn != nil && hookExecBadConn() {\n+\t\treturn nil, driver.ErrBadConn\n+\t}\n+\n 	err := checkSubsetTypes(args)
 	if err != nil {
 		return nil, err
@@ -565,10 +581,18 @@ func (s *fakeStmt) execInsert(args []driver.Value, doInsert bool) (driver.Result
 	return driver.RowsAffected(1), nil
 }
 
+// hook to simulate broken connections
+var hookQueryBadConn func() bool
+
 func (s *fakeStmt) Query(args []driver.Value) (driver.Rows, error) {
 	if s.closed {
 		return nil, errClosed
 	}\
+\n+\tif hookQueryBadConn != nil && hookQueryBadConn() {\n+\t\treturn nil, driver.ErrBadConn\n+\t}\n+\n 	err := checkSubsetTypes(args)
 	if err != nil {
 		return nil, err

src/pkg/database/sql/sql_test.go

--- a/src/pkg/database/sql/sql_test.go
+++ b/src/pkg/database/sql/sql_test.go
@@ -1255,6 +1254,111 @@ func TestStmtCloseOrder(t *testing.T) {
 	}
 }\
 
+// golang.org/issue/5781
+func TestErrBadConnReconnect(t *testing.T) {
+	db := newTestDB(t, \"foo\")
+	defer closeDB(t, db)
+	exec(t, db, \"CREATE|t1|name=string,age=int32,dead=bool\")
+
+	simulateBadConn := func(name string, hook *func() bool, op func() error) {
+		broken, retried := false, false
+		numOpen := db.numOpen
+
+		// simulate a broken connection on the first try
+		*hook = func() bool {
+			if !broken {
+				broken = true
+				return true
+			}
+			retried = true
+			return false
+		}
+
+		if err := op(); err != nil {
+			t.Errorf(name+\": %v\", err)
+			return
+		}
+
+		if !broken || !retried {
+			t.Error(name + \": Failed to simulate broken connection\")
+		}
+		*hook = nil
+
+		if numOpen != db.numOpen {
+			t.Errorf(name+\": leaked %d connection(s)!\", db.numOpen-numOpen)
+			numOpen = db.numOpen
+		}
+	}
+
+	// db.Exec
+	dbExec := func() error {
+		_, err := db.Exec(\"INSERT|t1|name=?,age=?,dead=?\", \"Gordon\", 3, true)
+		return err
+	}
+	simulateBadConn(\"db.Exec prepare\", &hookPrepareBadConn, dbExec)
+	simulateBadConn(\"db.Exec exec\", &hookExecBadConn, dbExec)
+
+	// db.Query
+	dbQuery := func() error {
+		rows, err := db.Query(\"SELECT|t1|age,name|\")
+		if err == nil {
+			err = rows.Close()
+		}
+		return err
+	}
+	simulateBadConn(\"db.Query prepare\", &hookPrepareBadConn, dbQuery)
+	simulateBadConn(\"db.Query query\", &hookQueryBadConn, dbQuery)
+
+	// db.Prepare
+	simulateBadConn(\"db.Prepare\", &hookPrepareBadConn, func() error {
+		stmt, err := db.Prepare(\"INSERT|t1|name=?,age=?,dead=?\")
+		if err != nil {
+			return err
+		}
+		stmt.Close()
+		return nil
+	})
+
+	// stmt.Exec
+	stmt1, err := db.Prepare(\"INSERT|t1|name=?,age=?,dead=?\")
+	if err != nil {
+		t.Fatalf(\"prepare: %v\", err)
+	}
+	defer stmt1.Close()
+	// make sure we must prepare the stmt first
+	for _, cs := range stmt1.css {
+		cs.dc.inUse = true
+	}
+
+	stmtExec := func() error {
+		_, err := stmt1.Exec(\"Gopher\", 3, false)
+		return err
+	}
+	simulateBadConn(\"stmt.Exec prepare\", &hookPrepareBadConn, stmtExec)
+	simulateBadConn(\"stmt.Exec exec\", &hookExecBadConn, stmtExec)
+
+	// stmt.Query
+	stmt2, err := db.Prepare(\"SELECT|t1|age,name|\")
+	if err != nil {
+		t.Fatalf(\"prepare: %v\", err)
+	}
+	defer stmt2.Close()
+	// make sure we must prepare the stmt first
+	for _, cs := range stmt2.css {
+		cs.dc.inUse = true
+	}
+
+	stmtQuery := func() error {
+		rows, err := stmt2.Query()
+		if err == nil {
+			err = rows.Close()
+		}
+		return err
+	}
+	simulateBadConn(\"stmt.Query prepare\", &hookPrepareBadConn, stmtQuery)
+	simulateBadConn(\"stmt.Query exec\", &hookQueryBadConn, stmtQuery)
+}
+
 type concurrentTest interface {
  	init(t testing.TB, db *DB)
  	finish(t testing.TB)

コアとなるコードの解説

このコミットの核心は、database/sql パッケージが driver.ErrBadConn を受け取った際の再接続とリトライのロジックを、より堅牢かつ一貫性のあるものにすることです。

  1. 一貫したリトライメカニズム: 以前は、DB オブジェクトのメソッド(Prepare, Exec, Query, Begin)と、Stmt オブジェクトのメソッド(Exec, Query)で、driver.ErrBadConn のハンドリングに差異がありました。このコミットでは、すべての主要な操作に対して maxBadConnRetries (10回) のリトライループを導入することで、一貫した自動再接続の振る舞いを保証しています。これにより、一時的なネットワークの問題やデータベースの再起動などによってコネクションが切断された場合でも、アプリケーションコードを変更することなく、透過的に回復を試みることができます。

  2. プリペアドステートメントの再準備とコネクションリークの修正: 特に重要だったのは、*sql.StmtExec および Query メソッドにおける修正です。プリペアドステートメントは、通常、特定のデータベースコネクションに紐付けられています。もしそのコネクションが driver.ErrBadConn によって無効になった場合、ステートメントは新しいコネクション上で再準備される必要があります。

    • 変更前は、Stmt.connStmt()driver.ErrBadConn を返した場合、Stmt.ExecStmt.Query はすぐにエラーを返していました。これにより、自動再接続が機能せず、アプリケーションがエラーを処理する必要がありました。
    • 変更後、Stmt.ExecStmt.Query は、s.connStmt() から driver.ErrBadConn が返された場合、またはその後の操作中に driver.ErrBadConn が返された場合に、リトライループ内で新しいコネクションを取得し、ステートメントを再準備しようとします。
    • また、Stmt.connStmt() 内で dc.prepareLocked が失敗した場合に s.db.putConn(dc, err) を呼び出すことで、コネクションが適切にプールに戻され、リークが防止されます。これにより、無効になったコネクションがプールに残り続け、利用可能なコネクションが枯渇する問題を解決しています。
  3. テストカバレッジの向上: fakedb_test.go に追加されたフックと、sql_test.go に追加された TestErrBadConnReconnect テストケースは、この修正の有効性を検証するために不可欠です。これらのテストは、driver.ErrBadConn が発生する様々なシナリオ(Prepare時、Exec時、Query時など)をシミュレートし、database/sql パッケージが期待通りに再接続し、コネクションリークが発生しないことを確認します。これにより、将来的な回帰を防ぎ、パッケージの信頼性を高めています。

このコミットは、Goアプリケーションがデータベースと対話する際の堅牢性と信頼性を大幅に向上させるものであり、特に長期間稼働するサービスや、ネットワークの不安定な環境下での運用においてその恩恵は大きいです。

関連リンク

参考にした情報源リンク