LoginSignup
53
29

More than 5 years have passed since last update.

database/sql の Rows.Scan で不要なカラムデータを読み捨てる

Posted at

はじめに

説明かいてたら長くなってしまったので、結論だけ最初に書きますね。

  • Scannerinterfaceを実装したダミー構造体をつくる
  • Rows.Scanで読み捨てたいフィールドに上記ダミー構造体を割り当てる

です。

type TrashScanner struct{}

func (TrashScanner) Scan(interface{}) error {
    return nil
}
res := &Row{}
err := rows.Scan(
    &res.Id,
    &res.Name,
    TrashScanner{},
)

こんな感じ。
こっから先はだらだら説明なので、理解できる方はここでおしまい。

どういうことなの

go の database/sql でデータを取得して、独自構造体に割当たるためには

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を書くのがベストなのはわかりきっていますが、コード記述量の問題や、大人の事情でまとめたいとかもあるわけです。

不要なカラムの情報をわざわざ構造体内に持ちたくない時もありますよね。

どんなとき?

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

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
}

結構しょっぱなに蹴られてしまいますね…。
そうですよね。渡すものと受け取るものは同じ数がいいですよね。すみません。

じゃあ数をあわせましょう

ってことで、メソッド内のスコープだけで初期化した変数を割り当てる方法を考えてみましょう。

res := &Row{}
var memo string
err := rows.Scan(
    &res.Id,
    &res.Name,
    &memo,
)

こんな感じ。
これなら返り値のresにも入らないし、メソッド抜けたら参照なくなるからずっとメモリ上に保持されることはないよねー。

まぁそれでもいいけど

go - database/sql convert.go
Scanの中で実行されているconvertAssignここを見てみると、

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

if scanner, ok := dest.(Scanner); ok {
    return scanner.Scan(src)
}

ScannerInterfaceを実装したものを渡してあげればそれによって独自の処理ができるようです。

Scanner って?

go - database/sql sql.go

// Scanner is an interface used by Scan.
type Scanner interface {
    Scan(src interface{}) error
}

ゴミ箱行き専用の構造体を作ってしまおう

type TrashScanner struct{}

func (TrashScanner) Scan(interface{}) error {
    return nil
}

なにも受け取らず、ただそのままnilを返すだけのScanメソッドを実装

これを使って

res := &Row{}
err := rows.Scan(
    &res.Id,
    &res.Name,
    TrashScanner{},
)

としてあげれば、無駄なメモリにコピーも走らず読み捨てることができます。
もっといい方法あれば教えてつかーさい。

53
29
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
53
29