2017/08/25

golangでSQLite3を使ってデータベースを操作する方法まとめ

golangでツールをつくるためにデータベースがほしかった。
MySQLやPostgreSQL、SQL Server、Oracleなど多種多様なDBMSがあるのだが、環境構築に消耗したくないということでSQLite3を選択。

小規模、もしくは大規模なサービスをつくるならORMを使うより直接SQLite3を操作したほうが問題が少なくすみそう。
ということで、当記事ではgolangからライブラリなどを使わずSQLite3を操作する方法をまとめる。
※ ただしSQLite3のドライバは使う


環境は以下のとおり

  • Mac OSX
  • go v1.8.3
  • sqlite3 v3.8.5


Go+SQLite3開発環境構築


まずはgoを動かすことができる開発環境を構築する。詳細は以下の記事を参照。
といってもすべてインストールする必要はない。
最低限、go v1.8.3だけでよい。パッケージ管理ツールのdepがあると便利だが必須ではない。

SQLite3については、Macならbrewでインストールすると簡単だ。
WindowsやLinuxについてはhttp://www.sqlite.org/からダウンロードし、インストールする。


SQLite3のドライバをインストールする


SQLite3ドライバはいくつかあるが、database/sqlインターフェースをサポートしているものは少ない。そこで今回はVim, golang界隈で有名?なmattn氏が作成したgo-sqlite3というドライバを使う。
$ go get github.com/mattn/go-sqlite3

depを使っている場合なら、main.goにimport文を追加し、以下のコマンドを実行する。
$ dep ensure



テーブルの作成


今回は開発用ディレクトリ直下にtest.dbというファイル(データベース)をつくる。
import (
  "database/sql"
  "fmt"
  "log"

  _ "github.com/mattn/go-sqlite3"
)

// データベースのコネクションを開く
db, err := sql.Open("sqlite3", "./test.db")
if err != nil {
  panic(err)
}

// テーブル作成
_, err = db.Exec(
  `CREATE TABLE IF NOT EXISTS "BOOKS" ("ID" INTEGER PRIMARY KEY, "TITLE" VARCHAR(255))`,
)
if err != nil {
  panic(err)
}
sql.Open("DBドライバ名", "接続先")でデータベースに接続できる。ファイルはなければ作成し、あればそれを使う。
テーブル作成はdb.Exec(...)でCREATE TABLE文を実行するだけ。

Node.jsと違い非同期処理(Callback、Promise、Async/Awaitなど)を考えなくてするのは楽だ。



レコードの挿入


// データの挿入
res, err := db.Exec(
  `INSERT INTO BOOKS (ID, TITLE) VALUES (?, ?)`,
  123,
  "title",
)
if err != nil {
  panic(err)
}

// 挿入処理の結果からIDを取得
id, err := res.LastInsertId()
if err != nil {
  panic(err)
}
データの挿入も同様にInsert文を書いて、?でバインドするだけ。
またdb.ExecはResultを返すので、Result.LastInsertId()で挿入したレコードのIDを取得できる。
※ テーブルの設定などにより取得できないこともある



複数レコードの取得


// 複数レコード取得
rows, err := db.Query(
  `SELECT * FROM BOOKS`,
)
if err != nil {
  panic(err)
}

// 処理が終わったらカーソルを閉じる
defer rows.Close()
for rows.Next() {
  var id int
  var title string

  // カーソルから値を取得
  if err := rows.Scan(&id, &title); err != nil {
    log.Fatal("rows.Scan()", err)
    return
  }

  fmt.Printf("id: %d, title: %s\n", id, title)
}

レコードを取得するときはdb.Query(...)を使う。db.Queryはrowsを返してくれるので、ループでカーソルをずらしながら1件ずつ処理していく。
defer rows.Close()で、処理が終わったときに(遅延させて)カーソルを閉じられる。



1件のみ取得


// 1件取得
row := db.QueryRow(
  `SELECT * FROM BOOKS WHERE ID=?`,
  id,
)

var id int
var title string
err := row.Scan(&id, &title);

// エラー内容による分岐
switch {
case err == sql.ErrNoRows:
  fmt.Printf("Not found")
case err != nil:
  panic(err)
default:
  fmt.Printf("id: %d, title: %s\n", id, title)
}

1件取得するときはdb.QueryRow(...)を使う。db.QueryRowはRowだけを返すので、取得できなくてもエラーにはならない。
エラー処理をする場合は、row.Scan(...)をしたときの戻り値errorで判定する。



レコードの更新


// 更新
res, err := db.Exec(
  `UPDATE BOOKS SET TITLE=? WHERE ID=?`,
  "update title",
  id,
)
if err != nil {
  panic(err)
}

// 更新されたレコード数
affect, err := res.RowsAffected()
if err != nil {
  panic(err)
}

fmt.Printf("affected by update: %d\n", affect)

Insert同様にdb.Exec(...)を使ってレコードを更新する。
またres.RowsAffected()により、影響を受けた(更新された)レコード数が取得できる。



レコードの削除


// 削除
res, err := db.Exec(
  `DELETE FROM BOOKS WHERE ID=?`,
  id,
)
if err != nil {
  panic(err)
}

// 削除されたレコード数
affect, err := res.RowsAffected()
if err != nil {
  panic(err)
}

fmt.Printf("affected by delete: %d\n", affect)

Insert、Update同様にdb.Exec(...)を使ってレコードを削除する。



トランザクションを使う


ここまで紹介してきた方法は、すべてトランザクションを使っていないのでロールバックなどできない。


なんらかのサービスを開発するとき、データベースはACIDの原則にもとづいて設計することになる。
途中で失敗したらロールバック、すべて成功したらコミット、のようにACIDにもとづいていないとデータに不整合が起きてしまう。
そこでトランザクションを使う。
// DBのレコードの構造体
type Book struct {
  ID    int64
  Title string
}


// データベース接続
db, err := sql.Open("sqlite3", "./test.db")
if err != nil {
  panic(err)
}


// テーブル作成
_, err = db.Exec(
  `CREATE TABLE IF NOT EXISTS "BOOKS" ("ID" INTEGER PRIMARY KEY, "TITLE" VARCHAR(255))`,
)
if err != nil {
  panic(err)
}
// 処理が終わったらDBのコネクションを閉じる
defer db.Close()


// トランザクション開始
tx, err := db.Begin()
if err != nil {
  panic(err)
}


// 10レコード挿入する
stmt, err := tx.Prepare(`INSERT INTO BOOKS (ID, TITLE) VALUES (?, ?)`)
if err != nil {
  panic(err)
}

defer stmt.Close()
for i := 0; i < 10; i++ {
  id := fmt.Sprintf("%d%d", time.Now().Unix(), i)
  // インサート処理の実行
  if _, err := stmt.Exec(id, fmt.Sprintf("title-%d", i)); err != nil {
    panic(err)
  }
}

isOk := true // なんらかの判定
if isOk {
  tx.Commit()
} else {
  tx.Rollback()
}


// 全レコード取得
rows, err := selectAll(db)
if err != nil {
  panic(err)
}

defer rows.Close()
for rows.Next() {
  var b Book
  if err := rows.Scan(&b.ID, &b.Title); err != nil {
    panic(err)
  }

  fmt.Printf("id: %d, title: %s\n", b.ID, b.Title)
}

db.Begin()でトランザクションを開始する。そのときに戻り値Txが返ってくる。あとは必要に応じてtx.Commit()tx.Rollback()を使ってコミット、ロールバックをする。



参考サイト



以上

written by @bc_rikko

0 件のコメント :

コメントを投稿