beegoで実装した掲示板サービスのGo言語を読んでみる|その2

完全にbeegoで実装した掲示板サービスの Go言語を読む|その1の続きで、引き続き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
<span class="hljs-comment">// GetAllPost retrieves all Post matches certain condition. Returns empty list if</span>
<span class="hljs-comment">// no records exist</span>
<span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">GetAllPost</span><span class="hljs-params">(query <span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">string</span>, fields []<span class="hljs-keyword">string</span>, sortby []<span class="hljs-keyword">string</span>, order []<span class="hljs-keyword">string</span>,
offset <span class="hljs-keyword">int64</span>, limit <span class="hljs-keyword">int64</span>)</span> <span class="hljs-params">(ml []<span class="hljs-keyword">interface</span>{}, err error)</span></span> {
o := orm.NewOrm()
qs := o.QueryTable(<span class="hljs-built_in">new</span>(Post))
<span class="hljs-comment">// query k=v</span>
<span class="hljs-keyword">for</span> k, v := <span class="hljs-keyword">range</span> query {
    <span class="hljs-comment">// rewrite dot-notation to Object__Attribute</span>
    k = strings.Replace(k, <span class="hljs-string">"."</span>, <span class="hljs-string">"__"</span>, <span class="hljs-number">-1</span>)
    qs = qs.Filter(k, v)
}
<span class="hljs-comment">// order by:</span>
<span class="hljs-keyword">var</span> sortFields []<span class="hljs-keyword">string</span>
<span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(sortby) != <span class="hljs-number">0</span> {
    <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(sortby) == <span class="hljs-built_in">len</span>(order) {
        <span class="hljs-comment">// 1) for each sort field, there is an associated order</span>
        <span class="hljs-keyword">for</span> i, v := <span class="hljs-keyword">range</span> sortby {
            orderby := <span class="hljs-string">""</span>
            <span class="hljs-keyword">if</span> order[i] == <span class="hljs-string">"desc"</span> {
                orderby = <span class="hljs-string">"-"</span> + v
            } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> order[i] == <span class="hljs-string">"asc"</span> {
                orderby = v
            } <span class="hljs-keyword">else</span> {
                <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, errors.New(<span class="hljs-string">"Error: Invalid order. Must be either [asc|desc]"</span>)
            }
            sortFields = <span class="hljs-built_in">append</span>(sortFields, orderby)
        }
        qs = qs.OrderBy(sortFields...)
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(sortby) != <span class="hljs-built_in">len</span>(order) && <span class="hljs-built_in">len</span>(order) == <span class="hljs-number">1</span> {
        <span class="hljs-comment">// 2) there is exactly one order, all the sorted fields will be sorted by this order</span>
        <span class="hljs-keyword">for</span> _, v := <span class="hljs-keyword">range</span> sortby {
            orderby := <span class="hljs-string">""</span>
            <span class="hljs-keyword">if</span> order[<span class="hljs-number">0</span>] == <span class="hljs-string">"desc"</span> {
                orderby = <span class="hljs-string">"-"</span> + v
            } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> order[<span class="hljs-number">0</span>] == <span class="hljs-string">"asc"</span> {
                orderby = v
            } <span class="hljs-keyword">else</span> {
                <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, errors.New(<span class="hljs-string">"Error: Invalid order. Must be either [asc|desc]"</span>)
            }
            sortFields = <span class="hljs-built_in">append</span>(sortFields, orderby)
        }
    } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(sortby) != <span class="hljs-built_in">len</span>(order) && <span class="hljs-built_in">len</span>(order) != <span class="hljs-number">1</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, errors.New(<span class="hljs-string">"Error: 'sortby', 'order' sizes mismatch or 'order' size is not 1"</span>)
    }
} <span class="hljs-keyword">else</span> {
    <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(order) != <span class="hljs-number">0</span> {
        <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, errors.New(<span class="hljs-string">"Error: unused 'order' fields"</span>)
    }
}

<span class="hljs-keyword">var</span> l []Post
qs = qs.OrderBy(sortFields...).RelatedSel()
<span class="hljs-keyword">if</span> _, err = qs.Limit(limit, offset).All(&l, fields...); err == <span class="hljs-literal">nil</span> {
    <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(fields) == <span class="hljs-number">0</span> {
        <span class="hljs-keyword">for</span> _, v := <span class="hljs-keyword">range</span> l {
            ml = <span class="hljs-built_in">append</span>(ml, v)
        }
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-comment">// trim unused fields</span>
        <span class="hljs-keyword">for</span> _, v := <span class="hljs-keyword">range</span> l {
            m := <span class="hljs-built_in">make</span>(<span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">interface</span>{})
            val := reflect.ValueOf(v)
            <span class="hljs-keyword">for</span> _, fname := <span class="hljs-keyword">range</span> fields {
                m[fname] = val.FieldByName(fname).Interface()
            }
            ml = <span class="hljs-built_in">append</span>(ml, m)
        }
    }
    <span class="hljs-keyword">return</span> ml, <span class="hljs-literal">nil</span>
}
<span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, 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(<span class="hljs-built_in">new</span>(Post))
<span class="hljs-comment">// query k=v</span>
<span class="hljs-keyword">for</span> k, v := <span class="hljs-keyword">range</span> query {
<span class="hljs-comment">// rewrite dot-notation to Object__Attribute</span>
k = strings.Replace(k, <span class="hljs-string">"."</span>, <span class="hljs-string">"__"</span>, <span class="hljs-number">-1</span>)
qs = qs.Filter(k, v)
}

まず orm のインスタンスを生成して o という変数に取ります。そのあと、ormQueryTable()メソッドで一番上の例の2つ目のオブジェクト自体をテーブル名として使い、 QuerySeter オブジェクトを取得します。(QuerySeter ってなんだろうって前回の記事では言っていたんですが、別の人から setter なんじゃないかとの助言をいただき、やっと意味が通りました!クエリをセットするものなんですね(まんま))
その後 range で 引数で受け取った query すべてを処理していきます。
k つまりは key を strings.Replace で 第4引数に -1 を与えているので文字列値のすべての.__ に置換して k へ再代入しています。そして qs.Filter へ渡してあげます。するといわゆる SQL の where での絞り込みができるわけです。

  • ソート処理
<span class="hljs-comment">// order by:</span>
<span class="hljs-keyword">var</span> sortFields []<span class="hljs-keyword">string</span>
<span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(sortby) != <span class="hljs-number">0</span> {
<span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(sortby) == <span class="hljs-built_in">len</span>(order) {
    <span class="hljs-comment">// 1) for each sort field, there is an associated order</span>
    <span class="hljs-keyword">for</span> i, v := <span class="hljs-keyword">range</span> sortby {
        orderby := <span class="hljs-string">""</span>
        <span class="hljs-keyword">if</span> order[i] == <span class="hljs-string">"desc"</span> {
            orderby = <span class="hljs-string">"-"</span> + v
        } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> order[i] == <span class="hljs-string">"asc"</span> {
            orderby = v
        } <span class="hljs-keyword">else</span> {
            <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, errors.New(<span class="hljs-string">"Error: Invalid order. Must be either [asc|desc]"</span>)
        }
        sortFields = <span class="hljs-built_in">append</span>(sortFields, orderby)
    }
    qs = qs.OrderBy(sortFields...)
} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(sortby) != <span class="hljs-built_in">len</span>(order) && <span class="hljs-built_in">len</span>(order) == <span class="hljs-number">1</span> {
    <span class="hljs-comment">// 2) there is exactly one order, all the sorted fields will be sorted by this order</span>
    <span class="hljs-keyword">for</span> _, v := <span class="hljs-keyword">range</span> sortby {
        orderby := <span class="hljs-string">""</span>
        <span class="hljs-keyword">if</span> order[<span class="hljs-number">0</span>] == <span class="hljs-string">"desc"</span> {
            orderby = <span class="hljs-string">"-"</span> + v
        } <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> order[<span class="hljs-number">0</span>] == <span class="hljs-string">"asc"</span> {
            orderby = v
        } <span class="hljs-keyword">else</span> {
            <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, errors.New(<span class="hljs-string">"Error: Invalid order. Must be either [asc|desc]"</span>)
        }
        sortFields = <span class="hljs-built_in">append</span>(sortFields, orderby)
    }
} <span class="hljs-keyword">else</span> <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(sortby) != <span class="hljs-built_in">len</span>(order) && <span class="hljs-built_in">len</span>(order) != <span class="hljs-number">1</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, errors.New(<span class="hljs-string">"Error: 'sortby', 'order' sizes mismatch or 'order' size is not 1"</span>)
}
} <span class="hljs-keyword">else</span> {
<span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(order) != <span class="hljs-number">0</span> {
    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, errors.New(<span class="hljs-string">"Error: unused 'order' fields"</span>)
}
}

長いですが if 文で場合分けされているだけなので冷静に読み解いていきましょう。
まずは var sortFields []stringsortFields という空の string のスライスを宣言します。ここにはソートする対象のフィールド名を格納していきます。
以下は分岐に入ります。

  • if len(sortby) != 0 {} else {}
    引数 sortbylength が 0 でない場合と 1 以上である場合でわけられています。

    • sortby が1つ以上指定されている場合
      さらに場合分け

      • if len(sortby) == len(order)
        引数で渡された sortbyorderスライスのサイズが一致している場合
        for i, v := range sortbysortbyrange を使って順番に処理していきます。
        まず結果を格納する orderby := "" を定義。次に分岐で if order[i] == "desc" sortby と同じ index の order"desc"(降順)であった場合は orderby = "-" + v sortby- を付けて orderby に格納します。
        はたまた else if order[0] == "asc" "asc" (昇順)であった場合はそのまま sortbyorderby に格納します。
        それ以外のパターンは order"asc" "desc" 以外の値が
        代入されているということでエラーを出力します。
        分岐を抜けたら最後に一番最初に定義したsortFieldsorderby を新たに append します。
      • if len(sortby) != len(order) && len(order) == 1
        引数で渡された sortbyorderスライスのサイズが一致しておらず、order が1つだけ指定されている場合
        基本的に上記の ordersortby と同じ数だけ定義した際と処理は変わらず、こちらは order を一つしか指定していないので常にorder[0] を指定してすべての sortby に適用し、orderby に格納、最後に sortFieldsappend しています。
      • if len(sortby) != len(order) && len(order) != 1
        引数で渡された sortbyorderスライスのサイズが一致しておらず、order の指定が1つで無い場合 error を生成して返します。
        sortby に対して数が一致してないところに order が2つ以上あってもどう返していいのかわからないのであたりまです。
    • sortby が指定されていない場合

      if len(order) != 0 {
       return nil, errors.New("Error: unused 'order' fields")
      }

      ここもさらに分岐があり、order が1つ以上指定されている場合は error を生成して返します。
      sortby が無いにもかかわらず order が指定されているのはおかしいということです。

  • 実際に値を返すまで

    <span class="hljs-keyword">var</span> l []Post
    qs = qs.OrderBy(sortFields...).RelatedSel()
    <span class="hljs-keyword">if</span> _, err = qs.Limit(limit, offset).All(&l, fields...); err == <span class="hljs-literal">nil</span> {
    <span class="hljs-keyword">if</span> <span class="hljs-built_in">len</span>(fields) == <span class="hljs-number">0</span> {
        <span class="hljs-keyword">for</span> _, v := <span class="hljs-keyword">range</span> l {
            ml = <span class="hljs-built_in">append</span>(ml, v)
        }
    } <span class="hljs-keyword">else</span> {
        <span class="hljs-comment">// trim unused fields</span>
        <span class="hljs-keyword">for</span> _, v := <span class="hljs-keyword">range</span> l {
            m := <span class="hljs-built_in">make</span>(<span class="hljs-keyword">map</span>[<span class="hljs-keyword">string</span>]<span class="hljs-keyword">interface</span>{})
            val := reflect.ValueOf(v)
            <span class="hljs-keyword">for</span> _, fname := <span class="hljs-keyword">range</span> fields {
                m[fname] = val.FieldByName(fname).Interface()
            }
            ml = <span class="hljs-built_in">append</span>(ml, m)
        }
    }
    <span class="hljs-keyword">return</span> ml, <span class="hljs-literal">nil</span>
    }
    <span class="hljs-keyword">return</span> <span class="hljs-literal">nil</span>, err

    まず var l []PostPost struct のスライスを定義しておきます。
    次に一番最初のほうで取得しておいた qs (QuerySeter(Setter)) の OrderBysortFields... を渡し、ソート処理を追加します。(例では一つのフィールドに対してですが可変長引数で複数の値を渡せるみたいですね)それに対してRelatedSel() で、何も引数を指定しないことで、関連テーブルをすべて Join します。
    ちなみに ...とは可変長引数に対して配列やスライスを展開して渡せるものです。
    qs.Limit(limit, offset).All(&l, fields...) こちらでは qs.Limit の第一引数に最大取得数 limit と オフセット offset を指定して取得数等を絞ります。そして All に最初に定義だけした lポインタと、取得するフィールド名を指定した fields... としてすべてを渡してあげ取得します。Allメソッドでは第一引数に渡した l に取得した []Post が代入されます。
    いつも通り errnil であれば if の中を実行します。
    指定した fields のサイズが0(未指定)であれば range を使い l の中身を全て mlappend していきます。
    指定した fields が存在していればこちらも range を使い l の中身を順に処理します。
    まず m := make(map[string]interface{})mapmakeで初期化し、変数 m へ代入します。その後 reflect.ValueOf で reflect.Value 型のオブジェクトを取得し val に代入します。reflect.ValueOf についてはこのへんが参考になるかと思います。
    続いて引数で指定されていた fieldsrange を使い、すべての指定されたフィールドの値を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.idPost の構造体を初期化して v に代入します。
    o.Read() に先ほど初期化した v を渡してあげて引数で受け取った id と一致する Post が DB 内に存在するかどうかを確認します。
    そして errnil であれば if 文の中に入ります。
    o.Update()に更新対象の ID と、更新をする各種値を持った Post を渡してあげ、エラー無く処理が出来たら fmt.Println にてメッセージを表示し、更新が完了します。

  • DeletePost

    <span class="hljs-comment">// DeletePost deletes Post by Id and returns error if</span>
    <span class="hljs-comment">// the record to be deleted doesn't exist</span>
    <span class="hljs-function"><span class="hljs-keyword">func</span> <span class="hljs-title">DeletePost</span><span class="hljs-params">(id <span class="hljs-keyword">int64</span>)</span> <span class="hljs-params">(err error)</span></span> {
    o := orm.NewOrm()
    v := Post{Id: id}
    <span class="hljs-comment">// ascertain id exists in the database</span>
    <span class="hljs-keyword">if</span> err = o.Read(&v); err == <span class="hljs-literal">nil</span> {
        <span class="hljs-keyword">var</span> num <span class="hljs-keyword">int64</span>
        <span class="hljs-keyword">if</span> num, err = o.Delete(&Post{Id: id}); err == <span class="hljs-literal">nil</span> {
            fmt.Println(<span class="hljs-string">"Number of records deleted in database:"</span>, num)
        }
    }
    <span class="hljs-keyword">return</span>
    }

    最後にレコードの削除です。 id を受け取ってその id の Post レコードを DB から削除します。
    毎度おなじみ orm のインスタンスを生成して o という変数に取り、引数で受け取った idPost の構造体を初期化して v に代入します。
    o.Read() に先ほど初期化した v を渡してあげて引数で受け取った id と一致する Post が DB 内に存在するかどうかを確認します。
    そして errnil であれば if 文の中に入ります。
    o.Delete()に削除対象の ID を持った Post を渡してあげ、エラー無く処理が出来たら fmt.Println にてメッセージを表示し、削除が完了します。
    ちなみにo.Delete()の引数に渡してる &Post{Id: id} ですが、同じものを変数に取ってる &v を渡しても結果は変わりませんでした。なぜ自動生成のコードが struct を作り直しているのかはちょっとよくわかりません。(明確な理由があればぜひ誰か教えてください!)

まとめ

ここまでで前回と合わせて /models/post.go のコードをすべて読んでいきました。だいぶ go の基本的な文法と beego での orm の使い方には慣れてきたんじゃないでしょうか。文法が分かれば複雑な処理も一つ一つ意味が見えてきて超長かった GetAllPost メソッドも結局はソート処理で配列二つを複雑に条件分岐で処理しているだけで大したことはないという感じでした。ちょっとまだ reflect みたいなちょっとトリッキーな型が絡んだ使い方みたいなのはまだまだ勉強が必要だと思いましたが。
みなさんも A Tour of Go 片手に go のコードを読みそして書いてみて web のバックエンドマスター目指してみてはいかがでしょう。