diff --git a/README.md b/README.md index 62b40ba3..1d90fd21 100644 --- a/README.md +++ b/README.md @@ -275,6 +275,17 @@ affected, err := engine.Where(...).Delete(&user) affected, err := engine.ID(2).Delete(&user) // DELETE FROM user Where id = ? + +// soft delete customer + +eg, err := xorm.NewEngine("mysql", dns) +if err != nil { + panic("failed to connect database " + err.Error()) +} +eg.ShowSQL(true) + +eg.SetSoftDeleteHandler(&xorm.DefaultSoftDeleteHandler{}) + ``` * `Count` count records diff --git a/engine.go b/engine.go index f04c702e..4cbb7dd3 100644 --- a/engine.go +++ b/engine.go @@ -55,6 +55,7 @@ type Engine struct { cacherLock sync.RWMutex defaultContext context.Context + softDelete SoftDelete } func (engine *Engine) setCacher(tableName string, cacher core.Cacher) { @@ -95,6 +96,9 @@ func (engine *Engine) CondDeleted(colName string) builder.Cond { if engine.dialect.DBType() == core.MSSQL { return builder.IsNull{colName} } + if engine.softDelete != nil { + return engine.softDelete.getSelectFilter(colName) + } return builder.IsNull{colName}.Or(builder.Eq{colName: zeroTime1}) } @@ -315,8 +319,14 @@ func (engine *Engine) Dialect() core.Dialect { func (engine *Engine) NewSession() *Session { session := &Session{engine: engine} session.Init() + if engine.softDelete != nil { + session.setSoftDelete(engine.softDelete) + } return session } +func (engine *Engine) SetSoftDeleteHandler(handler SoftDelete) { + engine.softDelete = handler +} // Close the engine func (engine *Engine) Close() error { diff --git a/interface.go b/interface.go index 0928f66a..47240928 100644 --- a/interface.go +++ b/interface.go @@ -70,7 +70,7 @@ type Interface interface { // EngineInterface defines the interface which Engine, EngineGroup will implementate. type EngineInterface interface { Interface - + SetSoftDeleteHandler(SoftDelete) Before(func(interface{})) *Session Charset(charset string) *Session ClearCache(...interface{}) error diff --git a/session.go b/session.go index b33955fd..49bb3e6d 100644 --- a/session.go +++ b/session.go @@ -60,6 +60,7 @@ type Session struct { ctx context.Context sessionType sessionType + softDelete SoftDelete } // Clone copy all the session's content and return a new session @@ -67,7 +68,10 @@ func (session *Session) Clone() *Session { var sess = *session return &sess } - +func (session *Session) setSoftDelete(softDelete SoftDelete) *Session { + session.softDelete = softDelete + return session +} // Init reset the session as the init status. func (session *Session) Init() { session.statement.Init() diff --git a/session_delete.go b/session_delete.go index 675d4d8c..1b54a7d5 100644 --- a/session_delete.go +++ b/session_delete.go @@ -8,6 +8,7 @@ import ( "errors" "fmt" "strconv" + "time" "xorm.io/core" ) @@ -192,15 +193,24 @@ func (session *Session) Delete(bean interface{}) (int64, error) { condArgs = append(condArgs, "") paramsLen := len(condArgs) copy(condArgs[1:paramsLen], condArgs[0:paramsLen-1]) - - val, t := session.engine.nowTime(deletedColumn) - condArgs[0] = val - var colName = deletedColumn.Name - session.afterClosures = append(session.afterClosures, func(bean interface{}) { - col := table.GetColumn(colName) - setColumnTime(bean, col, t) - }) + var t, val interface{} + if session.softDelete != nil { + val = session.softDelete.getDeleteValue() + condArgs[0] = val + session.afterClosures = append(session.afterClosures, func(bean interface{}) { + col := table.GetColumn(colName) + session.softDelete.setBeanConumenAttr(bean, col, val) + }) + } else { + + val, t = session.engine.nowTime(deletedColumn) + condArgs[0] = val + session.afterClosures = append(session.afterClosures, func(bean interface{}) { + col := table.GetColumn(colName) + setColumnTime(bean, col, t.(time.Time)) + }) + } } if cacher := session.engine.getCacher(tableNameNoQuote); cacher != nil && session.statement.UseCache { diff --git a/session_delete_test.go b/session_delete_test.go index 5edb0718..c4f62cec 100644 --- a/session_delete_test.go +++ b/session_delete_test.go @@ -5,11 +5,12 @@ package xorm import ( + "fmt" "testing" "time" - "xorm.io/core" "github.com/stretchr/testify/assert" + "xorm.io/core" ) func TestDelete(t *testing.T) { @@ -237,3 +238,92 @@ func TestUnscopeDelete(t *testing.T) { assert.NoError(t, err) assert.False(t, has) } + +func TestSoftDeleted(t *testing.T) { + assert.NoError(t, prepareEngine()) + + type Deleted struct { + Id int64 `xorm:"pk"` + Name string + DeletedAt int64 `xorm:"not null default '0' comment('删除状态') deleted "` + } + testEngine.SetSoftDeleteHandler(&DefaultSoftDeleteHandler{}) + + err := testEngine.DropTables(&Deleted{}) + assert.NoError(t, err) + + err = testEngine.CreateTables(&Deleted{}) + assert.NoError(t, err) + + _, err = testEngine.InsertOne(&Deleted{Id: 1, Name: "11111"}) + assert.NoError(t, err) + + _, err = testEngine.InsertOne(&Deleted{Id: 2, Name: "22222"}) + assert.NoError(t, err) + + _, err = testEngine.InsertOne(&Deleted{Id: 3, Name: "33333"}) + assert.NoError(t, err) + + // Test normal Find() + var records1 []Deleted + err = testEngine.Where("`"+testEngine.GetColumnMapper().Obj2Table("Id")+"` > 0").Find(&records1, &Deleted{}) + fmt.Printf("%+v", records1) + assert.EqualValues(t, 3, len(records1)) + // Test normal Get() + record1 := &Deleted{} + has, err := testEngine.ID(1).Get(record1) + assert.NoError(t, err) + assert.True(t, has) + + // Test Delete() with deleted + affected, err := testEngine.ID(1).Delete(&Deleted{}) + assert.NoError(t, err) + assert.EqualValues(t, 1, affected) + + has, err = testEngine.ID(1).Get(&Deleted{}) + assert.NoError(t, err) + assert.False(t, has) + + var records2 []Deleted + err = testEngine.Where("`" + testEngine.GetColumnMapper().Obj2Table("Id") + "` > 0").Find(&records2) + assert.NoError(t, err) + assert.EqualValues(t, 2, len(records2)) + + // Test no rows affected after Delete() again. + affected, err = testEngine.ID(1).Delete(&Deleted{}) + assert.NoError(t, err) + assert.EqualValues(t, 0, affected) + + // Deleted.DeletedAt must not be updated. + affected, err = testEngine.ID(2).Update(&Deleted{Name: "23", DeletedAt: 1}) + assert.NoError(t, err) + assert.EqualValues(t, 1, affected) + + record2 := &Deleted{} + has, err = testEngine.ID(2).Get(record2) + assert.NoError(t, err) + // fmt.Printf("%+v", reco) + assert.True(t, record2.DeletedAt == 0) + + // Test find all records whatever `deleted`. + var unscopedRecords1 []Deleted + err = testEngine.Unscoped().Where("`"+testEngine.GetColumnMapper().Obj2Table("Id")+"` > 0").Find(&unscopedRecords1, &Deleted{}) + assert.NoError(t, err) + assert.EqualValues(t, 3, len(unscopedRecords1)) + + // Delete() must really delete a record with Unscoped() + affected, err = testEngine.Unscoped().ID(1).Delete(&Deleted{}) + assert.NoError(t, err) + assert.EqualValues(t, 1, affected) + + var unscopedRecords2 []Deleted + err = testEngine.Unscoped().Where("`"+testEngine.GetColumnMapper().Obj2Table("Id")+"` > 0").Find(&unscopedRecords2, &Deleted{}) + assert.NoError(t, err) + assert.EqualValues(t, 2, len(unscopedRecords2)) + + var records3 []Deleted + err = testEngine.Where("`"+testEngine.GetColumnMapper().Obj2Table("Id")+"` > 0").And("`"+testEngine.GetColumnMapper().Obj2Table("Id")+"`> 1"). + Or("`"+testEngine.GetColumnMapper().Obj2Table("Id")+"` = ?", 3).Find(&records3) + assert.NoError(t, err) + assert.EqualValues(t, 2, len(records3)) +}