#2 SinatraでActiveRecordを使わずに掲示板アプリを作ってみた

[前回](https://qiita.com/divclass123/items/ab5256f83bf299af6278)rubyでSQL文発行して、データベースを操作するノリが分かってきたので、今回は

https://github.com/ttanimichi/0ch#sinatra%E3%81%A7%E6%8E%B2%E7%A4%BA%E6%9D%BF%E3%82%92%E4%BD%9C%E3%82%8D%E3%81%86

これのコードリーディングをやっていきたいと思います。
**(的外れな可能性大なので鵜呑みにしないでください。あくまで軽いメモ用です。。)**

# モデルから見ていく

# models/db.rb

```ruby
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はハッシュ のキーをシンボルにするメソッドらしい。(いやオプションかもしれん)
なんでこれをつけなければいけないのかが全く分からない。

```ruby
  def self.client
    @client ||= Mysql2::Client.new(host: 'localhost', username: 'root', database: '0ch')
  end
```

ここでどんなクライアントかを定義しますと

んで、@clientの中身に何かあればそれ使って、なかったクライアントを新たに生成する的な。
||=と書くことで、データベースへの問合せを最小限にしています。

とにかくクライアントを定義するメソッドと、クエリをデータベースに投げるメソッドがあるということです。理解度は全然ですが、とりあえず次。

# models/post.rb

なんだか投稿に関するモデルっぽいですね〜

```ruby

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はインスタンス変数の読み出し専用アクセサを定義できる。

はい、アクセサとはなんやねん。と

https://qiita.com/mofuko0213/items/e5d81ec3995cbcec6695

> インスタンス変数の値を読み書きするメソッドのこと。
Rubyはそのままではインスタンス変数にアクセスできない仕様になっています。


もっというとruby はclassの外からインスタンス変数を参照、再定義できないので、
それらをしようとしたときにアクセサメソッドが必要なのです。
今回は、参照するだけで、一度投稿した投稿は編集できない感じだと思うんで、
attr_readerなのでしょう

これでpostの属性を一気に定義できる。便利やのう

initializeメソッドで

    @name, @email, @body, @topic_id, @id, @created_at = name, email, body, topic_id, id, created_at

で一気に定義できるのも初めて知った。

```ruby
 　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がいつ、どの瞬間に入るのかがイメージできないですが、一旦次。

```ruby
map { |hash| new(hash) }
```

ですが、これはよくわらかなくて、取得した投稿全部をハッシュ化するのでしょうか、、、？
なぜハッシュ化するのかよく分からないので、次。

って思ったのですが、単純に、取得したデータからインスタンスを生成してるだけでした。
なぜhashという変数名にしたのかは、あとで谷道さんに聞くことにしましょう。
考えてみれば、データベースから取得したデータをそのまま使えるわけではなく、
インスタンス化しないことには、ruby内でそのデータを使えないってわけですね。
railsではそんなことを意識せずともなんとなくアプリが作れてたので、
感動するとともに、そりゃ実力つかんわーー。とも思った。
アンチrailsになりそうな気持ちを沈めつつ次へ。

```ruby
  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のメソッドらしいです。

```ruby

  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

```ruby

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を継承してるってことに今気づいた。
そのクラスも後々登場してきます。

```ruby
    result = DB.query("SELECT * FROM topics WHERE id = #{id} LIMIT 1").first
```

.fitstがなぜあるのか、
配列の先頭の要素を返します。要素がなければ nil を返します
と書いてあったが、なぜこのメソッドが必要なのかがイメージできてない。
取得したいデータがなかった場合にもエラーを起こさないためなのか。
未来の自分が解決してくれるだろう。

LIMIT 1は取得するデータの行数を制限。と書いてあった。
"行数"を制限という言い回しが気になるが、単純に取得するデータの個数を制限してるって感じでいいんすかね。

トピックがなぜ一個しか取得できない仕様なのかがよく分からない。
多分、トピックが被らないようにするため、、、？？

まぁなんとなくイメージができたんで次。

# application_entity.rb

post.rbとtopic.rbの継承元となっていた、モデルですね。

```ruby
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に継承させる必要があるのかが、分かりませんねー。
二度手間やん。って初心者は思っちゃう。

まぁ、理解はそこそこに全体像をつかみたいので、今回はこんな感じで。

# [#3はこちら](https://qiita.com/divclass123/items/9a9e2f9ccfba2286a901)