Go言語でDockerで立てたPostgreSQLを操作してみた

作成：2023年7月12日
PosgreSQLの練習で、DockerでPostgreSQLを立てて、Go言語でアクセスしてみました。

# docker-compose.ymlを作成
```:docker-compose.yml
# docker-composeで使用するバージョンを定義しています。2022年5月時点では、3.9が最新です。
version: '3.9'
# サービス (コンテナ) を定義します。
services:
  # 今回は postgres をサービスとして定義しました。
  postgres:
    # Docker Image は postgres:12-alpine を使います。postgres:12-alpine は postgres:12 と比較して、イメージサイズが小さくなっています。
    image: postgres:12-alpine
    # コンテナの名前を指定します。
    container_name: postgres
    # 環境変数を設定します。
    environment:
      - POSTGRES_USER=root
      - POSTGRES_PASSWORD=root
      - POSTGRES_DB=testdb
    # データの永続化
    volumes:
      # postgresボリュームを/var/lib/postgresql/dataにマウントします
      - postgres:/var/lib/postgresql/data
    # ポートの指定（HOST:CONTAINER）
    ports:
      - 5432:5432
# データの永続化：ボリュームを作成します。
volumes:
  postgres:
```
# コンテナ立ち上げ
コンテナを立ち上げて、PosgreSQLを確認します。
```:Terminal
$ docker-compose up -d
% docker ps
CONTAINER ID   IMAGE                        COMMAND                   CREATED              STATUS              PORTS                                       NAMES
835d849809e8   postgres:12-alpine           "docker-entrypoint.s…"   About a minute ago   Up About a minute   0.0.0.0:5432->5432/tcp, :::5432->5432/tcp   postgres
% docker exec -it postgres bash
835d849809e8:/# psql -l
                             List of databases
   Name    | Owner | Encoding |  Collate   |   Ctype    | Access privileges 
-----------+-------+----------+------------+------------+-------------------
 postgres  | root  | UTF8     | en_US.utf8 | en_US.utf8 | 
 template0 | root  | UTF8     | en_US.utf8 | en_US.utf8 | =c/root          +
           |       |          |            |            | root=CTc/root
 template1 | root  | UTF8     | en_US.utf8 | en_US.utf8 | =c/root          +
           |       |          |            |            | root=CTc/root
 testdb    | root  | UTF8     | en_US.utf8 | en_US.utf8 | 
(4 rows)

835d849809e8:/# 
```
# PostgreSQLに接続してテーブル作成
PostgreSQLに接続します。
```:Terminal
835d849809e8:/# psql -h localhost -p 5432 -U root -d testdb
psql (12.15)
Type "help" for help.

testdb=# 
```
テーブルを作成します。
```:Terminal
testdb=# create table members (
testdb(#   id          serial primary key,
testdb(#   first_name  varchar(255),
testdb(#   last_name   varchar(255),
testdb(#   email       varchar(255),
testdb(#   accessprev  boolean
testdb(# );
CREATE TABLE
testdb=# INSERT INTO members VALUES (1, 'ichiro', 'tanaka', 'ichiro@gmail.com', TRUE);
INSERT 0 1
testdb=# INSERT INTO members VALUES (2, 'jiro', 'sato', 'jiro@gmail.com', FALSE);
INSERT 0 1
testdb=# INSERT INTO members VALUES (3, 'hanako', 'suzuki', 'hanako@gmail.com', TRUE);
INSERT 0 1
testdb=# SELECT * FROM members;
 id | first_name | last_name |      email       | accessprev 
----+------------+-----------+------------------+------------
  1 | ichiro     | tanaka    | ichiro@gmail.com | t
  2 | jiro       | sato      | jiro@gmail.com   | f
  3 | hanako     | suzuki    | hanako@gmail.com | t
(3 rows)


testdb=# 
```

# Goのコード準備
```:main.go
package main

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

var Db *sql.DB

func init() {
	var err error
	// DB名やユーザー、テーブルは適宜適切な値を入れてください
	Db, err = sql.Open("postgres", "user=root dbname=testdb password=root sslmode=disable")
	if err != nil {
		panic(err)
	}
}

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

func main() {
	fmt.Println("----- Query 1 -----")
	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)
	}
	fmt.Println()

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

	fmt.Println("----- QueryRow -----")
	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)
	fmt.Println()

	fmt.Println("----- Prepare -----")
	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)
	}
	fmt.Println()

	fmt.Println("----- Exec -----")
	_, err = Db.Exec("INSERT INTO members VALUES ($1, $2, $3, $4, $5)", 4, "takashi", "yamamoto", "yamamoto@yahoo.co.jp", "FALSE")
	if err != nil {
		return
	}
	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)
	}
	fmt.Println()

}
```
・DB.Query：単純にクエリを実行したいときに使えます。[func (*DB) Query](https://pkg.go.dev/database/sql#DB.Query)
・Query()はRows型を返します。SQLの結果が複数行を想定しています。[type Rows](https://pkg.go.dev/database/sql#Rows)
　Rows型のメソッドの Next() で行があるか判断して、Scan() で値を取得しています。
・Query()関数は[プレイスホルダー](https://www.shiftsecurity.jp/blog/20210625-5#:~:text=%E3%83%97%E3%83%AC%E3%83%BC%E3%82%B9%E3%83%9B%E3%83%AB%E3%83%80%E3%81%A8%E3%81%AF%E3%80%81%E3%81%82%E3%82%89%E3%81%8B%E3%81%98%E3%82%81,%E3%82%92%E3%83%90%E3%82%A4%E3%83%B3%E3%83%89%E3%81%A8%E3%81%84%E3%81%84%E3%81%BE%E3%81%99%E3%80%82)も使えます。SQL文に $1 などの書き方で引数で実際の値を設定できます。
```:main.go
rows, err = Db.Query("SELECT * FROM members WHERE accessprev = $1 ORDER BY id", "false")
```
　この例では第１引数の false が $1 に入ります。
・QueryRow() は　Rows型を返します。一行のSQL結果が返るのを想定してScan()を使っています。[func (*DB) RueryRow](https://pkg.go.dev/database/sql#DB.QueryRow)
・Prepare() は SQL文だけで stmtオブジェクトを作り、クエリを実行するメソッドと分けることができます。[func (*DB) Prepare](https://pkg.go.dev/database/sql#DB.Prepare)
・Exec() は単純にクエリを実行して、結果の行を返さないメソッドです。大抵はdelete、insertで使うそうです。[func (*DB) Exec](https://pkg.go.dev/database/sql#DB.Exec)



# Goの postgresドライバーをインストール

go init してから、 go get します。
```:Terminal
% go mod init test-postgres
go: creating new go.mod: module test-postgres
go: to add module requirements and sums:
	go mod tidy

% go get github.com/lib/pq
go: added github.com/lib/pq v1.10.9
```

# 実行

Docker上の PostgrSQL に、　Goプログラムからアクセスできました。
```:Terminal
% ls
docker-compose.yml	go.mod			go.sum			main.go

% go run main.go 
----- Query 1 -----
{1 ichiro tanaka ichiro@gmail.com true}
{2 jiro sato jiro@gmail.com false}
{3 hanako suzuki hanako@gmail.com true}

----- Query 2 -----
{2 jiro sato jiro@gmail.com false}

----- QueryRow -----
{1 ichiro tanaka ichiro@gmail.com true}

----- Prepare -----
{2 jiro sato jiro@gmail.com false}

----- Exec -----
{1 ichiro tanaka ichiro@gmail.com true}
{2 jiro sato jiro@gmail.com false}
{3 hanako suzuki hanako@gmail.com true}
{4 takashi yamamoto yamamoto@yahoo.co.jp false}

 % docker-compose down
[+] Running 2/2
 ✔ Container postgres           Removed                                                                                                                         0.2s 
 ✔ Network sql20230711_default  Removed                                                                                                                         0.1s 

```

以上、お試しでやってみました。

# 参考
・[Docker で PostgreSQL 上にデータベースを作成しよう](https://tech-diary.net/how-to-create-postgresql-container/)
・[Go言語でSQLを実行してみる](https://blog.suganoo.net/entry/2019/01/25/190200)
・[PostgreSQLへの接続と切断](https://www.javadrive.jp/postgresql/connect/index2.html)
・[データを取得する(SELECT文)](https://www.javadrive.jp/postgresql/select/index1.html)
・[初心者の僕がGO言語でpostgresSQLをいじるまで。](https://qiita.com/shunsa10/items/91dc2e81494af040761a)
・https://github.com/lib/pq
・[データ接続のパッケージをインポート](https://nishinatoshiharu.com/connect-go-database/#i)
・[$ go mod init mysql-example](https://kazuhira-r.hatenablog.com/entry/2021/03/16/223253)
・[init関数のふしぎ #golang](https://qiita.com/tenntenn/items/7c70e3451ac783999b4f)