GoのフレームワークEchoで作った評価アプリでMySQLのデータからランキングを作った


はじめに

Webページの役立ち度(有用性)を評価できるサイトを作った

で作った Webアプリ、WebRepo☆彡 でランキングを実装したので、そのコードについて取り敢えず記録したいなと思ったので記事にします。こうしたらもっと良いよとかがあったら教えてください。

どんなランキング?

どちらかと言うと今回実装が迫られたというよりはランキングを作ってみたい(どういうコードを書いたら実現できるか確かめたい)と思ったので作ってみた感じなので、そんなに難しい実装はしないつもりでした。
取り敢えず、投稿してくれた評価の多いユーザーランキングを作ります(評価の高いページじゃないんかいっ)。

データベースの状況

作るにあたってまず、ユーザー毎の評価投稿数を格納するカラムがあればそれを並び替えるだけで済みますが、残念ながら最初にランキングをつくることなど想定していなかったので存在しません。
主なテーブルには以下の様なものがあります。

  • ユーザー情報
  • 評価対象となるページの情報
  • 投稿された評価

投稿された評価の格納されたテーブルから、ユーザー毎の評価数を検索を掛けて取ってこないといけません。
しかも、評価の格納されたテーブルにはユーザーIDしか記録しておらず、このままランキングを作るとユーザーIDが並ぶことになってしまいます(そんなばかな)。
というわけで、ランキングに見事ランクインしたユーザーIDで、さらにユーザー情報テーブルを検索してユーザー名を取ってくる必要があります。

やること

というわけで、作業すべき項目を挙げます。

  1. ユーザーごとの評価数の検索
  2. ユーザーIDを評価数の多い順に並び替え
  3. ユーザーIDからユーザー名を取ってくる
  4. ランキングを動的に表示できるようにページを作成

ソースコード

Go

というわけで、以下の様なものが出来上がりました。

構造体
IndividualEvalCount struct {
    UserID    int `db:"evaluator_id"`
    UserName  string
    EvalCount int `db:"count(num)"`
    RankNum   int
    // DeliberateCount int `db:"count(deliberate)"`
}

RankingContent struct {
    Ranking interface{}
    Title   string
}
ハンドラ
e.GET("/ranking", func(c echo.Context) error {

    // 取り敢えず、ユーザー一人ひとりの評価数を取ってくる。
    // 複数の評価データを格納するために構造体のスライスを作成
    var individualEvalCount []IndividualEvalCount
    _, err := dbSess.Select("evaluator_id", "count(num)").
        From("individual_eval").
        GroupBy("evaluator_id").Load(&individualEvalCount)
    if err != nil {
        panic(err)
    }

    // 審議中の評価は外す。(未実装)

    // 評価数の多い順にソートする。
    sort.Slice(individualEvalCount, func(i, j int) bool {
        return individualEvalCount[i].EvalCount > individualEvalCount[j].EvalCount
    })

    tmpEvalCount := 0
    tmpRank := 0
    // DB からユーザー名を取得
    for i, v := range individualEvalCount {
        // 恐らく参照渡しなので、v.に代入しても意味がない。
        individualEvalCount[i].UserName, err = dbSess.Select("name").From("userinfo").
            Where("id = ?", v.UserID).
            ReturnString()
        if err != nil {
            panic(err)
        }
        // 評価数が同じ場合は同順位に設定する
        if v.EvalCount == tmpEvalCount {
        } else {
            tmpRank++
            tmpEvalCount = v.EvalCount
        }
        individualEvalCount[i].RankNum = tmpRank
    }

    var rankingContent RankingContent
    rankingContent.Ranking = individualEvalCount
    rankingContent.Title = "ユーザーの評価数ランキング"

    return c.Render(http.StatusOK, "ranking", rankingContent)
})

審議中の評価~の部分がなんかやばい雰囲気を出してますが、ここは通報されていないいないされた評価を除外するといった処理が入る予定でした。

なお、パッケージが書かれてませんが、VisualStudio Code とかIDEとかでコードをベッて貼ると必要なパッケージを表示してくれます。VSCodeはエディタですが拡張機能をインストールすると保存時に追加してくれます。MSだからって侮ってましたが便利ですよ。

構造体が2つありますが、個々のユーザーの順位とユーザー情報をIndividualEvalCountで格納します。
RankingContentはHTMLテンプレートエンジンに情報を渡すための構造体です。

データベースの取得には github.com/gocraft/dbr を使っています(このパッケージ、Like検索が面倒なのであんまり使い勝手よくないです)。
評価の取得には、まずレコードの数を数えるcount関数を使っています。さらに、最初は where でユーザー一件一件調べるのかって思ってましたが、以下のページのように group by を使うことでユーザーごとの評価レコード数を一気に取ってきています。

MySQL でテーブル内の同値の数をカウント ( count ~ group by ~ の使い方) | MySQL | 阿部辰也のブログ――人生はひまつぶし。

因みに、構造体のタグにあるように、count関数 で取ってきたデータのカラム名はcount(指定カラム)です。

順位付けですが、データベースでやらせるのは面倒くさそうだし、Goでやった方が絶対に早いだろうということで、Goでやりました。スライスにはソート関数sort.Slice()があるので、それを使って簡単にできます。ググったらインターフェイスで云々という記事が出てきましたが過去の話らしいです。

あと、ユーザー名の取得部分ですが、v.~に格納しようとしても格納できないです。なので、ちゃんと元のスライスとインデックス数で指定します。

ページ生成

HTMLテンプレートエンジンには github.com/yosssi/ace を使っています。
このパッケージの使い方については以下の記事を辿っていけばわかると思います。

GoのHTMLテンプレートエンジン yosssi/ace の使い方

上記のページに書いてあるとおり、このパッケージでは標準パッケージのテンプレートエンジンの記法も使うことが出来ます。
というわけで、

  body
    = include header_menu
    #list
      .subject
        h2 
          span.star  
          span  {{.Title}} 
          span.star  .box
        #ranking
          / 順位とユーザー名と評価数を表示する。
          {{range .Ranking}}
            h3 {{.RankNum}}位 {{.UserName}} 評価数{{.EvalCount}}
          {{end}}

こんな感じで、繰り返し処理を実行しています。Go の方で、最後にrankingContent.Ranking = individualEvalCountで構造体に(構造体の)スライスを入れています。なので、.Ranking はスライスです。
また、.RankNum、.UserName、.EvalCount は.Ranking のフィールド(要素)です。

結果

https://webrepo.nal.ie.u-ryukyu.ac.jp/ranking

はい、出来ました!名無しさんがたくさんですね!

因みに名前はマイページのユーザー設定から変えられますよ。

なぜ評価の高いページのランキングじゃなかったの?

評価が一ページに一つしかついてない状況、ということに加えて、

  • 評価入力に競争力を持たせたい。
  • 結局こっちも平均評価を出した上で並び替えなければいけないので、似たような作業。
  • ユーザー名を取ってくる必要があったりして、こっちのほうが複雑というか面倒だった。
  • 評価自体、目的別に付けてもらってるので、平均評価を出すこと自体に意味があるか分かんない。

という理由からユーザーランキングになりました。

さいごに

評価入力と宣伝のほど、よろしくおねがいします!
https://webrepo.nal.ie.u-ryukyu.ac.jp/