This is all thanks to the support of OQM (Object Query-Language Mapping) technology, we developed a Go version called GoooQo.
Introduction to OQM
The biggest difference between OQM technology and traditional ORM (object-relational mapping) technology is that OQM proposes to build CRUD statements directly through objects.
The core function of OQM is to build a query clause through a query object, which is the origin of the Q
in the name of OQM.
Another significant discovery in OQM technology is that the field names in query objects and the conditions in query clauses can be converted interchangeably.
For example, in SQL, the query condition age > ?
can be transformed by rewriting >
as the string alias Gt
(short for Greater Than), and then attaching Gt
as a suffix to the column name age
, resulting in ageGt
, which can be used as a field name.
Then we can use OQM technology to parse such objects. When a field named ageGt
is assigned a value, the corresponding query condition can be generated by reversing the above process.
In this way, we only need to create an entity object and a query object to build CRUD statements. The entity object is used to determine the table name and the column names, and the instance of the query object is used to control the construction of the query clause.
By encapsulating all the boilerplate code, the code to build a CRUD Web API for a single table is reduced to just two lines.
Demo Introduction
We developed a separate demo to demonstrate the features of GoooQo.
The file test.db is a SQLite database containing a table t_user
with four rows:
id | username | password | mobile | nickname | memo | valid | |
---|---|---|---|---|---|---|---|
1 | f0rb | 123456 | f0rb@163.com | 18888888881 | test1 | true | |
2 | user2 | 123456 | test2@qq.com | 18888888882 | test2 | false | |
3 | user3 | 123456 | test3@qq.com | 18888888883 | test3 | memo | true |
4 | user4 | 123456 | test4@qq.com | 18888888884 | test4 | true |
Using GoooQo, we only need to define two structs, UserEntity
and UserQuery
, for the user table in user.go.
UserEntity
is a traditional entity object with fields corresponding to the columns in the user table.
UserQuery
is dedicated to constructing query clauses. The fields in UserQuery
follow the format of column name plus suffix (refer to the suffix mapping table in the appendix) to generate the corresponding query conditions:
type UserQuery struct {
goooqo.PageQuery
IdGt *int // id > ?
IdIn *[]int // id IN (?,?,?)
EmailContain *string // email LIKE "%value%"
MemoNull *bool // memo is [NOT] NULL
}
In addition, the embedded goooqo.PageQuery
defines three fields: PageNumber
, PageSize
, and Sort
, which are used to construct the paging clause and the sorting clause.
After establishing a database connection in the main method, the following two lines of code are all we need to build the CRUD APIs for the user table:
userDataAccess := rdb.NewTxDataAccess[UserEntity](tm)
goooqo.BuildRestService[UserEntity, UserQuery]("/user/", userDataAccess)
The first line of the code creates a data access module for the user table, and the second line builds a web access module based on the created data access module for the user table. The web access module handles requests at http://localhost:9090/user/.
Running the Demo
Checkout the demo repository locally and run the main method in the file demo.go to start the program, and then access the following URLs to view the response:
- http://localhost:9090/user/
- http://localhost:9090/user/3
- http://localhost:9090/user/?pageNumber=2&pageSize=2
- http://localhost:9090/user/?sort=id,desc
- http://localhost:9090/user/?sort=memo,desc%3Bemail,desc
- http://localhost:9090/user/?idIn=1,3
- http://localhost:9090/user/?emailContain=qq
- http://localhost:9090/user/?idGt=2&emailContain=qq
- http://localhost:9090/user/?emailContain=qq&memoNull=true
- http://localhost:9090/user/?emailContain=qq&memoNull=false
For example, the query string emailContain=qq&memoNull=false
will generate this SQL statement SELECT * FROM t_user WHERE email LIKE '%qq' AND memo IS NOT NULL
and get a response like this:
{
"data": {
"list": [
{
"id": 3,
"username": "user3",
"email": "test3@qq.com",
"mobile": "17778888883",
"nickname": "test3",
"memo": "memo",
"valid": true
}
],
"total": 1
},
"success": true
}
There are also some sample HTTP requests provided in the file user.http to test the POST/PUT/PATCH/DELETE
operations. For example:
### Create new users
POST http://localhost:9090/user/
Content-Type: application/json
[{
"username": "Ada Wong",
"email": "AdaW@gmail.com",
"mobile": "01066666",
"nickname": "ada",
"memo": "An agent.",
"valid": true
}, {
"username": "Leon Kennedy",
"email": "LeonKennedy@gmail.com",
"mobile": "01077777",
"nickname": "leon",
"memo": "The hero.",
"valid": true
}]
### Update specified fields for the user 3
PATCH http://localhost:9090/user/3
Content-Type: application/json
{
"username": "Leon Kennedy",
"email": "LeonKennedy@gmail.com"
}
### Delete users where the memo field is not null
DELETE http://localhost:9090/user/?memoNull=false
Epilogue
GoooQo is the Golang version of OQM technology following the Java version framework DoytoQuery. Currently, it is only an MVP that has undergone preliminary evaluation, and it will take multiple rounds of iterations to implement all the functions proposed by OQM technology. Please stay tuned and support us!
Appendix: The Suffix Mapping Table
The suffixes supported by version v0.1.3 and the corresponding query conditions can be found in this suffix mapping table:
Suffix | Field | Value | SQL Condition | MongoDBCondition |
---|---|---|---|---|
(EMPTY) | id | 5 | id = 5 | {"id":5} |
Eq | idEq | 5 | id = 5 | {"idEq":5} |
Not | idNot | 5 | id != 5 | {"idNot":{"$ne":5}} |
Ne | idNe | 5 | id <> 5 | {"idNe":{"$ne":5}} |
Gt | idGt | 5 | id > 5 | {"idGt":{"$gt":5}} |
Ge | idGe | 5 | id >= 5 | {"idGe":{"$gte":5}} |
Lt | idLt | 5 | id < 5 | {"idLt":{"$lt":5}} |
Le | idLe | 5 | id <= 5 | {"idLe":{"$lte":5}} |
NotIn | idNotIn | [1,2,3] | id NOT IN (1,2,3) | {"id":{"$nin":[1, 2, 3]}} |
In | idIn | [1,2,3] | id IN (1,2,3) | {"id":{"$in":[1, 2, 3]}} |
Null | memoNull | false | memo IS NOT NULL | {"memo":{"$not":{"$type", 10}}} |
Null | memoNull | true | memo IS NULL | {"memo":{"$type", 10}} |
NotLike | nameNotLike | "arg" | name NOT LIKE '%arg%' | {"name":{"$not":{"$regex":"arg"}}} |
Like | nameLike | "arg" | name LIKE '%arg%' | {"name":{"$regex":"arg"}} |
NotStart | nameNotStart | "arg" | name NOT LIKE 'arg%' | {"name":{"$not":{"$regex":"^arg"}}} |
Start | nameStart | "arg" | name LIKE 'arg%' | {"name":{"$regex":"^arg"}} |
NotEnd | nameNotEnd | "arg" | name NOT LIKE '%arg' | {"name":{"$not":{"$regex":"arg$"}}} |
End | nameEnd | "arg" | name LIKE '%arg' | {"name":{"$regex":"arg$"}} |
NotContain | nameNotContain | "arg" | name NOT LIKE '%arg%’ | {"name":{"$not":{"$regex":"arg"}}} |
Contain | nameContain | "arg" | name LIKE '%arg%’ | {"name":{"$regex":"arg"}} |
Rx | nameRx | "arg\d" | name REGEXP 'arg\d’ | {"name":{"$regex":"arg\d"}} |
© 2024 Yuan Zhen. All rights reserved.