チュートリアル: リレーショナルデータベースへのアクセス

このチュートリアルでは、Goと標準ライブラリのdatabase/sqlパッケージを使用してリレーショナルデータベースにアクセスする方法の基本を紹介します。

Goとそのツールに基本的な知識があれば、このチュートリアルを最大限に活用できます。Goを初めて使用する場合は、チュートリアル: Goを使い始めるで簡単な紹介をご覧ください。

使用するdatabase/sqlパッケージには、データベースへの接続、トランザクションの実行、進行中の操作のキャンセルなどを行うための型と関数が含まれています。パッケージの使用方法の詳細については、データベースへのアクセスを参照してください。

このチュートリアルでは、データベースを作成し、データベースにアクセスするためのコードを記述します。サンプルプロジェクトは、ビンテージジャズレコードに関するデータのリポジトリになります。

このチュートリアルでは、以下のセクションを進めていきます

  1. コード用のフォルダを作成します。
  2. データベースを設定します。
  3. データベースドライバをインポートします。
  4. データベースハンドルを取得して接続します。
  5. 複数の行をクエリします。
  6. 単一行をクエリします。
  7. データを追加します。

注: 他のチュートリアルについては、チュートリアルを参照してください。

前提条件

コード用のフォルダを作成する

まず、記述するコード用のフォルダを作成します。

  1. コマンドプロンプトを開き、ホームディレクトリに移動します。

    LinuxまたはMacの場合

    $ cd
    

    Windowsの場合

    C:\> cd %HOMEPATH%
    

    チュートリアルの残りの部分では、プロンプトとして$を表示します。使用するコマンドはWindowsでも動作します。

  2. コマンドプロンプトから、data-accessという名前のコード用のディレクトリを作成します。

    $ mkdir data-access
    $ cd data-access
    
  3. このチュートリアル中に追加する依存関係を管理できるモジュールを作成します。

    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があります。

  1. 新しいコマンドプロンプトを開きます。

  2. コマンドラインで、MySQLの次の例のように、DBMSにログインします。

    $ mysql -u root -p
    Enter password:
    
    mysql>
    
  3. mysqlコマンドプロンプトで、データベースを作成します。

    mysql> create database recordings;
    
  4. テーブルを追加できるように、作成したデータベースに変更します。

    mysql> use recordings;
    Database changed
    
  5. テキストエディタで、data-accessフォルダに、テーブルを追加するためのSQLスクリプトを保持するcreate-tables.sqlというファイルを作成します。

  6. ファイルに次の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というテーブルを削除(ドロップ)します。このコマンドを最初に実行すると、後でテーブルからやり直したい場合にスクリプトを再実行しやすくなります。

    • titleartistpriceの4つの列を持つalbumテーブルを作成します。各行のid値はDBMSによって自動的に作成されます。

    • 値を持つ4つの行を追加します。

  7. mysqlコマンドプロンプトから、作成したスクリプトを実行します。

    sourceコマンドを次の形式で使用します

    mysql> source /path/to/create-tables.sql
    
  8. 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パッケージの関数を通じて行うリクエストをデータベースが理解できるリクエストに変換するデータベースドライバを見つけてインポートします。

  1. ブラウザでSQLDrivers wikiページにアクセスして、使用できるドライバを特定します。

    ページのリストを使用して、使用するドライバを特定します。このチュートリアルでMySQLにアクセスするには、Go-MySQL-Driverを使用します。

  2. ドライバのパッケージ名(ここでは、github.com/go-sql-driver/mysql)に注意してください。

  3. テキストエディタを使用して、Goコードを記述するファイルを作成し、ファイルを以前に作成したdata-accessディレクトリにmain.goとして保存します。

  4. main.goに、次のコードを貼り付けてドライバパッケージをインポートします。

    package main
    
    import "github.com/go-sql-driver/mysql"
    

    このコードでは、

    • コードをmainパッケージに追加して、独立して実行できるようにします。

    • MySQLドライバgithub.com/go-sql-driver/mysqlをインポートします。

ドライバをインポートしたので、データベースにアクセスするためのコードの記述を開始します。

データベースハンドルを取得して接続する

次に、データベースハンドルを使用してデータベースアクセスを提供するGoコードを記述します。

特定のデータベースへのアクセスを表すsql.DB構造体へのポインタを使用します。

コードを書く

  1. 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が正常に接続された場合は、メッセージを出力します。

  2. main.goファイルの上部、パッケージ宣言のすぐ下に、記述したコードをサポートするために必要なパッケージをインポートします。

    ファイルの先頭は次のようになります

    package main
    
    import (
        "database/sql"
        "fmt"
        "log"
        "os"
    
        "github.com/go-sql-driver/mysql"
    )
    
  3. main.goを保存します。

コードを実行する

  1. 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はこの依存関係をダウンロードしました。依存関係の追跡の詳細については、依存関係の追加を参照してください。

  2. コマンドプロンプトから、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
    
  3. main.goを含むディレクトリのコマンドラインから、`go run`とドット引数を入力してコードを実行します。ドット引数は「カレントディレクトリのパッケージを実行する」という意味です。

    $ go run .
    Connected!
    

接続できます!次に、いくつかのデータをクエリします。

複数の行をクエリする

このセクションでは、Goを使用して、複数の行を返すように設計されたSQLクエリを実行します。

複数の行を返す可能性のあるSQLステートメントの場合、database/sqlパッケージの`Query`メソッドを使用して、返された行をループします。(単一行のクエリ方法は、後で単一行のクエリセクションで学習します。)

コードを書く

  1. main.goに、`func main`のすぐ上に、`Album`構造体の次の定義を貼り付けます。これを使用して、クエリから返された行データを保持します。

    type Album struct {
        ID     int64
        Title  string
        Artist string
        Price  float32
    }
    
  2. 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`を使用して、クエリ全体のエラーを確認します。クエリ自体が失敗した場合、ここでエラーを確認することが、結果が不完全であることを知る唯一の方法であることに注意してください。

  3. `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`を使用できます。

コードを書く

  1. `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`は、クエリが行を返さなかったことを示します。通常、そのエラーは、ここで示すように「そのようなアルバムはありません」などのより具体的なテキストに置き換える価値があります。

  2. `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`を使用します。

コードを書く

  1. `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を取得しようとしたときのエラーを確認します。

  2. 新しい`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を使用してリレーショナルデータベースで簡単な操作を実行しました。

推奨される次のトピック

完成したコード

このセクションには、このチュートリアルで作成するアプリケーションのコードが含まれています。

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
}