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

このエントリーは、GMOアドマーケティング [Advent Calendar 2018](https://qiita.com/advent-calendar/2018/gmo-am)【12/8】 の記事です。
GMOアドマーケティングとしては初のAdvent Calendar参戦です。

本日のAdvent Calendarは[こちらの記事を参考に](https://stackoverflow.com/questions/16184238/database-sql-tx-detecting-commit-or-rollback)Transactionを実装してみます。

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

##実装
```go: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>
<tr>
  <td><b>table</b></td>
  <td colspan="3">test</td>
</tr>
<tr>
  <td><b>colmun1</b></td>
  <td>id</td>
  <td>value</td>
  <td>1</td>
</tr>
<tr>
  <td><b>colmun2</b></td>
  <td>status</td>
  <td>value</td>
  <td>start</td>
</tr>
</table>

##結果を確認してみる
###Transaction内で強制的にpanicをおこす
```go: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>
<tr>
  <td><b>table</b></td>
  <td colspan="3">test</td>
</tr>
<tr>
  <td><b>colmun1</b></td>
  <td>id</td>
  <td><b>value</b></td>
  <td>1</td>
</tr>
<tr>
  <td><b>colmun2</b></td>
  <td>status</td>
  <td><b>value</b></td>
  <td>start</td>
</tr>
</table>

###Transaction内で強制的にerrorをおこす
```go: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>
<tr>
  <td><b>table</b></td>
  <td colspan="3">test</td>
</tr>
<tr>
  <td><b>colmun1</b></td>
  <td>id</td>
  <td><b>value</b></td>
  <td>1</td>
</tr>
<tr>
  <td><b>colmun2</b></td>
  <td>status</td>
  <td><b>value</b></td>
  <td>start</td>
</tr>
</table>

###Transactionを使って正常終了する
```go: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>
<tr>
  <td><b>table</b></td>
  <td colspan="3">test</td>
</tr>
<tr>
  <td><b>colmun1</b></td>
  <td>id</td>
  <td><b>value</b></td>
  <td>1</td>
</tr>
<tr>
  <td><b>colmun2</b></td>
  <td>status</td>
  <td><b>value</b></td>
  <td>done</td>
</tr>
</table>

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

##次回の[Advent Calendar 2018](https://qiita.com/advent-calendar/2018/gmo-am)
明日は、【@nodataka】さんの【JavaでTable Driven Testを実装する】についてのお話です。
お楽しみに。

クリスマスまで続くGMOアドマーケティング [Advent Calendar 2018](https://qiita.com/advent-calendar/2018/gmo-am)
ぜひ今後も投稿をウォッチしてください！