前回
Goの公式チュートリアルをやってみる【ジェネリクス】はじめに
前回はジェネリクス関数の使い方について学んでいきました。今回は標準ライブラリのdatabase/sql
パッケージを使用してリレーショナルデータベースにアクセスする基本的な処理を学んでいきます。
tabase/sql
パッケージにはデータベースへの接続、トランザクションの実行、実行中の操作のキャンセルなどの型と変数が含まれています。
今回はデータベースを作成して、そのデータベースにアクセスするコードを記述していきます。サンプルプロジェクトはビンテージジャズのレコードに関するデータのリポジトリです。
モジュールの作成
作業フォルダにdata-access
フォルダを作成して、コマンドプロンプトでdata-access
ディレクトリに移動し、go mod init example/data-access
をコマンドでモジュールを作成しましょう。
$ go mod init example/data-access
go: creating new go.mod: module example/data-access
データベースのセットアップ
この記事ではDBMSのCLIを使用してデータベース、テーブルを作成していきます。チュートリアルではMySQLを使用していますが、今回はPostgreSQLを使用しています。ほかのデータベースでも基本独自のCLIが用意されています。
データベースの作成
まずはPostgreSQLにログインします。$ psql -U ユーザー名
以下のコマンドを実行してデータベースを作成していきます。$ postgres=# create database recordings;
テーブルの追加
SQLファイルの作成
テキストエディタでdata-access
フォルダにcreate-tables.sql
というファイルを作成して以下のコードを追加します。
DROP TABLE IF EXISTS album;
CREATE TABLE album (
id INT AUTO_INCREMENT NOT NULL,
title VARCHAR(128) NOT NULL,
artist VARCHAR(255) NOT NULL,
price DECIMAL(5,2) NOT NULL,
PRIMARY KEY (`id`)
);
INSERT INTO album
(title, artist, price)
VALUES
('Blue Train', 'John Coltrane', 56.99),
('Giant Steps', 'John Coltrane', 63.99),
('Jeru', 'Gerry Mulligan', 17.99),
('Sarah Vaughan', 'Sarah Vaughan', 34.98);
このコードはこんなことをしています。
album
というテーブルを削除。
(後でテーブルをやり直したい場合の時のため)- ID・タイトル・アーティスト・価格の列を持つ
album
テーブルを作成。IDの値はDBMSによって自動作成される。 - 4行のデータを追加
SQLファイルの実行
まずは作成したデータベースに接続いたします。$ postgres=# \c recordings;
その後、作成したSQLファイルを実行します。$ postgres=# \i /create-tables.sql;
recordings=# \i ./create-tables.sql
psql:create-tables.sql:1: NOTICE: テーブル"album"は存在しません、スキップします
DROP TABLE
CREATE TABLE
INSERT 0 4
正常に追加されたかを確認してみましょう。$ postgres=# SELECT * FROM album;
以下のテーブルが表示されたら作成完了です。
recordings=# SELECT * FROM album;
id | title | artist | price
----+---------------+----------------+-------
1 | Blue Train | John Coltrane | 56.99
2 | Giant Steps | John Coltrane | 63.99
3 | Jeru | Gerry Mulligan | 17.99
4 | Sarah Vaughan | Sarah Vaughan | 34.98
(4 行)
データベースドライバーの検索とインポート
先ほどはデータベースを作成していくつかのデータを用意しました。続いてdatabase/sql
パッケージの関数を使用したリクエストを、データベースが理解できるリクエストに変換してくれるデータベースドライバーを探してインポートします。
まずはブラウザでSQLDrivers wikiページにアクセスし、使用できるドライバを特定します。チュートリアルではMySQLを使用しているためGo-MySQL-Driverを使用していますが、今回はPostgreSQLを使用しているためpgxを使用いたします。
pgxのパッケージ名はgithub.com/jackc/pgx/v5
ですが、database/sql
と一緒に使う場合はgithub.com/jackc/pgx/v5/stdlib
であることに注意してください。
data-access
ディレクトリでmain.go
ファイルを作成し、以下のコードを追加します。
package main
import "github.com/jackc/pgx/v5/stdlib
"
データベースハンドルの取得と接続
パッケージのインポートができたところで、データベースハンドルを使用してデータベースにアクセスるGoコードを書いてみます。特定のデータベースへのアクセスを表す、sql.DB
構造体へのポインターを使用します。
以下のコードをmain.go
に追加しましょう。
var db *sql.DB
func main() {
// 接続プロパティの宣言
user := os.Getenv("DBUSER")
passwd := os.Getenv("DBPASS")
host := "127.0.0.1"
port := "5432"
dbName := "recordings"
// 接続プロパティの代入を空白区切りで結合
keyValue := "user=" + user + " " +
"password=" + passwd + " " +
"host=" + host + " " +
"port=" + port + " " +
"database=" + dbName
// データベースハンドルの取得
var err error
db, err = sql.Open("pgx", keyValue)
if err != nil {
log.Fatal(err)
}
// データベースへの接続を閉じる
defer db.Close()
// 接続確認
pingErr := db.Ping()
if pingErr != nil {
log.Fatal(pingErr)
}
fmt.Println("Connected!")
}
このコードはこんなことをしています。
*sql.DB
型のグローバル変数db
を宣言する。
この例では単純化するためにグローバル変数としていますが、実運用では関数に渡すか構造体でラップする等、グローバル変数を使わないことが望ましいです。- 接続プロパティを収集してフォーマットする。
sql.Open
に接続プロパティを渡して対象のデータベースに接続する。sql.Open
でエラーが発生していないかをチェックする。
(今回はエラーの場合log.Fatal
を呼び出して実行を終了させています)- 関数のすべての処理が終了したら
DB.Close
を必ずするように、Openの直後に記述する。 DB.Ping
を呼び出してデータベースへの接続がうまくいっていることを確認します。ドライバによってはsql.Open
がすぐに接続しないかもしれません。database.sql
パッケージがっ必要な時に接続できることを確認するために、ここでPingを使用しています。- 接続に失敗した場合はエラーをだし、成功した場合はメッセージを表示する。
これらの処理を実行するためにはインポートするパッケージがないので、以下のパッケージを追加でインポートします。
import (
"database/sql"
"fmt"
"log"
"os"
_ "github.com/jackc/pgx/v5/stdlib"
)
よく見るとgithub.com/jackc/pgx/v5/stdlib
のインポートの書き方だけが異なっています。これは「blank import」と呼ばれます。ブランクインポートはパッケージ内のすべての宣言を無視してパッケージの初期化だけを行います。パッケージにinit
関数がある場合にinit
関数のみが実行されます。
ユーザー名・パスワードを環境変数に登録
main.go
を保存したらgo mod tidy
を行い、依存関係を整理しましょう。その後コマンドプロンプトで環境変数を設定します。<username>
と<password>
にはデータベースに登録したユーザー名とパスワードを入力してください。
$ set DBUSER=<username>
$ set DBPASS=<password>
実行確認
環境変数への登録が完了したら、コマンドプロンプトでdata-access
ディレクトリに移動しgo run .
を実行しましょう。以下の内容が出力されたらアクセス成功です。
$ go run .
Connected!
複数行のクエリ
先ほどテーブルへの接続まで成功しました。続いて、複数行を返すように設計されたSQLクエリを実装して実行します。
まずはmain.go
のmain
関数のすぐ上にalbum
構造体の定義を追加します。この構造体を使用してクエリから返された行のデータを保持します。
type Album struct {
ID int64
Title string
Artist string
Price float32
}
続いてmain.go
の最後にデータベースに問い合わせる、albumByArtist
関数を追加します。以下のコードを追加しましょう。
// 指定したアーティスト名を持つアルバムを検索します。
func albumByArtist(name string) ([]Album, error) {
// 返された行のデータを保持するスライス。
var albums []Album
rows, err := db.Query("SELECT * FROM album WHERE artist = $1", 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
型のスライスを宣言する。 DB.Query
を使用してSELECT
文を実行し、指定したアーティスト名のアルバムを検索する。DB.Query
の第一引数にはSQL文、第二引数以降には任意の型のパラメーターを渡すことができます。- すべての処理が完了したら保持しているリソースを開放するために
defer
でClose
関数を延期する。 - 取得した行データをループして、各列の値を構造体に割り当てる。
Scan
関数は列の値が書き込まれます。ここでは&
演算子を使用してalb
変数のフィールドへのポインタを渡しています。Scan
関数はポインターを通して書き込みを行います。 Scan
でエラーが発生した場合はエラーを返します。- エラーがなければ
Album
構造体のデータをスライスに追加します。 - ループが完了したら、クエリ全体のエラーをチェックします。クエリ自体が失敗した場合はここでエラーをチェックするのが唯一の方法ですので、忘れずにチェックしましょう。
main
関数を更新して、albumByArtist
を実行するように変更しましょう。main
関数の最後に以下のコードを追加します。
func main() {
// ...
albums, err := albumsByArtist("John Coltrane")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Albums found: %v\n", albums)
}
コードを追加したらgo run .
で実行確認してみましょう。以下の内容が出力されたらデータ取得の成功です。
$ go run .
Connected!
Albums found: [{1 Blue Train John Coltrane 56.99} {2 Giant Steps John Coltrane 63.99}]
一行のクエリ
先ほどは複数の行を取得しました。一つだけ取得することが分かっている場合はQueryRow
関数を使用することでさらに簡単にデータ取得することができます。
IDから行を取得する関数を追加してみましょう。以下のコードをmain.go
の最後に追加してください。
// 指定されたIDのアルバムを検索する
func albumByID(id int64) (Album, error) {
// 取得した行のデータを保持するアルバム.
var alb Album
row := db.QueryRow("SELECT * FROM album WHERE id = $1", id)
if err := row.Scan(&alb.ID, &alb.Title, &alb.Artist, &alb.Price); err != nil {
if err == sql.ErrNoRows {
return alb, fmt.Errorf("albumsById %d: no such album", id)
}
return alb, fmt.Errorf("albumsById %d: %v", id, err)
}
return alb, nil
}
このコードはこんなことをしています。
DB.QueryRow
を使用してSELECT
文を実行し、指定されたIDのアルバムを検索する。DB.QueryRow
はsqlRow
を返しますがエラーを返しません。その代わりにRow.Scan
で発生したエラーを後で返すようにします。Row.Scan
を使用して列の値を構造体フィールドにコピーする。Row.Scan
で発生したエラーの確認。
続いてmain
関数にalbumByID
関数を呼ぶ処理を追加します。main
関数の最後に以下の処理を追加してください。
func main() {
// ...
// クエリーをテストするために、ID 2をハードコードする。
alb, err := albumByID(2)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Album found: %v\n", alb)
}
コードを追加したらgo run .
を実行して確認してみましょう。以下の内容が出力されたら一行のデータ取得成功です。
$ go run .
Connected!
Albums found: [{1 Blue Train John Coltrane 56.99} {2 Giant Steps John Coltrane 63.99}]
Album found: {2 Giant Steps John Coltrane 63.99}
データの追加
ここまで一行・複数行のデータ取得の方法を学びました。続いて、INSERT
文を実行し、データベースに新しい行を追加する方法を学んでいきます。
main.go
の最後に以下のコードを追加しましょう。
// 指定されたアルバムをデータベースに追加し、新しいエントリーのアルバムIDを返す。
func addAlbum(alb Album) error {
_, err := db.Exec("INSERT INTO album (title, artist, price) VALUES ($1, $2, $3)", alb.Title, alb.Artist, alb.Price)
if err != nil {
return fmt.Errorf("addAlbum: %v", err)
}
// id, err = result.LastInsertId()
// if err != nil {
// return fmt.Errorf("addAlbum: %v", err)
// }
return nil
}
このコードはこんなことをしています。
DB.Exec
を使用してINSERT
文を実行する。DB.Query
と同じように第二引数以降でパラメータを入力することができる。INSERT
文実行によるエラーが発生しているかををチェックする。Result.LastInsertId
を使用して挿入されたデータベース行のIDを取得する。
(pgxは上記関数に対応していないためコメントアウトしています。)- ID取得によるエラーが発生しているかをチェックする。
続いてmain
関数にaddAlbum
関数を呼ぶ処理を追加していきます。以下のコードをmain
関数の最後に追加しましょう。
func main() {
// ...
addData := Album{
Title: "The Modern Sound of Betty Carter",
Artist: "Betty Carter",
Price: 49.99,
}
err = addAlbum(addData)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Added album: %v\n", addData)
}
コードを追加したらgo run .
を実行して確認してみます。以下の出力が出力されたら成功です。pgxはID取得に対応していないため、album
構造体の初期値である0が出力されます。
$ go run .
Connected!
Albums found: [{1 Blue Train John Coltrane 56.99} {2 Giant Steps John Coltrane 63.99}]
Album found: {2 Giant Steps John Coltrane 63.99}
Added album: {0 The Modern Sound of Betty Carter Betty Carter 49.99}
まとめ
database/sql
はデータベースアクセス用の標準パッケージ。- データベースを操作するためのインターフェースを提供する。
- dbDriverが実際にデータベースへの操作を行う。
github.com/jackc/pgx
はPostgreSQLドライバー。- インポートするパッケージ名は
github.com/jackc/pgx/v5
。 database/sql
と併用する場合はgithub.com/jackc/pgx/v5/stdlib
をインポートする。
- インポートするパッケージ名は
sql.Open
でデータベースハンドルを取得するDB.Close
でデータベースへの接続を閉じるDB.Ping
で接続確認を行うDB.QueryRow
でSELECT
文を実行し一つの行を取得できるDB.Query
でSELECT
文を実行し複数行を取得できるRow.Scan
に構造体等のポインターを渡すことで取得した行の各データを格納することができるDB.Exec
でINSERT
文を実行すると行をデータベースに追加することができるResult.LastInsertId
で追加した行のIDを取得できる。
- ドライバーによってはサポートしていない関数がある。(今回は
LastInsertId
)