【Go】Go言語でSQLを実行してみる

f:id:suganoo:20190125170942p:plain
こんにちは suganoo です。

Go言語からのDB操作を復習したいと思います。「Goプログラミング実践入門」でDBの操作が出ていたので、おさらいをしておきたいなと思ってました。
今回はORMは使いません。それはそれでいつかまた調べて記事にしてみようと思います。

DB設定

PostgreSQLのインストール

本記事ではPostgreSQLを使っています。インストール作業は他のサイトなどを見て設定してみてください。
www.dbonline.jp

DB, テーブル設定

プログラムの中でCREATE DATABASE/TABLE からやってみようかなと思いましたが、考えてみてもそんな場面はほとんどないので事前にDB設定とCREATE TABLEはしておきます。

-- データベースは postgresを使います
create table members (
  id          serial primary key,
  first_name  varchar(255),
  last_name   varchar(255),
  email       varchar(255),
  accessprev  boolean
);

INSERT INTO members VALUES (1, 'minoru', 'tanaka', 'tanaka@gmail.com', TRUE);
INSERT INTO members VALUES (2, 'tadashi', 'sato', 'sato@gmail.com', FALSE);
INSERT INTO members VALUES (3, 'sachiko', 'suzuki', 'suzuki@yahoo.co.jp', FALSE);

テーブルの中身を確認するとこうなってると思います。

id first_name last_name email accessprev
1 minoru tanaka tanaka@gmail.com t
2 tadashi sato sato@gmail.com f
3 sachiko suzuki suzuki@yahoo.co.jp f

DB操作

import設定

今回はPostgreSQLを使うので、それ用のdriverをインポートします。

import (
        "database/sql"
        _ "github.com/lib/pq"
)

データ取得用 Member構造体

データ取得用にMember構造体を定義しておきます。

type Member struct {
        Id         int
        FirstName  string
        LastName   string
        Email      string
        AccessPrev bool
}

サンプルコード全体

先に以降で説明するサンプルコードを掲載しておきます。

Go言語でDB操作

Query()

単純にクエリ実行したい場合に使えます。
sql - The Go Programming Language

func (db *DB) Query(query string, args ...interface{}) (*Rows, error)

サンプルコード

rows, err := Db.Query("SELECT * FROM members ORDER BY id")
if err != nil {
        return
}
for rows.Next() {
        m := Member{}
        rows.Scan(&m.Id, &m.FirstName, &m.LastName, &m.Email, &m.AccessPrev)
        fmt.Println(m)
}

Query()はRows型を返します。
sql - The Go Programming Language

SQLの結果が複数行を想定しているからです。
そんでそのメソッドのNext()で行があるか判断して、Scan()で値を取得しています。

Query() プレイスホルダー

このQuery()関数はプレイスホルダーも使えます。SQL分に$1, $2, $3 といった書き方をして、引数で実際の値を設定するというものです。

rows, err = Db.Query("SELECT * FROM members WHERE accessprev = $1 ORDER BY id", "false")

値はたいてい別々に取得するものなのでSQL文に入れる必要がないので見やすくなりますね。SQL文の中では$1, $2, $3...の順番はありません、引数に指定する実際の値の順番が$1, $2, $3...になるので順番を間違えないようにしましょう。

QueryRow()

そんで次にQueryRow()です。
https://golang.org/pkg/database/sql/#DB.QueryRow

func (db *DB) QueryRow(query string, args ...interface{}) *Row

QueryRow()はRow型を返します。一行のSQL結果が返るを想定してるのでScan()しかないですね。

サンプルコードです

m := Member{}
err = Db.QueryRow("SELECT * FROM members WHERE accessprev = $1 ORDER BY id", "true").Scan(&m.Id, &m.FirstName, &m.LastName, &m.Email, &m.AccessPrev)
if err != nil {
        return
}
fmt.Println(m)

Prepare()

SQL文だけでStmtオブジェクトを作り、クエリを実行するメソッドと分けることができます。
sql - The Go Programming Language

func (db *DB) Prepare(query string) (*Stmt, error)

サンプルコードです

statement := "SELECT * FROM members WHERE accessprev = $1 ORDER BY id"
stmt, err := Db.Prepare(statement)
if err != nil {
        return
}
defer stmt.Close()

rows, err = stmt.Query("false")
for rows.Next() {
        m := Member{}
        rows.Scan(&m.Id, &m.FirstName, &m.LastName, &m.Email, &m.AccessPrev)
        fmt.Println(m)
}

抜粋ですが実行メソッドが分けられているのがわかるかと思います。

statement := "SELECT * FROM members WHERE accessprev = $1 ORDER BY id"
stmt, err := Db.Prepare(statement)
defer stmt.Close()  // Close()が必要!!
rows, err = stmt.Query("false")

Exec()

単純にクエリを実行し、結果行を戻さないメソッドです。たいていは delete, insert で使うと思います。
sql - The Go Programming Language

func (db *DB) Exec(query string, args ...interface{}) (Result, error)

返すResult型を見てもどこの行に入ったかとかを返すようですね。データベース依存みたいです。
sql - The Go Programming Language
サンプルコードです

_, err = Db.Exec("INSERT INTO members VALUES ($1, $2, $3, $4, $5)", 4, "takashi", "yamamoto", "yamamoto@yahoo.co.jp", "FALSE")

Contextも渡せる

ここまで基本的なメソッドを見てきましたが、ドキュメントを見てみるとcontex.Contextを渡せるメソッドも用意されているんですね。
例えば QueryContext()とか
sql - The Go Programming Language

func (db *DB) QueryContext(ctx context.Context, query string, args ...interface{}) (*Rows, error)

確かにSQLでクエリに時間がかかることもありますから、タイムアウト時間も設定したいものです。けっこう便利ですな。

context.Context()の使い方については下記の記事が参考になります。
blog.suganoo.net

Named()はまだSQliteだけみたい

そんでまたドキュメントを見てみるとNamed()メソッドなるものも見つかりました。
sql - The Go Programming Language

func Named(name string, value interface{}) NamedArg

Exampleを見てみると、@で任意のプレイスホルダーを指定できるように見えます。

db.ExecContext(ctx, `
    delete from Invoice
    where
        TimeCreated < @end
        and TimeCreated >= @start;`,
    sql.Named("start", startTime),
    sql.Named("end", endTime),
)

なんだsql.Named() 優秀やーん!!
.....っと思ったのですが、実はこれまだ使えるDBは多くないそうです
mattn.kaoriya.net
Named()を使えるのはSQLiteだけみたいですね。

私はこれに気づかずPostgreSQLで試行錯誤してみましたが、ぜんぜん使えず半日以上を無駄にしました。

issueにはあるみたいですが、まだ使えないようですね。
github.com
github.com

database/sql ドキュメントのExample

あとdatabase/sqlのドキュメントを調べて気づきましたが、Exampleがことごとくすべてエラーになってます。これいいんですかねえ.....。
golang.org

まあDBがないとできないようなExampleだから、仕方ないといえばそうなのかもだけど。。。

最後

他にもいろいろ関数はありましてColumnType型やカラム一覧が取得できる関数などもありますが、上記の基本的な使い方がわかればあとは何とかなるかと思います。

次回はgormでもやってみたいと思います。

<こんな記事もあります>
blog.suganoo.net
blog.suganoo.net

達人に学ぶDB設計 徹底指南書 初級者で終わりたくないあなたへ

達人に学ぶDB設計 徹底指南書 初級者で終わりたくないあなたへ