sugiken のメモ帳

知ったことを書いていく

【Repを支える技術】サーバーサイド編

こんにちは!
前回から始めた「Repを支える技術」第二弾🎉
サーバーサイド編です。学生が独学で0から作り、2年間運営しているサービスの裏側がどうなっているのかをつらつらと書こうと思います。
大した仕組みは導入していないのですが、いくつかの工夫している点などをまとめてみます。

はじめに

f:id:kentear:20180714233021p:plain

Repは正式名称「Rep立教大学シラバス検索システム」という名前です。その名の通り立教大学のほぼ全ての授業データを収録しており、立教生は簡単に時間割を組むことができます。

Repは学生である僕が1人で開発を始めたサービスです。運営者(メイン開発者)が学生であることは、ユーザー視点の獲得や一次情報を得るための手段として重要な意味を持ちます。 故に僕が卒業した後にどういう運営体制にするかはいまだに決まっておらず、場合によっては創業者()である僕の手を離れて行くことも視野に入れて開発する必要があります。

というわけでRepの設計思想は基本的に 普通 になることを心がけています。 このことを理解した上で、広い心で続きをお読み下さい🙇

フレームワーク

Ruby on Railsを使用しています。

なぜRailsなのか

Railsは優れたwebアプリケーションのフレームワークであり、比較的簡単にその使い方を知ることができます。
例えばRails Tutorialという有名な教材が無料で(!)公開されていますが、Repではその教材を2周くらいしたら理解できるようなコードにしています。また、昨今誕生するプログラミングスクールもRailsコースを設置することが多く、今後数年間は充実した学習環境があると感じています。

というのは建前でして、実際は「僕が初めて触ったFWで、それしかできなかったから」というのが正しいです。しかし、この選択は前の理由で後悔したことありません。

いくつかの細かい工夫

では実際にRepのコードを部分的に見て行きましょう。

検索機能

Repでは立教生2万人の要望に応えるべく、合計2.5万近い授業データを収録しています。その中から学生が目当ての授業を見つけられるように検索機能を充実させる必要があります。

「RepではElastic Searchを導入しており〜」とかっこいいことを言いたいところなのですが、まだ導入できていません😭
Repでは至極シンプルにLIKE文でゴリゴリやっています。

検索では次のようなクエリが送られてきます。実際より簡単なクエリに加工しています。検索内容はURLのパラメータに保持されているので、controllerでこれらを元にwhere文を実行します。

https://www.rep-rikkyo.com/lesson/search?lesson_name=文化&professor_name=斉藤

  def search
    @lessons = Lesson.order(:lesson_name).all
    if params[:lesson_name].present?
      # 1文字以上の空白文字で区切る
      params[:lesson_name].split(/[[:blank:]]+/).each do |l_name|
        # マイナス「-」で始まる場合、その単語でwhere.notを実行する
        remove_word = l_name.match(/^[-ー](.+)/)
        @lessons = if remove_word.present?
                    # 取り除く単語
                    @lessons.remove_word(remove_word[1], 'lesson_name')
                   else
                    # 通常のwhere句
                    @lessons.get_by_name l_name
                   end
      end
    end
    ・・・
  end

get_by_nameremove_word はscopeとしてmodelに定義しています。

  # 授業名による検索
  scope :get_by_name, ->(lesson_name) { where('lesson_name like ?', "%#{lesson_name}%") }
  # 取り除く単語
  scope :remove_word, ->(text, type) { where.not("#{type} like ?", "%#{text}%") }

検索内容によってはいくらでもLIKE文が多くなるためパフォーマンスが気になるところですが、実際に2年間運用していて遅いと思ったことはありません。

時限のデータ

「時限のデータ」とは、とある授業Aがあった時に、その授業Aがどの時限に開講されるかのデータのことです。これを正規的に破綻しないようにDBに登録するにはどうするか、を開発初期に悩んだので工夫している点として紹介します。

結論からいうと、月曜1限の授業はa_1という文字列で表します。
授業データはLessonというモデルといくつかの子モデルで構成されていますが、時限データは大元のLessonモデルにperiodというカラムに文字列で持っています。
容易に想像がつくと思いますが、月〜土をa~fというアルファベットで表現しています。

この構造にしておくと、以下のscopeで検索に対応できます。

  scope :get_by_period, ->(period) { where('period like ?', "%#{period}%") }

火曜3限を取得する場合は Lesson.get_by_period('c').get_by_period('3') とクエリを繰り返します。

この管理方法の目的は、「授業Aが月曜1限と火曜2限の複数時限に開講されている」という状況に対応するためです。複数時限に開講される授業は、 a_3, b_2, e_1 となります。こうすることで、今の所時限にまつわるクエリで破綻したことはありません。

確かにPeriodのようなモデルとLessonを一対多で構成するのもありです。しかしそうする必要のない理由はデータの更新がほとんど起きないというRepならではの事情もあります。授業データは年度始まりに公開されてからほとんど変わることはありませんので、この構造で問題ないのです。

ちょっと一息

分析/クローラー編にも書きますが、授業データの取得プログラム(立教クローラー)は同じ学部・学年の友人の手を大きく借りました。僕のサービスのために結構コミットしてくれて感謝しかないです🙇

活用しているGemたち

次の2つは本当にRepを支える重要なGemです。

  • devise
  • seed-fu

devise

github.com

言わずもがな、ユーザー登録のgemです。人気でもあり、不人気でもあるgemとして有名ですね笑 開発初期はこんなに簡単にユーザー登録機能が実装できるのかと感動しました。シンプルなメールアドレスログインだけで、カスタムする必要がなかったためdeviseを使うのは良い選択でした。

Repではユーザー登録した人だけが授業のレビューを書いたりレビューを見ることができます。 また大学から独立したサービスを目指すため、ユーザー登録は立教生のメアドだけに限定しています。

seed-fu

github.com

シードデータを追加するGemです。たまーに更新されるマスターデータを取り扱うサービスでは積極採用するべきだと思います。
授業データは全てcsvで管理しているのですが、これを読み取ってデータを追加していきます。ポイントは、

idを指定すると、データの追加ではなく更新が行われる

という点にあります。
Railsには元からseedデータを投入する機能がありますが、これだと$ rails db:seedを実行するたびに新たにデータが追加されてしまいます。授業データはたまにデータの更新があるため、seed-fuの採用は良い選択だったと思います。

授業データの追加をするプログラムを少しだけ載せます。Lessonにデータを追加するプログラムが次になります。

year = 2018
LESSON_COUNT = 12355

Dir.glob("#{Rails.root}/db/fixtures/#{year}/*.csv").sort!.each do |f|
  CSV.read(f).each do |row|
    Lesson.seed do |s|
      # idに最後のLessonのidである12355を足す。
      s.id = row[0].to_i + LESSON_COUNT
      s.faculty_id = row[1]
      s.department_id = row[2]
      s.lesson_number = row[3]
      s.lesson_code = row[4]
      s.lesson_name = row[5]
      s.professor_name = row[6]
      s.term = row[7]
      s.period = row[8].to_s
      s.classroom = row[9]
      s.campus = row[10]
      s.note = row[11]
      s.url = row[12]
      s.year = row[13]
    end
  end
  puts "#{f}---完了!"
end

最後に

いかがでしたでしょうか。
帰省中のWiFiがない環境で黙々と書いたため、予想以上の長文となってしました。。。
こんな駄文をひとりでも最後まで読んでいただけたら嬉しいです☺️
まだフロントエンド編やインフラ編、分析/クローラー編も書くつもりです。もし「こういうのが知りたい」とかありましたら、ぜひ教えてください!