twitter rss
トランザクションのCommitでミスった話
Nov 16, 2017
2 minutes read

仕事上での反省の記録。(※初歩的なミス)

Golangでこのようなコードを書いていた。(ORMはgorpを使ってる)

func Post(dbmap *DbMap) error {
  transaction, err := dbmap.Begin()
  if err != nil {
    return err
  }
  defer func(){
    if err != nil {
      transaction.Rollback()
      return
    }
    transaction.Commit()
  }()
  ...(省略)
}

メソッド内の一連の処理において、errorがあればRollback、なければCommitと、とてもわかりやすい書き方だと思ってよく使っているのだが、 defer func(){}() 以降の処理が煩雑な場合は、注意が必要である。

意図しないCommitを引き起こしてしまうというバグを発生させてしまった。

例えば、以下のようなコード。ちなみに仕事で事故ったのはこのコード。

func Post(dbmap *DbMap) error {
  transaction, err := dbmap.Begin()
  if err != nil {
    return err
  }
  defer func(){
    if err != nil {
      transaction.Rollback()
      return
    }
    transaction.Commit()
  }()

  result1, err := getResult(time.Now())
  if err != nil {
    return err // ① Rollbackされる
  }

  ...(省略)

  if true {
    result2, err := getResult(time.Now())
    if err != nil {
      return err // ② Commitされる
    }
    ...(省略)
  }
  ...(省略)
}

②のerrorの場合にトランザクションをRollbackしたいと思っていても、errはif true{}内で新たに定義されており、{}外のerrとは異なるため、これまでのトランザクションはCommitされてしまう。


次の場合も注意が必要である。

func Post(dbmap *DbMap) error {
  transaction, err := dbmap.Begin()
  if err != nil {
    return err
  }
  defer func(){
    if err != nil {
      transaction.Rollback()
      return
    }
    transaction.Commit()
  }()
  
  ...(省略)

  user, err := getUser()
  if err != nil && err != sql.ErrNoRows{
    return err
  }
  ...(省略)
}

もしsql.ErrNoRows(userを抽出できない)という例外処理を無視させたいと思っても、sql.ErrNoRowsが実は起こっていた場合にerrには代入されてしまうため、 処理が最後まで走ってもトランザクションはRollbackされてしまう。


当たり前だがこれも注意

func Post(dbmap *DbMap) error {
  transaction, err := dbmap.Begin()
  if err != nil {
    return err
  }
  defer func(){
    if err != nil {
      transaction.Rollback()
      return
    }
    transaction.Commit()
  }()

  ...(省略)

  user, err := getUser()
  if err != nil {
    return err
  }
  if user.Age < 20 {
    return errors.New("Don't drink") // ① Commitされる
  }
  ...(省略)
}

①のようにエラーを返しても、errに代入されてないの、これまでのトランザクションはCommitされてしまう。


これからはこの方法で落ち着きたいと思う。

func Post(dbmap *DbMap) error {
  transaction, err := dbmap.Begin()
  if err != nil {
    return err
  }
  defer transaction.Rollback()

  ...(省略)

  transaction.Commit()

  return nil
}

Back to posts