Goのdatabase/sqlを利用してPostgreSQL/MySQLへ接続&CRUD操作してみた

[「**Goプログラミング実践入門 標準ライブラリでゼロからWebアプリを作る」**](https://amzn.to/3UI1wa3)という書籍を参考に、database/sqlを利用したデータベースへの接続とCRUD操作についてまとめました。

# PostgreSQLへの接続

### ドライバーのインストール

ドライバーをインストールする必要がある。

```bash
go get "github.com/lib/pq"
```

### 接続方法

```bash
sql.Open("postgres", "user=ユーザー名 dbname=DB名 password=パスワード sslmode=disable")
```

# MySQLへの接続方法

### ドライバーのインストール

こちらも同様にドライバーをインストールする必要がある。

```bash
go get -u github.com/go-sql-driver/mysql
```

### 接続方法

```sql
sql.Open("mysql", "ユーザー名:パスワード@tcp(ホスト:ポート番号)/DB名")
```

# 接続してみる

PostgreSQLへ接続してみます。

### データベース情報

ユーザー名：gwp
DB名：gwp
パスワード：gwp

### テーブル情報

```sql
create table posts (
  id      serial primary key,
  content text,
  author  varchar(255)
);
```

### コード

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

var Db *sql.DB

func init() {
	var err error
	Db, err = sql.Open("postgres", "user=gwp dbname=gwp password=gwp sslmode=disable")
	if err != nil {
		panic(err)
	}

	err = Db.Ping()
	if err != nil {
		fmt.Println("接続失敗")
		return
	} else {
		fmt.Println("接続成功")
	}
}
```

### ポイント

- `_ "github.com/lib/pq"`の部分はDriverインターフェースを実装したドライバー。データベースドライバーを登録するには通常`sql.Register`が使用されるがこの関数自体は書く必要がない。`_ "github.com/lib/pq"`の記述をすることで、init()関数が実行された時に`sql.Register`を利用しPostgreSQLをデータベースドライバーとして登録している。
- DBの種類ごとにドライバーをインポートする必要がある。`"github.com/lib/pq”`の前にある`_` は参照されていないパッケージを読み込むためのもの。パッケージを利用しないがinit()関数でのみ実行が必要なので記述する。
- `*sql.DB`はデータベースへのハンドル。これを変数目`Db`に入れている。`Db`は変数なので他の名前でも良い。
- `Opne()`は構造体`sql.DB`へのポインタを返す。（データベース接続に必要になる構造体）
- sql.Open()で返される構造体はこんな感じ

```bash
&{0 {user=gwp dbname=gwp password=gwp sslmode=disable 0x144c750} 0 {0 0} [] map[] 0 0 0xc00007e240 false map[] map[] 0 0 0 0 <nil> 0 0 0 0 0x107ffa0}
```

- 実際にデータベースへの接続を確認しているのは`err = Db.Ping()`の部分。ここがエラーだと接続できていない、ということ。

# 投稿の作成

```go
func (post *Post) Create() (err error) {
	statement := "insert into posts (content, author) values ($1, $2) returning id"
	stmt, err := Db.Prepare(statement)
	if err != nil {
		return
	}
	defer stmt.Close()
	err = stmt.QueryRow(post.Content, post.Author).Scan(&post.Id)
	return
}

func main() {
	post := Post{Content: "Hello World!", Author: "Amby"}
	post.Create()
}
```

### プリペアドステートメント

動的にSQLを生成する必要がある時、変更箇所だけを変数のようにしたSQL文を作成しておく方式のこと。
SQLインジェクションを防止する効果がある。

### ポイント

- `Create()`内ではプリペアドステートメントを利用している。`&1`,`&2`はレコードを作成する時に実際の値で置き換えられる。
- `stmt, err := Db.Prepare(statement)`の部分でステートメントを生成している。`Prepare()`はインターフェース`sql.Stmt`への参照を生成する。`stmt`の中身はこんな感じになっている。これが使用するステートメント。

```bash
&{0xc00010a8f0 insert into posts (content, author) values ($1, $2) returning id <nil> {{0 0} 0 0 0 0} <nil> 0xc0001a6000 <nil> {0 0} false [{0xc0001a0000 0xc0001a6000}] 0}
```

- `err = stmt.QueryRow(post.Content, post.Author).Scan(&post.Id)`の部分でプリペアドステートメントを実行している。`QueryRow()`は1レコードを取得する。先に生成されたプリペアドステートメント`stmt`に対して`QueryRow()`を実行している点に注意。
- `Scan`はfmtパッケージの関数で、`Scan(&格納先の変数)`とすることで、入力を「&格納先の変数」に格納することができる。この場合、`stmt.QueryRow(post.Content, post.Author)`で返ってくる値を`&post.Id`に格納している。（プリペアドステートメントで`returning id`としているのでidが返ってきている。）

# 投稿の取得

```go
func GetPost(id int) (post Post, err error) {
	post = Post{}
	err = Db.QueryRow("select id, content, author from posts where id = $1", id).Scan(&post.Id, &post.Content, &post.Author)
	return
}

func main() {
	readPost, _ := GetPost(post.Id)
}
```

### ポイント

- 構造体Postを返したいので、まず空の構造体Postを作成している。
- 投稿作成時と同様で、Scanを使うことで`(&post.Id, &post.Content, &post.Author)`に返ってきた値を格納している。
- 今回のケースではプリペアドステートメントは使用していない（使用しても良い）。プリペアドステートメントを使用しない場合、`sql.DB`のメソッドである`QueryRow`を使い、SQLを直接実行している。

# 投稿の更新

```go
func (post *Post) Update() (err error) {
	_, err = Db.Exec("update posts set content = $2, author = $3 where id = $1", post.Id, post.Content, post.Author)
	return
}

func main() {
	readPost.Content = "Wahahahahaha"
	readPost.Author = "Goromaru"
	readPost.Update()
}
```

### ポイント

- `Update()`は構造体Postに対するメソッドとして定義する。
- `sql.DB`のメソッドである`Exec()`を使用している。プリペアドステートメントを使用するよりも高速。
- エラーがあればエラーが返されるが、そうでなければ`_`によって返り値は無視される。

# 投稿の削除

```go
func (post *Post) Delete() (err error) {
	_, err = Db.Exec("delete from posts where id = $1", post.Id)
	return
}

func main() {
	readPost.Delete()
}
```

### ポイント

- 投稿の更新とほぼ同じ

# 全投稿の取得

```go
func Posts(limit int) (posts []Post, err error) {
	rows, err := Db.Query("select id, content, author from posts limit $1", limit)
	if err != nil {
		return
	}
	for rows.Next() {
		post := Post{}
		err = rows.Scan(&post.Id, &post.Content, &post.Author)
		if err != nil {
			return
		}
		posts = append(posts, post)
	}
	rows.Close()
	return
}

func main() {
	posts, _ = Posts(10)
}
```

### ポイント

- データベースから最初の10件のPostを取得する
- `Db.Query`の部分は`sql.DB`のメソッドで`Query()`を使用している。このメソッドはインターフェースRowsを返す。
- Rowsはイテレータ。
- `posts, _ = Posts(10)`の返り値はこのようになる

```bash
[{1 Wahahahahaha Goromaru}...]
```

# １対多の関係

## テーブルの用意

```sql
create table posts (
  id      serial primary key,
  content text,
  author  varchar(255)
);

create table comments (
  id      serial primary key,
  content text,
  author  varchar(255),
  post_id integer references posts(id)
);
```

### ポイント

- commentsテーブルにpost_idカラムを用意している。これは`references posts(id)`とすることでpostsテーブルのid列を参照することになり、postsのidを参照する外部キーになる。

## 関係構築

```go
type Post struct {
	Id       int
	Content  string
	Author   string
	Comments []Comment
}

type Comment struct {
	Id      int
	Content string
	Author  string
	Post    *Post
}
```

### ポイント

- 構造体PostにCommentsというフィールドを用意している。これは構造体Commentのスライスになっている。つまり、Postに関係するCommentをこのフィールドの中に格納する。
- 構造体CommentにはPostというフィールドがある。これは構造体Postへのポインタになっている。つまりCommentがどのPostに関係するか？を格納するフィールドになっている。
- 構造体PostのCommentsフィールド、構造体CommentのPostフィールドは共にポインタである。なぜなら関係する構造体のコピーを保持するのではなく、参照をしたいから。

## Commentの作成

```go
func (comment *Comment) Create() (err error) {
	if comment.Post == nil {
 		err = errors.New("投稿が見つかりません")
		return
	}
	err = Db.QueryRow("insert into comments (content, author, post_id) values ($1, $2, $3) returning id", comment.Content, comment.Author, comment.Post.Id).Scan(&comment.Id)
	return
}

func main() {
	post := Post{Content: "Hello World!", Author: "Sau Sheong"}
	post.Create()

 	comment := Comment{Content: "いい投稿だね！", Author: "Joe", Post: &post}
	comment.Create()
}
```

### ポイント

- `comment := Comment{Content: "いい投稿だね！", Author: "Joe", Post: &post}`の部分で構造体Commentを作成する際に`Post: &post`としてリレーションを作成している。この`&post`は`post := Post{Content: "Hello World!", Author: "Sau Sheong"}`へのポインタ。
- `Create()`は先に見た投稿の作成とおなじ(プリペアドステートメントを使っていない点は異なる)。SQL文で`returning id`としているのでidが返ってきて、それを`.Scan(&comment.Id)`でcommentのidに設定している。

## 関係の取得

```go
func GetPost(id int) (post Post, err error) {
	post = Post{}
	post.Comments = []Comment{}
	err = Db.QueryRow("select id, content, author from posts where id = $1", id).Scan(&post.Id, &post.Content, &post.Author)

	rows, err := Db.Query("select id, content, author from comments where post_id = $1", id)
	if err != nil {
		return
	}
	for rows.Next() {
		comment := Comment{Post: &post}
		err = rows.Scan(&comment.Id, &comment.Content, &comment.Author)
		if err != nil {
			return
		}
		post.Comments = append(post.Comments, comment)
	}
	rows.Close()
	return
}

func main() {
	readPost, _ := GetPost(post.Id)
}
```

### ポイント

- まず`post = Post{}`を作成する。これは`GetPost()`の返り値。
- `post.Comments = []Comment{}`とし、取得したCommentを`Post{}`に格納できるようにする。
- `rows`はインターフェースRowであり、イテレータ。Commentのレコードが複数件格納されている。
- `comment := Comment{Post: &post}`の部分で`post.Comments`のスライスの中に格納するCommentを生成している。このCommentは全て`post`(引数で受け取ったidに対応するPost)のポインタに紐づいている。
- `append(post.Comments, comment)`はスライスである`post.Comments`に`comment`を追加している。

# まとめ
非常にシンプルにデータベース接続とCRUD操作が実装できると思いました。