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

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

このコミットは、Go言語の標準ライブラリである database/sql パッケージに、データベースのNULL値を適切に扱うための新しい型 NullInt64NullFloat64、および NullBool を追加するものです。これにより、int64float64bool 型のデータがデータベースからNULLとして返される場合に、Goのプログラムで安全かつ明示的にそれらを処理できるようになります。

コミット

commit c21b343438dfd26a56e89278522b03ac6417926c
Author: James P. Cooper <jamespcooper@gmail.com>
Date:   Wed Jan 25 17:47:32 2012 -0800

    database/sql: add NullInt64, NullFloat64, NullBool
    
    Fixes #2699
    
    R=golang-dev, bradfitz
    CC=golang-dev
    https://golang.org/cl/5557063

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

https://github.com/golang/go/commit/c21b343438dfd26a56e89278522b03ac6417926c

元コミット内容

database/sql: add NullInt64, NullFloat64, NullBool

このコミットは、Goの database/sql パッケージに NullInt64NullFloat64NullBool の各型を追加します。これにより、データベースから取得した int64float64bool 型の値がNULLである可能性を、Goの型システム内で明示的に表現し、処理できるようになります。これは、Issue #2699 を解決するための変更です。

変更の背景

Go言語の database/sql パッケージは、SQLデータベースとのインタラクションを抽象化するための標準ライブラリです。しかし、SQLデータベースでは、値が存在しないことを示す NULL という概念があります。Goの組み込み型(intfloat64bool など)には、このような NULL の状態を直接表現するメカニズムがありません。

例えば、データベースの INT 型カラムが NULL を許容する場合、Goの int 型変数に直接スキャンしようとすると、NULL 値をどのように扱うかという問題が生じます。Goのゼロ値(int なら 0bool なら false)は NULL とは異なる意味を持つため、NULL をゼロ値として扱うと、実際の 0false と区別できなくなってしまいます。

この問題に対処するため、database/sql パッケージには既に NullString という型が存在していました。これは、文字列が NULL であるかどうかを Valid フィールドで示す構造体です。このコミットは、同様のパターンを int64float64bool 型にも拡張し、NullInt64NullFloat64NullBool を導入することで、これらの型の NULL 値の扱いを標準化し、開発者がより堅牢なデータベースアプリケーションを構築できるようにすることを目的としています。

特に、この変更は Issue #2699 で報告された問題に対応しています。このIssueでは、database/sqlNULL 値を適切に処理するための、より多くの「Null」型(NullInt64, NullFloat64, NullBool など)の必要性が議論されていました。

前提知識の解説

Go言語の database/sql パッケージ

database/sql は、Go言語でSQLデータベースを操作するための標準パッケージです。このパッケージは、データベースドライバとアプリケーションの間の抽象レイヤーを提供し、異なるデータベース(MySQL, PostgreSQL, SQLiteなど)に対して統一されたインターフェースでアクセスできるようにします。

主な機能:

  • DB: データベースへの接続プールを管理します。
  • Stmt: プリペアドステートメントを表現します。
  • Rows: クエリ結果の行をイテレートするために使用します。
  • Row: 単一の行をスキャンするために使用します。
  • Scan メソッド: クエリ結果の値をGoの変数に読み込むために使用します。
  • Exec メソッド: INSERT, UPDATE, DELETEなどのDML操作を実行します。

SQLにおけるNULL値

SQLにおいて NULL は、「値がない」「不明な値」「適用できない値」を意味する特別なマーカーです。これは、数値の 0 や空文字列 ''、ブール値の false とは明確に区別されます。NULL 値は、通常の比較演算子(=< など)では比較できず、IS NULL または IS NOT NULL を使用してチェックする必要があります。

Goの型とゼロ値

Go言語の各型には「ゼロ値」が定義されています。これは、変数が宣言されたが明示的に初期化されていない場合に自動的に割り当てられるデフォルト値です。

  • int, int64, float64: 0
  • bool: false
  • string: "" (空文字列)
  • ポインタ、スライス、マップ、チャネル、インターフェース: nil

このゼロ値の概念は、SQLの NULL とは異なります。例えば、データベースの INT カラムに NULL が格納されている場合、それをGoの int 型変数にスキャンして 0 になってしまうと、実際に 0 が格納されている場合と区別がつかなくなります。

driver.ValueConverter インターフェース

database/sql/driver パッケージは、データベースドライバが実装すべきインターフェースを定義しています。ValueConverter は、Goの値をデータベースドライバが理解できる形式(driver.Value)に変換するためのインターフェースです。

Scanner インターフェースと ScannerInto インターフェース

database/sql パッケージでは、データベースから読み込んだ値をGoの変数にスキャンする際に Scan メソッドを使用します。この Scan メソッドは、引数として渡された変数が sql.Scanner インターフェースを実装している場合、その Scan メソッドを呼び出します。

Scanner インターフェース:

type Scanner interface {
    Scan(src interface{}) error
}

このコミットで導入される ScannerInto は、Scanner と同様の目的を持ちますが、ポインタレシーバで実装されることを意図しており、より柔軟な型変換を可能にします。

driver.Valuer インターフェースと driver.SubsetValuer インターフェース

database/sql パッケージでは、Goの値をデータベースに書き込む際に、引数として渡された変数が driver.Valuer インターフェースを実装している場合、その Value メソッドを呼び出してデータベースに書き込む値を決定します。

Valuer インターフェース:

type Valuer interface {
    Value() (driver.Value, error)
}

SubsetValuerValuer のサブセットであり、Value メソッドが返す値が driver.Value のサブセット(例えば、nil や特定の基本型)であることを保証します。これにより、ドライバはより効率的に値を処理できます。

技術的詳細

このコミットの主要な技術的詳細は、NullInt64NullFloat64NullBool という新しい構造体の定義と、それらが ScannerInto および SubsetValuer インターフェースをどのように実装しているかにあります。

新しい構造体の定義

NullXxx 型は、対応するGoの基本型(int64, float64, bool)のフィールドと、その値がデータベースの NULL ではないことを示す Valid ブール型フィールドを持ちます。

例: NullInt64

type NullInt64 struct {
	Int64 int64
	Valid bool // Valid is true if Int64 is not NULL
}

Valid フィールドが true の場合、Int64 フィールドには有効な値が格納されています。Validfalse の場合、Int64 フィールドの値はゼロ値(この場合は 0)であり、データベースの NULL を表します。

ScanInto メソッドの実装

ScanInto メソッドは、データベースから読み込んだ値を NullXxx 型のインスタンスにスキャンするロジックを提供します。

例: NullInt64.ScanInto

func (n *NullInt64) ScanInto(value interface{}) error {
	if value == nil { // データベースの値がNULLの場合
		n.Int64, n.Valid = 0, false // ゼロ値とValid=falseを設定
		return nil
	}
	n.Valid = true // NULLではない場合、Valid=trueを設定
	return convertAssign(&n.Int64, value) // 実際の値の型変換と代入
}

このメソッドは、valuenil(データベースの NULL に相当)であるかどうかを最初にチェックします。nil であれば、Validfalse に設定し、対応する値フィールドをゼロ値に設定します。nil でなければ、Validtrue に設定し、内部ヘルパー関数 convertAssign を使用して、データベースの値を NullXxx 型の対応するフィールドに変換して代入します。

SubsetValue メソッドの実装

SubsetValue メソッドは、NullXxx 型のインスタンスをデータベースに書き込む際に、driver.Value に変換するロジックを提供します。

例: NullInt64.SubsetValue

func (n NullInt64) SubsetValue() (interface{}, error) {
	if !n.Valid { // Validがfalse(NULLを表す)の場合
		return nil, nil // データベースにNULLとして書き込む
	}
	return n.Int64, nil // 有効な値の場合、その値を書き込む
}

このメソッドは、Valid フィールドが false であるかどうかをチェックします。false であれば、nil を返してデータベースに NULL 値として書き込まれるようにします。true であれば、NullXxx 型の対応する値フィールドを返します。

convertAssign ヘルパー関数

convertAssigndatabase/sql パッケージ内部で使用されるヘルパー関数で、異なる型の値を互換性のある型に変換して代入する役割を担います。この関数は、データベースドライバから返された値をGoの変数に安全に割り当てるために重要です。

テストの追加

src/pkg/database/sql/sql_test.go には、新しい NullInt64NullFloat64NullBool 型が正しく機能するかを検証するための包括的なテストケースが追加されています。nullTestRun という汎用的なテストヘルパー関数が導入され、各 NullXxx 型に対して、NULL値の挿入、非NULL値の挿入、NULL値のスキャン、非NULL値のスキャンなど、様々なシナリオがテストされています。

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

このコミットのコアとなるコードの変更は、主に src/pkg/database/sql/sql.go ファイルに集中しています。

  1. NullInt64 構造体の追加:

    type NullInt64 struct {
    	Int64 int64
    	Valid bool // Valid is true if Int64 is not NULL
    }
    func (n *NullInt64) ScanInto(value interface{}) error { ... }
    func (n NullInt64) SubsetValue() (interface{}, error) { ... }
    
  2. NullFloat64 構造体の追加:

    type NullFloat64 struct {
    	Float64 float64
    	Valid   bool // Valid is true if Float64 is not NULL
    }
    func (n *NullFloat64) ScanInto(value interface{}) error { ... }
    func (n NullFloat64) SubsetValue() (interface{}, error) { ... }
    
  3. NullBool 構造体の追加:

    type NullBool struct {
    	Bool  bool
    	Valid bool // Valid is true if Bool is not NULL
    }
    func (n *NullBool) ScanInto(value interface{}) error { ... }
    func (n NullBool) SubsetValue() (interface{}, error) { ... }
    

また、テストファイル src/pkg/database/sql/sql_test.go にも、これらの新しい型を検証するためのテストロジックが追加されています。

コアとなるコードの解説

sql.go で定義された各 NullXxx 型は、Goの基本型と Valid ブール値のペアで構成されます。この Valid フィールドが、データベースの NULL 値とGoのゼロ値を区別するための鍵となります。

  • ScanInto(value interface{}) error メソッド: このメソッドは、Rows.Scan()Row.Scan() がデータベースから読み込んだ値を NullXxx 型の変数に割り当てる際に呼び出されます。

    • value == nil の場合: データベースの値が NULL であることを意味します。このとき、NullXxx の値フィールドはゼロ値に設定され(例: NullInt64 なら 0)、Valid フィールドは false に設定されます。これにより、NULL 値が明示的に表現されます。
    • value != nil の場合: データベースに有効な値が存在することを意味します。Valid フィールドは true に設定され、convertAssign ヘルパー関数を使って valueNullXxx の値フィールドに変換・代入されます。convertAssign は、データベースドライバが返す様々な型(例: []bytestringint64 など)を、Goの目的の型に安全に変換する役割を担います。
  • SubsetValue() (interface{}, error) メソッド: このメソッドは、DB.Exec()Stmt.Exec() がGoの変数をデータベースに書き込む際に呼び出されます。

    • !n.Valid の場合: NullXxx インスタンスが NULL 値を表していることを意味します。このとき、メソッドは nil, nil を返します。database/sql パッケージは、nil が返された場合、対応するデータベースカラムに NULL を書き込みます。
    • n.Valid の場合: NullXxx インスタンスが有効な値を持っていることを意味します。このとき、メソッドは NullXxx の値フィールド(例: n.Int64)と nil エラーを返します。この値がデータベースに書き込まれます。

これらの実装により、開発者はデータベースの NULL 値をGoのプログラム内で明示的に扱い、NULL とゼロ値の混同を防ぐことができます。

関連リンク

参考にした情報源リンク

  • Go database/sql パッケージのソースコード (特に sql.gosql_test.go)
  • Go Issue Tracker (Issue #2699 の議論)
  • Go言語の公式ドキュメント
  • SQLのNULL値に関する一般的な知識