Transaction Manager over GORM
Flaiers opened this issue · 0 comments
Flaiers commented
Description
Gorm currently has a transaction management feature, but the code for many operations looks overloaded.
Example
We have a service (UserService) to work with the user. When debiting a user's account, we must:
- Get the record with the user's balance
- Сheck whether funds can be debited from the user's account
- Debit from the user's account
- Create a transaction that the debit was successful
If we get an error on one of these, we must roll back. Code samples:
Implementation transaction service
cat internal/service/transaction.go
package service
...
type TransactionService struct {
repository *repository.TransactionRepository
}
...
func (s *TransactionService) Create(
transactionCreate *dto.TransactionCreate, tx ...*gorm.DB,
) (*entity.Transaction, error) {
return s.repository.Create(
&entity.Transaction{
Type: transactionCreate.Type,
Amount: transactionCreate.Amount,
UserID: transactionCreate.UserID,
OrderID: transactionCreate.OrderID,
ServiceID: transactionCreate.ServiceID,
}, tx...,
)
}
Implementation user service
cat internal/service/user.go
package service
...
type UserService struct {
repository *repository.UserRepository
transactionService *TransactionService
}
...
func (s *UserService) WithdrawUserBalance(
userID uuid.UUID, userWithdrawal *dto.UserWithdrawal, tx ...*gorm.DB,
) (*entity.User, error) {
var user *entity.User
err := s.repository.Transaction(func(tx *gorm.DB) (err error) {
user, err = s.FindOne(userID, tx)
if err != nil {
return
}
user, err = s.repository.WithdrawUserBalance(
user, userWithdrawal.Amount, tx,
)
if err != nil {
return
}
_, err = s.transactionService.Create(&dto.TransactionCreate{
Type: "withdrawal",
Amount: userWithdrawal.Amount,
UserID: userID,
OrderID: userWithdrawal.OrderID,
ServiceID: userWithdrawal.ServiceID,
}, tx)
if err != nil {
return
}
return
}, tx...)
return user, err
}
Implementation transaction repository
cat internal/repository/transaction.go
package repository
...
type TransactionRepository struct {
db *gorm.DB
}
...
func (r *TransactionRepository) txDefault(tx ...*gorm.DB) *gorm.DB {
if len(tx) < 1 {
return r.db
}
return tx[0]
}
func (r *TransactionRepository) Create(
transaction *entity.Transaction, tx ...*gorm.DB,
) (*entity.Transaction, error) {
err := r.txDefault(tx...).Create(transaction).Error
if err != nil {
return nil, err
}
return transaction, nil
}
Implementation user repository
cat internal/repository/user.go
package repository
...
type UserRepository struct {
db *gorm.DB
}
...
func (r *UserRepository) Transaction(
fn func(*gorm.DB) error, tx ...*gorm.DB,
) error {
return r.txDefault(tx...).Transaction(fn)
}
func (r *UserRepository) txDefault(tx ...*gorm.DB) *gorm.DB {
if len(tx) < 1 {
return r.db
}
return tx[0]
}
...
func (r *UserRepository) WithdrawUserBalance(
user *entity.User, amount uint, tx ...*gorm.DB,
) (*entity.User, error) {
if user.Balance < amount || user.Balance == 0 {
return nil, errors.New("user balance is not enough")
}
user.Balance -= amount
err := r.txDefault(tx...).Save(user).Error
if err != nil {
return nil, err
}
return user, nil
}
Сonclusion
Using the standard gorm transaction manager forces you to add transaction management logic on both the service and the repository level, and the code nesting when opening a transaction is not good enough.