「SEROKU フリーランス」の中の人をやっている syunsuke です。SEROKU では主にインフラ面の担当をしています。 最近 DevOps 界隈でよく聞く監視システム Prometheus。 WESEEK 社内 […]
beego で実装した掲示板サービスの Go言語を読んでみる その2
完全に前回の続きで、引き続き go言語での開発始めてみる〜beego で掲示板っぽいもの作ってみる編〜 で作成した 掲示板風アプリ で beego の scaffold 機能を使って自動生成された go のコードを読み進めていきたいと思います。
前回の重複にはなりますが 筆者は A Tour of Go を一周した程度であり、初心者の初心者による初心者のためにわかりやすく説明を行うことを心掛けていっています。
引き続きA Tour of Go のリンクなどを交えて解説していこうと思います。
/models/post.go
各種CRUD
https://github.com/haruhikonyan/beegotest/blob/master/haruch/models/post.go#L41-L143
前回の続きでここからスタートです。
- GetAllPost
// GetAllPost retrieves all Post matches certain condition. Returns empty list if // no records exist func GetAllPost(query map[string]string, fields []string, sortby []string, order []string, offset int64, limit int64) (ml []interface{}, err error) { o := orm.NewOrm() qs := o.QueryTable(new(Post)) // query k=v for k, v := range query { // rewrite dot-notation to Object__Attribute k = strings.Replace(k, ".", "__", -1) qs = qs.Filter(k, v) } // order by: var sortFields []string if len(sortby) != 0 { if len(sortby) == len(order) { // 1) for each sort field, there is an associated order for i, v := range sortby { orderby := "" if order[i] == "desc" { orderby = "-" + v } else if order[i] == "asc" { orderby = v } else { return nil, errors.New("Error: Invalid order. Must be either [asc|desc]") } sortFields = append(sortFields, orderby) } qs = qs.OrderBy(sortFields...) } else if len(sortby) != len(order) && len(order) == 1 { // 2) there is exactly one order, all the sorted fields will be sorted by this order for _, v := range sortby { orderby := "" if order[0] == "desc" { orderby = "-" + v } else if order[0] == "asc" { orderby = v } else { return nil, errors.New("Error: Invalid order. Must be either [asc|desc]") } sortFields = append(sortFields, orderby) } } else if len(sortby) != len(order) && len(order) != 1 { return nil, errors.New("Error: 'sortby', 'order' sizes mismatch or 'order' size is not 1") } } else { if len(order) != 0 { return nil, errors.New("Error: unused 'order' fields") } } var l []Post qs = qs.OrderBy(sortFields...).RelatedSel() if _, err = qs.Limit(limit, offset).All(&l, fields...); err == nil { if len(fields) == 0 { for _, v := range l { ml = append(ml, v) } } else { // trim unused fields for _, v := range l { m := make(map[string]interface{}) val := reflect.ValueOf(v) for _, fname := range fields { m[fname] = val.FieldByName(fname).Interface() } ml = append(ml, m) } } return ml, nil } return nil, err }
長い。。。
やろうとしていることは指定された引数に基づいて一致する Post を DB からすべて取得するというメソッドです。引数
- query map[string]string
map[string]string
とは string が key で value も string である map のことです。
この query には絞り込み対象のフィールド key に対して、絞り込み文字列の value を設定します。 - fields []string
DB から引っ張ってくるカラムを制限する際にここの引数にフィールド名を指定します。 - sortby []string
ソートをする際のフィールド名をここに指定し、複数指定することもできます。 - order []string
sortby で指定したソートするフィールドを昇順にするか降順にするかを指定できます。
型自体はただの string のスライスですが、何をスライスに入れても良いとう訳ではなく、認められているのは昇順であるasc
と、降順であるdesc
のどちらかの文字列のスライスです。
後述の処理を見ていただければわかりますが sortby のスライスと対応しており、数も一致させる必要があります。
例外的に全て同じ order でソートする場合には複数のsortby
に対して1つだけの指定ができます。 - offset int64
いわゆるオフセットで、指定した数文後ろのものから取得します。
paging とかの実装でよく使われるイメージです。 - limit int64
これも文字通りで、最大何件取得するかです。
処理内容
- クエリ作成まで
o := orm.NewOrm() qs := o.QueryTable(new(Post)) // query k=v for k, v := range query { // rewrite dot-notation to Object__Attribute k = strings.Replace(k, ".", "__", -1) qs = qs.Filter(k, v) }
まず
orm
のインスタンスを生成してo
という変数に取ります。そのあと、orm
のQueryTable()メソッドで一番上の例の2つ目のオブジェクト自体をテーブル名として使い、QuerySeter
オブジェクトを取得します。(QuerySeter ってなんだろうって前回の記事では言っていたんですが、別の人から setter なんじゃないかとの助言をいただき、やっと意味が通りました!クエリをセットするものなんですね(まんま))
その後 range で 引数で受け取った query すべてを処理していきます。
k
つまりは key を strings.Replace で 第4引数に-1
を与えているので文字列値のすべての.
を__
に置換してk
へ再代入しています。そして qs.Filter へ渡してあげます。するといわゆる SQL の where での絞り込みができるわけです。 - ソート処理
// order by: var sortFields []string if len(sortby) != 0 { if len(sortby) == len(order) { // 1) for each sort field, there is an associated order for i, v := range sortby { orderby := "" if order[i] == "desc" { orderby = "-" + v } else if order[i] == "asc" { orderby = v } else { return nil, errors.New("Error: Invalid order. Must be either [asc|desc]") } sortFields = append(sortFields, orderby) } qs = qs.OrderBy(sortFields...) } else if len(sortby) != len(order) && len(order) == 1 { // 2) there is exactly one order, all the sorted fields will be sorted by this order for _, v := range sortby { orderby := "" if order[0] == "desc" { orderby = "-" + v } else if order[0] == "asc" { orderby = v } else { return nil, errors.New("Error: Invalid order. Must be either [asc|desc]") } sortFields = append(sortFields, orderby) } } else if len(sortby) != len(order) && len(order) != 1 { return nil, errors.New("Error: 'sortby', 'order' sizes mismatch or 'order' size is not 1") } } else { if len(order) != 0 { return nil, errors.New("Error: unused 'order' fields") } }
長いですが if 文で場合分けされているだけなので冷静に読み解いていきましょう。
まずはvar sortFields []string
でsortFields
という空の string のスライスを宣言します。ここにはソートする対象のフィールド名を格納していきます。
以下は分岐に入ります。if len(sortby) != 0 {} else {}
引数sortby
の length が 0 でない場合と 1 以上である場合でわけられています。sortby
が1つ以上指定されている場合
さらに場合分けif len(sortby) == len(order)
引数で渡されたsortby
とorder
のスライスのサイズが一致している場合
for i, v := range sortby
でsortby
を range を使って順番に処理していきます。
まず結果を格納するorderby := ""
を定義。次に分岐でif order[i] == "desc"
sortby
と同じ index のorder
が"desc"
(降順)であった場合はorderby = "-" + v
sortby
に-
を付けてorderby
に格納します。
はたまたelse if order[0] == "asc"
"asc"
(昇順)であった場合はそのままsortby
をorderby
に格納します。
それ以外のパターンはorder
に"asc"
"desc"
以外の値が
代入されているということでエラーを出力します。
分岐を抜けたら最後に一番最初に定義したsortFields
へorderby
を新たに append します。if len(sortby) != len(order) && len(order) == 1
引数で渡されたsortby
とorder
のスライスのサイズが一致しておらず、order
が1つだけ指定されている場合
基本的に上記のorder
をsortby
と同じ数だけ定義した際と処理は変わらず、こちらはorder
を一つしか指定していないので常にorder[0]
を指定してすべてのsortby
に適用し、orderby
に格納、最後にsortFields
に append しています。if len(sortby) != len(order) && len(order) != 1
引数で渡されたsortby
とorder
のスライスのサイズが一致しておらず、order
の指定が1つで無い場合 error を生成して返します。
sortby
に対して数が一致してないところにorder
が2つ以上あってもどう返していいのかわからないのであたりまです。
sortby
が指定されていない場合if len(order) != 0 { return nil, errors.New("Error: unused 'order' fields") }
ここもさらに分岐があり、
order
が1つ以上指定されている場合は error を生成して返します。
sortby
が無いにもかかわらずorder
が指定されているのはおかしいということです。
- 実際に値を返すまで
var l []Post qs = qs.OrderBy(sortFields...).RelatedSel() if _, err = qs.Limit(limit, offset).All(&l, fields...); err == nil { if len(fields) == 0 { for _, v := range l { ml = append(ml, v) } } else { // trim unused fields for _, v := range l { m := make(map[string]interface{}) val := reflect.ValueOf(v) for _, fname := range fields { m[fname] = val.FieldByName(fname).Interface() } ml = append(ml, m) } } return ml, nil } return nil, err
まず
var l []Post
でPost
struct のスライスを定義しておきます。
次に一番最初のほうで取得しておいたqs
(QuerySeter(Setter)) の OrderByにsortFields
... を渡し、ソート処理を追加します。(例では一つのフィールドに対してですが可変長引数で複数の値を渡せるみたいですね)それに対してRelatedSel() で、何も引数を指定しないことで、関連テーブルをすべて Join します。
ちなみに ...とは可変長引数に対して配列やスライスを展開して渡せるものです。
qs.Limit(limit, offset).All(&l, fields...)
こちらでは qs.Limit の第一引数に最大取得数limit
と オフセットoffset
を指定して取得数等を絞ります。そして All に最初に定義だけしたl
のポインタと、取得するフィールド名を指定したfields...
としてすべてを渡してあげ取得します。Allメソッドでは第一引数に渡したl
に取得した []Post が代入されます。
いつも通りerr
がnil
であれば if の中を実行します。
指定したfields
のサイズが0(未指定)であれば range を使いl
の中身を全てml
へ append していきます。
指定したfields
が存在していればこちらも range を使いl
の中身を順に処理します。
まずm := make(map[string]interface{})
で map を makeで初期化し、変数m
へ代入します。その後 reflect.ValueOf で reflect.Value 型のオブジェクトを取得しval
に代入します。reflect.ValueOf
についてはこのへんが参考になるかと思います。
続いて引数で指定されていたfields
を range を使い、すべての指定されたフィールドの値をval.FieldByName(fname) で取得し、Interface() でinterface{}
を取得してm
に代入していきます。すべて代入が終わったところでml
へすべて append します。
どちらかの分岐で処理が完了し、エラーが無ければreturn ml, nil
値を返却します。またここまででエラーが出た場合はreturn nil, err
にて値は返さずに、エラーのみを返却します。
とても長かったですが、やっていることは与えられた引数のコンディションによって DB から取得し得てくる値を変えているだけです。
- query map[string]string
- UpdatePostById
// UpdatePost updates Post by Id and returns error if // the record to be updated doesn't exist func UpdatePostById(m *Post) (err error) { o := orm.NewOrm() v := Post{Id: m.Id} // ascertain id exists in the database if err = o.Read(&v); err == nil { var num int64 if num, err = o.Update(m); err == nil { fmt.Println("Number of records updated in database:", num) } } return }
次にレコードの更新です。
このメソッドは更新後の値の入ったPost
struct を引数で渡してあげ、DBの値を更新するものです。
最初はいつも通りorm
のインスタンスを生成してo
という変数に取り、引数で受け取った更新対象であるm.id
でPost
の構造体を初期化してv
に代入します。
o.Read() に先ほど初期化したv
を渡してあげて引数で受け取ったid
と一致する Post が DB 内に存在するかどうかを確認します。
そしてerr
がnil
であれば if 文の中に入ります。
o.Update()に更新対象の ID と、更新をする各種値を持った Post を渡してあげ、エラー無く処理が出来たらfmt.Println
にてメッセージを表示し、更新が完了します。 - DeletePost
// DeletePost deletes Post by Id and returns error if // the record to be deleted doesn't exist func DeletePost(id int64) (err error) { o := orm.NewOrm() v := Post{Id: id} // ascertain id exists in the database if err = o.Read(&v); err == nil { var num int64 if num, err = o.Delete(&Post{Id: id}); err == nil { fmt.Println("Number of records deleted in database:", num) } } return }
最後にレコードの削除です。
id
を受け取ってその id の Post レコードを DB から削除します。
毎度おなじみorm
のインスタンスを生成してo
という変数に取り、引数で受け取ったid
でPost
の構造体を初期化してv
に代入します。
o.Read() に先ほど初期化したv
を渡してあげて引数で受け取ったid
と一致する Post が DB 内に存在するかどうかを確認します。
そしてerr
がnil
であれば if 文の中に入ります。
o.Delete()に削除対象の ID を持った Post を渡してあげ、エラー無く処理が出来たらfmt.Println
にてメッセージを表示し、削除が完了します。
ちなみにo.Delete()の引数に渡してる&Post{Id: id}
ですが、同じものを変数に取ってる&v
を渡しても結果は変わりませんでした。なぜ自動生成のコードが struct を作り直しているのかはちょっとよくわかりません。(明確な理由があればぜひ誰か教えてください!)
[adinserter block="1"]
まとめ
ここまでで前回と合わせて /models/post.go
のコードをすべて読んでいきました。だいぶ go の基本的な文法と beego での orm の使い方には慣れてきたんじゃないでしょうか。文法が分かれば複雑な処理も一つ一つ意味が見えてきて超長かった GetAllPost メソッドも結局はソート処理で配列二つを複雑に条件分岐で処理しているだけで大したことはないという感じでした。ちょっとまだ reflect
みたいなちょっとトリッキーな型が絡んだ使い方みたいなのはまだまだ勉強が必要だと思いましたが。
みなさんも A Tour of Go 片手に go のコードを読みそして書いてみて web のバックエンドマスター目指してみてはいかがでしょう。
>Go言語での開発を試してみる 〜調べる編〜
>go言語での開発始めてみる〜開発環境を作る編〜
>Go言語での開発始めてみる〜beego のコード自動作成機能を試す編〜
>beego で実装した掲示板サービスの Go言語を読む〜その1〜
haruhikonyan
大きな野望を抱くホルン吹き。