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:
+12
-2
@@ -238,12 +238,22 @@ func (dd *DeleteDataset) ReturnsColumns() bool {
|
||||
// See Dataset#ToUpdateSQL for arguments
|
||||
func (dd *DeleteDataset) Executor() (executor exec.QueryExecutor) {
|
||||
if dd.hooks != nil {
|
||||
dd.SetError(dd.hooks.Before(dd))
|
||||
if qf, err := dd.hooks.UseTx(dd); err != nil {
|
||||
dd.SetError(err)
|
||||
} else if qf != nil {
|
||||
dd.queryFactory = qf
|
||||
}
|
||||
if herr := dd.hooks.Before(dd); herr != nil {
|
||||
dd.SetError(herr)
|
||||
}
|
||||
}
|
||||
executor = dd.queryFactory.FromSQLBuilder(dd.deleteSQLBuilder())
|
||||
if dd.hooks != nil {
|
||||
executor.Hook(func(result interface{}) error {
|
||||
return dd.hooks.After(dd, result)
|
||||
if herr := dd.hooks.After(dd, result); herr != nil {
|
||||
return herr
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return executor
|
||||
|
||||
+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))
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
package db
|
||||
|
||||
import "git.fsdpf.net/go/db/exec"
|
||||
|
||||
// HookError 是 Hooks.Before/After 失败时的包装错误:Before 失败时 SQL 还未执行,After 失败时
|
||||
// SQL 语句本身已经执行成功、失败的是钩子本身。调用方可以用 errors.As 把它和真正的 SQL 执行失败区分开。
|
||||
type HookError = exec.HookError
|
||||
+12
-2
@@ -306,12 +306,22 @@ func (id *InsertDataset) ReturnsColumns() bool {
|
||||
// db.Insert("test").Rows(Record{"name":"Bob"}).Executor().Exec()
|
||||
func (id *InsertDataset) Executor() (executor exec.QueryExecutor) {
|
||||
if id.hooks != nil {
|
||||
id.SetError(id.hooks.Before(id))
|
||||
if qf, err := id.hooks.UseTx(id); err != nil {
|
||||
id.SetError(err)
|
||||
} else if qf != nil {
|
||||
id.queryFactory = qf
|
||||
}
|
||||
if herr := id.hooks.Before(id); herr != nil {
|
||||
id.SetError(herr)
|
||||
}
|
||||
}
|
||||
executor = id.queryFactory.FromSQLBuilder(id.insertSQLBuilder())
|
||||
if id.hooks != nil {
|
||||
executor.Hook(func(result interface{}) error {
|
||||
return id.hooks.After(id, result)
|
||||
if herr := id.hooks.After(id, result); herr != nil {
|
||||
return herr
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return executor
|
||||
|
||||
+7
-2
@@ -573,12 +573,17 @@ func (sd *SelectDataset) ToSQL() (sql string, params []interface{}, err error) {
|
||||
// See Dataset#ToUpdateSQL for arguments
|
||||
func (sd *SelectDataset) Executor() (executor exec.QueryExecutor) {
|
||||
if sd.hooks != nil {
|
||||
sd.SetError(sd.hooks.Before(sd))
|
||||
if herr := sd.hooks.Before(sd); herr != nil {
|
||||
sd.SetError(herr)
|
||||
}
|
||||
}
|
||||
executor = sd.queryFactory.FromSQLBuilder(sd.selectSQLBuilder())
|
||||
if sd.hooks != nil {
|
||||
executor.Hook(func(result interface{}) error {
|
||||
return sd.hooks.After(sd, result)
|
||||
if herr := sd.hooks.After(sd, result); herr != nil {
|
||||
return herr
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return executor
|
||||
|
||||
+12
-2
@@ -236,12 +236,22 @@ func (ud *UpdateDataset) ReturnsColumns() bool {
|
||||
// db.Update("test").Set(Record{"name":"Bob", update: time.Now()}).Executor()
|
||||
func (ud *UpdateDataset) Executor() (executor exec.QueryExecutor) {
|
||||
if ud.hooks != nil {
|
||||
ud.SetError(ud.hooks.Before(ud))
|
||||
if qf, err := ud.hooks.UseTx(ud); err != nil {
|
||||
ud.SetError(err)
|
||||
} else if qf != nil {
|
||||
ud.queryFactory = qf
|
||||
}
|
||||
if herr := ud.hooks.Before(ud); herr != nil {
|
||||
ud.SetError(herr)
|
||||
}
|
||||
}
|
||||
executor = ud.queryFactory.FromSQLBuilder(ud.updateSQLBuilder())
|
||||
if ud.hooks != nil {
|
||||
executor.Hook(func(result interface{}) error {
|
||||
return ud.hooks.After(ud, result)
|
||||
if herr := ud.hooks.After(ud, result); herr != nil {
|
||||
return herr
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
return executor
|
||||
|
||||
Reference in New Issue
Block a user