Обертывание объекта БД в Go и запуск двух методов в одной транзакции
Чтобы немного лучше изучить Go, я пытаюсь реорганизовать ряд функций, которые принимают соединение с БД в качестве первого аргумента в методах структуры, и что-то более "идиоматически" Go.
Прямо сейчас мои методы "хранилища данных" примерно такие:
func CreateA(db orm.DB, a *A) error {
db.Exec("INSERT...")
}
func CreateB(db orm.DB, b *B) error {
db.Exec("INSERT...")
}
Эти функции работают отлично. orm.DB
это интерфейс БД go-pg.
Поскольку две функции принимают соединение БД, я могу передать фактическое соединение или транзакцию (которая реализует один и тот же интерфейс). Я могу быть уверен, что обе функции, выпускающие SQL INSERT, выполняются в одной и той же транзакции, избегая несовместимого состояния в БД в случае сбоя одной из них.
Проблемы начались, когда я решил прочитать больше о том, как немного лучше структурировать код и сделать его "подшучивающим" в случае необходимости.
Поэтому я немного погуглил, прочитал статью " Практическое постоянство в движении: организация доступа к базе данных" и попытался изменить код для использования надлежащих интерфейсов.
Результат примерно такой:
type Store {
CreateA(a *A) error
CreateB(a *A) error
}
type DB struct {
orm.DB
}
func NewDBConnection(p *ConnParams) (*DB, error) {
.... create db connection ...
return &DB{db}, nil
}
func (db *DB) CreateA(a *A) error {
...
}
func (db *DB) CreateB(b *B) error {
...
}
что позволяет мне писать код как:
db := NewDBConnection()
DB.CreateA(a)
DB.CreateB(b)
вместо:
db := NewDBConnection()
CreateA(db, a)
CreateB(db, b)
Фактическая проблема заключается в том, что я потерял способность запускать две функции в одной транзакции. Прежде чем я смог сделать:
pgDB := DB.DB.(*pg.DB) // convert the interface to an actual connection
pgDB.RunInTransaction(func(tx *pg.Tx) error {
CreateA(tx, a)
CreateB(tx, b)
})
или что-то вроде:
tx := db.DB.Begin()
err = CreateA(tx, a)
err = CreateB(tx, b)
if err != nil {
tx.Rollback()
} else {
tx.Commit()
}
что более или менее то же самое.
Так как функции принимали общий интерфейс между соединением и транзакцией, я мог абстрагировать от уровня моей модели логику транзакции, посылая либо полное соединение, либо транзакцию. Это позволило мне в "обработчике HTTP" решить, когда создавать транзакцию, а когда нет необходимости.
Имейте в виду, что соединение - это глобальный объект, представляющий пул соединений, которые обрабатываются автоматически с помощью go, поэтому взломанный мной вариант:
pgDB := DB.DB.(*pg.DB) // convert the interface to an actual connection
err = pgDB.RunInTransaction(func(tx *pg.Tx) error {
DB.DB = tx // replace the connection with a transaction
DB.CreateA(a)
DB.CreateB(a)
})
это явно плохая идея, потому что, хотя она работает, она работает только один раз, потому что мы заменяем глобальное соединение транзакцией. Следующий запрос ломает сервер.
Есть идеи? Я не могу найти информацию об этом, вероятно, потому что я не знаю правильных ключевых слов, являющихся нубом.
1 ответ
Я делал что-то подобное в прошлом (используя стандартные sql
пакет, вам может понадобиться адаптировать его к вашим потребностям):
var ErrNestedTransaction = errors.New("nested transactions are not supported")
// abstraction over sql.TX and sql.DB
// a similar interface seems to be already defined in go-pg. So you may not need this.
type executor interface {
Exec(query string, args ...interface{}) (sql.Result, error)
Query(query string, args ...interface{}) (*sql.Rows, error)
QueryRow(query string, args ...interface{}) *sql.Row
}
type Store struct {
// this is the actual connection(pool) to the db which has the Begin() method
db *sql.DB
executor executor
}
func NewStore(dsn string) (*Store, error) {
db, err := sql.Open("sqlite3", dsn)
if err != nil {
return nil, err
}
// the initial store contains just the connection(pool)
return &Store{db, db}, nil
}
func (s *Store) RunInTransaction(f func(store *Store) error) error {
if _, ok := s.executor.(*sql.Tx); ok {
// nested transactions are not supported!
return ErrNestedTransaction
}
tx, err := s.db.Begin()
if err != nil {
return err
}
transactedStore := &Store{
s.db,
tx,
}
err = f(transactedStore)
if err != nil {
tx.Rollback()
return err
}
return tx.Commit()
}
func (s *Store) CreateA(thing A) error {
// your implementation
_, err := s.executor.Exec("INSERT INTO ...", ...)
return err
}
И тогда вы используете это как
// store is a global object
store.RunInTransaction(func(store *Store) error {
// this instance of Store uses a transaction to execute the methods
err := store.CreateA(a)
if err != nil {
return err
}
return store.CreateB(b)
})
Хитрость заключается в том, чтобы использовать executor вместо *sql.DB в ваших методах CreateX, что позволяет динамически изменять базовую реализацию (tx и db). Однако, поскольку существует очень мало информации о том, как справиться с этой проблемой, я не могу заверить вас, что это "лучшее" решение. Другие предложения приветствуются!