NULLを許可する列をScanするときの注意点
NULLを許可する列をScanするときは要注意
database/sqlでMySQLのデータを取得するとき、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に失敗しているとは気づかないので、これは知っておいた方が良いでしょう。
最後までお読みくださりありとうございました。