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 から取得し得てくる値を変えているだけです。

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

大きな野望を抱くホルン吹き。