ねこものがたり

いちにちいっぽ

RailsとSQLでわちゃわちゃやってます

自分の課題

DBから前後のデータをとってくる - ねこものがたりの続きをやっていて感じた自分の課題。

  • 複雑なSQLの操作に慣れていない、間違えたり調べたりしてなんとかやってる
  • railsのコードとSQLが自分の中で一致してない(単独でやると操作できるけど相関関係の理解が抜けてる)
  • railsのコードもそもそもそんなに慣れてない。試行錯誤の末長くなったコード。やっとここまできただけで満身創痍。さらにきれいに書くなんてレベルが高い。だがここで倒れたら死ぬと思え(とは言われてない)。

課題に対する策

この課題を踏まえ、またFjordでもらったコードを考えるためのアドバイスをもとに

" .to_sqlで書いたコードがどんなSQLになるか確かめる"

を毎度やることで自分の中に定着させていくことにしました。

考えるときのドキュメント等については以下の3点を参照しています。

Ruby on Rails APIのActrive Record::Query Methodsのところ

Active Record クエリインターフェイス | Rails ガイド

改訂第3版 すらすらと手が動くようになる SQL書き方ドリル:書籍案内|技術評論社

1度分かったと思っても本当に理解してないとすぐに抜けてしまうので、また簡単なところから考えるというのを積み重ねる感じです。(なので今後も継続する)

やってみた

コードがどんなSQLになるか確かめる

前回のコードの一部。

def previous
  Report.order(created_at: :desc).where(user_id: user_id).find_by("created_at < ?", created_at)
end

このメソッドに"同じuser_idとcreate_atをもつレコードが複数あればidが小さいもの"を検索するコードを追加したいと思います。

まず"同じuser_idとcreate_atでidが小さいレコード"で考えました。

最初に書いたコード。

Report.order(created_at: :desc).order(id: :desc).where(user_id: report.user_id).where("created_at = ?", report.created_at).find_by("id < ?", report.id)

SQLに変換する。

$ Report.order(created_at: :desc).order(id: :desc).where(user_id: report.user_id).where("created_at = ?", report.created_at).find_by("id < ?", report.id).to_sql

-> Report Load (0.9ms)  SELECT  "reports".* FROM "reports" WHERE "reports"."user_id" = $1 AND (created_at = '2017-01-02 00:00:00') AND (id < 312758158) ORDER BY "reports"."created_at" DESC, "reports"."id" DESC LIMIT $2  [["user_id", 459775584], ["LIMIT", 1]]

一応できた。(きれいにするのはあと)

コードを書き直す

Report.order(created_at: :desc).where(user_id: user_id).find_by("created_at < ?", created_at)


Report.order(created_at: :desc).order(id: :desc).where(user_id: report.user_id).where("created_at = ?", report.created_at).find_by("id < ?", report.id)

次は、この2つを1つのpreviousメソッドに収めたい。

最初はこう書きました。

def previous

reoport = Report.order(created_at: :desc).where(user_id: user_id).find_by("created_at < ?", created_at)

if report == nil

Report.order(created_at: :desc).order(id: :desc).where(user_id: report.user_id).where("created_at = ?", report.created_at).find_by("id < ?", report.id)

report

この時点で1度レビューを受けましたが、いろいろだめでした。

  • 方針が違う
  • .orderや.whereなどを繰り返していて書き方が良くない
  • Active Recordの使い方が良くない というのがありました。

方針の違いはここではおいておいてActive Recordはメソッドが長い、複雑なものほど処理に時間がかかり重くなるので注意が必要だそうです。

私が書いたコードは
「1回検索する→見つかる」
ならすぐに処理完了となりますが
「1回検索する→nil→もう一度さっきとちょっとだけ違う条件で全部検索する→見つかる(見つからないかも)」
となり下手するとめちゃくちゃ遅くなるので、処理の結果がたとえ同じでも遅い時点で良くない。

なのできれいなコードにするのと同時に、以下のようにしました。

def previous
  Report
    .where("user_id = ? AND created_at = ? AND id < ?", user_id, created_at, id)
    .or(Report.where("user_id = ? AND created_at < ?", user_id, created_at))
    .order("created_at DESC, id DESC")
    .first
 end

いきなりすごい変わった!!!!

SQLに変換するとこう

$ Report.where("user_id = ? AND created_at = ? AND id < ? ", report.user_id, report.created_at, report.id).or(Report.where("user_id = ? AND created_at < ?", report.user_id, report.created_at)).order("created_at DESC, id DESC").first.to_sql


->SELECT  "reports".* FROM "reports" WHERE ((user_id = 459775584 AND created_at = '2017-01-02 00:00:00' AND id < 312758158 ) OR (user_id = 459775584 AND created_at < '2017-01-02 00:00:00')) ORDER BY created_at DESC, id DESC LIMIT $1  [["LIMIT", 1]]

ここに至るまでに、複数条件の書き方がうまくできなかったり、論理演算子と戦ったりして、本当はめちゃくちゃ時間がかかりました。

しかもまだこれで本当にOKかどうかはわからないというw

だけどあーでもないしこーでもないしとやっているうちに少し「自分の中でわかってきたな〜」と思いました。

その他

今回やってみたのはSQL変換ですが、ほかにも「GUIツールを使う」というのも教えてもらいました。

やらなかったのはとりあえず.to_sqlのほうが手っ取り早いと思ったのと、ツールを使ったことがなかったので最初はどう操作しても確実に誰にも迷惑かけないし大丈夫そうなやつで練習したいと思ったからですww

今自分のアプリを作ったりしてるので近々使ってみようと思います!

今後続けていくこと

最初に書いた
* .to_sqlで書いたコードがどんなSQLになるか確かめる

以外に

  • SQLの復習をすること(1度ちゃんとやったけど曖昧):書籍をもう1周中
  • rubyのメソッドrailsのメソッドを理解する:とくにapi.rubyonrails.orgに書いてあるようなこと、とりあえず読もう

この3点が今の自分にすぐできることかな〜と思っています。 他にもあれば教えてほしいです。

今もまだ若干混乱してますがActive RecordまわりのことSQLのことを自分に定着させていきたいです!