Skip to content
This repository has been archived by the owner on Sep 7, 2021. It is now read-only.
This repository is currently being migrated. It's locked while the migration is in progress.

soft delete support type tinyint #1420

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
10 changes: 10 additions & 0 deletions engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ type Engine struct {
cacherLock sync.RWMutex

defaultContext context.Context
softDelete SoftDelete
}

func (engine *Engine) setCacher(tableName string, cacher core.Cacher) {
Expand Down Expand Up @@ -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})
}

Expand Down Expand Up @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion interface.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 5 additions & 1 deletion session.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,18 @@ type Session struct {

ctx context.Context
sessionType sessionType
softDelete SoftDelete
}

// Clone copy all the session's content and return a new session
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()
Expand Down
26 changes: 18 additions & 8 deletions session_delete.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"errors"
"fmt"
"strconv"
"time"

"xorm.io/core"
)
Expand Down Expand Up @@ -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 {
Expand Down
94 changes: 93 additions & 1 deletion session_delete_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -237,3 +238,94 @@ func TestUnscopeDelete(t *testing.T) {
assert.NoError(t, err)
assert.False(t, has)
}

func TestSoftDeleted(t *testing.T) {
type YySoftDeleted struct {
Id int64 `xorm:"pk"`
Name string
DeletedAt int64 `xorm:"not null default '0' comment('删除状态') deleted "`
}
testSoftEngine, err := createEngine(dbType, connString)
assert.NoError(t, err)

testSoftEngine.SetSoftDeleteHandler(&DefaultSoftDeleteHandler{})
defer testSoftEngine.SetSoftDeleteHandler(nil)
err = testSoftEngine.DropTables(&YySoftDeleted{})
assert.NoError(t, err)

err = testSoftEngine.CreateTables(&YySoftDeleted{})
assert.NoError(t, err)

_, err = testSoftEngine.InsertOne(&YySoftDeleted{Id: 1, Name: "4444"})
assert.NoError(t, err)

_, err = testSoftEngine.InsertOne(&YySoftDeleted{Id: 2, Name: "5555"})
assert.NoError(t, err)

_, err = testSoftEngine.InsertOne(&YySoftDeleted{Id: 3, Name: "6666"})
assert.NoError(t, err)

// Test normal Find()
var records1 []YySoftDeleted
err = testSoftEngine.Where("`"+testSoftEngine.GetColumnMapper().Obj2Table("Id")+"` > 0").Find(&records1, &YySoftDeleted{})
fmt.Printf("%+v", records1)
assert.EqualValues(t, 3, len(records1))
// Test normal Get()
record1 := &YySoftDeleted{}
has, err := testSoftEngine.ID(1).Get(record1)
assert.NoError(t, err)
assert.True(t, has)

// Test Delete() with deleted
affected, err := testSoftEngine.ID(1).Delete(&YySoftDeleted{})
assert.NoError(t, err)
assert.EqualValues(t, 1, affected)

has, err = testSoftEngine.ID(1).Get(&YySoftDeleted{})
assert.NoError(t, err)
assert.False(t, has)

var records2 []YySoftDeleted
err = testSoftEngine.Where("`" + testSoftEngine.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 = testSoftEngine.ID(1).Delete(&YySoftDeleted{})
assert.NoError(t, err)
assert.EqualValues(t, 0, affected)

// Deleted.DeletedAt must not be updated.
affected, err = testSoftEngine.ID(2).Update(&YySoftDeleted{Name: "23", DeletedAt: 1})
assert.NoError(t, err)
assert.EqualValues(t, 1, affected)

record2 := &YySoftDeleted{}
has, err = testSoftEngine.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 []YySoftDeleted
err = testSoftEngine.Unscoped().Where("`"+testSoftEngine.GetColumnMapper().Obj2Table("Id")+"` > 0").Find(&unscopedRecords1, &YySoftDeleted{})
assert.NoError(t, err)
assert.EqualValues(t, 3, len(unscopedRecords1))

// Delete() must really delete a record with Unscoped()
affected, err = testSoftEngine.Unscoped().ID(1).Delete(&YySoftDeleted{})
assert.NoError(t, err)
assert.EqualValues(t, 1, affected)

var unscopedRecords2 []YySoftDeleted
err = testSoftEngine.Unscoped().Where("`"+testSoftEngine.GetColumnMapper().Obj2Table("Id")+"` > 0").Find(&unscopedRecords2, &YySoftDeleted{})
assert.NoError(t, err)
assert.EqualValues(t, 2, len(unscopedRecords2))

var records3 []YySoftDeleted
err = testSoftEngine.Where("`"+testSoftEngine.GetColumnMapper().Obj2Table("Id")+"` > 0").And("`"+testSoftEngine.GetColumnMapper().Obj2Table("Id")+"`> 1").
Or("`"+testSoftEngine.GetColumnMapper().Obj2Table("Id")+"` = ?", 3).Find(&records3)
assert.NoError(t, err)
assert.EqualValues(t, 2, len(records3))

}
39 changes: 39 additions & 0 deletions soft_delete.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package xorm

import (
"reflect"

"xorm.io/builder"
"xorm.io/core"
)

type SoftDelete interface {
getDeleteValue() interface{}
getSelectFilter(deleteField string) builder.Cond
setBeanConumenAttr(bean interface{}, col *core.Column, val interface{})
}

type DefaultSoftDeleteHandler struct {
}

func (h *DefaultSoftDeleteHandler) setBeanConumenAttr(bean interface{}, col *core.Column, val interface{}) {
t := val.(int64)
v, err := col.ValueOf(bean)
if err != nil {
return
}
if v.CanSet() {
switch v.Type().Kind() {
case reflect.Int, reflect.Int64, reflect.Int32:
v.SetInt(t)
case reflect.Uint, reflect.Uint64, reflect.Uint32:
v.SetUint(uint64(t))
}
}
}
func (h *DefaultSoftDeleteHandler) getDeleteValue() interface{} {
return int64(1)
}
func (h *DefaultSoftDeleteHandler) getSelectFilter(deleteField string) builder.Cond {
return builder.Eq{deleteField: int64(0)}
}
34 changes: 18 additions & 16 deletions xorm_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,52 +37,51 @@ var (
ignoreSelectUpdate = flag.Bool("ignore_select_update", false, "ignore select update if implementation difference, only for tidb")
)

func createEngine(dbType, connStr string) error {
if testEngine == nil {
var err error
func createEngine(dbType, connStr string) (testEngine EngineInterface,err error) {

if testEngine == nil {
if !*cluster {
switch strings.ToLower(dbType) {
case core.MSSQL:
db, err := sql.Open(dbType, strings.Replace(connStr, "xorm_test", "master", -1))
if err != nil {
return err
return nil,err
}
if _, err = db.Exec("If(db_id(N'xorm_test') IS NULL) BEGIN CREATE DATABASE xorm_test; END;"); err != nil {
return fmt.Errorf("db.Exec: %v", err)
return nil,fmt.Errorf("db.Exec: %v", err)
}
db.Close()
*ignoreSelectUpdate = true
case core.POSTGRES:
db, err := sql.Open(dbType, connStr)
if err != nil {
return err
return nil,err
}
rows, err := db.Query(fmt.Sprintf("SELECT 1 FROM pg_database WHERE datname = 'xorm_test'"))
if err != nil {
return fmt.Errorf("db.Query: %v", err)
return nil,fmt.Errorf("db.Query: %v", err)
}
defer rows.Close()

if !rows.Next() {
if _, err = db.Exec("CREATE DATABASE xorm_test"); err != nil {
return fmt.Errorf("CREATE DATABASE: %v", err)
return nil,fmt.Errorf("CREATE DATABASE: %v", err)
}
}
if *schema != "" {
if _, err = db.Exec("CREATE SCHEMA IF NOT EXISTS " + *schema); err != nil {
return fmt.Errorf("CREATE SCHEMA: %v", err)
return nil,fmt.Errorf("CREATE SCHEMA: %v", err)
}
}
db.Close()
*ignoreSelectUpdate = true
case core.MYSQL:
db, err := sql.Open(dbType, strings.Replace(connStr, "xorm_test", "mysql", -1))
if err != nil {
return err
return nil,err
}
if _, err = db.Exec("CREATE DATABASE IF NOT EXISTS xorm_test"); err != nil {
return fmt.Errorf("db.Exec: %v", err)
return nil,fmt.Errorf("db.Exec: %v", err)
}
db.Close()
default:
Expand All @@ -97,7 +96,7 @@ func createEngine(dbType, connStr string) error {
}
}
if err != nil {
return err
return nil,err
}

if *schema != "" {
Expand All @@ -124,20 +123,23 @@ func createEngine(dbType, connStr string) error {

tables, err := testEngine.DBMetas()
if err != nil {
return err
return nil,err
}
var tableNames = make([]interface{}, 0, len(tables))
for _, table := range tables {
tableNames = append(tableNames, table.Name)
}
if err = testEngine.DropTables(tableNames...); err != nil {
return err
return nil,err
}
return nil
return testEngine,nil
}

func prepareEngine() error {
return createEngine(dbType, connString)
var err error
testEngine ,err = createEngine(dbType, connString)

return err
}

func TestMain(m *testing.M) {
Expand Down