在 go 開發中, 查詢數據庫通常有兩種選擇:php
直接編寫 SQL 語義清晰, 不易出錯, 可是遇到多個可變條件時顯得不靈活mysql
ORM 有模型關係, 記錄預加載 (sql 生成優化) 等功能, 可是 sql 語句對開發人員相對透明, 管了太多數據庫相關的東西, 相對封閉, 語法晦澀語義不明確, 想要操做 db 鏈接、構造複雜 SQL 很繁瑣git
對於查詢場景少、查詢條件相對固定的系統, 直接寫 SQL 無疑是一種好的選擇。那麼, 對於 SQL 多變的場景而又不想使用 orm 的開發者, 如何能快速開發數據層呢? github
go 的官方包已經提供了好用的 database/sql 工具, 也有各個數據庫的驅動包, 屏蔽了底層驅動差別, 使數據庫查詢變得簡單, 只需提供 SQL 語句和佔位符參數便可快速查詢, 也無需考慮 SQL 注入等問題。那麼, 只要解決了 SQL 語句和佔位符參數的構造問題, 就解決了直接寫 SQL 的靈活性問題。sql
爲了解決 SQL 語句和佔位符參數的構造問題, 咱們須要查詢構造器 (Query Builder)。簡而言之, 查詢構造器就是利用 database/sql 的優點, 提供了一種 orm 和 raw sql 之間的中間方案。有了查詢構造器, 你能夠在遇到不定 SQL 時動態構造 SQL, 遇到複雜肯定 SQL 時直接寫原生 SQL, 使數據查詢更加靈活可控。數據庫
查詢構造器, 顧名思義, 最主要的就是構造。構造什麼? 查詢語句。查詢語句自己就是一個知足標準 SQL 規範的字符串, 因此咱們要作查詢構造器, 主要的任務就是構造字符串。數組
在構造一條 SQL 以前, 不妨看看一條 SQL 是什麼樣的吧。app
SELECT `name`,`age`,`school` FROM `test` WHERE `name` = 'jack'
複雜點的, 帶聯合查詢、分組、排序、分頁tcp
SELECT `t1`.`name`,`t1`.`age`,`t2`.`teacher`,`t3`.`address` FROM `test` as t1 LEFT JOIN `test2` as `t2` ON `t1`.`class` = `t2`.`class` INNER JOIN `test3` as t3 ON `t1`.`school` = `t3`.`school` WHERE `t1`.`age` >= 20 GROUP BY `t1`.`age` HAVING COUNT(`t1`.`age`) > 2 ORDER BY `t1`.`age` DESC LIMIT 10 OFFSET 0
固然, 標準 SQL 還有不少語法規定, 這裏就不一一舉例。而對於規範中最經常使用的語法, 咱們的查詢構造器必需要有構造它們的能力函數
一個標準的查詢語句結構以下:
SELECT [字段] FROM [表名] [JOIN 子句] [WHERE 子句] [GROUP BY 子句] [HAVING 子句] [ORDER BY 子句] [LIMIT 子句]
其中 JOIN 子句、WHERE 子句、 HAVING 子句和 LIMIT 子句會用到佔位符參數
再看 INSERT、UPDATE、DELETE 操做的結構:
INSERT
INSERT INTO [表名] ([字段名]) VALUES ([要插入的值])
要插入的值會用到佔位符參數
UPDATE
UPDATE [表名] [SET 子句] [WHERE 子句]
SET 子句和 WHERE 子句會用到佔位符參數
DELETE
DELETE FROM [表名] [WHERE 子句]
WHERE 子句會用到佔位符參數
OK, 拆解後是否是以爲 SQL 語句的基本結構很簡單? 要實現查詢構造器, 只需按照這些語句的結構構造出相應的字符串, 並保存須要的佔位符參數便可。
有了思路, 實現起來就簡單了。
參考其餘語言的查詢構造器, 方法名直接體現 SQL 語法, 多爲鏈式調用:
$db.table("`test`"). where("a", ">", 20). where("b", "=", "aaa"). get()
要實現查詢構造器, 這是一個好的示範。
話很少說, 開寫!
首先定義咱們的 SQLBuilder 類型:
type SQLBuilder struct { _select string // select 子句字符串 _insert string // insert 子句字符串 _update string // update 子句字符串 _delete string // delete 子句字符串 _table string // 表名 _join string // join 子句字符串 _where string // where 子句字符串 _groupBy string // group by 子句字符串 _having string // having 子句字符串 _orderBy string // order by 子句字符串 _limit string // limit 子句字符串 _insertParams []interface{} // insert 插入值須要的佔位符參數 _updateParams []interface{} // update SET 子句須要的佔位符參數 _whereParams []interface{} // where 子句須要的佔位符參數 _havingParams []interface{} // having 子句須要的佔位符參數 _limitParams []interface{} // limit 子句須要的佔位符參數 _joinParams []interface{} // join 子句須要的佔位符參數 }
SQLBuilder 的構造函數:
func NewSQLBuilder() *SQLBuilder { return &SQLBuilder{} }
獲取字符串很簡單, 只要按照 SQL 的規定將各個子句組合便可。
獲取 QuerySQL:
var ErrTableEmpty = errors.New("table empty") func (sb *SQLBuilder) GetQuerySQL() (string, error) { if sb._table == "" { return "", ErrTableEmpty } var buf strings.Builder buf.WriteString("SELECT ") if sb._select != "" { buf.WriteString(sb._select) } else { buf.WriteString("*") } buf.WriteString(" FROM ") buf.WriteString(sb._table) if sb._join != "" { buf.WriteString(" ") buf.WriteString(sb._join) } if sb._where != "" { buf.WriteString(" ") buf.WriteString(sb._where) } if sb._groupBy != "" { buf.WriteString(" ") buf.WriteString(sb._groupBy) } if sb._having != "" { buf.WriteString(" ") buf.WriteString(sb._having) } if sb._orderBy != "" { buf.WriteString(" ") buf.WriteString(sb._orderBy) } if sb._limit != "" { buf.WriteString(" ") buf.WriteString(sb._limit) } return buf.String(), nil }
tips: 上述代碼使用 strings.Builder 包來拼接字符串。固然構造查詢語句自己不是一個高頻操做, 不考慮效率使用 + 來拼接也是能夠的
獲取 InsertSQL:
var ErrInsertEmpty = errors.New("insert content empty") func (sb *SQLBuilder) GetInsertSQL() (string, error) { if sb._table == "" { return "", ErrTableEmpty } if sb._insert == "" { return "", ErrInsertEmpty } var buf strings.Builder buf.WriteString("INSERT INTO ") buf.WriteString(sb._table) buf.WriteString(" ") buf.WriteString(sb._insert) return buf.String(), nil }
獲取 UpdateSQL:
var ErrUpdateEmpty = errors.New("update content empty") func (sb *SQLBuilder) GetUpdateSQL() (string, error) { if sb._table == "" { return "", ErrTableEmpty } if sb._update == "" { return "", ErrUpdateEmpty } var buf strings.Builder buf.WriteString("UPDATE ") buf.WriteString(sb._table) buf.WriteString(" ") buf.WriteString(sb._update) if sb._where != "" { buf.WriteString(" ") buf.WriteString(sb._where) } return buf.String(), nil }
獲取 DeteleSQL:
func (sb *SQLBuilder) GetDeleteSQL() (string, error) { if sb._table == "" { return "", ErrTableEmpty } var buf strings.Builder buf.WriteString("DELETE FROM ") buf.WriteString(sb._table) if sb._where != "" { buf.WriteString(" ") buf.WriteString(sb._where) } return buf.String(), nil }
一樣, 咱們要填充佔位符 "?" 的參數也須要得到, query、insert、update、delete 擁有的參數類型都有差異, 也都有着不一樣的順序
func (sb *SQLBuilder) GetQueryParams() []interface{} { params := []interface{}{} params = append(params, sb._joinParams...) params = append(params, sb._whereParams...) params = append(params, sb._havingParams...) params = append(params, sb._limitParams...) return params } func (sb *SQLBuilder) GetInsertParams() []interface{} { params := []interface{}{} params = append(params, sb._insertParams...) return params } func (sb *SQLBuilder) GetUpdateParams() []interface{} { params := []interface{}{} params = append(params, sb._updateParams...) params = append(params, sb._whereParams...) return params } func (sb *SQLBuilder) GetDeleteParams() []interface{} { params := []interface{}{} params = append(params, sb._whereParams...) return params }
設置表名, 這裏咱們設置完成後返回 SQLBuilder 指針本身, 能夠完成鏈式調用。以後大部分方法都會使用這種方式。
func (sb *SQLBuilder) Table(table string) *SQLBuilder { sb._table = table return sb }
用例:
package main import ( "github.com/wazsmwazsm/QueryBuilder/builder" "log" ) func main() { sb := builder.NewSQLBuilder() sql, err := sb.Table("`test`"). Select("*"). GetQuerySQL() if err != nil { log.Fatal(err) } params := sb.GetQueryParams() log.Println(sql) // SELECT * FROM `test` log.Println(params) // [] }
設置 select 子句, 支持多個參數用逗號隔開, 注意最後一個逗號要去掉
func (sb *SQLBuilder) Select(cols ...string) *SQLBuilder { var buf strings.Builder for k, col := range cols { buf.WriteString(col) if k != len(cols)-1 { buf.WriteString(",") } } sb._select = buf.String() return sb }
用例:
package main import ( "github.com/wazsmwazsm/QueryBuilder/builder" "log" ) func main() { sb := builder.NewSQLBuilder() sql, err := sb.Table("`test`"). Select("`age`", "COUNT(age)"). GetQuerySQL() if err != nil { log.Fatal(err) } params := sb.GetQueryParams() log.Println(sql) // SELECT `age`,COUNT(age) FROM `test` log.Println(params) // [] }
對於 where 子句, 第一個 where 條件須要 WHERE 關鍵字, 再有其它條件, 會經過 AND 和 OR 來鏈接, 那麼咱們能夠增長 Where() 和 OrWhere() 方法, 兩個方法公共邏輯能夠提出來:
func (sb *SQLBuilder) Where(field string, condition string, value interface{}) *SQLBuilder { return sb.where("AND", condition, field, value) } func (sb *SQLBuilder) OrWhere(field string, condition string, value interface{}) *SQLBuilder { return sb.where("OR", condition, field, value) } func (sb *SQLBuilder) where(operator string, condition string, field string, value interface{}) *SQLBuilder { var buf strings.Builder buf.WriteString(sb._where) // 載入以前的 where 子句 if buf.Len() == 0 { // where 子句還沒設置 buf.WriteString("WHERE ") } else { // 已經設置, 拼接 OR 或 AND 操做符 buf.WriteString(" ") buf.WriteString(operator) buf.WriteString(" ") } buf.WriteString(field) // 拼接字段 buf.WriteString(" ") buf.WriteString(condition) // 拼接條件 =、!=、<、>、like 等 buf.WriteString(" ") buf.WriteString("?") // 拼接佔位符 sb._where = buf.String() // 寫字符串 sb._whereParams = append(sb._whereParams, value) // push 佔位符參數到數組 return sb }
用例:
package main import ( "github.com/wazsmwazsm/QueryBuilder/builder" "log" ) func main() { sb := builder.NewSQLBuilder() sql, err := sb.Table("`test`"). Select("`name`", "`age`", "`school`"). Where("`name`", "=", "jack"). Where("`age`", ">=", 18). OrWhere("`name`", "like", "%admin%"). GetQuerySQL() if err != nil { log.Fatal(err) } params := sb.GetQueryParams() log.Println(sql) // SELECT `name`,`age`,`school` FROM `test` WHERE `name` = ? AND `age` >= ? OR `name` like ? log.Println(params) // [jack 18 %admin%] }
上述代碼能夠解決簡單的條件子句, 若是遇到 WHERE a = ? AND (b = ? OR c = ?) 這樣的複雜子句, 該如何構造呢? 面對這種場景, 咱們須要提供書寫原生 where 子句的能力, 增長 WhereRaw() 和 OrWhereRaw() 方法:
func (sb *SQLBuilder) WhereRaw(s string, values ...interface{}) *SQLBuilder { return sb.whereRaw("AND", s, values) } func (sb *SQLBuilder) OrWhereRaw(s string, values ...interface{}) *SQLBuilder { return sb.whereRaw("OR", s, values) } func (sb *SQLBuilder) whereRaw(operator string, s string, values []interface{}) *SQLBuilder { var buf strings.Builder buf.WriteString(sb._where) // append if buf.Len() == 0 { buf.WriteString("WHERE ") } else { buf.WriteString(" ") buf.WriteString(operator) buf.WriteString(" ") } buf.WriteString(s) // 直接使用 raw SQL 字符串 sb._where = buf.String() for _, value := range values { sb._whereParams = append(sb._whereParams, value) } return sb }
用例:
package main import ( "github.com/wazsmwazsm/QueryBuilder/builder" "log" ) func main() { sb := builder.NewSQLBuilder() sql, err := sb.Table("`test`"). Select("`name`", "`age`", "`school`"). WhereRaw("`title` = ?", "hello"). Where("`name`", "=", "jack"). OrWhereRaw("(`age` = ? OR `age` = ?) AND `class` = ?", 22, 25, "2-3"). GetQuerySQL() if err != nil { log.Fatal(err) } params := sb.GetQueryParams() log.Println(sql) // SELECT `name`,`age`,`school` FROM `test` WHERE `title` = ? AND `name` = ? OR (`age` = ? OR `age` = ?) AND `class` = ? log.Println(params) // [hello jack 22 25 2-3] }
where in 也是常見的 where 子句, where in 子句分爲 where in、or where in、where not in、or where not in 四種模式, 佔位符數量等於 where in 的集合數量。
咱們但願構造 where in 子句的方法入參是一個 slice, 佔位符的數量等於 slice 的長度, 那麼咱們須要封裝一個生成佔位符的函數:
func GenPlaceholders(n int) string { var buf strings.Builder for i := 0; i < n-1; i++ { buf.WriteString("?,") // 生成 n-1 個 "?" 佔位符 } if n > 0 { buf.WriteString("?") // 生成最後一個佔位符, 若是 n <= 0 則不生成任何佔位符 } return buf.String() }
按照 where in 子句的四種模式, 增長 WhereIn() OrWhereIn() WhereNotIn() OrWhereNotIn() 方法:
func (sb *SQLBuilder) WhereIn(field string, values ...interface{}) *SQLBuilder { return sb.whereIn("AND", "IN", field, values) } func (sb *SQLBuilder) OrWhereIn(field string, values ...interface{}) *SQLBuilder { return sb.whereIn("OR", "IN", field, values) } func (sb *SQLBuilder) WhereNotIn(field string, values ...interface{}) *SQLBuilder { return sb.whereIn("AND", "NOT IN", field, values) } func (sb *SQLBuilder) OrWhereNotIn(field string, values ...interface{}) *SQLBuilder { return sb.whereIn("OR", "NOT IN", field, values) } func (sb *SQLBuilder) whereIn(operator string, condition string, field string, values []interface{}) *SQLBuilder { var buf strings.Builder buf.WriteString(sb._where) // append if buf.Len() == 0 { buf.WriteString("WHERE ") } else { buf.WriteString(" ") buf.WriteString(operator) buf.WriteString(" ") } buf.WriteString(field) plhs := GenPlaceholders(len(values)) // 生成佔位符 buf.WriteString(" ") buf.WriteString(condition) buf.WriteString(" ") buf.WriteString("(") buf.WriteString(plhs) // 拼接佔位符 buf.WriteString(")") sb._where = buf.String() for _, value := range values { sb._whereParams = append(sb._whereParams, value) // push 佔位符參數 } return sb }
用例:
package main import ( "github.com/wazsmwazsm/QueryBuilder/builder" "log" ) func main() { sb := builder.NewSQLBuilder() sql, err := sb.Table("`test`"). Select("`name`", "`age`", "`school`"). WhereIn("`id`", 1, 2, 3). OrWhereNotIn("`uid`", 2, 4). GetQuerySQL() if err != nil { log.Fatal(err) } params := sb.GetQueryParams() log.Println(sql) // SELECT `name`,`age`,`school` FROM `test` WHERE `id` IN (?,?,?) OR `uid` NOT IN (?,?) log.Println(params) // [1 2 3 2 4] }
group by 子句能夠根據多個字段分組:
func (sb *SQLBuilder) GroupBy(fields ...string) *SQLBuilder { var buf strings.Builder buf.WriteString("GROUP BY ") for k, field := range fields { buf.WriteString(field) if k != len(fields)-1 { buf.WriteString(",") } } sb._groupBy = buf.String() return sb }
having 子句和 where 子句基本相同, 這裏就不費篇幅說明了, 詳細見 QueryBuilder/builder/builder.go
用例:
package main import ( "github.com/wazsmwazsm/QueryBuilder/builder" "log" ) func main() { sb := builder.NewSQLBuilder() sql, err := sb.Table("`test`"). Select("`school`", "`class`", "COUNT(*) as `ct`"). GroupBy("`school`", "`class`"). Having("`ct`", ">", "2"). GetQuerySQL() if err != nil { log.Fatal(err) } params := sb.GetQueryParams() log.Println(sql) // SELECT `school`,`class`,COUNT(*) as `ct` FROM `test` GROUP BY `school`,`class` HAVING `ct` > ? log.Println(params) // [2] }
order by 子句能夠根據多個字段來排序:
func (sb *SQLBuilder) OrderBy(operator string, fields ...string) *SQLBuilder { var buf strings.Builder buf.WriteString("ORDER BY ") for k, field := range fields { buf.WriteString(field) if k != len(fields)-1 { buf.WriteString(",") } } buf.WriteString(" ") buf.WriteString(operator) // DESC 或 ASC sb._orderBy = buf.String() return sb }
limit 來限制查詢的結果, 這裏咱們使用 LIMIT OFFSET 語法, 這個語法是標準 SQL 規定的, LIMIT x,x 這個形式只有 mysql 支持
func (sb *SQLBuilder) Limit(offset, num interface{}) *SQLBuilder { var buf strings.Builder buf.WriteString("LIMIT ? OFFSET ?") sb._limit = buf.String() sb._limitParams = append(sb._limitParams, num, offset) return sb }
用例:
package main import ( "github.com/wazsmwazsm/QueryBuilder/builder" "log" ) func main() { sb := builder.NewSQLBuilder() sql, err := sb.Table("`test`"). Select("`name`", "`age`", "`school`"). Where("`name`", "=", "jack"). Where("`age`", ">=", 18). OrderBy("DESC", "`age`", "`class`"). Limit(1, 10). GetQuerySQL() if err != nil { log.Fatal(err) } params := sb.GetQueryParams() log.Println(sql) // SELECT `name`,`age`,`school` FROM `test` WHERE `name` = ? AND `age` >= ? ORDER BY `age`,`class` DESC LIMIT ? OFFSET ? log.Println(params) // [jack 18 10 1] }
使用 join 子句後, SQL 變得複雜。標準 SQL join 有 left join、right join、inner join、full join 幾種模式 join 子句的 on 條件相似 where 子句, 連表後須要給表起別名用來區分字段所屬...面對這樣靈活多變的語法, 咱們這裏較好的方式就是提供 raw sql 的形式來處理 join 操做:
func (sb *SQLBuilder) JoinRaw(join string, values ...interface{}) *SQLBuilder { var buf strings.Builder buf.WriteString(sb._join) if buf.Len() != 0 { buf.WriteString(" ") } buf.WriteString(join) // 拼接 raw join sql sb._join = buf.String() for _, value := range values { sb._joinParams = append(sb._joinParams, value) } return sb }
用例 (構造一個複雜的查詢):
package main import ( "github.com/wazsmwazsm/QueryBuilder/builder" "log" ) func main() { sb := builder.NewSQLBuilder() sql, err := sb.Table("`test` as t1"). Select("`t1`.`name`", "`t1`.`age`", "`t2`.`teacher`", "`t3`.`address`"). JoinRaw("LEFT JOIN `test2` as `t2` ON `t1`.`class` = `t2`.`class`"). JoinRaw("INNER JOIN `test3` as t3 ON `t1`.`school` = `t3`.`school`"). Where("`t1`.`age`", ">=", 18). GroupBy("`t1`.`age`"). Having("COUNT(`t1`.`age`)", ">", 2). OrderBy("DESC", "`t1`.`age`"). Limit(1, 10). GetQuerySQL() if err != nil { log.Fatal(err) } params := sb.GetQueryParams() log.Println(sql) // SELECT `t1`.`name`,`t1`.`age`,`t2`.`teacher`,`t3`.`address` FROM `test` as t1 LEFT JOIN `test2` as `t2` ON `t1`.`class` = `t2`.`class` INNER JOIN `test3` as t3 ON `t1`.`school` = `t3`.`school` WHERE `t1`.`age` >= ? GROUP BY `t1`.`age` HAVING COUNT(`t1`.`age`) > ? ORDER BY `t1`.`age` DESC LIMIT ? OFFSET ? log.Println(params) // [18 2 10 1] }
insert SQL 構建:
func (sb *SQLBuilder) Insert(cols []string, values ...interface{}) *SQLBuilder { var buf strings.Builder // 拼接字段 buf.WriteString("(") for k, col := range cols { buf.WriteString(col) if k != len(cols)-1 { buf.WriteString(",") } } buf.WriteString(") VALUES (") // 拼接佔位符 for k := range cols { buf.WriteString("?") if k != len(cols)-1 { buf.WriteString(",") } } buf.WriteString(")") sb._insert = buf.String() for _, value := range values { // push 佔位符參數 sb._insertParams = append(sb._insertParams, value) } return sb }
用例:
package main import ( "github.com/wazsmwazsm/QueryBuilder/builder" "log" ) func main() { sb := builder.NewSQLBuilder() sql, err := sb.Table("`test`"). Insert([]string{"`name`", "`age`"}, "jack", 18). GetInsertSQL() if err != nil { log.Fatal(err) } params := sb.GetInsertParams() log.Println(sql) // INSERT INTO `test` (`name`,`age`) VALUES (?,?) log.Println(params) // [jack 18] }
update SQL 構建:
package main import ( "github.com/wazsmwazsm/QueryBuilder/builder" "log" ) func main() { sb := builder.NewSQLBuilder() sql, err := sb.Table("`test`"). Update([]string{"`name`", "`age`"}, "jack", 18). Where("`id`", "=", 11). GetUpdateSQL() if err != nil { log.Fatal(err) } params := sb.GetUpdateParams() log.Println(sql) // UPDATE `test` SET `name` = ?,`age` = ? WHERE `id` = ? log.Println(params) // [jack 18 11] }
delete SQL 構建:
package main import ( "github.com/wazsmwazsm/QueryBuilder/builder" "log" ) func main() { sb := builder.NewSQLBuilder() sql, err := sb.Table("`test`"). Where("`id`", "=", 11). GetDeleteSQL() if err != nil { log.Fatal(err) } params := sb.GetDeleteParams() log.Println(sql) // DELETE FROM `test` WHERE `id` = ? log.Println(params) // [11] }
OK, 查詢構造器的實現到此結束, 是否是很簡單呢?
查詢構造器實現了, 那麼就結合 database/sql 用用吧!
以 mysql 爲例:
package main import ( "database/sql" "fmt" _ "github.com/go-sql-driver/mysql" "github.com/wazsmwazsm/QueryBuilder/builder" "log" ) // Info 定義一個數據模型, 用於接收查詢數據 type Info struct { Age int AgeCount int } func main() { // 建立 mysql 鏈接 dataSource := fmt.Sprintf("%s:%s@tcp(%s:%v)/%s?charset=utf8", "test", "test", "127.0.0.1", 3306, "test") mysqlConn, err := sql.Open("mysql", dataSource) if err != nil { log.Panic("Db connect failed!" + err.Error()) } // 建立查詢構造器實例 sb := builder.NewSQLBuilder() querySQL, err := sb.Table("`test`"). Select("`age`", "COUNT(age)"). GroupBy("`age`"). GetQuerySQL() if err != nil { log.Fatal(err) } params := sb.GetQueryParams() // 執行查詢 rows, err := mysqlConn.Query(querySQL, params...) if err != nil { log.Panic(err) } defer rows.Close() // 查詢數據綁定到 info 結構中 infos := []*Info{} for rows.Next() { info := new(Info) if err := rows.Scan( &info.Age, &info.AgeCount, ); err != nil { log.Panicln(err) } infos = append(infos, info) } for _, info := range infos { fmt.Println(info) } }
該項目的所有源碼詳見 QueryBuilder, 單元測試已 100% 覆蓋