feat: Hooks 新增 UseTx,Before/After 改用类型化的 HookError
之前 Hooks.Before/After 返回裸 error,调用方没法区分"钩子本身失败"和"SQL 执行失败"。改成返回 *HookError(包一层 exec.HookError),配合 errors.As 能精确识别。 同时给 Hooks 加一个 UseTx(dataset) (QueryFactory, error) 方法,在 Before 之前调用,允许钩子在写操作真正执行前替换掉默认连接(比如需要自动开事务包住 写操作和写后回调的场景),返回 nil, nil 表示沿用默认连接。目前只有 InsertDataset/UpdateDataset/DeleteDataset 会调用 UseTx,SelectDataset (只读)不需要。
This commit is contained in:
+21
-2
@@ -1,7 +1,26 @@
|
||||
package exec
|
||||
|
||||
import "fmt"
|
||||
|
||||
// HookError 是 Hooks.Before/After 失败时必须返回的类型,外部通过 db.HookError 引用。
|
||||
type HookError struct {
|
||||
Err error
|
||||
}
|
||||
|
||||
func (e *HookError) Error() string {
|
||||
return fmt.Sprintf("hook error: %s", e.Err)
|
||||
}
|
||||
|
||||
func (e *HookError) Unwrap() error {
|
||||
return e.Err
|
||||
}
|
||||
|
||||
// Hooks 钩子实例
|
||||
type Hooks interface {
|
||||
Before(dataset interface{}) error
|
||||
After(dataset interface{}, result interface{}) error
|
||||
Before(dataset interface{}) *HookError
|
||||
After(dataset interface{}, result interface{}) *HookError
|
||||
// UseTx 在 Before 之前调用,dataset 是即将执行的 *InsertDataset/*UpdateDataset/*DeleteDataset
|
||||
// (类型与 Before 收到的一致),此时数据集尚未绑定要执行的连接。返回非 nil 的 QueryFactory 时,
|
||||
// 调用方会用它替换默认连接来执行这次写操作;返回 nil, nil 表示沿用默认连接。
|
||||
UseTx(dataset interface{}) (QueryFactory, error)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,149 @@
|
||||
package exec_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
dbv2 "git.fsdpf.net/go/db"
|
||||
"git.fsdpf.net/go/db/exec"
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/stretchr/testify/suite"
|
||||
)
|
||||
|
||||
// mockHooks 是 exec.Hooks 的最小实现,用来验证 UseTx/Before/After 与 db.HookError 的接线是否正确
|
||||
type mockHooks struct {
|
||||
useTxFactory exec.QueryFactory
|
||||
useTxErr error
|
||||
beforeErr error
|
||||
afterErr error
|
||||
|
||||
useTxCalls int
|
||||
beforeCalls int
|
||||
afterCalls int
|
||||
}
|
||||
|
||||
func (h *mockHooks) UseTx(dataset interface{}) (exec.QueryFactory, error) {
|
||||
h.useTxCalls++
|
||||
return h.useTxFactory, h.useTxErr
|
||||
}
|
||||
|
||||
func (h *mockHooks) Before(dataset interface{}) *exec.HookError {
|
||||
h.beforeCalls++
|
||||
if h.beforeErr != nil {
|
||||
return &exec.HookError{Err: h.beforeErr}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (h *mockHooks) After(dataset interface{}, result interface{}) *exec.HookError {
|
||||
h.afterCalls++
|
||||
if h.afterErr != nil {
|
||||
return &exec.HookError{Err: h.afterErr}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type hooksSuite struct {
|
||||
suite.Suite
|
||||
}
|
||||
|
||||
// TestBeforeError:Before 失败时 SQL 不会被执行,error 能用 errors.As 解出 db.HookError
|
||||
func (hs *hooksSuite) TestBeforeError() {
|
||||
mDB, mock, err := sqlmock.New()
|
||||
hs.NoError(err)
|
||||
|
||||
hooks := &mockHooks{beforeErr: errors.New("before failed")}
|
||||
|
||||
ds := dbv2.New("mock", mDB).Insert("items").
|
||||
Rows(dbv2.Record{"name": "Test1"}).
|
||||
WithHook(hooks)
|
||||
|
||||
_, err = ds.Executor().Exec()
|
||||
hs.Error(err)
|
||||
|
||||
var hookErr *dbv2.HookError
|
||||
hs.True(errors.As(err, &hookErr))
|
||||
hs.Equal("before failed", hookErr.Unwrap().Error())
|
||||
|
||||
hs.Equal(1, hooks.beforeCalls)
|
||||
hs.Equal(0, hooks.afterCalls)
|
||||
hs.NoError(mock.ExpectationsWereMet())
|
||||
}
|
||||
|
||||
// TestAfterError:SQL 已经执行成功,After 失败时 error 依然能用 errors.As 解出 db.HookError,
|
||||
// 且 result 仍然可用(区别于 SQL 本身执行失败)
|
||||
func (hs *hooksSuite) TestAfterError() {
|
||||
mDB, mock, err := sqlmock.New()
|
||||
hs.NoError(err)
|
||||
|
||||
mock.ExpectExec(`INSERT INTO "items"`).WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
|
||||
hooks := &mockHooks{afterErr: errors.New("after failed")}
|
||||
|
||||
ds := dbv2.New("mock", mDB).Insert("items").
|
||||
Rows(dbv2.Record{"name": "Test1"}).
|
||||
WithHook(hooks)
|
||||
|
||||
result, err := ds.Executor().Exec()
|
||||
hs.Error(err)
|
||||
hs.NotNil(result)
|
||||
|
||||
rows, rowsErr := result.RowsAffected()
|
||||
hs.NoError(rowsErr)
|
||||
hs.EqualValues(1, rows)
|
||||
|
||||
var hookErr *dbv2.HookError
|
||||
hs.True(errors.As(err, &hookErr))
|
||||
hs.Equal("after failed", hookErr.Unwrap().Error())
|
||||
|
||||
hs.NoError(mock.ExpectationsWereMet())
|
||||
}
|
||||
|
||||
// TestUseTxSwapsConnection:UseTx 返回的 QueryFactory 会替换默认连接,SQL 应该在返回的
|
||||
// factory 上执行,原始连接完全不会被用到
|
||||
func (hs *hooksSuite) TestUseTxSwapsConnection() {
|
||||
origDB, origMock, err := sqlmock.New()
|
||||
hs.NoError(err)
|
||||
|
||||
txDB, txMock, err := sqlmock.New()
|
||||
hs.NoError(err)
|
||||
txMock.ExpectExec(`INSERT INTO "items"`).WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
|
||||
hooks := &mockHooks{useTxFactory: exec.NewQueryFactory(txDB)}
|
||||
|
||||
ds := dbv2.New("mock", origDB).Insert("items").
|
||||
Rows(dbv2.Record{"name": "Test1"}).
|
||||
WithHook(hooks)
|
||||
|
||||
result, err := ds.Executor().Exec()
|
||||
hs.NoError(err)
|
||||
hs.NotNil(result)
|
||||
|
||||
hs.Equal(1, hooks.useTxCalls)
|
||||
hs.NoError(txMock.ExpectationsWereMet())
|
||||
hs.NoError(origMock.ExpectationsWereMet())
|
||||
}
|
||||
|
||||
// TestUseTxNil:UseTx 返回 nil factory 时沿用默认连接
|
||||
func (hs *hooksSuite) TestUseTxNil() {
|
||||
mDB, mock, err := sqlmock.New()
|
||||
hs.NoError(err)
|
||||
|
||||
mock.ExpectExec(`INSERT INTO "items"`).WillReturnResult(sqlmock.NewResult(1, 1))
|
||||
|
||||
hooks := &mockHooks{}
|
||||
|
||||
ds := dbv2.New("mock", mDB).Insert("items").
|
||||
Rows(dbv2.Record{"name": "Test1"}).
|
||||
WithHook(hooks)
|
||||
|
||||
_, err = ds.Executor().Exec()
|
||||
hs.NoError(err)
|
||||
|
||||
hs.Equal(1, hooks.useTxCalls)
|
||||
hs.NoError(mock.ExpectationsWereMet())
|
||||
}
|
||||
|
||||
func TestHooks(t *testing.T) {
|
||||
suite.Run(t, new(hooksSuite))
|
||||
}
|
||||
Reference in New Issue
Block a user