In the article, “Go Microservice with Clean Architecture: Transaction Support”, I talked about how to implement a non-intrusive transaction management system in the Clean Architecture. It allows you separating transaction code with business logic and not thinking about the transaction when writing business logic. But it has some drawbacks. First, it is embedded in the framework, so it can’t be used separately. Second, even though it is non-intrusive to the business logic, it is intrusive to the framework. I need to change all layers of the Clean Architecture framework to get it to work and it is not very easy to use. Third, there is a dependency leak problem as I mentioned in the article. I did point out a solution to the last problem, but since it is a big change, I decided to postpone the change to a later time. Now, I finally get some time to get back to it.
The followings are the new requirements for my changes:
- Make the transaction management code a lib, so people can use it without the framework
- Make it non-intrusive to the framework, meaning there is no transaction code in any layer of the project except in the use case layer. Almost all the transaction code is in the transaction lib.
- Fix the dependency leak in the code.
I achieved all of them and the result is exciting. I will write two articles about it. This one talks about how to use the lib and the next one talks about how the lib works.
How to use the lib in your project
To make a business function supporting the transaction, you need to do two things. First, create a database connection through the lib; second, use the connection to run SQL queries. I assume that you are using the Clean Architecture or some kind of layered architecture in your project. In that case, the first piece is done in the application container and the second piece is done in the business logic code. If you don’t have any framework or layered architecture, then those two pieces of code will be in one place. You may be wondering how is it different from the code without transaction support? Almost none. When using the lib, You pretty much writing the same code for a transactional or non-transactional function, the lib will take care of everything in the back-end.
All the code in this article is in “jfeng45/servicetmpl1”, which is a self-evolved Microservice Framework. It has examples of how to use the transaction lib.
Create a database connection
There are two different ways to create the database connection, which one to use depends on whether you need to cache the underline database connection.
Get database connection
The following is the code to create a database connection. It is in “sqlFactory.go”. Because the database connection is cached, the code needs to call function “BuildSqlDB()” in the transaction lib. It first checks to see if the database connection exists. If not, it calls “factory.BuildSqlDB(&tdbc)” to create one. Before that, it creates the parameters needed and saves them in “DatabaseConfig”, which is also defined in the transaction lib. After that, it calls internal function “buildGdbc()” to generates an appropriate concrete implementation of “gdbc.SqlGdbc” interface according to whether you need a transaction or not. Finally, it caches the database connection if it is not in cache.
The following is the internal function to create “SqlGdbc” interface. There are two implementations of “SqlGdbc” interface, one is “SqlConnTx”, which supports transaction. The other is “SqlDBTx”, which doesn’t support transaction.
There is an easier way from which you can get the “SqlGdbc” directly. The function to call is “factory.Build()”. However, when using it, you can’t cache the database connection, so I didn’t use it in the code. But if you don’t need to cache the database connection, calling “factory.Build()” is better.
The database connection configuration data are defined in the transaction lib, but the configuration data is saved in the business project. The application first needs to assemble the parameters and passes them to the transaction lib to retrieve the appropriate database connection. In our framework, all the application configuration data including database parameters are saved in a file. The framework code will take care of retrieving data from the file. You can hard-coded them in your application if you don’t want to save the parameters in a file.
The following is the partial code in the configuration file, “appConfigDev.yaml”. As far as the database concern, the key is how to flag a function as transaction support. There are different ways to do it. For example, you can set up a transaction flag for each function, but that is a lot of change to the current code. I figured out that the easiest way is putting all the transactional functions in a special use case. In the following example, there are three use cases, “registration”, “listUser” and “registrationTx”, among them only “registrationTx” is transactional. It uses “sqlConfigTx” as “dataStoreConfig”; for non-transactional use cases, such as “registration”, it uses “sqlConfig”.
The following is the partial code from the same configuration file. In “sqlConfigTx”, there is an attribute “tx:ture”, which tells that it is transactional.
Access database in business logic code
Let’s see show how to make a business function transactional. We will use one business function as an example and show it in two different forms (transaction and non-transaction), so you can see the difference.
The following is an non-transactional function for “ModifyAndUnregister(user *model.User)”. It is in file “registration.go”. It is a wrapper on the business function “ModifyAndUnregister(ruc.UserDataInterface, user)”, which is shared by both transactional and non-transactional code.
The following is the code for the shared business function “ModifyAndUnregister(ruc.UserDataInterface, user)”.
The following is the same business function, but with transaction support. It is in file “registrationTx.go”. You need to wrap the business function in a function “EnableTx()”, that’s all you need to do in the use case layer.
The following is the implementation code for the function “EnableTx()” (It is in file “userDataSql.go”) in the persistence layer.
The above is all you need to do to enable transaction support for a business function and the rest of the code is in the transaction lib.
If you want to know more about the transaction lib itself, please read “A Non-Intrusive Transaction Management Lib in Go — How It Works”.
I upgraded the transaction management code written in last year, and make it a non-intrusive lightweight transaction management lib. It is very easy to use, you just need to add two additional lines of code to make a function transactional. Besides those, there is no other code related to the transaction in your application, and everything else is taking care of by the lib code. It did a good job to isolate your business code from transaction concerns. It is a lib, not a framework, so you can use it in any environment.
The complete code is in “jfeng45/servicetmpl1”