23
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

GMOアドマーケティングAdvent Calendar 2018

Day 8

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
ぜひ今後も投稿をウォッチしてください!

23
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
23
18

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?