github.com/gogf/gf/v2@v2.7.4/database/gdb/gdb_core_transaction.go (about)

     1  // Copyright GoFrame Author(https://goframe.org). All Rights Reserved.
     2  //
     3  // This Source Code Form is subject to the terms of the MIT License.
     4  // If a copy of the MIT was not distributed with this file,
     5  // You can obtain one at https://github.com/gogf/gf.
     6  
     7  package gdb
     8  
     9  import (
    10  	"context"
    11  	"database/sql"
    12  	"reflect"
    13  
    14  	"github.com/gogf/gf/v2/container/gtype"
    15  	"github.com/gogf/gf/v2/errors/gcode"
    16  	"github.com/gogf/gf/v2/errors/gerror"
    17  	"github.com/gogf/gf/v2/internal/reflection"
    18  	"github.com/gogf/gf/v2/text/gregex"
    19  	"github.com/gogf/gf/v2/util/gconv"
    20  )
    21  
    22  // TXCore is the struct for transaction management.
    23  type TXCore struct {
    24  	db               DB              // db is the current gdb database manager.
    25  	tx               *sql.Tx         // tx is the raw and underlying transaction manager.
    26  	ctx              context.Context // ctx is the context for this transaction only.
    27  	master           *sql.DB         // master is the raw and underlying database manager.
    28  	transactionId    string          // transactionId is a unique id generated by this object for this transaction.
    29  	transactionCount int             // transactionCount marks the times that Begins.
    30  	isClosed         bool            // isClosed marks this transaction has already been committed or rolled back.
    31  }
    32  
    33  const (
    34  	transactionPointerPrefix    = "transaction"
    35  	contextTransactionKeyPrefix = "TransactionObjectForGroup_"
    36  	transactionIdForLoggerCtx   = "TransactionId"
    37  )
    38  
    39  var transactionIdGenerator = gtype.NewUint64()
    40  
    41  // Begin starts and returns the transaction object.
    42  // You should call Commit or Rollback functions of the transaction object
    43  // if you no longer use the transaction. Commit or Rollback functions will also
    44  // close the transaction automatically.
    45  func (c *Core) Begin(ctx context.Context) (tx TX, err error) {
    46  	return c.doBeginCtx(ctx)
    47  }
    48  
    49  func (c *Core) doBeginCtx(ctx context.Context) (TX, error) {
    50  	master, err := c.db.Master()
    51  	if err != nil {
    52  		return nil, err
    53  	}
    54  	var out DoCommitOutput
    55  	out, err = c.db.DoCommit(ctx, DoCommitInput{
    56  		Db:            master,
    57  		Sql:           "BEGIN",
    58  		Type:          SqlTypeBegin,
    59  		IsTransaction: true,
    60  	})
    61  	return out.Tx, err
    62  }
    63  
    64  // Transaction wraps the transaction logic using function `f`.
    65  // It rollbacks the transaction and returns the error from function `f` if
    66  // it returns non-nil error. It commits the transaction and returns nil if
    67  // function `f` returns nil.
    68  //
    69  // Note that, you should not Commit or Rollback the transaction in function `f`
    70  // as it is automatically handled by this function.
    71  func (c *Core) Transaction(ctx context.Context, f func(ctx context.Context, tx TX) error) (err error) {
    72  	if ctx == nil {
    73  		ctx = c.db.GetCtx()
    74  	}
    75  	ctx = c.injectInternalCtxData(ctx)
    76  	// Check transaction object from context.
    77  	var tx TX
    78  	tx = TXFromCtx(ctx, c.db.GetGroup())
    79  	if tx != nil {
    80  		return tx.Transaction(ctx, f)
    81  	}
    82  	tx, err = c.doBeginCtx(ctx)
    83  	if err != nil {
    84  		return err
    85  	}
    86  	// Inject transaction object into context.
    87  	tx = tx.Ctx(WithTX(tx.GetCtx(), tx))
    88  	defer func() {
    89  		if err == nil {
    90  			if exception := recover(); exception != nil {
    91  				if v, ok := exception.(error); ok && gerror.HasStack(v) {
    92  					err = v
    93  				} else {
    94  					err = gerror.NewCodef(gcode.CodeInternalPanic, "%+v", exception)
    95  				}
    96  			}
    97  		}
    98  		if err != nil {
    99  			if e := tx.Rollback(); e != nil {
   100  				err = e
   101  			}
   102  		} else {
   103  			if e := tx.Commit(); e != nil {
   104  				err = e
   105  			}
   106  		}
   107  	}()
   108  	err = f(tx.GetCtx(), tx)
   109  	return
   110  }
   111  
   112  // WithTX injects given transaction object into context and returns a new context.
   113  func WithTX(ctx context.Context, tx TX) context.Context {
   114  	if tx == nil {
   115  		return ctx
   116  	}
   117  	// Check repeat injection from given.
   118  	group := tx.GetDB().GetGroup()
   119  	if ctxTx := TXFromCtx(ctx, group); ctxTx != nil && ctxTx.GetDB().GetGroup() == group {
   120  		return ctx
   121  	}
   122  	dbCtx := tx.GetDB().GetCtx()
   123  	if ctxTx := TXFromCtx(dbCtx, group); ctxTx != nil && ctxTx.GetDB().GetGroup() == group {
   124  		return dbCtx
   125  	}
   126  	// Inject transaction object and id into context.
   127  	ctx = context.WithValue(ctx, transactionKeyForContext(group), tx)
   128  	return ctx
   129  }
   130  
   131  // TXFromCtx retrieves and returns transaction object from context.
   132  // It is usually used in nested transaction feature, and it returns nil if it is not set previously.
   133  func TXFromCtx(ctx context.Context, group string) TX {
   134  	if ctx == nil {
   135  		return nil
   136  	}
   137  	v := ctx.Value(transactionKeyForContext(group))
   138  	if v != nil {
   139  		tx := v.(TX)
   140  		if tx.IsClosed() {
   141  			return nil
   142  		}
   143  		tx = tx.Ctx(ctx)
   144  		return tx
   145  	}
   146  	return nil
   147  }
   148  
   149  // transactionKeyForContext forms and returns a string for storing transaction object of certain database group into context.
   150  func transactionKeyForContext(group string) string {
   151  	return contextTransactionKeyPrefix + group
   152  }
   153  
   154  // transactionKeyForNestedPoint forms and returns the transaction key at current save point.
   155  func (tx *TXCore) transactionKeyForNestedPoint() string {
   156  	return tx.db.GetCore().QuoteWord(transactionPointerPrefix + gconv.String(tx.transactionCount))
   157  }
   158  
   159  // Ctx sets the context for current transaction.
   160  func (tx *TXCore) Ctx(ctx context.Context) TX {
   161  	tx.ctx = ctx
   162  	if tx.ctx != nil {
   163  		tx.ctx = tx.db.GetCore().injectInternalCtxData(tx.ctx)
   164  	}
   165  	return tx
   166  }
   167  
   168  // GetCtx returns the context for current transaction.
   169  func (tx *TXCore) GetCtx() context.Context {
   170  	return tx.ctx
   171  }
   172  
   173  // GetDB returns the DB for current transaction.
   174  func (tx *TXCore) GetDB() DB {
   175  	return tx.db
   176  }
   177  
   178  // GetSqlTX returns the underlying transaction object for current transaction.
   179  func (tx *TXCore) GetSqlTX() *sql.Tx {
   180  	return tx.tx
   181  }
   182  
   183  // Commit commits current transaction.
   184  // Note that it releases previous saved transaction point if it's in a nested transaction procedure,
   185  // or else it commits the hole transaction.
   186  func (tx *TXCore) Commit() error {
   187  	if tx.transactionCount > 0 {
   188  		tx.transactionCount--
   189  		_, err := tx.Exec("RELEASE SAVEPOINT " + tx.transactionKeyForNestedPoint())
   190  		return err
   191  	}
   192  	_, err := tx.db.DoCommit(tx.ctx, DoCommitInput{
   193  		Tx:            tx.tx,
   194  		Sql:           "COMMIT",
   195  		Type:          SqlTypeTXCommit,
   196  		IsTransaction: true,
   197  	})
   198  	if err == nil {
   199  		tx.isClosed = true
   200  	}
   201  	return err
   202  }
   203  
   204  // Rollback aborts current transaction.
   205  // Note that it aborts current transaction if it's in a nested transaction procedure,
   206  // or else it aborts the hole transaction.
   207  func (tx *TXCore) Rollback() error {
   208  	if tx.transactionCount > 0 {
   209  		tx.transactionCount--
   210  		_, err := tx.Exec("ROLLBACK TO SAVEPOINT " + tx.transactionKeyForNestedPoint())
   211  		return err
   212  	}
   213  	_, err := tx.db.DoCommit(tx.ctx, DoCommitInput{
   214  		Tx:            tx.tx,
   215  		Sql:           "ROLLBACK",
   216  		Type:          SqlTypeTXRollback,
   217  		IsTransaction: true,
   218  	})
   219  	if err == nil {
   220  		tx.isClosed = true
   221  	}
   222  	return err
   223  }
   224  
   225  // IsClosed checks and returns this transaction has already been committed or rolled back.
   226  func (tx *TXCore) IsClosed() bool {
   227  	return tx.isClosed
   228  }
   229  
   230  // Begin starts a nested transaction procedure.
   231  func (tx *TXCore) Begin() error {
   232  	_, err := tx.Exec("SAVEPOINT " + tx.transactionKeyForNestedPoint())
   233  	if err != nil {
   234  		return err
   235  	}
   236  	tx.transactionCount++
   237  	return nil
   238  }
   239  
   240  // SavePoint performs `SAVEPOINT xxx` SQL statement that saves transaction at current point.
   241  // The parameter `point` specifies the point name that will be saved to server.
   242  func (tx *TXCore) SavePoint(point string) error {
   243  	_, err := tx.Exec("SAVEPOINT " + tx.db.GetCore().QuoteWord(point))
   244  	return err
   245  }
   246  
   247  // RollbackTo performs `ROLLBACK TO SAVEPOINT xxx` SQL statement that rollbacks to specified saved transaction.
   248  // The parameter `point` specifies the point name that was saved previously.
   249  func (tx *TXCore) RollbackTo(point string) error {
   250  	_, err := tx.Exec("ROLLBACK TO SAVEPOINT " + tx.db.GetCore().QuoteWord(point))
   251  	return err
   252  }
   253  
   254  // Transaction wraps the transaction logic using function `f`.
   255  // It rollbacks the transaction and returns the error from function `f` if
   256  // it returns non-nil error. It commits the transaction and returns nil if
   257  // function `f` returns nil.
   258  //
   259  // Note that, you should not Commit or Rollback the transaction in function `f`
   260  // as it is automatically handled by this function.
   261  func (tx *TXCore) Transaction(ctx context.Context, f func(ctx context.Context, tx TX) error) (err error) {
   262  	if ctx != nil {
   263  		tx.ctx = ctx
   264  	}
   265  	// Check transaction object from context.
   266  	if TXFromCtx(tx.ctx, tx.db.GetGroup()) == nil {
   267  		// Inject transaction object into context.
   268  		tx.ctx = WithTX(tx.ctx, tx)
   269  	}
   270  	err = tx.Begin()
   271  	if err != nil {
   272  		return err
   273  	}
   274  	defer func() {
   275  		if err == nil {
   276  			if exception := recover(); exception != nil {
   277  				if v, ok := exception.(error); ok && gerror.HasStack(v) {
   278  					err = v
   279  				} else {
   280  					err = gerror.NewCodef(gcode.CodeInternalPanic, "%+v", exception)
   281  				}
   282  			}
   283  		}
   284  		if err != nil {
   285  			if e := tx.Rollback(); e != nil {
   286  				err = e
   287  			}
   288  		} else {
   289  			if e := tx.Commit(); e != nil {
   290  				err = e
   291  			}
   292  		}
   293  	}()
   294  	err = f(tx.ctx, tx)
   295  	return
   296  }
   297  
   298  // Query does query operation on transaction.
   299  // See Core.Query.
   300  func (tx *TXCore) Query(sql string, args ...interface{}) (result Result, err error) {
   301  	return tx.db.DoQuery(tx.ctx, &txLink{tx.tx}, sql, args...)
   302  }
   303  
   304  // Exec does none query operation on transaction.
   305  // See Core.Exec.
   306  func (tx *TXCore) Exec(sql string, args ...interface{}) (sql.Result, error) {
   307  	return tx.db.DoExec(tx.ctx, &txLink{tx.tx}, sql, args...)
   308  }
   309  
   310  // Prepare creates a prepared statement for later queries or executions.
   311  // Multiple queries or executions may be run concurrently from the
   312  // returned statement.
   313  // The caller must call the statement's Close method
   314  // when the statement is no longer needed.
   315  func (tx *TXCore) Prepare(sql string) (*Stmt, error) {
   316  	return tx.db.DoPrepare(tx.ctx, &txLink{tx.tx}, sql)
   317  }
   318  
   319  // GetAll queries and returns data records from database.
   320  func (tx *TXCore) GetAll(sql string, args ...interface{}) (Result, error) {
   321  	return tx.Query(sql, args...)
   322  }
   323  
   324  // GetOne queries and returns one record from database.
   325  func (tx *TXCore) GetOne(sql string, args ...interface{}) (Record, error) {
   326  	list, err := tx.GetAll(sql, args...)
   327  	if err != nil {
   328  		return nil, err
   329  	}
   330  	if len(list) > 0 {
   331  		return list[0], nil
   332  	}
   333  	return nil, nil
   334  }
   335  
   336  // GetStruct queries one record from database and converts it to given struct.
   337  // The parameter `pointer` should be a pointer to struct.
   338  func (tx *TXCore) GetStruct(obj interface{}, sql string, args ...interface{}) error {
   339  	one, err := tx.GetOne(sql, args...)
   340  	if err != nil {
   341  		return err
   342  	}
   343  	return one.Struct(obj)
   344  }
   345  
   346  // GetStructs queries records from database and converts them to given struct.
   347  // The parameter `pointer` should be type of struct slice: []struct/[]*struct.
   348  func (tx *TXCore) GetStructs(objPointerSlice interface{}, sql string, args ...interface{}) error {
   349  	all, err := tx.GetAll(sql, args...)
   350  	if err != nil {
   351  		return err
   352  	}
   353  	return all.Structs(objPointerSlice)
   354  }
   355  
   356  // GetScan queries one or more records from database and converts them to given struct or
   357  // struct array.
   358  //
   359  // If parameter `pointer` is type of struct pointer, it calls GetStruct internally for
   360  // the conversion. If parameter `pointer` is type of slice, it calls GetStructs internally
   361  // for conversion.
   362  func (tx *TXCore) GetScan(pointer interface{}, sql string, args ...interface{}) error {
   363  	reflectInfo := reflection.OriginTypeAndKind(pointer)
   364  	if reflectInfo.InputKind != reflect.Ptr {
   365  		return gerror.NewCodef(
   366  			gcode.CodeInvalidParameter,
   367  			"params should be type of pointer, but got: %v",
   368  			reflectInfo.InputKind,
   369  		)
   370  	}
   371  	switch reflectInfo.OriginKind {
   372  	case reflect.Array, reflect.Slice:
   373  		return tx.GetStructs(pointer, sql, args...)
   374  
   375  	case reflect.Struct:
   376  		return tx.GetStruct(pointer, sql, args...)
   377  	}
   378  	return gerror.NewCodef(
   379  		gcode.CodeInvalidParameter,
   380  		`in valid parameter type "%v", of which element type should be type of struct/slice`,
   381  		reflectInfo.InputType,
   382  	)
   383  }
   384  
   385  // GetValue queries and returns the field value from database.
   386  // The sql should query only one field from database, or else it returns only one
   387  // field of the result.
   388  func (tx *TXCore) GetValue(sql string, args ...interface{}) (Value, error) {
   389  	one, err := tx.GetOne(sql, args...)
   390  	if err != nil {
   391  		return nil, err
   392  	}
   393  	for _, v := range one {
   394  		return v, nil
   395  	}
   396  	return nil, nil
   397  }
   398  
   399  // GetCount queries and returns the count from database.
   400  func (tx *TXCore) GetCount(sql string, args ...interface{}) (int64, error) {
   401  	if !gregex.IsMatchString(`(?i)SELECT\s+COUNT\(.+\)\s+FROM`, sql) {
   402  		sql, _ = gregex.ReplaceString(`(?i)(SELECT)\s+(.+)\s+(FROM)`, `$1 COUNT($2) $3`, sql)
   403  	}
   404  	value, err := tx.GetValue(sql, args...)
   405  	if err != nil {
   406  		return 0, err
   407  	}
   408  	return value.Int64(), nil
   409  }
   410  
   411  // Insert does "INSERT INTO ..." statement for the table.
   412  // If there's already one unique record of the data in the table, it returns error.
   413  //
   414  // The parameter `data` can be type of map/gmap/struct/*struct/[]map/[]struct, etc.
   415  // Eg:
   416  // Data(g.Map{"uid": 10000, "name":"john"})
   417  // Data(g.Slice{g.Map{"uid": 10000, "name":"john"}, g.Map{"uid": 20000, "name":"smith"})
   418  //
   419  // The parameter `batch` specifies the batch operation count when given data is slice.
   420  func (tx *TXCore) Insert(table string, data interface{}, batch ...int) (sql.Result, error) {
   421  	if len(batch) > 0 {
   422  		return tx.Model(table).Ctx(tx.ctx).Data(data).Batch(batch[0]).Insert()
   423  	}
   424  	return tx.Model(table).Ctx(tx.ctx).Data(data).Insert()
   425  }
   426  
   427  // InsertIgnore does "INSERT IGNORE INTO ..." statement for the table.
   428  // If there's already one unique record of the data in the table, it ignores the inserting.
   429  //
   430  // The parameter `data` can be type of map/gmap/struct/*struct/[]map/[]struct, etc.
   431  // Eg:
   432  // Data(g.Map{"uid": 10000, "name":"john"})
   433  // Data(g.Slice{g.Map{"uid": 10000, "name":"john"}, g.Map{"uid": 20000, "name":"smith"})
   434  //
   435  // The parameter `batch` specifies the batch operation count when given data is slice.
   436  func (tx *TXCore) InsertIgnore(table string, data interface{}, batch ...int) (sql.Result, error) {
   437  	if len(batch) > 0 {
   438  		return tx.Model(table).Ctx(tx.ctx).Data(data).Batch(batch[0]).InsertIgnore()
   439  	}
   440  	return tx.Model(table).Ctx(tx.ctx).Data(data).InsertIgnore()
   441  }
   442  
   443  // InsertAndGetId performs action Insert and returns the last insert id that automatically generated.
   444  func (tx *TXCore) InsertAndGetId(table string, data interface{}, batch ...int) (int64, error) {
   445  	if len(batch) > 0 {
   446  		return tx.Model(table).Ctx(tx.ctx).Data(data).Batch(batch[0]).InsertAndGetId()
   447  	}
   448  	return tx.Model(table).Ctx(tx.ctx).Data(data).InsertAndGetId()
   449  }
   450  
   451  // Replace does "REPLACE INTO ..." statement for the table.
   452  // If there's already one unique record of the data in the table, it deletes the record
   453  // and inserts a new one.
   454  //
   455  // The parameter `data` can be type of map/gmap/struct/*struct/[]map/[]struct, etc.
   456  // Eg:
   457  // Data(g.Map{"uid": 10000, "name":"john"})
   458  // Data(g.Slice{g.Map{"uid": 10000, "name":"john"}, g.Map{"uid": 20000, "name":"smith"})
   459  //
   460  // The parameter `data` can be type of map/gmap/struct/*struct/[]map/[]struct, etc.
   461  // If given data is type of slice, it then does batch replacing, and the optional parameter
   462  // `batch` specifies the batch operation count.
   463  func (tx *TXCore) Replace(table string, data interface{}, batch ...int) (sql.Result, error) {
   464  	if len(batch) > 0 {
   465  		return tx.Model(table).Ctx(tx.ctx).Data(data).Batch(batch[0]).Replace()
   466  	}
   467  	return tx.Model(table).Ctx(tx.ctx).Data(data).Replace()
   468  }
   469  
   470  // Save does "INSERT INTO ... ON DUPLICATE KEY UPDATE..." statement for the table.
   471  // It updates the record if there's primary or unique index in the saving data,
   472  // or else it inserts a new record into the table.
   473  //
   474  // The parameter `data` can be type of map/gmap/struct/*struct/[]map/[]struct, etc.
   475  // Eg:
   476  // Data(g.Map{"uid": 10000, "name":"john"})
   477  // Data(g.Slice{g.Map{"uid": 10000, "name":"john"}, g.Map{"uid": 20000, "name":"smith"})
   478  //
   479  // If given data is type of slice, it then does batch saving, and the optional parameter
   480  // `batch` specifies the batch operation count.
   481  func (tx *TXCore) Save(table string, data interface{}, batch ...int) (sql.Result, error) {
   482  	if len(batch) > 0 {
   483  		return tx.Model(table).Ctx(tx.ctx).Data(data).Batch(batch[0]).Save()
   484  	}
   485  	return tx.Model(table).Ctx(tx.ctx).Data(data).Save()
   486  }
   487  
   488  // Update does "UPDATE ... " statement for the table.
   489  //
   490  // The parameter `data` can be type of string/map/gmap/struct/*struct, etc.
   491  // Eg: "uid=10000", "uid", 10000, g.Map{"uid": 10000, "name":"john"}
   492  //
   493  // The parameter `condition` can be type of string/map/gmap/slice/struct/*struct, etc.
   494  // It is commonly used with parameter `args`.
   495  // Eg:
   496  // "uid=10000",
   497  // "uid", 10000
   498  // "money>? AND name like ?", 99999, "vip_%"
   499  // "status IN (?)", g.Slice{1,2,3}
   500  // "age IN(?,?)", 18, 50
   501  // User{ Id : 1, UserName : "john"}.
   502  func (tx *TXCore) Update(table string, data interface{}, condition interface{}, args ...interface{}) (sql.Result, error) {
   503  	return tx.Model(table).Ctx(tx.ctx).Data(data).Where(condition, args...).Update()
   504  }
   505  
   506  // Delete does "DELETE FROM ... " statement for the table.
   507  //
   508  // The parameter `condition` can be type of string/map/gmap/slice/struct/*struct, etc.
   509  // It is commonly used with parameter `args`.
   510  // Eg:
   511  // "uid=10000",
   512  // "uid", 10000
   513  // "money>? AND name like ?", 99999, "vip_%"
   514  // "status IN (?)", g.Slice{1,2,3}
   515  // "age IN(?,?)", 18, 50
   516  // User{ Id : 1, UserName : "john"}.
   517  func (tx *TXCore) Delete(table string, condition interface{}, args ...interface{}) (sql.Result, error) {
   518  	return tx.Model(table).Ctx(tx.ctx).Where(condition, args...).Delete()
   519  }
   520  
   521  // QueryContext implements interface function Link.QueryContext.
   522  func (tx *TXCore) QueryContext(ctx context.Context, sql string, args ...interface{}) (*sql.Rows, error) {
   523  	return tx.tx.QueryContext(ctx, sql, args...)
   524  }
   525  
   526  // ExecContext implements interface function Link.ExecContext.
   527  func (tx *TXCore) ExecContext(ctx context.Context, sql string, args ...interface{}) (sql.Result, error) {
   528  	return tx.tx.ExecContext(ctx, sql, args...)
   529  }
   530  
   531  // PrepareContext implements interface function Link.PrepareContext.
   532  func (tx *TXCore) PrepareContext(ctx context.Context, sql string) (*sql.Stmt, error) {
   533  	return tx.tx.PrepareContext(ctx, sql)
   534  }
   535  
   536  // IsOnMaster implements interface function Link.IsOnMaster.
   537  func (tx *TXCore) IsOnMaster() bool {
   538  	return true
   539  }
   540  
   541  // IsTransaction implements interface function Link.IsTransaction.
   542  func (tx *TXCore) IsTransaction() bool {
   543  	return true
   544  }