学校向けのサービスを作るということ

このエントリは Classi Advent Calendar 2016 - Qiita の24日目です

前回話した複数DBは?

進捗ダメでした

学校向けのサービスを作るということ

7月からClassiにJOINして「学校」というものに久し振りに触れてみて、いろいろ思うことを書いていこうかと思います。

ピークがわかりやすい

これは想像つきやすいと思うのですが、学校という現場においては想定外のイベントによってアクセスが急増するということはそうそうありません。 Classiは校務や授業などの支援だったりご家庭への連絡だったりといろいろ機能がありますが、1日毎でピークタイムがかわるということがないため そのピークを捌けるか? 逆にアイドルタイムに過剰にならないようにということだけを考えていればいいのはかなりうれしい話です。

どのサービスでもピークタイムとアイドルタイムは来まってくるとは思うのですが、まれによるあるスパイクみたいなのが少ないのは運用する側としては安心できる要素の1つです。

「学校」は思ったよりも多様

学校といったときにどういう学校を想像するでしょう? 多くの場合、それは自分が通っていた学校がベースになってくると思います。
しかし学校といってもいろんな形の学校、たとえば偏差値的な話もそうですし地域だったりいろいろな学校がありそれぞれに抱えている悩みややりたいことは異なっているようです。 機能だったりサービスを作るときに、変に自分の学校での経験をベースに考えてしまうと実際の現場と合わなかったり、ある学校には非常にマッチする機能が別の学校では問題になりかねないということがあったりして悩ましい限りです。

顧客はだれか?

Classiは「学校向け」サービスという形なので、先生からの要望が多く上がってきます。

ただ学校という中にはいろんな登場人物がいるわけです。
先生と言ってもいろんな役割の方がいますし、もちろん生徒さんたち、そしてその保護者の方々… どこかの立場を尊重した結果、他の立場の方に不満を抱かせるようなことのなように慎重に考えなくてはいけません。

自分個人としては、学校における主役は生徒さんたちだと思っているので全ての要望にたいして生徒のためになるか?という基準で考えているつもりです。


当初の予定を変更してポエムっぽいものを書きましたが、こんなことを考えているエンジニアに興味がある方はぜひ話を聞きにきていただればと思います

日本の教育を変える!Classiを一緒に良くするRailsエンジニア募集! - Classi株式会社のWeb エンジニア中途の求人 - Wantedly

最終日は部長の @sasata299 が唐揚げを揚げるんでしょうか、乞御期待!

ジェネレータ系コマンドを使う際に便利なgitサブコマンドを作った

a_matsudaさんの My Favorite Tools // Speaker Deck というスライドにあった↓の話

こういうのやらないとなと感じてはいたけど、ついついジェネレータ呼んだ後にすぐに編集を始めてしまっていた。 なのでそもそもジェネレータを呼ぶと同時にコミットをしちゃえばいいだろうということでgitサブコマンドを作った

gist781654396ade961ced79894cfefa4a53

パッと作っただけなのでエラー処理とか全然ないけど使ってみたら便利感あった ↓のようにgit recordの後にコマンドを実行した結果に出来たファイルをすぐコミットしてくれる コミットメッセージに実行したコマンドが記録されるので後から何やったかわかりやすくていいんじゃないかな

git record rails generate model user name:string

Railsと複数DBとPreloadingと

このエントリは Classi Advent Calendar 2016 8日目の記事です。

そういえばいまだに転職エントリーを書いていませんが7月に転職をしていましてClassiのファースト社員となりました ?

複数DB on Rails

Classiではセキュリティやら何やらの関係でみんな大好き複数DB on Railsになっています。
R/W Splittingやシャーディングではなく、1つの共通DBとテナント毎のDBがあるマルチテナンシー(?)構成です。

class User < ActiveRecord::Base
  has_many :entries

  # (共通DBへの接続情報)
end

class Entry < ActiveRecord::Base
  belongs_to :user

  # (テナントDBへの接続情報)
end

User.connection # => 共通DBのdatabase名のconnection
Entry.connection # => テナントDBのdatabase名のconnection

といった感じで使用するmodelに応じて適切にコネクションが切り替わるようになっています

異DB間association

基本的にはテナント側で閉じる世界観になるのですが、稀によく頻繁につらいことに共通側とテナント側のモデル間でassociationが貼られていたりもします。
ここで問題になるのがPreloadingの処理です。

preload を使う分にはそれぞれのモデルに対して問い合わせが走るのでコネクションもいい感じに切り替わってくれるのですが、joinseager_loadの場合はそうも行きません?

# OK
User.preload(:entries)

# NG
User.joins(:entries)
User.eager_load(:entries)

#=> Mysql2::Error: Table 'entries' doesn't exist

そもそもそんなこと出来るの?って思っていたんですが↓の記事を見た感じ、MySQLの場合テーブル名の前にdatabase名を入れることで可能とのこと(もちろん同じDBサーバ内にそれぞれのdatabaseが入ってる必要はあります)
MySQL5:異なるデータベース間のテーブル結合 DB名:db1 TABLE名… - 人力検索はてな

たとえばこんな感じ?

SELECT
    common.users.*,
    tenant_1.entries.*
FROM
    common.users
JOIN tenant_1.entries
    ON common.users.id = tenant_1.entries.user_id

Classiの場合はこの条件を満たしていたので、後はRailsが発行するクエリのテーブル名の前に[database名].を追加するだけで上手くいきそうです。

たとえばtable_name_prefixを使う

困ったときのStack Overflowを頼ったところ以下のようなものが。 Rails 3 - Multiple database with joins condition - Stack Overflow

テーブル名の先頭にprefixを付けるとこで、database名を追加している感じ。 ということで試してました。

 class User < ActiveRecord::Base
+  def self.table_name_prefix
+   "common."
+  end
 end
 class Entry < ActiveRecord::Base
+  def self.table_name_prefix
+    "tenant."
+  end
 end

早速JOINを試してみます

[0] pry(main)> User.joins(:entries).to_sql

SELECT
    `common`.`users`.*
FROM
    `common`.`users`
JOIN `tenant`.`entries`
    ON `common`.`users`.id = `tenant`.`entries`.user_id

ということで上手くいきました。
実際にはbatchなどでtenantを切り替えたりする処理があったりしてtable_name_prefixの更新などが必要になってきますが、 ActiveRecordreset_table_nameを使えばそれもなんとかなりそうです。


と、ここまで書きましたが深淵なる理由によりこのコードはClassiの内部ではまだ動いていません ? 本当はこの処理をいい感じにgem化して今日までにリリース!!とかっこいい感じにしたかったのですが、自分が 怪盗業プロデューサー業 で多忙だったため進捗ダメでした…
あと1回Advent Calendarが回ってくる予定なのでそれまでに何らかの形にできればなと思ってます


こんな風に複数DBが大好き!!、または日本の教育を変えたい!!というRailsエンジニアの方はぜひClassiに話を聞きに来てください!

明日は @kenjiskywalker の「?」です

宿の名は。

去年の前フリ

RubyKaigiの前泊当日にホストからキャンセルをお願いされる

そもそも

シン・モリタハウス

コーヒー楽しい

昨年末にウィッシュリストに入れてたこの手動ミルをもらったのをきっかけにコーヒーを始めてすっかりはまってる。

ついに電動ミルにまで手を出したので記念に今使ってるもののまとめエントリーを書いておく。

ドリッパー

ハリオのV60を使い続けてる。 特にこだわりがあったわけでもなく手頃だったから選んだ感じ。

ドリップポット

当初は急須にスキッパーをつけて注いでたがさすがにつらくなって、 MONOQLO (モノクロ) 2015年 10月号 [雑誌] のコーヒー特集で評価の高かったこれを使ってる

Kalita 細口ポット 0.7L

Kalita 細口ポット 0.7L

狙ったとこに落とせるしちゃんとドリップ時にコーヒーが膨らむようになって大満足。

珈琲きゃろっと の定期便で毎月400gを買って、足りないときに近所の自家焙煎のお店で購入して密封びんに入れて保存してる。

セラーメイト 密封びん 1L 220018

セラーメイト 密封びん 1L 220018

保存は最初は冷凍庫に入れるようにしてたけど、自家焙煎のとこの店長に「冷蔵庫に入れるぐらいの量を買うのではなく、常温でいいので1週間で使い切るほうがよい」と言われてそうしてる。

豆は自分でローストするのも考えたんだけど、先述の店長が「豆を焼くのはいいんだけどハンドピックが大変」とかぼやいてたのと、ハンドピックで捨てる豆を見せてもらって自分じゃこれの良し悪しの判断つかないなと思ってあきらめた。

電動ミル

デロンギ コーン式 コーヒーグラインダー KG364J

デロンギ コーン式 コーヒーグラインダー KG364J

プロペラ式でもいいのかなと最初は思ってたけど、触る機会あって試したら挽加減難しいしムラもあって微妙かなと思ってこれにした。 噂のナイスカットミルも欲しかったんだけど、ちょうど製造終了のタイミングらしく在庫は高騰してたのであきらめた。

デロンギのはネットとかだと1つの目盛で挽きの細かさが大きく変わってしまうみたいなのを聞いたり粗挽きが苦手とか言われてたけど、特に今のところ不満はない。 それ以上に電動の威力がすごくて豆の消費量が一気に増えた。


自分はめんどくさがりなので手間のかかるハンドドリップは飽きちゃうかなと心配してたけど順調に沼にはまっていってる気がする。 おすすめのグッズだったり豆だったりお店だったり知ってる人がいたらぜひ教えてどんどん沼に引きずっていってほしい。

gitの別リポジトリを合流させる

TLでtyru さんの 全く履歴が被らない (別リポジトリとかの) ブランチ同士を無理矢理マージ - Humanity を見て、gitなら履歴を残してマージ出来るのでは?と思ったのでメモ

やること

全く別々のリポジトリAとBの、それぞれhogeブランチとfooブランチをマージする

# リポジトリAをブランチを指定してclone
$ git clone -b hoge (リポジトリA)

# リモートブランチとしてBを追加
$ git remote add repoB (リポジトリB)

# Bの内容を取得
$ git fetch repoB

# 現在のブランチ(Aのhoge)にBのfooをマージ
$ git merge repoB/foo

HerokuのReviewAppsを使うときの小ネタ(HEROKU_APP_NAME,MySQL)

アプリ毎の名前やホスト名を設定したい

ReviewAppsがPR1つずつにアプリ名を環境変数で渡してくれる仕組みがあるのでそれを使うとよい

Review Apps | Heroku Dev Center

app.jsonに以下のような設定を追記すればOK

{
  "env":{
    "HEROKU_APP_NAME": {
      "required": true
    }
  }
}

HEROKU_APP_NAMEは (appname)-pr-(pull request No.) といったフォーマットが渡される。
自分のプロジェクトではこれを使って、S3へアップロードする処理でPR毎に出力ディレクトリを変更するようにしている

mysql2.gemを使いたい

プロジェクトのDBはMySQLなのでReviewAppsでもMySQL(ClearDB MySQL)を使った。
この時、DBへの接続URLは環境変数CLEARDB_DATABASE_URLに入ってくるのだが、URLmysql2:// ではなく mysql://から始まるためこのままだとmysql2.gemが使えない。

単体のアプリなら CLEARDB_DATABASE_URL を置き換えればいいのだが、ReviewAppsだとPR1つ1つに対して置き換えを行う必要が出てくるので現実的ではない。

結局以下のように強引に置き換えることにした

development:
  <<: *default
  url: <%= (ENV['CLEARDB_DATABASE_URL'] || ENV['DATABASE_URL'] || 'mysql2://localhost').gsub(/^mysql:/, 'mysql2:') %>