日本综合一区二区|亚洲中文天堂综合|日韩欧美自拍一区|男女精品天堂一区|欧美自拍第6页亚洲成人精品一区|亚洲黄色天堂一区二区成人|超碰91偷拍第一页|日韩av夜夜嗨中文字幕|久久蜜综合视频官网|精美人妻一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營銷解決方案
如何用Go實(shí)現(xiàn)一個(gè)ORM

為了提高開發(fā)效率和質(zhì)量,我們常常需要ORM來幫助我們快速實(shí)現(xiàn)持久層增刪改查API,目前go語言實(shí)現(xiàn)的ORM有很多種,他們都有自己的優(yōu)劣點(diǎn),有的實(shí)現(xiàn)簡單,有的功能復(fù)雜,有的API十分優(yōu)雅。在使用了多個(gè)類似的工具之后,總是會(huì)發(fā)現(xiàn)某些點(diǎn)無法滿足解決我們生產(chǎn)環(huán)境中碰到的實(shí)際問題,比如無法集成公司內(nèi)部的監(jiān)控,Trace組件,沒有database層的超時(shí)設(shè)置,沒有熔斷等,所以有必要公司自己內(nèi)部實(shí)現(xiàn)一款滿足我們可自定義開發(fā)的ORM,好用的生產(chǎn)工具常常能夠?qū)ιa(chǎn)力產(chǎn)生飛躍式的提升。

為什么需要ORM

直接使用database/sql的痛點(diǎn)

首先看看用database/sql如何查詢數(shù)據(jù)庫我們用user表來做例子,一般的工作流程是先做技術(shù)方案,其中排在比較前面的是數(shù)據(jù)庫表的設(shè)計(jì),大部分公司應(yīng)該有嚴(yán)格的數(shù)據(jù)庫權(quán)限控制,不會(huì)給線上程序使用比較危險(xiǎn)的操作權(quán)限,比如創(chuàng)建刪除數(shù)據(jù)庫,表,刪除數(shù)據(jù)等。表結(jié)構(gòu)如下:

CREATE TABLE `user` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id',
`name` varchar(100) NOT NULL COMMENT '名稱',
`age` int(11) NOT NULL DEFAULT '0' COMMENT '年齡',
`ctime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '創(chuàng)建時(shí)間',
`mtime` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '更新時(shí)間',
PRIMARY KEY (`id`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

首先我們要寫出和表結(jié)構(gòu)對(duì)應(yīng)的結(jié)構(gòu)體User,如果你足夠勤奮和努力,相應(yīng)的json tag 和注釋都可以寫上,這個(gè)過程無聊且重復(fù),因?yàn)樵谠O(shè)計(jì)表結(jié)構(gòu)的時(shí)候你已經(jīng)寫過一遍了。

type User struct {
Id int64 `json:"id"`
Name string `json:"name"`
Age int64
Ctime time.Time
Mtime time.Time // 更新時(shí)間
}

定義好結(jié)構(gòu)體,我們寫一個(gè)查詢年齡在20以下且按照id字段順序排序的前20名用戶的 go代碼

func FindUsers(ctx context.Context) ([]*User, error) {
rows, err := db.QueryContext(ctx, "SELECT `id`,`name`,`age`,`ctime`,`mtime` FROM user WHERE `age` if err != nil {
return nil, err
}
defer rows.Close()
result := []*User{}
for rows.Next() {
a := &User{}
if err := rows.Scan(&a.Id, &a.Name, &a.Age, &a.Ctime, &a.Mtime); err != nil {
return nil, err
}
result = append(result, a)
}
if rows.Err() != nil {
return nil, rows.Err()
}
return result, nil
}

當(dāng)我們寫少量這樣的代碼的時(shí)候我們可能還覺得輕松,但是當(dāng)你業(yè)務(wù)工期排的很緊,并且要寫大量的定制化查詢的時(shí)候,這樣的重復(fù)代碼會(huì)越來越多。上面的的代碼我們發(fā)現(xiàn)有這么幾個(gè)問題:

  1. SQL 語句是硬編碼在程序里面的,當(dāng)我需要增加查詢條件的時(shí)候我需要另外再寫一個(gè)方法,整個(gè)方法需要拷貝一份,很不靈活。
  2. 在查詢表所有字段的情況下,第2行下面的代碼都是一樣重復(fù)的,不管sql語句后面的條件是怎么樣的。
  3. 我們發(fā)現(xiàn)第1行SQL語句編寫和rows.Scan()那行,寫的枯燥層度是和表字段的數(shù)量成正比的,如果一個(gè)表有50個(gè)字段或者100個(gè)字段,手寫是非常乏味的。
  4. 在開發(fā)過程中rows.Close() 和 rows.Err()忘記寫是常見的錯(cuò)誤。

我們總結(jié)出來用database/sql標(biāo)準(zhǔn)庫開發(fā)的痛點(diǎn):

開發(fā)效率很低

很顯然寫上面的那種代碼是很耗費(fèi)時(shí)間的,因?yàn)槭终`容易寫錯(cuò),無可避免要增加自測(cè)的時(shí)間。如果上面的結(jié)構(gòu)體User、 查詢方法FindUsers() 代碼能夠自動(dòng)生成,那么那將會(huì)極大的提高開發(fā)效率并且減少human error的發(fā)生從而提高開發(fā)質(zhì)量。

心智負(fù)擔(dān)很重

如果一個(gè)開發(fā)人員把大量的時(shí)間花在這些代碼上,那么他其實(shí)是在浪費(fèi)自己的時(shí)間,不管在工作中還是在個(gè)人項(xiàng)目中,應(yīng)該把重點(diǎn)花在架構(gòu)設(shè)計(jì),業(yè)務(wù)邏輯設(shè)計(jì),困難點(diǎn)攻堅(jiān)上面,去探索和開拓自己沒有經(jīng)驗(yàn)的領(lǐng)域,這塊Dao層的代碼最好在10分鐘內(nèi)完成。

ORM的核心組成

明白了上面的痛點(diǎn),為了開發(fā)工作更舒服,更高效,我們嘗試著自己去開發(fā)一個(gè)ORM,核心的地方在于兩個(gè)方面:

  1. SQLBuilder:SQL語句要非硬編碼,通過某種鏈?zhǔn)秸{(diào)用構(gòu)造器幫助我構(gòu)建SQL語句。
  2. Scanner:從數(shù)據(jù)庫返回的數(shù)據(jù)可以自動(dòng)映射賦值到結(jié)構(gòu)體中。

SQL SelectBuilder

我們嘗試做個(gè)簡略版的查詢語句構(gòu)造器,最終我們要達(dá)到如下圖所示的效果。

我們可以通過和SQL關(guān)鍵字同名的方法來表達(dá)SQL語句的固有關(guān)鍵字,通過go方法參數(shù)來設(shè)置其中動(dòng)態(tài)變化的元素,這樣鏈?zhǔn)秸{(diào)用和寫SQL語句的思維順序是一致的,只不過我們之前通過硬編碼的方式變成了方法調(diào)用。

具體代碼如下:

type SelectBuilder struct {
builder *strings.Builder
column []string
tableName string
where []func(s *SelectBuilder)
args []interface{}
orderby string
offset *int64
limit *int64
}

func (s *SelectBuilder) Select(field ...string) *SelectBuilder {
s.column = append(s.column, field...)
return s
}

func (s *SelectBuilder) From(name string) *SelectBuilder {
s.tabelName = name
return s
}
func (s *SelectBuilder) Where(f ...func(s *SelectBuilder)) *SelectBuilder {
s.where = append(s.where, f...)
return s
}
func (s *SelectBuilder) OrderBy(field string) *SelectBuilder {
s.orderby = field
return s
}
func (s *SelectBuilder) Limit(offset, limit int64) *SelectBuilder {
s.offset = &offset
s.limit = &limit
return s
}
func GT(field string, arg interface{}) func(s *SelectBuilder) {
return func(s *SelectBuilder) {
s.builder.WriteString("`" + field + "`" + " > ?")
s.args = append(s.args, arg)
}
}
func (s *SelectBuilder) Query() (string, []interface{}) {
s.builder.WriteString("SELECT ")
for k, v := range s.column {
if k > 0 {
s.builder.WriteString(",")
}
s.builder.WriteString("`" + v + "`")
}
s.builder.WriteString(" FROM ")
s.builder.WriteString("`" + s.tableName + "` ")
if len(s.where) > 0 {
s.builder.WriteString("WHERE ")
for k, f := range s.where {
if k > 0 {
s.builder.WriteString(" AND ")
}
f(s)
}
}
if s.orderby != "" {
s.builder.WriteString(" ORDER BY " + s.orderby)
}
if s.limit != nil {
s.builder.WriteString(" LIMIT ")
s.builder.WriteString(strconv.FormatInt(*s.limit, 10))
}
if s.offset != nil {
s.builder.WriteString(" OFFSET ")
s.builder.WriteString(strconv.FormatInt(*s.offset, 10))
}
return s.builder.String(), s.args
}

  1. 通過結(jié)構(gòu)體上的方法調(diào)用返回自身,使其具有鏈?zhǔn)秸{(diào)用能力,并通過方法調(diào)用設(shè)置結(jié)構(gòu)體中的值,用以構(gòu)成SQL語句需要的元素。
  2. SelectBuilder 包含性能較高的strings.Builder 來拼接字符串。
  3. Query()方法構(gòu)建出真正的SQL語句,返回包含占位符的SQL語句和args參數(shù)。
  4. []func(s *SelectBuilder)通過函數(shù)數(shù)組來創(chuàng)建查詢條件,可以通過函數(shù)調(diào)用的順序和層級(jí)來生成 AND OR這種有嵌套關(guān)系的查詢條件子句。
  5. Where() 傳入的是查詢條件函數(shù),為可變參數(shù)列表,查詢條件之間默認(rèn)是AND關(guān)系。

外部使用起來效果:

b := SelectBuilder{builder: &strings.Builder{}}
sql, args := b.
Select("id", "name", "age", "ctime", "mtime").
From("user").
Where(GT("id", 0), GT("age", 0)).
OrderBy("id").
Limit(0, 20).
Query()

Scanner的實(shí)現(xiàn)

顧名思義Scanner的作用就是把查詢結(jié)果設(shè)置到對(duì)應(yīng)的go對(duì)象上去,完成關(guān)系和對(duì)象的映射,關(guān)鍵核心就是通過反射獲知傳入對(duì)象的類型和字段類型,通過反射創(chuàng)建對(duì)象和值,并通過golang結(jié)構(gòu)體的字段后面的tag來和查詢結(jié)果的表頭一一對(duì)應(yīng),達(dá)到動(dòng)態(tài)給結(jié)構(gòu)字段賦值的能力。

具體實(shí)現(xiàn)如下:

func ScanSlice(rows *sql.Rows, dst interface{}) error {
defer rows.Close()
// dst的地址
val := reflect.ValueOf(dst) // &[]*main.User
// 判斷是否是指針類型,go是值傳遞,只有傳指針才能讓更改生效
if val.Kind() != reflect.Ptr {
return errors.New("dst not a pointer")
}
// 指針指向的Value
val = reflect.Indirect(val) // []*main.User
if val.Kind() != reflect.Slice {
return errors.New("dst not a pointer to slice")
}
// 獲取slice中的類型
struPointer := val.Type().Elem() // *main.User
// 指針指向的類型 具體結(jié)構(gòu)體
stru := struPointer.Elem() // main.User

cols, err := rows.Columns() // [id,name,age,ctime,mtime]
if err != nil {
return err
}
// 判斷查詢的字段數(shù)是否大于 結(jié)構(gòu)體的字段數(shù)
if stru.NumField() < len(cols) { // 5,5
return errors.New("NumField and cols not match")
}
//結(jié)構(gòu)體的json tag的value對(duì)應(yīng)字段在結(jié)構(gòu)體中的index
tagIdx := make(map[string]int) //map tag -> field idx
for i := 0; i < stru.NumField(); i++ {
tagname := stru.Field(i).Tag.Get("json")
if tagname != "" {
tagIdx[tagname] = i
}
}
resultType := make([]reflect.Type, 0, len(cols)) // [int64,string,int64,time.Time,time.Time]
index := make([]int, 0, len(cols)) // [0,1,2,3,4,5]
// 查找和列名相對(duì)應(yīng)的結(jié)構(gòu)體jsontag name的字段類型,保存類型和序號(hào)到resultType和index中
for _, v := range cols {
if i, ok := tagIdx[v]; ok {
resultType = append(resultType, stru.Field(i).Type)
index = append(index, i)
}
}
for rows.Next() {
// 創(chuàng)建結(jié)構(gòu)體指針,獲取指針指向的對(duì)象
obj := reflect.New(stru).Elem() // main.User
result := make([]interface{}, 0, len(resultType)) //[]
// 創(chuàng)建結(jié)構(gòu)體字段類型實(shí)例的指針,并轉(zhuǎn)化為interface{} 類型
for _, v := range resultType {
result = append(result, reflect.New(v).Interface()) // *Int64 ,*string ....
}
// 掃描結(jié)果
err := rows.Scan(result...)
if err != nil {
return err
}
for i, v := range result {
// 找到對(duì)應(yīng)的結(jié)構(gòu)體index
fieldIndex := index[i]
// 把scan 后的值通過反射得到指針指向的value,賦值給對(duì)應(yīng)的結(jié)構(gòu)體字段
obj.Field(fieldIndex).Set(reflect.ValueOf(v).Elem()) // 給obj 的每個(gè)字段賦值
}
// append 到slice
vv := reflect.Append(val, obj.Addr()) // append到 []*main.User, maybe addr change
val.Set(vv) // []*main.User
}
return rows.Err()
}

通過反射賦值流程,如果想知道具體的實(shí)現(xiàn)細(xì)節(jié)可以仔細(xì)閱讀上面代碼里面的注釋

  1. 以上主要的思想就是通過reflect包來獲取傳入dst的Slice類型,并通過反射創(chuàng)建其包含的對(duì)象,具體的步驟和解釋請(qǐng)仔細(xì)閱讀注釋和圖例。
  2. 通過指定的json tag 可以把查詢結(jié)果和結(jié)構(gòu)體字段mapping起來,即使查詢語句中字段不按照表結(jié)構(gòu)順序。
  3. ScanSlice是通用的Scanner。
  4. 使用反射創(chuàng)建對(duì)象明顯創(chuàng)建了多余的對(duì)象,沒有傳統(tǒng)的方式賦值高效,但是換來的巨大的靈活性在某些場(chǎng)景下是值得的。

有了SQLBuilder和Scanner 我們就可以這樣寫查詢函數(shù)了:

func FindUserReflect() ([]*User, error) {
b := SelectBuilder{builder: &strings.Builder{}}
sql, args := b.
Select("id", "name", "age", "ctime", "mtime").
From("user").
Where(GT("id", 0), GT("age", 0)).
OrderBy("id").
Limit(0, 20).
Query()

rows, err := db.QueryContext(ctx, sql, args...)
if err != nil {
return nil, err
}
result := []*User{}
err = ScanSlice(rows, &result)
if err != nil {
return nil, err
}
return result, nil
}

生成的查詢SQL語句和args如下:

SELECT `id`,`name`,`age`,`ctime`,`mtime` FROM `user` WHERE `id` > ? AND `age` > ? ORDER BY id LIMIT 20 OFFSET 0  [0 0]

自動(dòng)生成

通過上面的使用的例子來看,我們的工作輕松了不少:

  • 第一:SQL語句不需要硬編碼了;
  • 第二:Scan不需要寫大量結(jié)構(gòu)體字段和的乏味的重復(fù)代碼。

著實(shí)幫我們省了很大的麻煩。但是查詢字段還需要我們自己手寫,像這種

Select("id", "name", "age", "ctime", "mtime").

  • 其中傳入的字段需要我們硬編碼,我們可不可以再進(jìn)一步,通過表結(jié)構(gòu)定義來生成我們的golang結(jié)構(gòu)體呢?答案是肯定的,要實(shí)現(xiàn)這一步我們需要一個(gè)SQL語句的解析器(https://github.com/xwb1989/sqlparser),把SQL DDL語句解析成go語言中如下的Table對(duì)象,其所包含的表名,列名、列類型、注釋等都能獲取到,再通過這些對(duì)象和寫好的模板代碼來生成我們實(shí)際業(yè)務(wù)使用的代碼。

Table對(duì)象如下:

type Table struct {
TableName string // table name
GoTableName string // go struct name
PackageName string // package name
Fields []*Column // columns
}
type Column struct {
ColumnName string // column_name
ColumnType string // column_type
ColumnComment string // column_comment
}

使用以上Table對(duì)象的模板代碼:

type {{.GoTableName}} struct {
{{- range .Fields }}
{{ .GoColumnName }} {{ .GoColumnType }} `json:"` `.`ColumnName `"` // {{ .ColumnComment }}
{{- end}}
}
const (
table = "``.`TableName`"
{{- range .Fields}}
{{ .GoColumnName}} = "``.`ColumnName`"
{{- end }}
)
var columns = []string{
{{- range .Fields}}
{{ .GoColumnName}},
{{- end }}
}

通過上面的模板我們用user表的建表SQL語句生成如下代碼:

type User struct {
Id int64 `json:"id"` // id字段
Name string `json:"name"` // 名稱
Age int64 `json:"age"` // 年齡
Ctime time.Time `json:"ctime"` // 創(chuàng)建時(shí)間
Mtime time.Time `json:"mtime"` // 更新時(shí)間
}
const (
table = "user"
Id = "id"
Name = "name"
Age = "age"
Ctime = "ctime"
Mtime = "mtime"
)
var Columns = []string{"id","name","age","ctime","mtime"}

那么我們?cè)诓樵兊臅r(shí)候就可以這樣使用

Select(Columns...)

通過模板自動(dòng)生成代碼,可以大大的減輕開發(fā)編碼負(fù)擔(dān),使我們從繁重的代碼中解放出來。

reflect真的有必要嗎?

由于我們SELECT時(shí)選擇查找的字段和順序是不固定的,我們有可能 SELECT id, name, age FROM user,也可能 SELECT name, id FROM user,有很大的任意性,這種情況使用反射出來的結(jié)構(gòu)體tag和查詢的列名來確定映射關(guān)系是必須的。但是有一種情況我們不需要用到反射,而且是一種最常用的情況,即:查詢的字段名和表結(jié)構(gòu)的列名一致,且順序一致。這時(shí)候我們可以這么寫,通過DeepEqual來判斷查詢字段和表結(jié)構(gòu)字段是否一致且順序一致來決定是否通過反射還是通過傳統(tǒng)方法來創(chuàng)建對(duì)象。用傳統(tǒng)方式創(chuàng)建對(duì)象(如下圖第12行)令我們編碼痛苦,不過可以通過模板來自動(dòng)生成下面的代碼,以避免手寫,這樣既靈活方便好用,性能又沒有損耗,看起來是一個(gè)比較完美的解決方案。

func FindUserNoReflect(b *SelectBuilder) ([]*User, error) {
sql, args := b.Query()
rows, err := db.QueryContext(ctx, sql, args...)
if err != nil {
return nil, err
}
result := []*User{}
if DeepEqual(b.column, Columns) {
defer rows.Close()
for rows.Next() {
a := &User{}
if err := rows.Scan(&a.Id, &a.Name, &a.Age, &a.Ctime, &a.Mtime); err != nil {
return nil, err
}
result = append(result, a)
}
if rows.Err() != nil {
return nil, rows.Err()
}
return result, nil
}
err = ScanSlice(rows, &result)
if err != nil {
return nil, err
}
return result, nil
}

總結(jié)

  1. 通過database/sql 庫開發(fā)有較大痛點(diǎn),ORM就是為了解決以上問題而生,其存在是有意義的。
  2. ORM 兩個(gè)關(guān)鍵的部分是SQLBuilder和Scanner的實(shí)現(xiàn)。
  3. ORM Scanner 使用反射創(chuàng)建對(duì)象在性能上肯定會(huì)有一定的損失,但是帶來極大的靈活性,同時(shí)在查詢?nèi)碜侄芜@種特殊情況下規(guī)避使用反射來提高性能。

展望

通過表結(jié)構(gòu),我們可以生成對(duì)應(yīng)的結(jié)構(gòu)體和持久層增刪改查代碼,我們?cè)偻皵U(kuò)展一步,能否通過表結(jié)構(gòu)生成的proto格式的message,以及一些常用的CRUD GRPC rpc接口定義。通過工具,我們甚至可以把前端的代碼都生成好,實(shí)現(xiàn)半自動(dòng)化編程。我想這個(gè)是值得期待的。

參考資料:[1] ??https://github.com/ent/ent??

?本期作者:洪勝杰

B端技術(shù)中心高級(jí)開發(fā)工程師?


分享名稱:如何用Go實(shí)現(xiàn)一個(gè)ORM
網(wǎng)頁鏈接:http://www.dlmjj.cn/article/cccdsco.html