Preface
In go projects, it is often necessary to query the database. Based on previous Java development experience, many methods will be written according to the query criteria, such as:
- GetUserByUserID
- GetUsersByName
- GetUsersByAge
Write a method for each query condition. This method is very good for external use, and follows strict principles to ensure that each external method interface is clear. However, internally, it should be as versatile as possible to achieve code reuse and write less code, making the code look more elegant and tidy.
Problem
When reviewing the code, the general writing method for the above three methods is
1 | func GetUserByUserID(ctx context.Context, userID int64) (*User, error){ |
When there are dozens of fields on the User table, there will be more and more similar methods above, and the code will not be reused. When there are Teacher tables, Class tables, and other tables, the above query methods need to be doubled.
The caller can also write very rigidly, with fixed parameters. When adding a query condition, either change the original function and add a parameter, so that other calls will also need to be changed; Either write a new function, which makes it more and more difficult to maintain and read.
The above is the bronze writing method. In response to this situation, the following describes several types of writing methods for silver, gold, and kings
Silver
Define the input parameter as a structure
1 | type UserParam struct { |
Place all input parameters in the UserParam structure
1 | func GetUserInfo(ctx context.Context, info *UserParam) ([]*User, error) { |
When this code is written here, it is actually much better than the initial method. At least, the method in the dao layer has changed from many input parameters to one. The caller’s code can also build parameters according to its own needs, without requiring many empty placeholders. However, the existing problems are also quite obvious: there are still many empty sentences and a redundant structure has been introduced. It would be somewhat regrettable if we ended up here.
In addition, if we extend the business scenario again, and instead of using equivalent queries, we use multi valued queries or interval queries, such as querying status in (a, b), how can the above code be extended? Is it necessary to introduce a method, which is cumbersome and cumbersome for the time being. Whatever the method name is, it will make us tangle for a long time; Perhaps you can try expanding each parameter from a single value to an array, and then changing the assignment from=to in(). Using in for all parameter queries is obviously not that performance friendly.
Gold
A more advanced optimization method is to use higher order functions.
1 | type Option func(*gorm.DB) |
Define Option as a function whose input parameter type is * gorm.DB, and the return value is null.
Then define a higher order function for each field that needs to be queried
1 | func UserID(ID int64) Option { |
The return value is of type Option.
So that the above three methods can be combined into one method
1 | func GetUsersByCondition(ctx context.Context, opts ...Option)([]*User, error) { |
Without comparison, there is no harm. By comparing with the initial method, it can be seen that the input parameters of the method have changed from multiple parameters of different types to a set of functions of the same type. Therefore, when processing these parameters, there is no need to determine the null one by one. Instead, you can directly use a for loop to handle it, which is much simpler than before.
You can also extend other query criteria, such as IN, greater than, and so on
1 | func UserIDs(IDs int64) Option { |
Moreover, this query condition is ultimately converted into a Where condition, independent of the specific table, which means that these definitions can be reused by other tables.
King
Optimization to the above methods is already possible, but the king generally continues to optimize.
The above method GetUsersByCondition can only query the User table. Can it be more general and query arbitrary tables? Sharing the GetUsersByCondition method, I found that there are two obstacles to querying any table:
The table name is written dead in the method
The return value is defined as [] * User and cannot be generalized
For the first question, we can define an Option to implement
1 | func TableName(tableName string) Option { |
For the second problem, you can use the return parameter as an input parameter and pass it in by reference
1 | func GetRecords(ctx context.Context, in any, opts ...Option) { |
Sum Up
Here, by abstracting the Grom query conditions, it greatly simplifies the writing of DB composite queries and improves the simplicity of the code.