チュートリアル: リレーショナルデータベースへのアクセス
このチュートリアルでは、Goと標準ライブラリのdatabase/sql
パッケージを使用してリレーショナルデータベースにアクセスする方法の基本を紹介します。
Goとそのツールに基本的な知識があれば、このチュートリアルを最大限に活用できます。Goを初めて使用する場合は、チュートリアル: Goを使い始めるで簡単な紹介をご覧ください。
使用するdatabase/sql
パッケージには、データベースへの接続、トランザクションの実行、進行中の操作のキャンセルなどを行うための型と関数が含まれています。パッケージの使用方法の詳細については、データベースへのアクセスを参照してください。
このチュートリアルでは、データベースを作成し、データベースにアクセスするためのコードを記述します。サンプルプロジェクトは、ビンテージジャズレコードに関するデータのリポジトリになります。
このチュートリアルでは、以下のセクションを進めていきます
- コード用のフォルダを作成します。
- データベースを設定します。
- データベースドライバをインポートします。
- データベースハンドルを取得して接続します。
- 複数の行をクエリします。
- 単一行をクエリします。
- データを追加します。
注: 他のチュートリアルについては、チュートリアルを参照してください。
前提条件
- MySQLリレーショナルデータベース管理システム(DBMS)のインストール。
- Goのインストール。 インストール手順については、Goのインストールを参照してください。
- コードを編集するためのツール。 任意のテキストエディタで問題ありません。
- コマンドターミナル。 Goは、LinuxおよびMacの任意のターミナル、およびWindowsのPowerShellまたはcmdで正常に動作します。
コード用のフォルダを作成する
まず、記述するコード用のフォルダを作成します。
-
コマンドプロンプトを開き、ホームディレクトリに移動します。
LinuxまたはMacの場合
$ cd
Windowsの場合
C:\> cd %HOMEPATH%
チュートリアルの残りの部分では、プロンプトとして$を表示します。使用するコマンドはWindowsでも動作します。
-
コマンドプロンプトから、data-accessという名前のコード用のディレクトリを作成します。
$ mkdir data-access $ cd data-access
-
このチュートリアル中に追加する依存関係を管理できるモジュールを作成します。
go mod init
コマンドを実行し、新しいコードのモジュールパスを指定します。$ go mod init example/data-access go: creating new go.mod: module example/data-access
このコマンドは、追加する依存関係が追跡のためにリストされるgo.modファイルを作成します。詳細については、依存関係の管理を参照してください。
注: 実際の開発では、ニーズにより具体的なモジュールパスを指定します。詳細については、依存関係の管理を参照してください。
次に、データベースを作成します。
データベースを設定する
この手順では、使用するデータベースを作成します。DBMS自体のCLIを使用して、データベースとテーブルを作成し、データを追加します。
ビニール盤のビンテージジャズ録音に関するデータを含むデータベースを作成します。
ここでのコードはMySQL CLIを使用していますが、ほとんどのDBMSには同様の機能を持つ独自のCLIがあります。
-
新しいコマンドプロンプトを開きます。
-
コマンドラインで、MySQLの次の例のように、DBMSにログインします。
$ mysql -u root -p Enter password: mysql>
-
mysql
コマンドプロンプトで、データベースを作成します。mysql> create database recordings;
-
テーブルを追加できるように、作成したデータベースに変更します。
mysql> use recordings; Database changed
-
テキストエディタで、data-accessフォルダに、テーブルを追加するためのSQLスクリプトを保持するcreate-tables.sqlというファイルを作成します。
-
ファイルに次の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);
このSQLコードでは、
-
album
というテーブルを削除(ドロップ)します。このコマンドを最初に実行すると、後でテーブルからやり直したい場合にスクリプトを再実行しやすくなります。 -
title
、artist
、price
の4つの列を持つalbum
テーブルを作成します。各行のid
値はDBMSによって自動的に作成されます。 -
値を持つ4つの行を追加します。
-
-
mysql
コマンドプロンプトから、作成したスクリプトを実行します。source
コマンドを次の形式で使用しますmysql> source /path/to/create-tables.sql
-
DBMSコマンドプロンプトで、
SELECT
ステートメントを使用して、データを含むテーブルが正常に作成されたことを確認します。mysql> 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 rows in set (0.00 sec)
次に、クエリできるように接続するためのGoコードを記述します。
データベースドライバを見つけてインポートする
データを含むデータベースができたので、Goコードを開始します。
database/sql
パッケージの関数を通じて行うリクエストをデータベースが理解できるリクエストに変換するデータベースドライバを見つけてインポートします。
-
ブラウザでSQLDrivers wikiページにアクセスして、使用できるドライバを特定します。
ページのリストを使用して、使用するドライバを特定します。このチュートリアルでMySQLにアクセスするには、Go-MySQL-Driverを使用します。
-
ドライバのパッケージ名(ここでは、
github.com/go-sql-driver/mysql
)に注意してください。 -
テキストエディタを使用して、Goコードを記述するファイルを作成し、ファイルを以前に作成したdata-accessディレクトリにmain.goとして保存します。
-
main.goに、次のコードを貼り付けてドライバパッケージをインポートします。
package main import "github.com/go-sql-driver/mysql"
このコードでは、
-
コードを
main
パッケージに追加して、独立して実行できるようにします。 -
MySQLドライバ
github.com/go-sql-driver/mysql
をインポートします。
-
ドライバをインポートしたので、データベースにアクセスするためのコードの記述を開始します。
データベースハンドルを取得して接続する
次に、データベースハンドルを使用してデータベースアクセスを提供するGoコードを記述します。
特定のデータベースへのアクセスを表すsql.DB
構造体へのポインタを使用します。
コードを書く
-
main.goに、追加した
import
コードの下に、次のGoコードを貼り付けてデータベースハンドルを作成します。var db *sql.DB func main() { // Capture connection properties. cfg := mysql.Config{ User: os.Getenv("DBUSER"), Passwd: os.Getenv("DBPASS"), Net: "tcp", Addr: "127.0.0.1:3306", DBName: "recordings", } // Get a database handle. var err error db, err = sql.Open("mysql", cfg.FormatDSN()) if err != nil { log.Fatal(err) } pingErr := db.Ping() if pingErr != nil { log.Fatal(pingErr) } fmt.Println("Connected!") }
このコードでは、
-
*sql.DB
型のdb
変数を宣言します。これがデータベースハンドルです。db
をグローバル変数にすると、この例が簡略化されます。本番環境では、変数を必要な関数に渡すか、構造体にラップするなどして、グローバル変数を回避します。 -
MySQLドライバの
Config
と、型のFormatDSN
を使用して、接続プロパティを収集し、接続文字列のDSNにフォーマットします。Config
構造体は、接続文字列よりも読みやすいコードを作成します。 -
sql.Open
を呼び出してdb
変数を初期化し、FormatDSN
の戻り値を渡します。 -
sql.Open
からのエラーを確認します。たとえば、データベース接続の詳細が正しくフォーマットされていない場合、失敗する可能性があります。コードを簡略化するために、
log.Fatal
を呼び出して実行を終了し、エラーをコンソールに出力しています。本番コードでは、より適切な方法でエラーを処理する必要があります。 -
DB.Ping
を呼び出して、データベースへの接続が機能することを確認します。実行時に、ドライバによっては、sql.Open
がすぐに接続しない場合があります。ここでPing
を使用して、database/sql
パッケージが必要なときに接続できることを確認しています。 -
接続に失敗した場合に備えて、
Ping
からのエラーを確認します。 -
Ping
が正常に接続された場合は、メッセージを出力します。
-
-
main.goファイルの上部、パッケージ宣言のすぐ下に、記述したコードをサポートするために必要なパッケージをインポートします。
ファイルの先頭は次のようになります
package main import ( "database/sql" "fmt" "log" "os" "github.com/go-sql-driver/mysql" )
-
main.goを保存します。
コードを実行する
-
MySQLドライバモジュールを依存関係として追跡を開始します。
go get
を使用して、github.com/go-sql-driver/mysqlモジュールを独自のモジュールの依存関係として追加します。ドット引数を使用して、「カレントディレクトリのコードの依存関係を取得する」という意味にします。$ go get . go get: added github.com/go-sql-driver/mysql v1.6.0
前の手順で`import`宣言に追加したため、Goはこの依存関係をダウンロードしました。依存関係の追跡の詳細については、依存関係の追加を参照してください。
-
コマンドプロンプトから、Goプログラムで使用するために
DBUSER
およびDBPASS
環境変数を設定します。LinuxまたはMacの場合
$ export DBUSER=username $ export DBPASS=password
Windowsの場合
C:\Users\you\data-access> set DBUSER=username C:\Users\you\data-access> set DBPASS=password
-
main.goを含むディレクトリのコマンドラインから、`go run`とドット引数を入力してコードを実行します。ドット引数は「カレントディレクトリのパッケージを実行する」という意味です。
$ go run . Connected!
接続できます!次に、いくつかのデータをクエリします。
複数の行をクエリする
このセクションでは、Goを使用して、複数の行を返すように設計されたSQLクエリを実行します。
複数の行を返す可能性のあるSQLステートメントの場合、database/sql
パッケージの`Query`メソッドを使用して、返された行をループします。(単一行のクエリ方法は、後で単一行のクエリセクションで学習します。)
コードを書く
-
main.goに、`func main`のすぐ上に、`Album`構造体の次の定義を貼り付けます。これを使用して、クエリから返された行データを保持します。
type Album struct { ID int64 Title string Artist string Price float32 }
-
func main
の下に、データベースをクエリする次のalbumsByArtist
関数を貼り付けます。// albumsByArtist queries for albums that have the specified artist name. func albumsByArtist(name string) ([]Album, error) { // An albums slice to hold data from returned rows. var albums []Album 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() // Loop through rows, using Scan to assign column data to struct fields. 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`型の`albums`スライスを宣言します。これは、返された行のデータを保持します。構造体のフィールド名と型は、データベースの列名と型に対応します。
-
指定されたアーティスト名のアルバムを照会するための`SELECT`ステートメントを実行するには、
DB.Query
を使用します。`Query`の最初のパラメータはSQLステートメントです。パラメータの後には、任意の型のゼロ個以上のパラメータを渡すことができます。これらは、SQLステートメントのパラメータの値を指定するための場所を提供します。 SQLステートメントをパラメータ値から分離する(たとえば、`fmt.Sprintf`で連結するのではなく)ことで、`database/sql`パッケージはSQLテキストとは別に値を送信できるようになり、SQLインジェクションのリスクがなくなります。
-
`rows`を閉じるのをdeferすることで、保持しているリソースは関数が終了したときに解放されます。
-
返された行をループ処理し、
Rows.Scan
を使用して、各行の列値を`Album`構造体のフィールドに代入します。`Scan`は、Go値へのポインタのリストを受け取ります。そこに列値が書き込まれます。ここでは、`&`演算子を使用して作成された`alb`変数のフィールドへのポインタを渡します。 `Scan`はポインタを介して書き込み、構造体のフィールドを更新します。
-
ループ内で、列値を構造体のフィールドにスキャンした際のエラーを確認します。
-
ループ内で、新しい`alb`を`albums`スライスに追加します。
-
ループの後、`rows.Err`を使用して、クエリ全体のエラーを確認します。クエリ自体が失敗した場合、ここでエラーを確認することが、結果が不完全であることを知る唯一の方法であることに注意してください。
-
-
`albumsByArtist`を呼び出すように`main`関数を更新します。
`func main`の最後に、次のコードを追加します。
albums, err := albumsByArtist("John Coltrane") if err != nil { log.Fatal(err) } fmt.Printf("Albums found: %v\n", albums)
新しいコードでは、以下のようになります。
-
追加した`albumsByArtist`関数を呼び出し、その戻り値を新しい`albums`変数に代入します。
-
結果を出力します。
-
コードを実行する
main.goを含むディレクトリのコマンドラインから、コードを実行します。
$ go run .
Connected!
Albums found: [{1 Blue Train John Coltrane 56.99} {2 Giant Steps John Coltrane 63.99}]
次に、単一行を照会します。
単一行の照会
このセクションでは、Goを使用してデータベース内の単一行を照会します。
最大で1行が返されることがわかっているSQLステートメントの場合、`Query`ループを使用するよりも単純な`QueryRow`を使用できます。
コードを書く
-
`albumsByArtist`の下に、次の`albumByID`関数を貼り付けます。
// albumByID queries for the album with the specified ID. func albumByID(id int64) (Album, error) { // An album to hold data from the returned row. var alb Album row := db.QueryRow("SELECT * FROM album WHERE id = ?", 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 }
このコードでは、
-
指定されたIDを持つアルバムを照会するための`SELECT`ステートメントを実行するには、
DB.QueryRow
を使用します。これは`sql.Row`を返します。呼び出し元のコード(あなたのコード!)を簡素化するために、`QueryRow`はエラーを返しません。代わりに、後で`Rows.Scan`からクエリエラー(`sql.ErrNoRows`など)を返すようにします。
-
列の値を構造体のフィールドにコピーするには、
Row.Scan
を使用します。 -
`Scan`からのエラーを確認します。
特別なエラー`sql.ErrNoRows`は、クエリが行を返さなかったことを示します。通常、そのエラーは、ここで示すように「そのようなアルバムはありません」などのより具体的なテキストに置き換える価値があります。
-
-
`albumByID`を呼び出すように`main`を更新します。
`func main`の最後に、次のコードを追加します。
// Hard-code ID 2 here to test the query. alb, err := albumByID(2) if err != nil { log.Fatal(err) } fmt.Printf("Album found: %v\n", alb)
新しいコードでは、以下のようになります。
-
追加した`albumByID`関数を呼び出します。
-
返されたアルバムIDを出力します。
-
コードを実行する
main.goを含むディレクトリのコマンドラインから、コードを実行します。
$ 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}
次に、データベースにアルバムを追加します。
データの追加
このセクションでは、Goを使用してSQL`INSERT`ステートメントを実行し、データベースに新しい行を追加します。
データを返すSQLステートメントで`Query`と`QueryRow`を使用する方法を見てきました。データを*返さない* SQLステートメントを実行するには、`Exec`を使用します。
コードを書く
-
`albumByID`の下に、次の`addAlbum`関数を貼り付けて、データベースに新しいアルバムを挿入し、main.goを保存します。
// addAlbum adds the specified album to the database, // returning the album ID of the new entry 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 }
このコードでは、
-
`INSERT`ステートメントを実行するには、
DB.Exec
を使用します。`Query`と同様に、`Exec`はSQLステートメントとその後にSQLステートメントのパラメータ値を受け取ります。
-
`INSERT`の試行によるエラーを確認します。
-
Result.LastInsertId
を使用して、挿入されたデータベース行のIDを取得します。 -
IDを取得しようとしたときのエラーを確認します。
-
-
新しい`addAlbum`関数を呼び出すように`main`を更新します。
`func main`の最後に、次のコードを追加します。
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)
新しいコードでは、以下のようになります。
- 新しいアルバムを使用して`addAlbum`を呼び出し、追加するアルバムのIDを`albID`変数に代入します。
コードを実行する
main.goを含むディレクトリのコマンドラインから、コードを実行します。
$ 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}
ID of added album: 5
まとめ
おめでとうございます! Goを使用してリレーショナルデータベースで簡単な操作を実行しました。
推奨される次のトピック
-
ここでは簡単に触れただけの موضوعات について、より詳しい情報を含むデータアクセスガイドをご覧ください。
-
Goを初めて使用する場合は、Effective GoおよびGoコードの書き方に記載されている役立つベストプラクティスがあります。
-
Go Tourは、Goの基礎を学ぶための優れたステップバイステップの入門書です。
完成したコード
このセクションには、このチュートリアルで作成するアプリケーションのコードが含まれています。
package main
import (
"database/sql"
"fmt"
"log"
"os"
"github.com/go-sql-driver/mysql"
)
var db *sql.DB
type Album struct {
ID int64
Title string
Artist string
Price float32
}
func main() {
// Capture connection properties.
cfg := mysql.Config{
User: os.Getenv("DBUSER"),
Passwd: os.Getenv("DBPASS"),
Net: "tcp",
Addr: "127.0.0.1:3306",
DBName: "recordings",
}
// Get a database handle.
var err error
db, err = sql.Open("mysql", cfg.FormatDSN())
if err != nil {
log.Fatal(err)
}
pingErr := db.Ping()
if pingErr != nil {
log.Fatal(pingErr)
}
fmt.Println("Connected!")
albums, err := albumsByArtist("John Coltrane")
if err != nil {
log.Fatal(err)
}
fmt.Printf("Albums found: %v\n", albums)
// Hard-code ID 2 here to test the query.
alb, err := albumByID(2)
if err != nil {
log.Fatal(err)
}
fmt.Printf("Album found: %v\n", alb)
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)
}
// albumsByArtist queries for albums that have the specified artist name.
func albumsByArtist(name string) ([]Album, error) {
// An albums slice to hold data from returned rows.
var albums []Album
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()
// Loop through rows, using Scan to assign column data to struct fields.
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
}
// albumByID queries for the album with the specified ID.
func albumByID(id int64) (Album, error) {
// An album to hold data from the returned row.
var alb Album
row := db.QueryRow("SELECT * FROM album WHERE id = ?", 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
}
// addAlbum adds the specified album to the database,
// returning the album ID of the new entry
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
}