Go言語のDB接続処理

Go言語の公式の[DB処理チュートリアル](https://go.dev/doc/tutorial/database-access)を元にしていますが、DBをMySQLからSqlite3に変更、データの追加にトランザクションを利用するように変更しています。

## 構成

- Sqlite3 3.36.0
- go 1.17
- github.com/mattn/go-sqlite3 v1.14.11

## DDLとDML

※Sqlite3の使い方については本件の本質ではないので割愛します。

### DDL

```sql
CREATE TABLE album (
  id         INTEGER PRIMARY KEY AUTOINCREMENT,
  title      VARCHAR(128) NOT NULL,
  artist     VARCHAR(255) NOT NULL,
  price      DECIMAL(5,2) NOT NULL
);
```

### DML

```sql
INSERT INTO album
  (id, title, artist, price)
VALUES
  (1, 'Blue Train', 'John Coltrane', 56.99);
INSERT INTO album
  (id, title, artist, price)
VALUES
  (2, 'Giant Steps', 'John Coltrane', 63.99);
INSERT INTO album
  (id, title, artist, price)
VALUES
  (3, 'Jeru', 'Gerry Mulligan', 17.99);
INSERT INTO album
  (id, title, artist, price)
VALUES
  (4, 'Sarah Vaughan', 'Sarah Vaughan', 34.98);
```

## DB接続

database/sqlパッケージには、DBの接続、操作、トランザクションなどのRDBMSの基本的処理をするタイプと関数が含まれています。
あとは、DB接続は使いたいDBのドライバーをimportすれば、接続時のコンフィグレーション以外は実装に差異はほぼありません。

```go
package main

import (
	"database/sql" // DB用汎用インターフェース
	"fmt"
	"log"

	_ "github.com/mattn/go-sqlite3" // SQLite3ドライバーのパッケージ
)

// DBの接続
var db *sql.DB // コード単純化をするために、グローバル変数にする

func main(){
	// DBの接続
	var err error
	db, err = sql.Open("sqlite3", "./recordings.db")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close() // 関数終了時に自動でリソースを解放する

	// 接続の確認
	pingErr := db.Ping()
	if pingErr != nil {
		log.Fatal(pingErr)
	}
	fmt.Println("Connectied!")
}
```

## テーブルの検索

```go

// IDからAlbumを取得する
func albumByID(id int64) (Album, error) {
	var album Album

	// SQLインジェクジョン回避のためにプレペアードステートメントを生成する
	stmt, err := db.Prepare("SELECT * FROM album where id = ?")
	if err != nil {
		return album, fmt.Errorf("albumByID %q: %v", id, err)
	}
	defer stmt.Close()

	// プレペアードステートメントのプレースホルダに変数を設定してクエリを実行する
	rows := stmt.QueryRow(id)
	if err := rows.Scan(&album.ID, &album.Title, &album.Artist, &album.Price); err != nil {
		if err == sql.ErrNoRows {
			return album, fmt.Errorf("albumByID %q: %v", id, err)
		}
		return album, err
	}

	return album, nil
}
```

## テーブルにレコードの追加

```sql
// albumを追加する
func addAlbums(albums []Album) ([]int64, error) {
	// トランザクション開始
	tx, err := db.Begin()
	if err != nil {
		return nil, fmt.Errorf("addAlbums %v: %v", albums, err)
	}
	defer tx.Rollback() // 関数終了時に自動でロールバックする

	// プレペアステートメントの生成
	stmt, err := tx.Prepare("INSERT INTO album (title, artist, price) VALUES (?, ?, ?)")
	if err != nil {
		return nil, fmt.Errorf("addAlbums %v: %v", albums, err)
	}
	defer stmt.Close() // 関数終了時に自動でリソースを解放する

	var indexs []int64

	for _, album := range albums {
		// プレースホルダに値を設定してクエリを実行する
		result, err := stmt.Exec(album.Title, album.Artist, album.Price)
		if err != nil {
			return nil, fmt.Errorf("addAlbums %v: %v", albums, err)
		}
		id, err := result.LastInsertId()
		if err != nil {
			return nil, fmt.Errorf("addAlbums %v: %v", albums, err)
		}
		indexs = append(indexs, id)
	}

   // コミット
	if err = tx.Commit(); err != nil {
		return nil, fmt.Errorf("addAlbums %v: %v", albums, err)
	}

	return indexs, nil
}
```

## 完成したコード

```go
package main

import (
	"database/sql" // DB用汎用インターフェース
	"fmt"
	"log"

	_ "github.com/mattn/go-sqlite3" // SQLite3ドライバーのパッケージ
)

// DBの接続
var db *sql.DB

// Albumテーブルのスキーマ構造体
type Album struct {
	ID     int64
	Title  string
	Artist string
	Price  float32
}

// IDからAlbumを取得する
func albumByID(id int64) (Album, error) {
	var album Album

	// SQLインジェクジョン回避のためにプレペアードステートメントを生成する
	stmt, err := db.Prepare("SELECT * FROM album where id = ?")
	if err != nil {
		return album, fmt.Errorf("albumByID %q: %v", id, err)
	}
	defer stmt.Close()

	// プレペアードステートメントのプレースホルダに変数を設定してクエリを実行する
	rows := stmt.QueryRow(id)
	if err := rows.Scan(&album.ID, &album.Title, &album.Artist, &album.Price); err != nil {
		if err == sql.ErrNoRows {
			return album, fmt.Errorf("albumByID %q: %v", id, err)
		}
		return album, err
	}

	return album, nil
}

// アーティストからAlbumを取得する
func albumsByArtist(name string) ([]Album, error) {
	var albums []Album

	// DBの検索する
	rows, err := db.Query("SELECT * FROM album where artist = ?", name)
	if err != nil {
		return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
	}
	defer rows.Close() // 関数終了時に自動でリソース解放する

	for rows.Next() {
		var alb Album
		if err := rows.Scan(
			&alb.ID,
			&alb.Title,
			&alb.Artist,
			&alb.Price); err != nil {
			return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
		}
		albums = append(albums, alb)
	}
	if err := rows.Err(); err != nil {
		return nil, fmt.Errorf("albumsByArtist %q: %v", name, err)
	}
	return albums, nil
}

// albumを追加する
func addAlbum(alb Album) (int64, error) {
	result, err := db.Exec("INSERT INTO album (title, artist, price) VALUES (?, ?, ?)", alb.Title, alb.Artist, alb.Price)
	if err != nil {
		return 0, fmt.Errorf("addAlbum: %v", err)
	}

	id, err := result.LastInsertId()
	if err != nil {
		return 0, fmt.Errorf("addAlbum: %v", err)
	}
	return id, nil
}

// albumを追加する
func addAlbums(albums []Album) ([]int64, error) {
	// トランザクション開始
	tx, err := db.Begin()
	if err != nil {
		return nil, fmt.Errorf("addAlbums %v: %v", albums, err)
	}
	defer tx.Rollback() // 関数終了時に自動でロールバックする

	// プレペアステートメントの生成
	stmt, err := tx.Prepare("INSERT INTO album (title, artist, price) VALUES (?, ?, ?)")
	if err != nil {
		return nil, fmt.Errorf("addAlbums %v: %v", albums, err)
	}
	defer stmt.Close() // 関数終了時に自動でリソースを解放する

	var indexs []int64

	for _, album := range albums {
		// プレースホルダに値を設定してクエリを実行する
		result, err := stmt.Exec(album.Title, album.Artist, album.Price)
		if err != nil {
			return nil, fmt.Errorf("addAlbums %v: %v", albums, err)
		}
		id, err := result.LastInsertId()
		if err != nil {
			return nil, fmt.Errorf("addAlbums %v: %v", albums, err)
		}
		indexs = append(indexs, id)
	}

	// コミット
	if err = tx.Commit(); err != nil {
		return nil, fmt.Errorf("addAlbums %v: %v", albums, err)
	}

	return indexs, nil
}

func main() {

	// DBの接続
	var err error
	db, err = sql.Open("sqlite3", "./recordings.db")
	if err != nil {
		log.Fatal(err)
	}
	defer db.Close() // 関数終了時に自動でリソースを解放する

	// 接続の確認
	pingErr := db.Ping()
	if pingErr != nil {
		log.Fatal(pingErr)
	}
	fmt.Println("Connectied!")

	albums, err := albumsByArtist("John Coltrane")
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Albums found: %v\n", albums)

	album, err := albumByID(2)
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("Album found: %v\n", album)

	albID, err := addAlbum(Album{
		Title:  "The Modern Sound of Betty Carter",
		Artist: "Betty Carter",
		Price:  49.99,
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("ID of added album: %v\n", albID)

	albIDs, err := addAlbums([]Album{
		{
			Title:  "The Modern Sound of Betty Carter",
			Artist: "Betty Carter",
			Price:  49.99,
		},
		{
			Title:  "The Modern Sound of Betty Carter",
			Artist: "Betty Carter",
			Price:  49.99,
		},
	})
	if err != nil {
		log.Fatal(err)
	}
	fmt.Printf("ID of added albums: %v\n", albIDs)

}
```