今川館

都内勤務の地味OLです

NULLを許可する列をScanするときの注意点

NULLを許可する列をScanするときは要注意

database/sqlMySQLのデータを取得するとき、NULLを許可する列の扱いがちょっと要注意だった。

まず、こういうNULLを許すテーブルにデータを入れる。

CREATE TABLE `students` (
  id INT PRIMARY KEY,
  active TINYINT NULL,
  name VARCHAR(255) NULL,
  grade INT NULL,
  score DOUBLE NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

INSERT INTO `students` (id, active, name, grade, score) VALUES
  (1, 1, 'John Doe', 65535, 0.009876),
  (2, NULL, NULL, NULL, NULL),
  (3, NULL, 'Akira Toriyama', NULL, 100)
;

これを検索して、*Rows.Scan()を呼ぶとNULLを読み出してエラーを返す。

type Student struct {
	ID     int
	Active bool
	Name   string
	Grade  int
	Score  float64
}

rows, err := db.Query(`SELECT id, active, name, grade, score FROM students`)
if err != nil {
	log.Fatal(err)
}
defer rows.Close()

for rows.Next() {
	s := Student{}
	if err := rows.Scan(&s.ID, &s.Active, &s.Name, &s.Grade, &s.Score); err != nil {
		log.Fatal(err) // ここでエラー
	}
	log.Println(s)
}

コンソール出力を見ると、1行目は問題なくプリントできるが、2行目のデータがactiveにNULLが入っているため、下記の通りプログラムが異常終了する。

2019/08/12 08:45:26 {1 true John Doe 65535 0.009876}
2019/08/12 08:45:26 sql: Scan error on column index 1, name "active": sql/driver: couldn't convert <nil> (<nil>) into type bool

database/sqlのNullBool, NullString, NullInt64, NullFloat64を使う

database/sqlパッケージを見ると、このようにNULL列をScanするときに使うべきものが定義されている。

https://golang.org/pkg/database/sql/#NullBool

さきほどのプログラムをNullBool, NullString, NullInt64, NullFloat64を使うよう変えると今度はエラーにならない。

type NullableStudent struct {
	ID     int
	Active sql.NullBool
	Name   sql.NullString
	Grade  sql.NullInt64
	Score  sql.NullFloat64
}

rows, err := db.Query(`SELECT id, active, name, grade, score FROM students`)
if err != nil {
	log.Fatal(err)
}
defer rows.Close()

for rows.Next() {
	s := NullableStudent{}
	if err := rows.Scan(&s.ID, &s.Active, &s.Name, &s.Grade, &s.Score); err != nil {
		log.Fatal(err)
	}
	log.Println(s)
}

出力結果

2019/08/12 09:03:49 {1 {true true} {John Doe true} {65535 true} {0.009876 true}}
2019/08/12 09:03:49 {2 {false false} { false} {0 false} {0 false}}
2019/08/12 09:03:49 {3 {false false} {Akira Toriyama true} {0 false} {100 true}}

NullXXから値を取り出す方法

ただ、最初のプログラムと後のものの出力内容を比べるとちょっと違いがある。

最初の出力
2019/08/12 08:45:26 {1 true John Doe 65535 0.009876}

NullXX使ったプログラムの出力
2019/08/12 09:03:49 {1 {true true} {John Doe true} {65535 true} {0.009876 true}}

activeという列の出力内容が true ではなく {true true} となっている。
これは、NullBoolがBoolとValidという二つのフィールドに分かれているからである。
ValidはScanに成功したか否かを保持しており、実際の値はBoolから取り出す。

なので、NullableStudentを使ってScanしてStudentに詰め替えるプログラムに書きかえる(以下)。

for rows.Next() {
    s := NullableStudent{}
    if err := rows.Scan(&s.ID, &s.Active, &s.Name, &s.Grade, &s.Score); err != nil {
        log.Fatal(err)
    }

    st := Student{
        ID: s.ID,
        Active: s.Active.Bool,
        Name: s.Name.String,
        Grade: int(s.Grade.Int64),
        Score: s.Score.Float64,
    }
    log.Println(st)

途中の log.Fatal() にはもはやScanのエラーは来ないのであそこでは異常終了しない。
また、Scanに失敗したフィールドにはゼロ値が入るので、出力結果は以下の通り、NULLだった場合はそのgoの型におけるゼロ値、NULLでなければテーブルに格納されている値が出力される。

2019/08/12 09:17:52 {1 true John Doe 65535 0.009876}
2019/08/12 09:17:52 {2 false  0 0}
2019/08/12 09:17:52 {3 false Akira Toriyama 0 100}

サンプルコードの全体はここに置きました。

https://github.com/oyakata/gooooolang/blob/master/scan_no_nazo/hello2/main.go


特に、NULL許可する列にNULLを入れずにテストしているとまさかScanに失敗しているとは気づかないので、これは知っておいた方が良いでしょう。


最後までお読みくださりありとうございました。