1. wanko

    Posted

    wanko
Changes in title
+database/sql の Rows.Scan で不要なカラムデータを読み捨てる
Changes in tags
Changes in body
Source | HTML | Preview
@@ -0,0 +1,224 @@
+# はじめに
+説明かいてたら長くなってしまったので、結論だけ最初に書きますね。
+
+* `Scanner`interfaceを実装したダミー構造体をつくる
+* `Rows.Scan`で読み捨てたいフィールドに上記ダミー構造体を割り当てる
+
+です。
+
+```go
+type TrashScanner struct{}
+
+func (TrashScanner) Scan(interface{}) error {
+ return nil
+}
+res := &Row{}
+err := rows.Scan(
+ &res.Id,
+ &res.Name,
+ TrashScanner{},
+)
+```
+
+こんな感じ。
+こっから先はだらだら説明なので、理解できる方はここでおしまい。
+
+
+# どういうことなの
+go の database/sql でデータを取得して、独自構造体に割当たるためには
+
+```go
+type Row struct {
+ Id int32
+ Name string
+}
+func GetTableName() string {
+ return "hoge"
+}
+func New(rows *sql.Rows) *Row {
+ if rows.Next() == true {
+ res := &Row{}
+ err := rows.Scan(
+ &res.Id,
+ &res.Name,
+ )
+ if err != nil {
+ fmt.Printf("%s Scan Error!!!! err:%v\n", GetTableName(), err)
+ return nil
+ }
+ return res
+ }
+ return nil
+}
+
+func main() {
+ rows, _ := conn.Query("SELECT * FROM " + GetTableName())
+ var result []Row
+ for {
+ rower := New(rows)
+ if rower == nil {
+ break
+ }
+ result = append(result, rower)
+ }
+ fmt.Println(result)
+}
+```
+
+こんな感じのコードを書くことが多いと思います。
+
+# これだと困るときもある
+Scanに渡す引数の数と、queryの結果のカラム数が違うとダメなんです。
+実際プログラム側では不要なんだけど、透過的に複数のテーブルを扱うコードを書きたい場合、
+`SELECT * FROM hoge`とかやっちゃうじゃないですか。
+本来はきちんと必要な情報だけを取得するSQLを書くのがベストなのはわかりきっていますが、コード記述量の問題や、大人の事情でまとめたいとかもあるわけです。
+
+不要なカラムの情報をわざわざ構造体内に持ちたくない時もありますよね。
+
+# どんなとき?
+
+```sql
+CREATE TABLE `hoge` (
+ `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(32) NOT NULL DEFAULT '',
+ `memo` text NOT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+```
+こんなテーブルがあったとします。
+`memo`カラムには、データとしての意味を持たず、どういう行なのかというコメントが入っているイメージです。
+ゲーム系でMasterDataなどの設定データをMySQLなどに持たせたりする場合、非エンジニアがCSVでデータを作ったりする場合が多いのですが、その時どんなデータだったか、どのイベント用のものだったのか。などの情報をCSVやエクセル上などで見やすくするためだけの情報です。
+
+こんな~~糞~~データをわざわざプログラム側で持たせる必要ありません。
+しかもたいてい`TEXT型`だったりで結構なメモリを食ってしまいます。
+
+
+こういった状況でも、できるだけメモリを無駄にせず、そして汎用的なコードを書くためのTipsだと思ってください。
+他にもっといい方法とか、これじゃダメだよ!ってことがあればどんどんコメントください。
+
+# どうすんの?
+## なぜScanでカラムの数と一致してないとダメなのか
+[go - database/sql sql.go](https://github.com/golang/go/blob/master/src/database/sql/sql.go#L1807-L1809)
+
+```go
+func (rs *Rows) Scan(dest ...interface{}) error {
+ if rs.closed {
+ return errors.New("sql: Rows are closed")
+ }
+ if rs.lastcols == nil {
+ return errors.New("sql: Scan called without calling Next")
+ }
+
+
+ // ▼ここ!!!!!!!!!!!!!!!!!
+ if len(dest) != len(rs.lastcols) {
+ return fmt.Errorf("sql: expected %d destination arguments in Scan, not %d", len(rs.lastcols), len(dest))
+ }
+
+
+ for i, sv := range rs.lastcols {
+ err := convertAssign(dest[i], sv)
+ if err != nil {
+ return fmt.Errorf("sql: Scan error on column index %d: %v", i, err)
+ }
+ }
+ return nil
+}
+```
+結構しょっぱなに蹴られてしまいますね…。
+そうですよね。渡すものと受け取るものは同じ数がいいですよね。すみません。
+
+## じゃあ数をあわせましょう
+ってことで、メソッド内のスコープだけで初期化した変数を割り当てる方法を考えてみましょう。
+
+```go
+res := &Row{}
+var memo string
+err := rows.Scan(
+ &res.Id,
+ &res.Name,
+ &memo,
+)
+```
+こんな感じ。
+これなら返り値の`res`にも入らないし、メソッド抜けたら参照なくなるからずっとメモリ上に保持されることはないよねー。
+
+
+## まぁそれでもいいけど
+[go - database/sql convert.go](https://github.com/golang/go/blob/master/src/database/sql/convert.go#L85)
+Scanの中で実行されている`convertAssign`ここを見てみると、
+
+```go
+func convertAssign(dest, src interface{}) error {
+ // Common cases, without reflect.
+ switch s := src.(type) {
+ case string:
+ switch d := dest.(type) {
+ case *string:
+ if d == nil {
+ return errNilPtr
+ }
+ *d = s
+ return nil
+ case *[]byte:
+ if d == nil {
+ return errNilPtr
+ }
+ *d = []byte(s)
+ return nil
+ }
+```
+渡したポインタに対してデータコピーが走りますね…そうですね…。はい。
+
+
+ゴミのためにそんなことされたくない…ですよね。
+
+## 読み捨てたい!!!
+`nil`とか渡せて読み捨ててくれたらいいんですけど、そうも行かなかったので、コードを読み進めて行きました。
+すると`func convertAssign`の中にこんな記述がありました。やったね!
+[go - database/sql convert.go](https://github.com/golang/go/blob/master/src/database/sql/convert.go#L189-L191)
+
+```go
+if scanner, ok := dest.(Scanner); ok {
+ return scanner.Scan(src)
+}
+```
+`Scanner`Interfaceを実装したものを渡してあげればそれによって独自の処理ができるようです。
+
+## Scanner って?
+[go - database/sql sql.go](https://github.com/golang/go/blob/master/src/database/sql/sql.go#L188-L206)
+
+```go
+// Scanner is an interface used by Scan.
+type Scanner interface {
+ Scan(src interface{}) error
+}
+```
+
+
+## ゴミ箱行き専用の構造体を作ってしまおう
+
+```go
+type TrashScanner struct{}
+
+func (TrashScanner) Scan(interface{}) error {
+ return nil
+}
+```
+なにも受け取らず、ただそのまま`nil`を返すだけの`Scan`メソッドを実装
+
+これを使って
+
+```go
+res := &Row{}
+err := rows.Scan(
+ &res.Id,
+ &res.Name,
+ TrashScanner{},
+)
+```
+としてあげれば、無駄なメモリにコピーも走らず読み捨てることができます。
+もっといい方法あれば教えてつかーさい。
+
+
+