[インデックス 11407] ファイルの概要
このコミットは、Go言語の標準ライブラリである database/sql
パッケージに、データベースのNULL値を適切に扱うための新しい型 NullInt64
、NullFloat64
、および NullBool
を追加するものです。これにより、int64
、float64
、bool
型のデータがデータベースから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
パッケージに NullInt64
、NullFloat64
、NullBool
の各型を追加します。これにより、データベースから取得した int64
、float64
、bool
型の値がNULLである可能性を、Goの型システム内で明示的に表現し、処理できるようになります。これは、Issue #2699 を解決するための変更です。
変更の背景
Go言語の database/sql
パッケージは、SQLデータベースとのインタラクションを抽象化するための標準ライブラリです。しかし、SQLデータベースでは、値が存在しないことを示す NULL
という概念があります。Goの組み込み型(int
、float64
、bool
など)には、このような NULL
の状態を直接表現するメカニズムがありません。
例えば、データベースの INT
型カラムが NULL
を許容する場合、Goの int
型変数に直接スキャンしようとすると、NULL
値をどのように扱うかという問題が生じます。Goのゼロ値(int
なら 0
、bool
なら false
)は NULL
とは異なる意味を持つため、NULL
をゼロ値として扱うと、実際の 0
や false
と区別できなくなってしまいます。
この問題に対処するため、database/sql
パッケージには既に NullString
という型が存在していました。これは、文字列が NULL
であるかどうかを Valid
フィールドで示す構造体です。このコミットは、同様のパターンを int64
、float64
、bool
型にも拡張し、NullInt64
、NullFloat64
、NullBool
を導入することで、これらの型の NULL
値の扱いを標準化し、開発者がより堅牢なデータベースアプリケーションを構築できるようにすることを目的としています。
特に、この変更は Issue #2699 で報告された問題に対応しています。このIssueでは、database/sql
が NULL
値を適切に処理するための、より多くの「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)
}
SubsetValuer
は Valuer
のサブセットであり、Value
メソッドが返す値が driver.Value
のサブセット(例えば、nil
や特定の基本型)であることを保証します。これにより、ドライバはより効率的に値を処理できます。
技術的詳細
このコミットの主要な技術的詳細は、NullInt64
、NullFloat64
、NullBool
という新しい構造体の定義と、それらが 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
フィールドには有効な値が格納されています。Valid
が false
の場合、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) // 実際の値の型変換と代入
}
このメソッドは、value
が nil
(データベースの NULL
に相当)であるかどうかを最初にチェックします。nil
であれば、Valid
を false
に設定し、対応する値フィールドをゼロ値に設定します。nil
でなければ、Valid
を true
に設定し、内部ヘルパー関数 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
ヘルパー関数
convertAssign
は database/sql
パッケージ内部で使用されるヘルパー関数で、異なる型の値を互換性のある型に変換して代入する役割を担います。この関数は、データベースドライバから返された値をGoの変数に安全に割り当てるために重要です。
テストの追加
src/pkg/database/sql/sql_test.go
には、新しい NullInt64
、NullFloat64
、NullBool
型が正しく機能するかを検証するための包括的なテストケースが追加されています。nullTestRun
という汎用的なテストヘルパー関数が導入され、各 NullXxx
型に対して、NULL値の挿入、非NULL値の挿入、NULL値のスキャン、非NULL値のスキャンなど、様々なシナリオがテストされています。
コアとなるコードの変更箇所
このコミットのコアとなるコードの変更は、主に src/pkg/database/sql/sql.go
ファイルに集中しています。
-
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) { ... }
-
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) { ... }
-
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
ヘルパー関数を使ってvalue
がNullXxx
の値フィールドに変換・代入されます。convertAssign
は、データベースドライバが返す様々な型(例:[]byte
、string
、int64
など)を、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
パッケージのドキュメント: https://pkg.go.dev/database/sql - Go
database/sql/driver
パッケージのドキュメント: https://pkg.go.dev/database/sql/driver - Go Issue #2699:
database/sql: add NullInt64, NullFloat64, NullBool
(このコミットが解決したIssue) - Go CL 5557063 (Gerrit Code Review): https://golang.org/cl/5557063
参考にした情報源リンク
- Go
database/sql
パッケージのソースコード (特にsql.go
とsql_test.go
) - Go Issue Tracker (Issue #2699 の議論)
- Go言語の公式ドキュメント
- SQLのNULL値に関する一般的な知識