LoginSignup
22
18

More than 5 years have passed since last update.

golangでMySQLのTransactionを実装してみる

Last updated at Posted at 2018-12-07

このエントリーは、GMOアドマーケティング Advent Calendar 2018【12/8】 の記事です。
GMOアドマーケティングとしては初のAdvent Calendar参戦です。

本日のAdvent Calendarはこちらの記事を参考にTransactionを実装してみます。

環境

go1.9.2 darwin/amd64
mysql Ver 14.14 Distrib 5.7.19, for osx10.12 (x86_64) using EditLine wrapper

実装

database.go
package src

import (
    "database/sql"
    "log"
    _ "github.com/go-sql-driver/mysql"
)

type MyDB struct {
    db *sql.DB
}

var Db MyDB

// Connection
func (m *MyDB) Connection() error {
    var err error
    m.db, err = sql.Open("mysql", "user:password@tcp(localhost:3306)/testdb")
    if err != nil {
        return err
    }
    return nil
}

// Close
func (m *MyDB) Close() {
    if m.db != nil {
        m.db.Close()
    }
}

// Fetch
func (m *MyDB) Fetch(query string, tx *sql.Tx) ([][]interface{}, error) {
    rows, err := tx.Query(query)
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    columns, _ := rows.Columns()
    count := len(columns)
    valuePtrs := make([]interface{}, count)

    ret := make([][]interface{}, 0)
    for rows.Next() {

        values := make([]interface{}, count)
        for i, _ := range columns {
            valuePtrs[i] = &values[i]
        }
        rows.Scan(valuePtrs...)

        for i, _ := range columns {
            var v interface{}
            val := values[i]
            b, ok := val.([]byte)
            if ok {
                v = string(b)
            } else {
                v = val
            }
            values[i] = v
        }
        ret = append(ret, values)
    }

    return ret, nil
}

// Transaction
func (m *MyDB) Transaction(txFunc func(*sql.Tx) error) error {
    tx, err := m.db.Begin()
    if err != nil {
        return err
    }

    defer func() {
        if p := recover(); p != nil {
            log.Println("recover")
            tx.Rollback()
            panic(p)
        } else if err != nil {
            log.Println("rollback")
            tx.Rollback()
        } else {
            log.Println("commit")
            err = tx.Commit()
        }
    }()
    err = txFunc(tx)
    return err
}

Transactionで行われていること

  • Transaction関数内でトランザクションを開始
  • Transaction関数の引数でうけとっった無名関数にトランザクション(tx)を無名関数に渡す
  • 無名関数の処理結果(正常終了、error、panic)でdeferでcommitかrollbackを実行する

更新前のレコード

table test
colmun1 id value 1
colmun2 status value start

結果を確認してみる

Transaction内で強制的にpanicをおこす

main.go
package main

import (
    "./src"
    "database/sql"
    "log"
)

func main() {
    err := src.Db.Connection()
    if err != nil {
        log.Fatal(err)
    }
    defer src.Db.Close()

    err = src.Db.Transaction(func(tx *sql.Tx) error {
        res, _ := src.Db.Fetch("SELECT * FROM test", tx)
        if err != nil {
            log.Fatal(err)
        }

        id := res[0][0].(string)
        _, err = tx.Exec("UPDATE test SET status = '" + "done" + "' WHERE id=" + id)
        if err != nil {
            log.Fatal(err)
        }
        panic("panic")
    })
    if err != nil {
        log.Fatal(err)
    }
}

panic発生後はrollbackされるのでレコードが更新前の状態にrollbackされる

table test
colmun1 id value 1
colmun2 status value start

Transaction内で強制的にerrorをおこす

main.go
package main

import (
    "./src"
    "database/sql"
    "log"
    "pkg/errors"
)

func main() {
    err := src.Db.Connection()
    if err != nil {
        log.Fatal(err)
    }
    defer src.Db.Close()

    err = src.Db.Transaction(func(tx *sql.Tx) error {
        res, _ := src.Db.Fetch("SELECT * FROM test", tx)
        if err != nil {
            log.Fatal(err)
        }

        id := res[0][0].(string)
        _, err = tx.Exec("UPDATE test SET status = '" + "done" + "' WHERE id=" + id)
        if err != nil {
            log.Fatal(err)
        }
        return errors.New("error")
    })
    if err != nil {
        log.Fatal(err)
    }
}

error発生後はrollbackされるのでレコードが更新前の状態にrollbackされる

table test
colmun1 id value 1
colmun2 status value start

Transactionを使って正常終了する

main.go
package main

import (
    "./src"
    "database/sql"
    "log"
)

func main() {
    err := src.Db.Connection()
    if err != nil {
        log.Fatal(err)
    }
    defer src.Db.Close()

    err = src.Db.Transaction(func(tx *sql.Tx) error {
        res, _ := src.Db.Fetch("SELECT * FROM test", tx)
        if err != nil {
            log.Fatal(err)
        }

        id := res[0][0].(string)
        _, err = tx.Exec("UPDATE test SET status = '" + "done" + "' WHERE id=" + id)
        if err != nil {
            log.Fatal(err)
        }
        return nil
    })
    if err != nil {
        log.Fatal(err)
    }
}

正常終了後はcommitされるのでstatusが更新されている

table test
colmun1 id value 1
colmun2 status value done

まとめ

ラッパー関数を作ることによって、ラッパー関数に処理したい内容を渡すだけでTransaction、Rollback、Commitを意識しなくていいので、だいぶ楽にTransactionを使うことができそうです。

次回のAdvent Calendar 2018

明日は、【@nodataka】さんの【JavaでTable Driven Testを実装する】についてのお話です。
お楽しみに。

クリスマスまで続くGMOアドマーケティング Advent Calendar 2018
ぜひ今後も投稿をウォッチしてください!

22
18
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
22
18