Your Question

I am using GIN framework and Gorm to write restful APIs with Clean Architecture pattern in different layers (controller, usecase, repository, domain) but facing a problem that single connection use in all functions. Once I use transaction begin and commit/ rollback in one function. Then client request another api will cause sql transaction has already been committed or rolled back (I know that a transaction can be only used once) so how to handle in this use case? Thanks

Case: 1. call api/dummy/update 2. call api/dummy/list

Result: app/repositories/dummy_repository.go:46 sql: transaction has already been committed or rolled back

main.go

package main

import (
    "myapp/app/di"
    "myapp/app/repositories/db"

    "github.com/gin-gonic/gin"
    "github.com/spf13/viper"
)



func main() {

    // Set the router as the default one provided by Gin
    route_engine = gin.Default()

    // initial database connection - mysql
    dbConn := db.ConnectMysqlGormDatabase()

    // depdency injection
    di.InitializeAPIs(dbConn, route_engine)

    // Start serving the application
    route_engine.Run(viper.GetString("server.address"))
}

dbconnection.go // for connect mysql

package db

import (
    "gorm.io/driver/mysql"
    "github.com/spf13/viper"
    "gorm.io/gorm"
)

// var db *gorm.DB

func ConnectMysqlGormDatabase() (db *gorm.DB) {

    dbHost := viper.GetString(`database.host`)
    dbPort := viper.GetInt(`database.port`)
    dbUser := viper.GetString(`database.user`)
    dbPass := viper.GetString(`database.pass`)
    dbName := viper.GetString(`database.name`)

    dsn := fmt.Sprintf("%s:%s@tcp(%s:%v)/%s?charset=utf8mb4&collation=utf8mb4_unicode_520_ci&parseTime=True&loc=Local", dbUser, dbPass, dbHost, dbPort, dbName)

    gormDB, err := gorm.Open(mysql.Open(dsn), &gorm.Config{
        DisableForeignKeyConstraintWhenMigrating: true,
    })

    if err != nil {
        fmt.Println("connection to mysql failed:", err)
        panic("exit")

    }

    MysqlMigration(gormDB)

    return gormDB
}

di.go // declare all repositories, usecases, controllers and routes

/*
dependency injection -
*/
package di

import (
    "myapp/app/deliveries/httpDelivery"
    "myapp/app/repositories"
    "myapp/app/usecases"
    "myapp/middlewares"

    "github.com/gin-gonic/gin"
    "gorm.io/gorm"
)

func InitializeAPIs(dbConn *gorm.DB, route_engine *gin.Engine) {

    // repositories
    dummyRepository := repositories.NewDummy(dbConn)

    // usecase
    dummyUseCase := usecases.NewDummyUseCase(
        dbConn, 
        dummyRepository,
    )

    // controller
    dummyHandler := httpDelivery.NewDummyHandler(
        route_engine,
        dummyUseCase,
    )

    api := route_engine.Group("/api")
    api.POST("/dummy/update", dummyHandler.PostUpdate)
    api.GET("/dummy/list", dummyHandler.GetList)

}

dummy_handler.go // controller

package httpDelivery

import (
    "net/http"
    "myapp/app/usecases"

    "github.com/gin-gonic/gin"
    "github.com/go-playground/validator"
)

type DummyHandler struct {
    dummy_usecase usecases.InterfaceDummyUsecase
}

func NewDummyHandler(route_engine *gin.Engine,
    dummy_usecase usecases.InterfaceDummyUsecase,

) *DummyHandler {

    handler := &DummyHandler{
        dummy_usecase: dummy_usecase,
    }
    return handler
}

func (dh *DummyHandler) PostUpdate(ctx *gin.Context) {

    resp_obj := NewCustomResponseJson(ctx.Request.Context())

    var request_body struct {
        UserId uint32 `json:"user_id" validate:"required"`
        Email  string `json:"email" validate:"required"`
    }

    err := ctx.ShouldBindJSON(&request_body)
    if err != nil {
        ctx.JSON(http.StatusOK, resp_obj.Fail(err))
        return
    }
    validate := validator.New()
    err = validate.Struct(&request_body)
    if err != nil {
        ctx.JSON(http.StatusOK, resp_obj.Fail(err))
        return
    }

    dummy, err := dh.dummy_usecase.FindById(request_body.UserId)
    if err != nil {
        ctx.JSON(http.StatusOK, resp_obj.Fail(err))
        return
    }

    dummy.Email = request_body.Email
    err = dh.dummy_usecase.Update(&dummy)
    if err != nil {
        ctx.JSON(http.StatusOK, resp_obj.Fail(err))
        return
    }
    result := resp_obj.Success()
    ctx.JSON(http.StatusOK, result)
}

func (dh *DummyHandler) GetList(ctx *gin.Context) {

    resp_obj := NewCustomResponseJson(ctx.Request.Context())

    list, err := dh.dummy_usecase.FindAll()
    if err != nil {
        ctx.JSON(http.StatusOK, resp_obj.Fail(err))
        return
    }

    result := resp_obj.Success()
    result["list"] = list
    ctx.JSON(http.StatusOK, result)
}

dummy_usecase.go

package usecases

import (
    "fmt"
    "reflect"
    "myapp/app/domains"
    "myapp/app/repositories"

    "gorm.io/gorm"
)

type InterfaceDummyUsecase interface {
    WithTrx(*gorm.DB) InterfaceDummyUsecase

    FindAll() ([]domains.Dummy, error)
    Update(model *domains.Dummy) error
}

type dummyUseCase struct {
    db        *gorm.DB
    dummyRepo repositories.InterfaceDummyRepository
}

func NewDummyUseCase(db *gorm.DB, repo repositories.InterfaceDummyRepository) InterfaceDummyUsecase {
    return &dummyUseCase{
        db:        db,
        dummyRepo: repo,
    }
}

func (m *dummyUseCase) WithTrx(trxHandle *gorm.DB) InterfaceDummyUsecase {
    m.dummyRepo = m.dummyRepo.WithTrx(trxHandle)
    return m
}

func (m *dummyUseCase) FindAll() ([]domains.Dummy, error) {
    newssubscription_list, err := m.dummyRepo.FindAll()
    return newssubscription_list, err
}

func (m *dummyUseCase) Update(model *domains.Dummy) error {
    tx := m.db.Begin()
    if err := tx.Error; err != nil {
        fmt.Println("tx.Error;")
        return err
    }
    // ....
    // other usecase code
    // ....
    err := m.dummyRepo.WithTrx(tx).Update(model)
    if err != nil {
        tx.Rollback()
        return err
    }
    // ....
    // other usecase code
    // ....
    err = tx.Commit().Error
    if err != nil {
        fmt.Println("Rollback: sync error 2")
        tx.Rollback()
        return err
    }
    fmt.Println("Commit")
    return err
}

// dummy_repository.go

package repositories

import (
    "log"
    "myapp/app/domains"

    "gorm.io/gorm"
)

type InterfaceDummyRepository interface {
    WithTrx(*gorm.DB) InterfaceDummyRepository

    FindAll() ([]domains.Dummy, error)
    FindById(id uint32) (domains.Dummy, error)
    Update(model *domains.Dummy) error
}

type dummyRepository struct {
    Conn *gorm.DB
}

// GORM - delcare repository
func NewDummy(conn *gorm.DB) InterfaceDummyRepository {
    return &dummyRepository{
        Conn: conn,
    }
}

func (m *dummyRepository) WithTrx(trxHandle *gorm.DB) InterfaceDummyRepository {
    if trxHandle == nil {
        log.Print("Transaction Database not found")
        return m
    }
    m.Conn = trxHandle
    return m
}

func (m *dummyRepository) FindAll() ([]domains.Dummy, error) {
    var dummy_list []domains.Dummy
    err := m.Conn.Find(&dummy_list).Error

    return dummy_list, err
}

func (m *dummyRepository) FindById(id uint32) (domains.Dummy, error) {
    var dummy domains.Dummy
    err := m.Conn.First(&dummy, id).Error

    return dummy, err
}

func (m *dummyRepository) Update(model *domains.Dummy) error {
    err := m.Conn.Omit("id").Updates(&model).Error
    return err
}

func (m *dummyRepository) UpdateModel(model *domains.Dummy) error {
    err := m.Conn.Debug().Model(&model).Omit("id").Updates(&model).Error
    return err
}

The document you expected this should be explained

Expected answer

Comment From: a631807682

Can you provide a simpler repro code? Or put the appeal file into a git repository and make sure it works. Actually, I don't have enough time to watch the business code if you don't do that.

Comment From: kentestforjob

@a631807682 Thanks your help. I just create a repository in github https://github.com/kentestforjob/gorm-transactionerror You can follow the instructions in readMe to run the program. Thanks a lot.

Comment From: a631807682

This has nothing to do with gorm. In fact, you should not reuse dummyRepo. First, WithTrx will modify the referenced db instance. Usually, the http framework will create goroutine for each connection, which will also cause data race.