概要

本記事では、Golangにおけるdatabase/sqlライブラリを用いたMySQLデータベースに対する処理例を示す。
処理例では、ユーザーデータを仮定し、エラー処理についても例を示す。

以下のようになテーブルを想定する。


CREATE TABLE user_db.users (
  id INT NOT NULL AUTO_INCREMENT,
  first_name VARCHAR(45) NULL,
  last_name VARCHAR(45) NULL,
  email VARCHAR(45) NOT NULL,
    data_created DATETIME NULL DEFAULT CURRENT_TIMESTAMP,
    status TINYINT(1) NOT NULL,
  PRIMARY KEY (id),
    UNIQUE INDEX email_UNIQUE (email ASC));

pkgのインストール

mysqlのドライバーpkgをインストールする。

go get github.com/go-sql-driver/mysql

処理の流れ

実行する処理は、以下のプログラムの通りである。
1. ユーザ1データの挿入
2. ユーザーデータ取得ミス例(エラー処理を示すため)
3. ユーザ2データ挿入
4. Status1のユーザデータを探索、取得
5. ユーザーデータ取得(4で取得したがGETの例を示すため)
6. ユーザーデータの更新
7. ユーザーデータの削除

package main

import (
    "fmt"
    "log"

    "github.com/k-washi/golang-cookbook/database/user_db/method"
)

func main() {

    //ユーザデータの挿入
    user := &method.User{ID: 1, FirstName: "Trou", LastName: "Tanaka", Email: "123s456@test.com", Status: 1}
    if err := user.Save(); err != nil {
        log.Println(err)
        //2020/01/23 03:15:08 email already exist
    }
    log.Println(user)
    //2020/01/23 04:06:32 &{1 Trou Tanaka 123s456@test.com <nil>}

    //ユーザーデータの取得(IDミス)
    user = &method.User{ID: 3}
    if err := user.Get(); err != nil {
        log.Println(err)
        //2020/01/23 03:26:24 user not found
    }
    log.Println(user)

    //ユーザデータ挿入
    user = &method.User{ID: 2, FirstName: "k", LastName: "washi", Email: "654321@test.com", Status: 1}
    if err := user.Save(); err != nil {
        log.Println(err)
    }

    //ユーザデータのStatus:1をFind
    res, err := user.FindByStatus(1)
    if err != nil {
        log.Println(err)
    }
    for i, u := range res {
        fmt.Println("user :", i, u)
        //user : 0 {37 Trou Tanaka 123s456@test.com 2020-01-23 05:07:35 +0000 UTC 1}

        //ユーザデータの取得(IDを使用するため、ループ内で処理)
        user = &method.User{ID: u.ID}
        if err := user.Get(); err != nil {
            log.Println(err)
        }
        log.Println(user)
        //2020/01/23 03:03:52 &{1 Trou Tanaka 123s456@test.com 2020-01-23 02:29:56 +0000 UTC}

        //ユーザデータ更新
        user.FirstName = "Taro2"
        user.LastName = "Tanaka2"
        user.Email = "123s456@test.com"
        user.Status = 1
        if err := user.Update(); err != nil {
            log.Println(err)
        }
        log.Println(user)
        //2020/01/23 03:54:51 &{1 Taro2 Tanaka2 123s456@test.com 2020-01-23 02:29:56 +0000 UTC}

        //ユーザデータの削除
        user = &method.User{ID: u.ID}
        if err := user.Delete(); err != nil {
            log.Println(err)
        }
        log.Println("Success Delete")

    }

}

データベースの初期設定

sql.OpenでDBと接続を確立し、接続を確立している。
_ “github.com/go-sql-driver/mysql”をmysqlを扱うため、インポートしている。
設定は,user:password@tcp(host:port)/dbnameの形式の引数をとり、parseTime=trueは、時刻を扱うために設定している。

Client, err := sql.Open()とせず、予め、Client,errを定義し、client, err = sql.Open()としている。前者では、もしClientを関数外で定義している場合、nilになるため。

また、コメントアウトしているが、実際は、環境変数を用いて、DBの設定を行うほうが良い。

package user_db

import (
    "database/sql"
    "fmt"
    "log"

    _ "github.com/go-sql-driver/mysql"
)

var (
    //Client user_id database
    Client *sql.DB
    err    error
)


/*
//実際は、環境変数を読み込んで、DBの設定を行う。
//export mysql_user_name="root"
const (
    mysql_user_naem: "mysql_user_name"
)

var userName := os.Getenv(mysql_user_naem)
*/

func init() {
    //user:password@tcp(host:port)/dbname
    dataSourceName := fmt.Sprintf("%s:%s@tcp(%s)/%s?charset=utf8&parseTime=true",
        "root", "", "127.0.0.1:3306", "user_db",
    )

    log.Println(fmt.Sprintf("about to connect to %s", dataSourceName))
    //open db by mysql driver and data source name.
    //https://github.com/go-sql-driver/mysql/issues/150 (Issue)
    Client, err = sql.Open("mysql", dataSourceName)
    if err != nil {
        panic(err)
    }

    if err = Client.Ping(); err != nil {
        panic(err)
    }
    log.Println("database successfully configured")
}

データベース処理

GET, POSTなどのメソッドを定義している。
ここで定義しているUser 構造体が本記事で扱っているユーザデータである。const内には、ここで扱うSQLを定義している。

エラーはparseErrorとして定義している。mysqlに関するエラーは個別に取り出せ、switcで場合できることを例で示している。1062は、Uniqueに対するエラーである。

GET, SELECTの際、*.Scanを実行する場合は、アドレスを渡す必要があることに注意する。

package method

import (
    "fmt"
    "log"
    "strings"
    "time"

    "github.com/go-sql-driver/mysql"

    "github.com/k-washi/golang-cookbook/database/user_db/user_db"
)

const (
    queryInsertUser         = "INSERT INTO users(first_name, last_name, email, status) VALUES(?, ?, ?, ?);"
    queryGetUser            = "SELECT id, first_name, last_name, email, data_created, status FROM users WHERE id=?;"
    queryUpdateUser         = "UPDATE users SET first_name=?, last_name=?, email=?, status=? WHERE id=?;"
    queryDeleteUser         = "DELETE FROM users WHERE id=?;"
    querySelectUserByStatus = "SELECT id, first_name, last_name, email, data_created, status FROM users WHERE status=?;"
    errorNoRow              = "no rows in result set"
)

//User info
type User struct {
    ID        int64
    FirstName string
    LastName  string
    Email     string
    Date      *time.Time
    Status    int
}

var (
//UserDB = make(map[int64]*User)
)

func (user *User) Get() error {
    stmt, err := user_db.Client.Prepare(queryGetUser)
    if err != nil {
        return err
    }
    defer stmt.Close()

    result := stmt.QueryRow(user.ID)
    if getErr := result.Scan(&user.ID, &user.FirstName, &user.LastName, &user.Email, &user.Date, &user.Status); getErr != nil {

        return parseError(getErr)
    }
    return nil
}

func (user *User) Save() error {
    stmt, err := user_db.Client.Prepare(queryInsertUser)
    if err != nil {
        return err
    }
    defer stmt.Close()

    insertResult, saveErr := stmt.Exec(user.FirstName, user.LastName, user.Email, user.Status)
    if saveErr != nil {
        return parseError(saveErr)
    }

    userID, err := insertResult.LastInsertId()
    if err != nil {
        return err
    }
    user.ID = userID

    log.Println("Save user data")

    return nil
}

func (user *User) Update() error {
    stmt, err := user_db.Client.Prepare(queryUpdateUser)
    if err != nil {
        return err
    }
    defer stmt.Close()

    if _, updErr := stmt.Exec(user.FirstName, user.LastName, user.Email, user.Status, user.ID); updErr != nil {
        return parseError(updErr)
    }
    return nil
}

func (user *User) Delete() error {
    stmt, err := user_db.Client.Prepare(queryDeleteUser)
    if err != nil {
        return err
    }
    defer stmt.Close()

    if _, delErr := stmt.Exec(user.ID); delErr != nil {
        return parseError(delErr)
    }

    return nil
}

func (user *User) FindByStatus(status int) ([]User, error) {
    stmt, err := user_db.Client.Prepare(querySelectUserByStatus)
    if err != nil {
        return nil, err
    }
    defer stmt.Close()

    rows, err := stmt.Query(status)
    if err != nil {
        return nil, parseError(err)
    }
    defer rows.Close()

    res := make([]User, 0)
    for rows.Next() {
        var user User
        if err := rows.Scan(&user.ID, &user.FirstName, &user.LastName, &user.Email, &user.Date, &user.Status); err != nil {
            return nil, parseError(err)
        }
        res = append(res, user)
    }

    return res, nil
}

func parseError(err error) error {
    sqlErr, ok := err.(*mysql.MySQLError)
    if !ok {
        if strings.Contains(err.Error(), errorNoRow) {
            return fmt.Errorf("user not found")
        }
        return err
    }
    //以下,sql依存のエラー
    switch sqlErr.Number {
    case 1062:
        //email_UNIQUE error
        return fmt.Errorf("email already exist")
    }
    return err
}

まとめ

GolangによるMySQLデータベースに対する処理の例を示しました。
コードは、k-washi/golang-cookbook/database/user_db/を参考にしてみてください。