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

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

このコミットは、Go言語の標準ライブラリであるdatabase/sqlパッケージにおけるテストの追加に関するものです。具体的には、ポインタ型のパラメータとスキャン(データベースからの値の読み込み)に関する高レベルなテストがsrc/pkg/database/sql/sql_test.goに追加されています。これにより、データベースのNULL値とGoのnilポインタ間の挙動が正しく扱われることを保証します。

コミット

  • コミットハッシュ: 29df93735c85a5eaf1388df7ffa7f8c410dfce0b
  • Author: Brad Fitzpatrick bradfitz@golang.org
  • Date: Thu Feb 9 15:01:29 2012 +1100
  • 変更ファイル: src/pkg/database/sql/sql_test.go
  • 変更行数: 32行追加

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

https://github.com/golang/go/commit/29df93735c85a5eaf1388df7ffa7f8c410dfce0b

元コミット内容

database/sql: more tests

Higher level tests for the pointer parameters
and scanning, complementing the existing ones
included in the previous CL.

R=golang-dev, dsymonds
CC=golang-dev
https://golang.org/cl/5646050

変更の背景

database/sqlパッケージは、Goアプリケーションがリレーショナルデータベースと対話するための汎用インターフェースを提供します。データベース操作において、NULL値を適切に扱うことは非常に重要です。特に、GoのnilポインタがデータベースのNULL値とどのようにマッピングされるか、またその逆のマッピングがどのように行われるかは、アプリケーションの堅牢性に直結します。

このコミットの背景には、以前の変更リスト(CL)で導入された既存のテストを補完し、ポインタ型のパラメータとスキャンに関するより高レベルなテストカバレッジを確保するという目的があります。これにより、開発者がdatabase/sqlパッケージを使用する際に、ポインタとNULL値の扱いで予期せぬ挙動に遭遇するリスクを低減し、より信頼性の高いデータベース操作を可能にすることが意図されています。

前提知識の解説

Goのdatabase/sqlパッケージ

database/sqlパッケージは、GoプログラムからSQLデータベースにアクセスするための標準インターフェースです。このパッケージ自体は特定のデータベースドライバを含まず、データベース固有の操作は、このパッケージが定義するインターフェースを実装する外部ドライバ(例: github.com/go-sql-driver/mysqlgithub.com/lib/pqなど)によって提供されます。

主要な概念は以下の通りです。

  • DB: データベースへの接続プールを表します。
  • Stmt: プリペアドステートメントを表します。
  • Tx: トランザクションを表します。
  • Rows: クエリ結果の行を表します。
  • Row: 単一のクエリ結果の行を表します。

NULL値の扱いとポインタ

リレーショナルデータベースでは、列に値が存在しないことを示すためにNULLという特殊なマーカーを使用します。GoにはNULLに直接対応する型はありませんが、database/sqlパッケージはいくつかの方法でこれを扱います。

  1. sql.Null*: database/sqlパッケージは、sql.NullStringsql.NullInt64sql.NullBoolsql.NullFloat64などの特殊な型を提供します。これらの型は、値とその値がNULLであるかどうかを示すブール値(Validフィールド)を持ちます。これは、NULL値を明示的に扱うための最も一般的で推奨される方法です。

    var s sql.NullString
    err := row.Scan(&s)
    if s.Valid {
        // 値がNULLではない場合
        fmt.Println(s.String)
    } else {
        // 値がNULLの場合
        fmt.Println("NULL")
    }
    
  2. 基本型のポインタ: 別の方法は、基本型(string, int, float64など)のポインタを使用することです。データベースの列がNULLの場合、対応するポインタはnilに設定されます。列に値がある場合、ポインタはその値を指します。この方法は、特にJSONマーシャリングを行う際に便利です。nilポインタはJSONでnullにマーシャリングされ、omitemptyタグと組み合わせることで、NULL値のフィールドをJSON出力から省略できます。

    var name *string
    err := row.Scan(&name)
    if err != nil {
        // エラーハンドリング
    }
    if name != nil {
        // 値がNULLではない場合
        fmt.Println(*name) // ポインタをデリファレンスして値を取得
    } else {
        // 値がNULLの場合
        fmt.Println("NULL")
    }
    

    このコミットで追加されたテストは、この「基本型のポインタ」を使ったNULL値の扱い、特にパラメータとしてポインタを渡し、結果をポインタにスキャンする際の挙動に焦点を当てています。ポインタをデリファレンスする前にnilチェックを行うことが重要です。

技術的詳細

このコミットは、database/sqlパッケージがポインタ型の引数をSQLクエリのパラメータとして受け取り、またクエリ結果をポインタ型の変数にスキャンする際の挙動を検証するためのテストを追加しています。

具体的には、以下のシナリオがテストされています。

  1. ポインタパラメータとしてのnilでない値の挿入: *string型の変数に実際の文字列値を格納し、そのポインタをINSERT文のパラメータとして渡します。データベースにはその文字列値が格納されることを期待します。
  2. ポインタパラメータとしてのnil値の挿入: *string型の変数をnilに設定し、そのポインタをINSERT文のパラメータとして渡します。データベースにはNULL値が格納されることを期待します。
  3. ポインタへのスキャン(NULLでない値): データベースからNULLでない文字列値をSELECTし、その結果を*string型の変数にスキャンします。スキャン後、ポインタがnilではなく、かつ正しい文字列値を指していることを期待します。
  4. ポインタへのスキャン(NULL値): データベースからNULL値をSELECTし、その結果を*string型の変数にスキャンします。スキャン後、ポインタがnilであることを期待します。

これらのテストは、database/sqlパッケージがGoのポインタとデータベースのNULL値の間で正確なマッピングと変換を行えることを保証するために不可欠です。特に、nilポインタがデータベースのNULLとして扱われ、データベースのNULLがGoのnilポインタとして正しく読み取られることは、アプリケーションのデータ整合性を保つ上で極めて重要です。

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

変更はsrc/pkg/database/sql/sql_test.goファイルに集中しており、TestPointerParamsAndScansという新しいテスト関数が追加されています。

--- a/src/pkg/database/sql/sql_test.go
+++ b/src/pkg/database/sql/sql_test.go
@@ -386,6 +386,38 @@ func TestNullByteSlice(t *testing.T) {
 	}\n
 }\n
 \n
+func TestPointerParamsAndScans(t *testing.T) {
+\tdb := newTestDB(t, "")
+\tdefer closeDB(t, db)
+\texec(t, db, "CREATE|t|id=int32,name=nullstring")
+\n
+\tbob := "bob"
+\tvar name *string
+\n
+\tname = &bob
+\texec(t, db, "INSERT|t|id=10,name=?", name)
+\tname = nil
+\texec(t, db, "INSERT|t|id=20,name=?", name)
+\n
+\terr := db.QueryRow("SELECT|t|name|id=?", 10).Scan(&name)
+\tif err != nil {
+\t\tt.Fatalf("querying id 10: %v", err)
+\t}\n
+\tif name == nil {
+\t\tt.Errorf("id 10's name = nil; want bob")
+\t} else if *name != "bob" {
+\t\tt.Errorf("id 10's name = %q; want bob", *name)
+\t}\n
+\n
+\terr = db.QueryRow("SELECT|t|name|id=?", 20).Scan(&name)
+\tif err != nil {
+\t\tt.Fatalf("querying id 20: %v", err)
+\t}\n
+\tif name != nil {
+\t\tt.Errorf("id 20 = %q; want nil", *name)
+\t}\n
+}\n+\n
 func TestQueryRowClosingStmt(t *testing.T) {
 \tdb := newTestDB(t, "people")
 \tdefer closeDB(t, db)

コアとなるコードの解説

新しく追加されたTestPointerParamsAndScans関数は、以下の手順でポインタの挙動をテストします。

  1. テスト用データベースの初期化:

    db := newTestDB(t, "")
    defer closeDB(t, db)
    

    newTestDBはテスト用のデータベース接続を確立し、closeDBはテスト終了時に接続を閉じます。

  2. テスト用テーブルの作成:

    exec(t, db, "CREATE|t|id=int32,name=nullstring")
    

    execヘルパー関数を使って、id(整数型)とname(NULL可能な文字列型)を持つtという名前のテーブルを作成します。nullstringは、このテストフレームワーク内でNULL可能な文字列型を意味するプレースホルダーです。

  3. ポインタパラメータとしての値の挿入:

    bob := "bob"
    var name *string
    
    name = &bob
    exec(t, db, "INSERT|t|id=10,name=?", name)
    

    bobという文字列変数を定義し、そのアドレスをnameポインタに代入します。その後、idが10のレコードとして、nameポインタ("bob"を指す)をパラメータとして使用してデータを挿入します。これにより、データベースには"bob"という値が格納されます。

  4. ポインタパラメータとしてのnilの挿入:

    name = nil
    exec(t, db, "INSERT|t|id=20,name=?", name)
    

    nameポインタをnilに設定し、idが20のレコードとして、このnilポインタをパラメータとして使用してデータを挿入します。これにより、データベースにはname列がNULLとして格納されます。

  5. NULLでない値のポインタへのスキャンと検証:

    err := db.QueryRow("SELECT|t|name|id=?", 10).Scan(&name)
    if err != nil {
        t.Fatalf("querying id 10: %v", err)
    }
    if name == nil {
        t.Errorf("id 10's name = nil; want bob")
    } else if *name != "bob" {
        t.Errorf("id 10's name = %q; want bob", *name)
    }
    

    idが10のレコードのname列をSELECTし、その結果をnameポインタにスキャンします。

    • エラーが発生しないことを確認します。
    • スキャン後、nameポインタがnilでないこと(NULL値ではないこと)を確認します。
    • nameポインタが指す値が"bob"であることを確認します。
  6. NULL値のポインタへのスキャンと検証:

    err = db.QueryRow("SELECT|t|name|id=?", 20).Scan(&name)
    if err != nil {
        t.Fatalf("querying id 20: %v", err)
    }
    if name != nil {
        t.Errorf("id 20 = %q; want nil", *name)
    }
    

    idが20のレコードのname列をSELECTし、その結果を再びnameポインタにスキャンします。

    • エラーが発生しないことを確認します。
    • スキャン後、nameポインタがnilであること(NULL値であることを正しく認識していること)を確認します。もしnilでなければ、エラーとして報告します。

このテストは、database/sqlパッケージがポインタを介した値の受け渡しとNULL値の処理を正確に行うことを、具体的なシナリオを通じて保証しています。

関連リンク

参考にした情報源リンク