WESEEK Tech Blog

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 という変数に取ります。そのあと、ormQueryTable()メソッドで一番上の例の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 []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 が指定されているのはおかしいということです。
    • 実際に値を返すまで
      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 []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
    // 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 という変数に取り、引数で受け取った 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 のバックエンドマスター目指してみてはいかがでしょう。