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

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

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

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

このチュートリアルでは、データベースを作成し、そのデータベースにアクセスするコードを作成します。例として作成するプロジェクトは、ビンテージジャズレコードに関するデータのリポジトリです。

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

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

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

前提条件

  • MySQLリレーショナルデータベース管理システム(DBMS)のインストール。
  • Goのインストール。 インストール手順については、Goのインストールをご覧ください。
  • コードを編集するツール。 任意のテキストエディターで問題ありません。
  • コマンドターミナル. Go は Linux および Mac の任意のターミナル、および Windows の PowerShell または cmd でうまく動作します。

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

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

  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.NewConfig()
        cfg.User = os.Getenv("DBUSER")
        cfg.Passwd = os.Getenv("DBPASS")
        cfg.Net = "tcp"
        cfg.Addr = "127.0.0.1:3306"
        cfg.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: added filippo.io/edwards25519 v1.1.0
    go: added github.com/go-sql-driver/mysql v1.8.1
    

    前のステップで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スライスを宣言します。これは返された行からのデータを保持します。構造体フィールド名と型は、データベースの列名と型に対応します。

    • DB.Queryを使用して、指定されたアーティスト名のアルバムをクエリするためのSELECTステートメントを実行します。

      Queryの最初のパラメータはSQLステートメントです。パラメータの後には、任意の型の0個以上のパラメータを渡すことができます。これらは、SQLステートメントのパラメータの値を指定する場所を提供します。SQLステートメントとパラメータ値を分離することで(例えばfmt.Sprintfで連結するのではなく)、database/sqlパッケージはSQLテキストとは別に値を送信することができ、SQLインジェクションのリスクを排除します。

    • 関数が終了するときに、rowsが保持するリソースが解放されるように、rowsを閉じるのを延期します。

    • 返された行をループ処理し、Rows.Scanを使用して各行の列値をAlbum構造体フィールドに割り当てます。

      ScanはGoの値へのポインタのリストを受け取り、そこに列値が書き込まれます。ここでは、&演算子を使用して作成されたalb変数のフィールドへのポインタを渡します。Scanはポインタを介して書き込み、構造体フィールドを更新します。

    • ループ内で、列値を構造体フィールドにスキャンする際のエラーをチェックします。

    • ループ内で、新しいalbalbumsスライスに追加します。

    • ループの後、rows.Errを使用して全体のクエリからのエラーをチェックします。クエリ自体が失敗した場合、ここでエラーをチェックすることが結果が不完全であることを知る唯一の方法であることに注意してください。

  3. main関数を更新してalbumsByArtistを呼び出すようにします。

    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を使用してデータベース内の単一の行をクエリします。

最大で単一の行を返すことがわかっている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
    }
    

    このコードでは、以下のことを行います。

    • DB.QueryRowを使用して、指定されたIDを持つアルバムをクエリするためのSELECTステートメントを実行します。

      これはsql.Rowを返します。呼び出し元のコード(あなたのコード!)を簡素化するために、QueryRowはエラーを返しません。代わりに、後でRows.Scanからクエリエラー(sql.ErrNoRowsなど)を返すように手配します。

    • Row.Scanを使用して列値を構造体フィールドにコピーします。

    • Scanからのエラーをチェックします。

      特別なエラーsql.ErrNoRowsは、クエリがどの行も返さなかったことを示します。通常、このエラーは、ここでの「そのようなアルバムはありません」のようなより具体的なテキストに置き換える価値があります。

  2. mainを更新してalbumByIDを呼び出すようにします。

    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ステートメントでQueryQueryRowを使用する方法を見てきました。データを返さない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
    }
    

    このコードでは、以下のことを行います。

    • DB.Execを使用してINSERTステートメントを実行します。

      Queryと同様に、ExecはSQLステートメントとそれに続くSQLステートメントのパラメータ値を受け取ります。

    • INSERTの試行からのエラーをチェックします。

    • Result.LastInsertIdを使用して、挿入されたデータベース行のIDを取得します。

    • IDの取得の試行からのエラーをチェックします。

  2. mainを更新して新しいaddAlbum関数を呼び出すようにします。

    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 GoGoコードの書き方に役立つベストプラクティスが記載されています。

  • Goツアーは、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.NewConfig()
    cfg.User = os.Getenv("DBUSER")
    cfg.Passwd = os.Getenv("DBPASS")
    cfg.Net = "tcp"
    cfg.Addr = "127.0.0.1:3306"
    cfg.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
}