前回rubyでSQL文発行して、データベースを操作するノリが分かってきたので、今回は
これのコードリーディングをやっていきたいと思います。
(的外れな可能性大なので鵜呑みにしないでください。あくまで軽いメモ用です。。)
モデルから見ていく
models/db.rb
require 'mysql2'
class DB
def self.query(sql)
client.query(sql, symbolize_keys: true)
end
private
def self.client
@client ||= Mysql2::Client.new(host: 'localhost', username: 'root', database: '0ch')
end
end
client.queryはなんか前回出てきたやつですね
前回は、client = Mysql2::Client.new(host: "localhost", username: "root", password: '', database: 'mysql')
こんな感じで定義されていたと思います。
クライアント、いわゆるどこにどんなユーザー情報、パスワードでデータベース(MySQL)に接続するのかを提示します。
どんなクライアントかはまだ定義してないですね。
symbolize_keyはハッシュ のキーをシンボルにするメソッドらしい。(いやオプションかもしれん)
なんでこれをつけなければいけないのかが全く分からない。
def self.client
@client ||= Mysql2::Client.new(host: 'localhost', username: 'root', database: '0ch')
end
ここでどんなクライアントかを定義しますと
んで、@clientの中身に何かあればそれ使って、なかったクライアントを新たに生成する的な。
||=と書くことで、データベースへの問合せを最小限にしています。
とにかくクライアントを定義するメソッドと、クエリをデータベースに投げるメソッドがあるということです。理解度は全然ですが、とりあえず次。
models/post.rb
なんだか投稿に関するモデルっぽいですね〜
require_relative 'application_entity'
class Post < ApplicationEntity
MIN_SIZE = 1
MAX_SIZE = 140
attr_reader :name, :email, :body, :topic_id, :id, :created_at
def initialize(name: nil, email: nil, body:, topic_id:, id: nil, created_at: nil)
@name, @email, @body, @topic_id, @id, @created_at = name, email, body, topic_id, id, created_at
end
def self.of_topic(topic_id)
DB.query("SELECT * FROM posts WHERE topic_id = #{topic_id}").map { |hash| new(hash) }
end
def save
validate
DB.query("INSERT INTO posts (name, email, body, topic_id, created_at) VALUES ('#{@name}', '#{@email}', '#{@body}', '#{@topic_id}', NOW())")
end
def validate
unless @body.size >= MIN_SIZE && @body.size <= MAX_SIZE
raise Invalid, "本文は#{MIN_SIZE}文字以上、#{MAX_SIZE}文字以下で指定してください"
end
end
end
'require_relative 'application_entity'は一旦置いといて、
attr_reader :name, :email, :body, :topic_id, :id, :created_at
とはなんぞや。と、attr_accessorは知っとるけど、attr_readerは知らんぞと
https://qiita.com/aberyotaro/items/626a88388d44802240a0
が分かりやすかったので、拝見してみた。
attr_readerはインスタンス変数の読み出し専用アクセサを定義できる。
はい、アクセサとはなんやねん。と
インスタンス変数の値を読み書きするメソッドのこと。
Rubyはそのままではインスタンス変数にアクセスできない仕様になっています。
もっというとruby はclassの外からインスタンス変数を参照、再定義できないので、
それらをしようとしたときにアクセサメソッドが必要なのです。
今回は、参照するだけで、一度投稿した投稿は編集できない感じだと思うんで、
attr_readerなのでしょう
これでpostの属性を一気に定義できる。便利やのう
initializeメソッドで
@name, @email, @body, @topic_id, @id, @created_at = name, email, body, topic_id, id, created_at
で一気に定義できるのも初めて知った。
def self.of_topic(topic_id)
DB.query("SELECT * FROM posts WHERE topic_id = #{topic_id}").map { |hash| new(hash) }
end
で、さっきのDBクラスのqueryメソッドを呼んでるって感じです。
こないだやった通り、生SQLが書いてありますね、訳してみると
postsテーブルの全情報を取得、ただし条件はトピックidが◯のトピックの投稿だけを取得してきてねーー。
って感じですね。
topic_idがいつ、どの瞬間に入るのかがイメージできないですが、一旦次。
map { |hash| new(hash) }
ですが、これはよくわらかなくて、取得した投稿全部をハッシュ化するのでしょうか、、、?
なぜハッシュ化するのかよく分からないので、次。
って思ったのですが、単純に、取得したデータからインスタンスを生成してるだけでした。
なぜhashという変数名にしたのかは、あとで谷道さんに聞くことにしましょう。
考えてみれば、データベースから取得したデータをそのまま使えるわけではなく、
インスタンス化しないことには、ruby内でそのデータを使えないってわけですね。
railsではそんなことを意識せずともなんとなくアプリが作れてたので、
感動するとともに、そりゃ実力つかんわーー。とも思った。
アンチrailsになりそうな気持ちを沈めつつ次へ。
def save
validate
DB.query("INSERT INTO posts (name, email, body, topic_id, created_at) VALUES ('#{@name}', '#{@email}', '#{@body}', '#{@topic_id}', NOW())")
end
最初にvalidateを呼び出していますが、validateメソッドは後々定義するから一旦飛ばして、
NOW()メソッドは、現在時刻を取得するMySQLのメソッドらしいです。
def validate
unless @body.size >= MIN_SIZE && @body.size <= MAX_SIZE
raise Invalid, "本文は#{MIN_SIZE}文字以上、#{MAX_SIZE}文字以下で指定してください"
end
end
そして、肝心のvalidateメソッド
raise Invalidってのが引っかかった。raiseは例外処理を発生させるメソッドらしいが、
Invalidが何やと。その答えは後々登場。
MIN_SIZE MAX_SIZEを変数にすることで後から、やっぱ最大文字数200文字で!って言われたときも変更に対応しやすいコードになってます。
こういった細かい工夫も注目しつつ次いきましょう。
models/topic.rb
require_relative 'application_entity'
class Topic < ApplicationEntity
MIN_SIZE = 1
MAX_SIZE = 50
attr_reader :title, :id, :created_at, :updated_at
def initialize(id: nil, title:, created_at: nil, updated_at: nil)
@id, @title, @created_at, @updated_at = id, title, created_at, updated_at
end
def self.all
DB.query('SELECT * FROM topics ORDER BY updated_at DESC').map { |hash| new(hash) }
end
def self.find(id)
result = DB.query("SELECT * FROM topics WHERE id = #{id} LIMIT 1").first
new(result)
end
def save
validate
DB.query("INSERT INTO topics (title, created_at, updated_at) VALUES ('#{@title}', NOW(), NOW())")
end
def validate
unless @title.size >= MIN_SIZE && @title.size <= MAX_SIZE
raise Invalid, "タイトルは#{MIN_SIZE}文字以上、#{MAX_SIZE}文字以下で指定してください"
end
end
end
てか、どのクラスもApplicationEntityを継承してるってことに今気づいた。
そのクラスも後々登場してきます。
result = DB.query("SELECT * FROM topics WHERE id = #{id} LIMIT 1").first
.fitstがなぜあるのか、
配列の先頭の要素を返します。要素がなければ nil を返します
と書いてあったが、なぜこのメソッドが必要なのかがイメージできてない。
取得したいデータがなかった場合にもエラーを起こさないためなのか。
未来の自分が解決してくれるだろう。
LIMIT 1は取得するデータの行数を制限。と書いてあった。
"行数"を制限という言い回しが気になるが、単純に取得するデータの個数を制限してるって感じでいいんすかね。
トピックがなぜ一個しか取得できない仕様なのかがよく分からない。
多分、トピックが被らないようにするため、、、??
まぁなんとなくイメージができたんで次。
application_entity.rb
post.rbとtopic.rbの継承元となっていた、モデルですね。
require_relative 'db'
class ApplicationEntity
class Invalid < StandardError
end
end
お、見覚えがあるInvalidが出てきた。
しかもStandardErrorを継承している。。。
Invalidクラスに何も書かないなら、raise Invalidとかじゃなくて、
railse StandardErrorでええやん。とか
Post < StandardErrorでええやん。とか思ったりしてる。
なんでやねん。。。。調べてみると、、
StandardErrorのスーパークラス(親クラス)がExceptionらしい。
https://yarb.hatenadiary.org/entry/20121005/p1
https://qiita.com/kasei-san/items/75ad2bb384fdb7e05941
この記事出てきたが、rescueに関係ありそうなので、一旦置いといて、
raiseは、プログラムの中で意図的に例外(エラー)を発生させるときに使います。
と書いてあった。
んーー、でもなぜStandardErrorをInvalidに継承させる必要があるのかが、分かりませんねー。
二度手間やん。って初心者は思っちゃう。
まぁ、理解はそこそこに全体像をつかみたいので、今回はこんな感じで。