一個非侵入的Go事務管理庫——如何使用

倚天碼農發表於2020-06-21

在文章"清晰架構(Clean Architecture)的Go微服務: 事物管理"中,我談到了如何在清晰架構中實現非侵入的事務管理。

它允許你把事務程式碼與業務邏輯程式碼分開,並且讓你在編寫業務邏輯時不必考慮事務。但它也有一些缺點。首先,它是整個清晰框架(Clean Architecture)的一部分,所以你不能拋開框架單獨使用它。其次,儘管它對業務邏輯沒有侵入,但它對框架有侵入。你需要修改框架的各個層,使其工作,這使他看起來比較複雜。 第三,正如我在文章中提到的,它存在一個依賴洩漏的漏洞。我雖然在文章中指出瞭解決方案,但它是一個比較大的改動,因此我當時就把它先放下了。現在,我終於有時間重新拾起它,並做了重要改進,結果令人非常滿意。

專案需求

以下是新的專案需求:

  1. 把事務管理程式碼寫成一個單獨的第三方庫,這樣人們就可以在任何框架中使用它。
  2. 使其對框架無侵入,這意味著除了在用例層之外,在清晰架構的任何層中都沒有事務程式碼。幾乎所有的事務程式碼都在第三方的事務庫中。
  3. 修復以前設計中的依賴洩漏。

最終,我完成了所有的目標,結果出乎意料的好。我將寫兩篇文章來描述它,這篇文章討論如何使用這個第三方庫,下一篇文章討論事務管理庫的工作原理。當你要在應用程式裡使用事務管理庫時,你的程式分成了兩部分。一部分是第三方庫的程式,另一部分是應用程式的程式碼(它需要呼叫事務管理庫中的函式)。本文只講應用程式程式碼。

如何在專案中使用事務管理庫

要想讓業務函式支援事務,需要做兩件事。首先,建立資料庫連結;其次,使用建立的資料庫連結執行SQL語句。我假設你在專案中使用了清晰架構。在這種情況下,“建立資料庫連結”會在應用程式容器((詳情參見"清晰架構(Clean Architecture)的Go微服務: 程式容器(Application Container)" )中完成,“執行SQL語句”會在業務邏輯(資料持久層)中完成。如果沒有使用清晰架構,你可能會使用某種非常類似的分層架構,結構還是一樣。如果你沒有使用任何框架或分層架構,那麼這兩種程式碼可能會在一個地方。

你可能想知道它與沒有事務支援的程式碼有什麼不同?幾乎沒有。不管有沒有事務支援,應用程式都要編寫相同的程式碼,事務管理庫會在後端處理所有事情。

本文中的所有程式碼都在"jfeng45/servicetmpl1"中,這是一個能自我進化的微服務框架,它提供瞭如何使用事務庫的例子。

建立資料庫連結

建立資料庫連結有兩種不同的方法,使用哪種方法取決於是否需要快取資料庫連結。

獲取資料庫連結

下面是建立資料庫連結的程式碼。它在"sqlFactory.go"檔案中。因為清晰架構使用了工廠方法模式(factory method pattern),這裡的程式碼是它的一部分。如果你不想使用工廠方法模式,也是一點問題都沒有的。因為資料庫連結在架構中是要被快取的,所以框架程式碼需要呼叫事務庫中的函式“BuildSqlDB()”,它首先檢查資料庫連結是否已經存在。如果沒有,則呼叫“factory.BuildSqlDB(&tdbc)”來建立一個。在此之前,它獲取需要的引數並將它們儲存在“DatabaseConfig”中,“DatabaseConfig”也是在事務庫中定義的。之後它呼叫內部函式“buildGdbc()”生成合適的“gdbc.SqlGdbc"介面(要根據你是否需要事務)。最後,檢查如果資料庫連結不在快取中,則把它放入快取。

// implement Build method for SQL database
func (sf *sqlFactory) Build(c container.Container, dsc *config.DataStoreConfig) (DataStoreInterface, error) {
	logger.Log.Debug("sqlFactory")
	key := dsc.Code
	//if it is already in container, return
	if value, found := c.Get(key); found {
		logger.Log.Debug("found db in container for key:", key)
		sdb := value.(*sql.DB)
		return buildGdbc(sdb, dsc.Tx)
	}
	tdbc :=databaseConfig.DatabaseConfig{dsc.DriverName, dsc.UrlAddress, dsc.Tx}
	db, err := factory.BuildSqlDB(&tdbc)
	if err != nil {
		return nil, err
	}
	gdbc, err := buildGdbc(db, dsc.Tx)
	if err != nil {
		return nil, err
	}
	c.Put(key, gdbc)
	return gdbc, nil

}

下面是建立“SqlGdbc”介面的內部函式。"SqlGdbc"介面有兩種實現,一種是"SqlConnTx",它支援事務。另一個是“SqlDBTx”,它不支援事務。

func buildGdbc(sdb *sql.DB,tx bool) (gdbc.SqlGdbc, error){
	var sdt gdbc.SqlGdbc
	if tx {
		tx, err := sdb.Begin()
		if err != nil {
			return nil, err
		}
		sdt = &gdbc.SqlConnTx{DB: tx}
		logger.Log.Debug("buildGdbc(), create TX:")
	} else {
		sdt = &gdbc.SqlDBTx{sdb}
		logger.Log.Debug("buildGdbc(), create DB:")
	}
	return sdt, nil
}

有一種更簡單的方法可以直接從事務庫中獲得"SqlGdbc",函式是"factory.Build()"。但是當使用它時,你不能快取資料庫連結,所以我沒有在框架中使用它。但是如果你不需要快取資料庫連結,呼叫“factory.Build()”是一個更好的方法。

資料庫配置引數

資料庫配置引數是在第三方事務庫中定義的,但資料本身是儲存在業務專案中。應用程式首先需要組裝引數並將它們傳遞給事務庫,以便得到合適的資料庫連結。在我們的框架中,包括資料庫引數在內的所有應用程式配置資料都儲存在一個檔案中。框架程式碼將負責從檔案中獲取資料。你如果不想將引數儲存在檔案中,直接將引數寫成程式中的硬編碼傳遞給事務庫更容易。

下面是配置檔案“appConfigDev.yaml”中的部分程式碼。對於資料庫來說,關鍵是如何讓事務庫知道需要的是事務連結還是非事務連結。它有多種辦法可以完成。例如,你可以為每個函式設定一個事務標誌,但這需要改動大量的程式碼。我發現最簡單的方法是將所有支援事務的函式放在一個特殊的用例(Use Case)中。在下面的示例中,有三個用例:“registration”、“listUser”和“registrationTx”,其中只有“registrationTx”是支援事務的,因為它使用“*sqlConfigTx”作為“dataStoreConfig”。

useCaseConfig:
    registration:
        code: registration
        userDataConfig: &userDataConfig
            code: userData
            dataStoreConfig: *sqlConfig
    listUser:
        code: listUser
        userDataConfig: *userDataConfig
        cacheDataConfig: &cacheDataConfig
            code: cacheData
            dataStoreConfig: *cacheGrpcConfig
    registrationTx:
        code: registrationTx
        userDataConfig: &userDataConfigTx
            code: userData
            dataStoreConfig: *sqlConfigTx

下面是來自同一配置檔案的部分程式碼。可以看到,在“sqlConfigTx”中,有一個引數“tx:ture”,它表明它是支援事務的。

sqlConfig: &sqlConfig
    code: sqldb
    driverName: mysql
    urlAddress: "root:@tcp(localhost:4333)/service_config?charset=utf8"
    dbName:
    tx:  false
sqlConfigTx: &sqlConfigTx
    code: sqldb
    driverName: mysql
    urlAddress: "root:@tcp(localhost:4333)/service_config?charset=utf8"
    dbName:
    tx: true

在業務邏輯中訪問資料庫

我們用一個業務函式做例子,來展示支援事務和不支援事務的兩種不同實現方式,這樣你就能看到他們的區別。

不支援事務的程式碼

下面是“ModifyAndUnregister(user *model.User)”的非事務函式, 它在“registration.go”檔案中。它是對業務函式“ModifyAndUnregister(ruc.UserDataInterface, user)”的一個簡單封裝,這個業務函式是被事務和非事務程式碼共享的。

// The use case of ModifyAndUnregister without transaction
func (ruc *RegistrationUseCase) ModifyAndUnregister(user *model.User) error {
	return ModifyAndUnregister(ruc.UserDataInterface, user)
}

下面是共享的業務函式"ModifyAndUnregister(ruc.UserDataInterface, user)"的程式碼,所有的業務邏輯都在這個函式裡。

func ModifyAndUnregister(udi dataservice.UserDataInterface, user *model.User) error {
	//loggera.Log.Debug("ModifyAndUnregister")
	err := modifyUser(udi, user)
	if err != nil {
		return errors.Wrap(err, "")
	}
	err = unregisterUser(udi, user.Name)
	if err != nil {
		return errors.Wrap(err, "")
	}
	return nil
}
支援事務的程式碼

下面是相同的業務函式,但支援事務的程式碼。它在“registrationTx.go”檔案中。你要做全部工作就是在“EnableTx()”中呼叫業務函式。

// The use case of ModifyAndUnregister with transaction
func (rtuc *RegistrationTxUseCase) ModifyAndUnregisterWithTx(user *model.User) error {

	udi := rtuc.UserDataInterface
	return udi.EnableTx(func() error {
		// wrap the business function inside the TxEnd function
		return ModifyAndUnregister(udi, user)
	})
}

下面是函式“EnableTx()”的實現程式碼(它在檔案“userDataSql.go”中)。 這個程式碼是在持久層中。它的實現非常簡單,只需呼叫事務庫中的函式“TxEnd()”。

func (uds *UserDataSql) EnableTx(txFunc func() error) error {
	return uds.DB.TxEnd(txFunc)
}

以上就是為業務函式新增事務支援所需要做的全部工作,其餘程式碼均在事務庫中。

如果你想了解更多關於事務庫本身的資訊,請閱讀“一個非侵入的Go事務管理庫——工作原理”,

結論:

我對去年寫的事務管理程式碼進行了升級,使其成為一個非侵入式的輕量級事務管理庫。當你使用它時,只需要在應用程式中額外增加兩三行程式碼就能搞定,所有其他程式碼都放在了事務管理庫。它很好地將業務程式碼與資料庫事務程式碼隔離開來,這樣你的業務程式碼裡就只有純粹的業務邏輯。它是一個庫而不是框架,所以不論你使用任何框架都可以使用它。

原始碼:

完整的原始碼: "jfeng45/servicetmpl1"

索引:

1 "清晰架構(Clean Architecture)的Go微服務: 事物管理"

2 "清晰架構(Clean Architecture)的Go微服務: 程式容器(Application Container)"

3 "一個非侵入的Go事務管理庫——工作原理"

相關文章