パーフェクト Ruby on Rails 第2章

パーフェクト Ruby on Rails 第2章

こんにちは!鳥貴士です!

前回は第1章を終わらせました!今回は第2章を終わらせ、Part.1を終わらせたいですね!

前回↓

 BLOG AS CATALYST
BLOG AS CATALYST
あなたと私をつなぐ触媒

さあじゃんじゃん始めていきましょう!

第2章 Ruby on RailsとMVC

MVC、angularでも出てきたっけなあ。
なんかちょっと違った気がするがと思って調べた。M(model)V(view)W(whatever)だった。
疑問は氷解、さあ進めよう

MVCアーキテクチャ

M=>Model
データベースの操作を行ったりビジネスロジックを組んだりする。
コントローラとデータベースの間の橋渡しをして大活躍するやつ。

V=>View
コントローラから返されたデータをエンドユーザーに返すやつ。
HTMLでなくでもXMLやjsonでもイケる。
APIサーバー作るならjson。

C=>Controller
モデルから取得したデータを加工したり操作したりする。
モデルのロジックを呼び出したり、どんなデータを返すか設定したりする。

こんな感じで適当に理解した感じにしておく。
まぁ実際使ってるし大体理解はあってるでしょ。

とりあえず本の指示に従い新しくアプリを作る。

$ rails new book_admin
      create
      create  README.rdoc
      create  Rakefile
      create  config.ru
(略)
To update, run `gem install bundler`
         run  bundle exec spring binstub --all
* bin/rake: spring inserted
* bin/rails: spring inserted
$ 

生成できた。
本に曰くこの章では”「このような書き方をすると、このような動作になるのだな」ということを把握していければ十分”とのことなので気楽にすすめることにする。
なお本当に進むかは現時点では不明。

モデルとモデリング

はいでました!モデリング!業務分析!要件定義!DOA!

というわけでここではData Oriented Approachを取っています(習いたての言葉を使いたがるアレ)

DOAについては”達人”からデータベースを設計することを学んだ際に教わりました。

詳しくはこちら。

この章で作るのは「書籍管理アプリ」なので、まずはDOAに則って、このアプリで使われるデータはどんなものかを考えます。

  • 出版社
  • 作者
  • アプリを使うユーザー

ですね。

ActiveRecordでモデルを実装する

本には

  • タイトル
  • 発売日
  • 価格
  • ページ数
  • 出版社
  • 作者

の情報が含まれます。
目次とかISBNとか判のサイズとか寸法とかは面倒なのでナシで。

それでは本に従ってモデルを作成します。

$ rails g model Book name:string published_on:date price:integer number_of_page:integer
/var/lib/gems/2.3.0/gems/activesupport-4.1.1/lib/active_support/values/time_zone.rb:285: warning: circular argument reference - now
Running via Spring preloader in process 12642
Expected string default value for '--helper'; got true (boolean)
      invoke  active_record
      create    db/migrate/20180824051630_create_books.rb
      create    app/models/book.rb
      invoke    test_unit
      create      test/models/book_test.rb
      create      test/fixtures/books.yml
/book_admin $

modelsディレクトリにモデルが追加されていますね。
マイグレーションファイルも見てみましょう。

class CreateBooks < ActiveRecord::Migration
  def change
    create_table :books do |t|
      t.string :name
      t.date :published_on
      t.integer :price
      t.integer :number_of_page

      t.timestamps
    end
  end
end

こんな感じですね!

ただコマンド入力の際に間違えて”number_of_page: integer”のように空白を入れてしまうと

      t.string :number_of_page
      t.string :integer

のようになってしまうので空白に注意です!

うまくできたのを確認したら、db:migrateしましょう!

/book_admin $ bundle exec rake db:migrate
/var/lib/gems/2.3.0/gems/activesupport-4.1.1/lib/active_support/values/time_zone.rb:285: warning: circular argument reference - now
== 20180824051630 CreateBooks: migrating ======================================
-- create_table(:books)
   -> 0.0007s
== 20180824051630 CreateBooks: migrated (0.0008s) =============================

/book_admin $

OK!

次にapp/models/book.rbを見ていきます。

class Book < ActiveRecord::Base
end

これしか書いていませんね。
ただActiveRecord::Baseを継承しているので基本的な機能は実装されています。
rails consoleで見てみましょう。

irb(main):001:0> Book
=> Book (call 'Book.connection' to establish a connection)
irb(main):002:0>

あれ、うまくいかねえ。
データベースとの接続がうまく行ってないご様子。
いったんBook.allを叩いてから試してみる。

/book_admin $ rails c
Running via Spring preloader in process 13164
Loading development environment (Rails 4.1.1)
irb(main):001:0> Book.all
  Book Load (0.6ms)  SELECT "books".* FROM "books"
=> #
irb(main):002:0> Book
=> Book(id: integer, name: string, published_on: date, price: integer, number_of_page: integer, created_at: datetime, updated_at: datetime)
irb(main):003:0>

allメソッドなどを確認することができました。
理由はよくわからん

とりあえず次へすすむ…

モデルとCRUD

CRUDとはCreate,Read,Update,Deleteの略語

Create=>Book.createやBook#save

Read=>Book.findやBook.all,Book.where など

Update=>Book#save,Book#update_attributes など

Delete=>Book#delete, Book#destroy

に対応する。
ふーん。

モデルを見つける

とりあえずrails sでサーバーを立ち上げ、rails consoleを起動する。

本の指示に従ってレコードを作る。
単純に従うのは嫌なので個数を増やしてみる。

/book_admin $ rails c
Running via Spring preloader in process 13388
Loading development environment (Rails 4.1.1)
irb(main):001:0> (1..10).each do |i|
irb(main):002:1* Book.create(name: "Book #{i}", published_on: i.months.ago, price: (i*500), number_of_page: (11 -i)*100 )
irb(main):003:1> end
   (0.1ms)  begin transaction
  SQL (0.3ms)  INSERT INTO "books" ("created_at", "name", "number_of_page", "price", "published_on", "updated_at") VALUES (?, ?, ?, ?, ?, ?)  [["created_at", "2018-08-24 06:45:03.137149"], ["name", "Book 1"], ["number_of_page", 1000], ["price", 500], ["published_on", "2018-07-24"], ["updated_at", "2018-08-24 06:45:03.137149"]]
   (9.0ms)  commit transaction
   (0.0ms)  begin transaction
  SQL (0.1ms)  INSERT INTO "books" ("created_at", "name", "number_of_page", "price", "published_on", "updated_at") VALUES (?, ?, ?, ?, ?, ?)  [["created_at", "2018-08-24 06:45:03.150763"], ["name", "Book 2"], ["number_of_page", 900], ["price", 1000], ["published_on", "2018-06-24"], ["updated_at", "2018-08-24 06:45:03.150763"]]
   (12.0ms)  commit transaction
   (0.1ms)  begin transaction
  SQL (0.3ms)  INSERT INTO "books" ("created_at", "name", "number_of_page", "price", "published_on", "updated_at") VALUES (?, ?, ?, ?, ?, ?)  [["created_at", "2018-08-24 06:45:03.164478"], ["name", "Book 3"], ["number_of_page", 800], ["price", 1500], ["published_on", "2018-05-24"], ["updated_at", "2018-08-24 06:45:03.164478"]]
   (8.3ms)  commit transaction
   (0.1ms)  begin transaction
  SQL (0.2ms)  INSERT INTO "books" ("created_at", "name", "number_of_page", "price", "published_on", "updated_at") VALUES (?, ?, ?, ?, ?, ?)  [["created_at", "2018-08-24 06:45:03.174840"], ["name", "Book 4"], ["number_of_page", 700], ["price", 2000], ["published_on", "2018-04-24"], ["updated_at", "2018-08-24 06:45:03.174840"]]
   (6.3ms)  commit transaction
   (0.0ms)  begin transaction
  SQL (0.1ms)  INSERT INTO "books" ("created_at", "name", "number_of_page", "price", "published_on", "updated_at") VALUES (?, ?, ?, ?, ?, ?)  [["created_at", "2018-08-24 06:45:03.182615"], ["name", "Book 5"], ["number_of_page", 600], ["price", 2500], ["published_on", "2018-03-24"], ["updated_at", "2018-08-24 06:45:03.182615"]]
   (6.0ms)  commit transaction
   (0.1ms)  begin transaction
  SQL (0.1ms)  INSERT INTO "books" ("created_at", "name", "number_of_page", "price", "published_on", "updated_at") VALUES (?, ?, ?, ?, ?, ?)  [["created_at", "2018-08-24 06:45:03.189731"], ["name", "Book 6"], ["number_of_page", 500], ["price", 3000], ["published_on", "2018-02-24"], ["updated_at", "2018-08-24 06:45:03.189731"]]
   (4.3ms)  commit transaction
   (0.1ms)  begin transaction
  SQL (0.2ms)  INSERT INTO "books" ("created_at", "name", "number_of_page", "price", "published_on", "updated_at") VALUES (?, ?, ?, ?, ?, ?)  [["created_at", "2018-08-24 06:45:03.195387"], ["name", "Book 7"], ["number_of_page", 400], ["price", 3500], ["published_on", "2018-01-24"], ["updated_at", "2018-08-24 06:45:03.195387"]]
   (6.4ms)  commit transaction
   (0.1ms)  begin transaction
  SQL (0.1ms)  INSERT INTO "books" ("created_at", "name", "number_of_page", "price", "published_on", "updated_at") VALUES (?, ?, ?, ?, ?, ?)  [["created_at", "2018-08-24 06:45:03.203673"], ["name", "Book 8"], ["number_of_page", 300], ["price", 4000], ["published_on", "2017-12-24"], ["updated_at", "2018-08-24 06:45:03.203673"]]
   (4.1ms)  commit transaction
   (0.0ms)  begin transaction
  SQL (0.1ms)  INSERT INTO "books" ("created_at", "name", "number_of_page", "price", "published_on", "updated_at") VALUES (?, ?, ?, ?, ?, ?)  [["created_at", "2018-08-24 06:45:03.208937"], ["name", "Book 9"], ["number_of_page", 200], ["price", 4500], ["published_on", "2017-11-24"], ["updated_at", "2018-08-24 06:45:03.208937"]]
   (6.0ms)  commit transaction
   (0.0ms)  begin transaction
  SQL (0.1ms)  INSERT INTO "books" ("created_at", "name", "number_of_page", "price", "published_on", "updated_at") VALUES (?, ?, ?, ?, ?, ?)  [["created_at", "2018-08-24 06:45:03.216121"], ["name", "Book 10"], ["number_of_page", 100], ["price", 5000], ["published_on", "2017-10-24"], ["updated_at", "2018-08-24 06:45:03.216121"]]
   (8.0ms)  commit transaction
=> 1..10
irb(main):004:0>

よしこれでOK

いろいろ検索してみる
findメソッドは”id”から検索を行う

irb(main):004:0> Book.find(1)
  Book Load (0.4ms)  SELECT  "books".* FROM "books"  WHERE "books"."id" = ? LIMIT 1  [["id", 1]]
=> #<Book id: 1, name: "Book 1", published_on: "2018-07-24", price: 500, number_of_page: 1000, created_at: "2018-08-24 06:45:03", updated_at: "2018-08-24 06:45:03">
irb(main):005:0>

find_byメソッドは引数にカラム名と検索条件を1つ以上指定できる。

irb(main):005:0> Book.find_by(name: "Book 3")
  Book Load (0.1ms)  SELECT  "books".* FROM "books"  WHERE "books"."name" = 'Book 3' LIMIT 1
=> #<Book id: 3, name: "Book 3", published_on: "2018-05-24", price: 1500, number_of_page: 800, created_at: "2018-08-24 06:45:03", updated_at: "2018-08-24 06:45:03">
irb(main):006:0> Book.find_by(name: "Book 3", price: 1500)
  Book Load (0.2ms)  SELECT  "books".* FROM "books"  WHERE "books"."name" = 'Book 3' AND "books"."price" = 1500 LIMIT 1
=> #<Book id: 3, name: "Book 3", published_on: "2018-05-24", price: 1500, number_of_page: 800, created_at: "2018-08-24 06:45:03", updated_at: "2018-08-24 06:45:03">
irb(main):007:0>

find_by_XXXメソッドは引数にXXXカラムに存在可能な値を指定できる。XXX=nameのとき

irb(main):007:0> Book.find_by_name("Book 7")
  Book Load (0.4ms)  SELECT  "books".* FROM "books"  WHERE "books"."name" = 'Book 7' LIMIT 1
=> #<Book id: 7, name: "Book 7", published_on: "2018-01-24", price: 3500, number_of_page: 400, created_at: "2018-08-24 06:45:03", updated_at: "2018-08-24 06:45:03">
irb(main):008:0>

find系メソッドは発行されるSQLにLIMIT 1がついているように、ただ1つの返り値を持つ。
それに対しwhereメソッドは条件で絞り込むことが可能で、複数のレコードを取得できる。

irb(main):008:0> Book.where("price > ?", 2500)
  Book Load (0.4ms)  SELECT "books".* FROM "books"  WHERE (price > 2500)
=> #<ActiveRecord::Relation[
#<Book id: 6, name: "Book 6", published_on: "2018-02-24", price: 3000, number_of_page: 500, created_at: "2018-08-24 06:45:03", updated_at: "2018-08-24 06:45:03">,
#<Book id: 7,name: "Book 7", published_on: "2018-01-24", price: 3500, number_of_page: 400, created_at: "2018-08-24 06:45:03", updated_at: "2018-08-24 06:45:03">,
#<Book id: 8, name: "Book 8", published_on: "2017-12-24", price: 4000, number_of_page: 300, created_at: "2018-08-24 06:45:03", updated_at: "2018-08-24 06:45:03">,
#<Book id: 9, name: "Book 9", published_on: "2017-11-24", price: 4500, number_of_page: 200, created_at: "2018-08-24 06:45:03", updated_at: "2018-08-24 06:45:03">,
#<Book id: 10, name: "Book 10", published_on: "2017-10-24", price: 5000, number_of_page: 100, created_at: "2018-08-24 06:45:03", updated_at: "2018-08-24 06:45:03">
]>
irb(main):009:0>

他にもクエリインターフェースと呼ばれる、SQLに対応するメソッドをつなげてメソッドチェーンを形成することも可能ですね。

irb(main):009:0> Book.where("price > ?", 2500).order("price DESC").limit(3)
  Book Load (0.6ms)  SELECT  "books".* FROM "books"  WHERE (price > 2500)  ORDER BY price DESC LIMIT 3
=> #<ActiveRecord::Relation [
#<Book id: 10, name: "Book 10", published_on: "2017-10-24", price: 5000, number_of_page: 100, created_at: "2018-08-24 06:45:03", updated_at: "2018-08-24 06:45:03">,
#<Book id: 9, name: "Book 9", published_on: "2017-11-24", price: 4500, number_of_page: 200, created_at: "2018-08-24 06:45:03", updated_at: "2018-08-24 06:45:03">,
#<Book id: 8, name: "Book 8", published_on: "2017-12-24", price: 4000, number_of_page: 300, created_at: "2018-08-24 06:45:03", updated_at: "2018-08-24 06:45:03">
]>
irb(main):010:0>

うんうん、うまくできました!

ActiveRecord::Relationクラスの説明は本買って読んでください。
書くのが面倒なので読むだけ読んでおく。

Scopeを定義する

さきほどのBook.rbの中にスコープの定義を書き加えます。
デフォルトスコープも一緒に定義しておきます。

class Book < ActiveRecord::Base 
    scope :costly, -> { where("price > ?", 3000)}
    scope :written_about, -> (theme){ where("name like '#{theme}'")}
    default_scope -> { order("price DESC")}
end

ここで動きをrails consoleで見ていきましょう。

/book_admin $ rails c
Running via Spring preloader in process 14004
Loading development environment (Rails 4.1.1)
irb(main):001:0> Book.all
  Book Load (0.6ms)  SELECT "books".* FROM "books"   ORDER BY price DESC
=> #<ActiveRecord::Relation [
#<Book id: 10, name: "Book 10", published_on: "2017-10-24", price: 5000, number_of_page: 100, created_at: "2018-08-24 06:45:03", updated_at: "2018-08-24 06:45:03">, 
#<Book id: 9, name: "Book 9", published_on: "2017-11-24", price: 4500, number_of_page: 200, created_at: "2018-08-24 06:45:03", updated_at: "2018-08-24 06:45:03">, 
#<Book id: 8, name: "Book 8", published_on: "2017-12-24", price: 4000, number_of_page: 300, created_at: "2018-08-24 06:45:03", updated_at: "2018-08-24 06:45:03">, 
#<Book id: 7, name: "Book 7", published_on: "2018-01-24", price: 3500, number_of_page: 400, created_at: "2018-08-24 06:45:03", updated_at: "2018-08-24 06:45:03">, 
#<Book id: 6, name: "Book 6", published_on: "2018-02-24", price: 3000, number_of_page: 500, created_at: "2018-08-24 06:45:03", updated_at: "2018-08-24 06:45:03">, 
#<Book id: 5, name: "Book 5", published_on: "2018-03-24", price: 2500, number_of_page: 600, created_at: "2018-08-24 06:45:03", updated_at: "2018-08-24 06:45:03">, 
#<Book id: 4, name: "Book 4", published_on: "2018-04-24", price: 2000, number_of_page: 700, created_at: "2018-08-24 06:45:03", updated_at: "2018-08-24 06:45:03">,
#<Book id: 3, name: "Book 3", published_on: "2018-05-24", price: 1500, number_of_page: 800, created_at: "2018-08-24 06:45:03", updated_at: "2018-08-24 06:45:03">,
#<Book id: 2, name: "Book 2", published_on: "2018-06-24", price: 1000, number_of_page: 900, created_at: "2018-08-24 06:45:03", updated_at: "2018-08-24 06:45:03">, 
#<Book id: 1, name: "Book 1", published_on: "2018-07-24", price: 500, number_of_page: 1000, created_at: "2018-08-24 06:45:03", updated_at: "2018-08-24 06:45:03">]>
irb(main):002:0> Book.costly
  Book Load (0.2ms)  SELECT "books".* FROM "books"  WHERE (price > 3000)  ORDER BY price DESC
=> #<ActiveRecord::Relation [
#<Book id: 10, name: "Book 10", published_on: "2017-10-24", price: 5000, number_of_page: 100, created_at: "2018-08-24 06:45:03", updated_at: "2018-08-24 06:45:03">, 
#<Book id: 9, name: "Book 9", published_on: "2017-11-24", price: 4500, number_of_page: 200, created_at: "2018-08-24 06:45:03", updated_at: "2018-08-24 06:45:03">, 
#<Book id: 8, name: "Book 8", published_on: "2017-12-24", price: 4000, number_of_page: 300, created_at: "2018-08-24 06:45:03", updated_at: "2018-08-24 06:45:03">, 
#<Book id: 7, name: "Book 7", published_on: "2018-01-24", price: 3500, number_of_page: 400, created_at: "2018-08-24 06:45:03", updated_at: "2018-08-24 06:45:03">
]>
irb(main):003:0>

scopeの使い方とdefault_scopeの挙動についても確認できました!

スコープは

  • よく使うクエリを保存し、再利用性を高める
  • クエリに名前をつけることで可読性を上げる

役割を果たします。

モデル同士のリレーション

ここでは出版社と著者について見ていきます。

出版社には

  • 出版社名
  • 住所

著者には

  • 著者名
  • ペンネーム

のカラムを設定します。
rails gでモデルを作り、db:migrateします。

/book_admin $ rails g model Publisher name:string address:text
Running via Spring preloader in process 14605
Expected string default value for '--helper'; got true (boolean)
      invoke  active_record
      create    db/migrate/20180824082503_create_publishers.rb
      create    app/models/publisher.rb
      invoke    test_unit
      create      test/models/publisher_test.rb
      create      test/fixtures/publishers.yml
/book_admin $ rails g model Author name:string penname:string
Running via Spring preloader in process 14629
Expected string default value for '--helper'; got true (boolean)
      invoke  active_record
      create    db/migrate/20180824082521_create_authors.rb
      create    app/models/author.rb
      invoke    test_unit
      create      test/models/author_test.rb
      create      test/fixtures/authors.yml
/book_admin $ rake db:migrate
/var/lib/gems/2.3.0/gems/activesupport-4.1.1/lib/active_support/values/time_zone.rb:285: warning: circular argument reference - now
== 20180824051630 CreateBooks: migrating ======================================
-- create_table(:books)
   -> 0.0007s
== 20180824051630 CreateBooks: migrated (0.0008s) =============================

== 20180824083447 CreatePublishers: migrating =================================
-- create_table(:publishers)
   -> 0.0004s
== 20180824083447 CreatePublishers: migrated (0.0005s) ========================

== 20180824083511 CreateAuthors: migrating ====================================
-- create_table(:authors)
   -> 0.0006s
== 20180824083511 CreateAuthors: migrated (0.0007s) ===========================

/book_admin $

ここにきてようやくbundle execなしでもいけることに気づく…
そして本の指示に従って別のモデルへの参照を定義する

/book_admin $ rails g migration AddPublisherIdToBooks publisher:references
Running via Spring preloader in process 15016
Expected string default value for '--helper'; got true (boolean)
      invoke  active_record
      create    db/migrate/20180824083818_add_publisher_id_to_books.rb
/book_admin $

このreferencesというカラムの型は外部キー制約を付与するということです。
インデックスも自動で張ってくれます。

それではdb:migrateして、ファイルを開いて見てみましょう

class AddPublisherIdToBooks < ActiveRecord::Migration
  def change
    add_reference :books, :publisher, index: true
  end
end

おー、できてますね。
indexもtrueになっています。

これをすることで、Bookモデルに対してPublisherモデルへの参照を追加する、ということになります。

1つの出版社に対して本は無数にあります。
また1つの本に対して出版社は1つです。

この関係は1対多、多対1の関係と呼ばれます。
達人にならいました笑

この関係をRailsで表現するときに使われるクラスメソッドがhas_manyとbelongs_toです。
たびたびインターン先で作っているときに出現していたのですが、いまいちよくわかっていませんでした。
ははあ。なるほどなるほど。

というわけで本の指示通りに記述していきます。

class Book < ActiveRecord::Base scope :costly, -> { where("price > ?", 3000)}
    scope :written_about, -> (theme){ where("name like '#{theme}'")}
    default_scope -> { order("price DESC")}

    belongs_to :publisher
end
class Publisher < ActiveRecord::Base
    has_many :books
end

さあrails consoleで確認していきましょう。

/book_admin $ rails c
Running via Spring preloader in process 15629
Loading development environment (Rails 4.1.1)
irb(main):001:0> publisher = Publisher.create(name: "技術評論社", address: "hidden")
   (0.1ms)  begin transaction
  SQL (1.4ms)  INSERT INTO "publishers" ("address", "created_at", "name", "updated_at") VALUES (?, ?, ?, ?)  [["address", "hidden"], ["created_at", "2018-08-24 09:07:22.392373"], ["name", "技術評論社"], ["updated_at", "2018-08-24 09:07:22.392373"]]
   (6.9ms)  commit transaction
=> #
irb(main):002:0> publisher.books << Book.find(1) /var/lib/gems/2.3.0/gems/activerecord-4.1.1/lib/active_record/associations/has_many_association.rb:74:warning: circular argument reference - reflection /var/lib/gems/2.3.0/gems/activerecord-4.1.1/lib/active_record/associations/has_many_association.rb:78:warning: circular argument reference - reflection /var/lib/gems/2.3.0/gems/activerecord-4.1.1/lib/active_record/associations/has_many_association.rb:82:warning: circular argument reference - reflection /var/lib/gems/2.3.0/gems/activerecord-4.1.1/lib/active_record/associations/has_many_association.rb:101: warning: circular argument reference - reflection Book Load (0.1ms) SELECT "books".* FROM "books" WHERE "books"."id" = ? ORDER BY price DESC LIMIT1 [["id", 1]] (0.0ms) begin transaction SQL (0.2ms) UPDATE "books" SET "publisher_id" = ?, "updated_at" = ? WHERE "books"."id" = 1 [["publisher_id", 2], ["updated_at", "2018-08-24 09:07:39.145540"]] (4.4ms) commit transaction Book Load (0.1ms) SELECT "books".* FROM "books" WHERE "books"."publisher_id" = ? ORDER BY price DESC [["publisher_id", 2]] => #]>
irb(main):003:0> publisher.books.to_a
=> [#]
irb(main):004:0> book = Book.find(1)
  Book Load (0.2ms)  SELECT  "books".* FROM "books"  WHERE "books"."id" = ?  ORDER BY price DESC LIMIT1  [["id", 1]]
=> #
irb(main):005:0> book.publisher.name
  Publisher Load (0.3ms)  SELECT  "publishers".* FROM "publishers"  WHERE "publishers"."id" = ? LIMIT 1  [["id", 2]]
=> "技術評論社"
irb(main):006:0>

できたー!
中途半端にRailsをかじっているとbelongs_toのところにhas_oneとか書いて詰まったりします(え)

has_oneとbelongs_toの違いは、どっちに書くかです。
さらっと書くと

has_manyとbelongs_to、has_oneとbelongs_toはこれでワンセットです。
belongs_toはすべて従属する側に書き、has_XXXは従属させる側に書きます。

それでは次の多対多の関係を作る場合もみていきます。

まず多対多の関係はどう表現するでしょうか。
この関係は登場している2つのテーブルだけで表すことはできません。
あるレコードに対して、もう片方のどのレコードが対応するかを特定するために必要な情報が互いに存在しないからですね。

達人から学んだ教えには、多対多の関係は中間テーブルをあたらしく導入して、これを用いて多対対多(色が同じものは実体として同じという意味です)を表現するとありました。
たぶんこの説明だけではピンとこないと思うので実際に作ってみましょう。

/book_admin $ rails g model BookAuthor book:references author:references
/var/lib/gems/2.3.0/gems/activesupport-4.1.1/lib/active_support/values/time_zone.rb:285: warning: circular argument reference - now
Running via Spring preloader in process 5287
Expected string default value for '--helper'; got true (boolean)
      invoke  active_record
      create    db/migrate/20180825014334_create_book_authors.rb
      create    app/models/book_author.rb
      invoke    test_unit
      create      test/models/book_author_test.rb
      create      test/fixtures/book_authors.yml
/book_admin $ rake db:migrate
/var/lib/gems/2.3.0/gems/activesupport-4.1.1/lib/active_support/values/time_zone.rb:285: warning: circular argument reference - now
== 20180825014334 CreateBookAuthors: migrating ================================
-- create_table(:book_authors)
   -> 0.0032s
== 20180825014334 CreateBookAuthors: migrated (0.0032s) =======================

/book_admin $

bookとauthorを結びつける中間テーブルを作成しました。

しかしこれだけ作ってもまだコード側には反映されていないので、この関係をコードにも記述します。

class Book < ActiveRecord::Base scope :costly, -> { where("price > ?", 3000)}
    scope :written_about, -> (theme){ where("name like '#{theme}'")}
    default_scope -> { order("price DESC")}

    belongs_to :publisher
    
    has_many :book_authors
    has_many :authors, through: :book_authors
end
class Publisher < ActiveRecord::Base
    has_many :books
end

これによって、bookはauthorsを、authorはbooksを特定できるようになりました。
さあ確認してみましょう。

(main):001:0> matz = Author.create name: "松本ゆきひろ" ,penname: "Matz"
   (0.1ms)  begin transaction
  SQL (0.2ms)  INSERT INTO "authors" ("created_at", "name", "penname", "updated_at") VALUES (?, ?, ?, ?)  [["created_at", "2018-08-25 07:03:37.719583"], ["name", "松本ゆきひろ"], ["penname", "Matz"], ["updated_at", "2018-08-25 07:03:37.719583"]]
   (8.7ms)  commit transaction
=> #
irb(main):002:0> matz.books << Book.find(1)
/var/lib/gems/2.3.0/gems/activerecord-4.1.1/lib/active_record/associations/has_many_association.rb:74:warning: circular argument reference - reflection
/var/lib/gems/2.3.0/gems/activerecord-4.1.1/lib/active_record/associations/has_many_association.rb:78:warning: circular argument reference - reflection
/var/lib/gems/2.3.0/gems/activerecord-4.1.1/lib/active_record/associations/has_many_association.rb:82:warning: circular argument reference - reflection
/var/lib/gems/2.3.0/gems/activerecord-4.1.1/lib/active_record/associations/has_many_association.rb:101: warning: circular argument reference - reflection
  Book Load (0.1ms)  SELECT  "books".* FROM "books"  WHERE "books"."id" = ?  ORDER BY price DESC LIMIT1  [["id", 1]]
   (0.0ms)  begin transaction
  SQL (0.2ms)  INSERT INTO "book_authors" ("author_id", "book_id", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["author_id", 3], ["book_id", 1], ["created_at", "2018-08-25 07:03:53.355259"], ["updated_at", "2018-08-25 07:03:53.355259"]]
   (0.1ms)  rollback transaction
NoMethodError: undefined method `name' for nil:NilClass
        from /var/lib/gems/2.3.0/gems/activerecord-4.1.1/lib/active_record/associations/has_many_association.rb:79:in `cached_counter_attribute_name'
        from /var/lib/gems/2.3.0/gems/activerecord-4.1.1/lib/active_record/associations/has_many_association.rb:75:in `has_cached_counter?'
        from /var/lib/gems/2.3.0/gems/activerecord-4.1.1/lib/active_record/associations/has_many_association.rb:83:in `update_counter'
        from /var/lib/gems/2.3.0/gems/activerecord-4.1.1/lib/active_record/associations/has_many_through_association.rb:65:in `insert_record'
        from /var/lib/gems/2.3.0/gems/activerecord-4.1.1/lib/active_record/associations/collection_association.rb:522:in `block (2 levels) in concat_records'
        from /var/lib/gems/2.3.0/gems/activerecord-4.1.1/lib/active_record/associations/collection_association.rb:389:in `add_to_target'
        from /var/lib/gems/2.3.0/gems/activerecord-4.1.1/lib/active_record/associations/collection_association.rb:521:in `block in concat_records'
        from /var/lib/gems/2.3.0/gems/activerecord-4.1.1/lib/active_record/associations/collection_association.rb:519:in `each'
        from /var/lib/gems/2.3.0/gems/activerecord-4.1.1/lib/active_record/associations/collection_association.rb:519:in `concat_records'
        from /var/lib/gems/2.3.0/gems/activerecord-4.1.1/lib/active_record/associations/has_many_through_association.rb:42:in `concat_records'
        from /var/lib/gems/2.3.0/gems/activerecord-4.1.1/lib/active_record/associations/collection_association.rb:153:in `block in concat'
        from /var/lib/gems/2.3.0/gems/activerecord-4.1.1/lib/active_record/associations/collection_association.rb:168:in `block in transaction'
        from /var/lib/gems/2.3.0/gems/activerecord-4.1.1/lib/active_record/connection_adapters/abstract/database_statements.rb:211:in `block in transaction'
        from /var/lib/gems/2.3.0/gems/activerecord-4.1.1/lib/active_record/connection_adapters/abstract/database_statements.rb:219:in `within_new_transaction'
        from /var/lib/gems/2.3.0/gems/activerecord-4.1.1/lib/active_record/connection_adapters/abstract/database_statements.rb:211:in `transaction'
        from /var/lib/gems/2.3.0/gems/activerecord-4.1.1/lib/active_record/transactions.rb:208:in `transaction'
... 6 levels...
        from /var/lib/gems/2.3.0/gems/railties-4.1.1/lib/rails/commands/console.rb:9:in `start'
        from /var/lib/gems/2.3.0/gems/railties-4.1.1/lib/rails/commands/commands_tasks.rb:69:in `console'
        from /var/lib/gems/2.3.0/gems/railties-4.1.1/lib/rails/commands/commands_tasks.rb:40:in `run_command!'
        from /var/lib/gems/2.3.0/gems/railties-4.1.1/lib/rails/commands.rb:17:in `'
        from /var/lib/gems/2.3.0/gems/activesupport-4.1.1/lib/active_support/dependencies.rb:247:in `require'
        from /var/lib/gems/2.3.0/gems/activesupport-4.1.1/lib/active_support/dependencies.rb:247:in `block in require'
        from /var/lib/gems/2.3.0/gems/activesupport-4.1.1/lib/active_support/dependencies.rb:232:in `load_dependency'
        from /var/lib/gems/2.3.0/gems/activesupport-4.1.1/lib/active_support/dependencies.rb:247:in `require'
        from /home/aporia2718/perfect-ror-part1/book_admin/bin/rails:9:in `'
        from /var/lib/gems/2.3.0/gems/activesupport-4.1.1/lib/active_support/dependencies.rb:241:in `load'
        from /var/lib/gems/2.3.0/gems/activesupport-4.1.1/lib/active_support/dependencies.rb:241:in `block in load'
        from /var/lib/gems/2.3.0/gems/activesupport-4.1.1/lib/active_support/dependencies.rb:232:in `load_dependency'
        from /var/lib/gems/2.3.0/gems/activesupport-4.1.1/lib/active_support/dependencies.rb:241:in `load'
        from /usr/lib/ruby/2.3.0/rubygems/core_ext/kernel_require.rb:55:in `require'
        from /usr/lib/ruby/2.3.0/rubygems/core_ext/kernel_require.rb:55:in `require'
        from -e:1:in `'
irb(main):003:0>

 

????

なぜだろうと思って”NoMethodError: undefined method `name’ for nil:NilClass パーフェクト”でぐぐるとこんなページが見つかる(そしてこれで治る、ここはただの詳細なログなので飛ばしても無問題)

はっきりいってわけがわからないがとりあえずやってみる。
いったん最初に作ったGemfileを4.1.2に書き換える。

source "http://rubygems.org"

gem "rdefs"
gem "rails", "4.1.2"

そしてbundle install

$ bundle install
Fetching gem metadata from http://rubygems.org/..........
Fetching version metadata from http://rubygems.org/..
Fetching dependency metadata from http://rubygems.org/.
Resolving dependencies...
(略)
Fetching actionmailer 4.1.2 (was 4.1.1)
Installing actionmailer 4.1.2 (was 4.1.1)
Fetching railties 4.1.2 (was 4.1.1)
Installing railties 4.1.2 (was 4.1.1)
Using sprockets-rails 2.3.3
Fetching rails 4.1.2 (was 4.1.1)
Installing rails 4.1.2 (was 4.1.1)
Bundle complete! 2 Gemfile dependencies, 29 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.
The latest bundler is 1.16.4, but you are currently running 1.15.1.
To update, run `gem install bundler`
$

さあどうだろう

$ rails c
Running via Spring preloader in process 7837
Loading development environment (Rails 4.1.1)
irb(main):001:0> 

あれ、4.1.1のままじゃん
確認したらbook_adminの中にGemfileがあるのを確認。
そして以下のように修正

source 'https://rubygems.org'

gem 'rails', '4.1.2'

gem 'sqlite3'

gem 'sass-rails', '~> 4.0.3'

gem 'uglifier', '>= 1.3.0'

gem 'coffee-rails', '~> 4.0.0'

gem 'jquery-rails'

gem 'turbolinks'

gem 'jbuilder', '~> 2.0'

gem 'sdoc', '~> 0.4.0',          group: :doc

gem 'spring',        group: :development

bundle installするも…

/book_admin $ bundle
Fetching gem metadata from https://rubygems.org/..........
Fetching version metadata from https://rubygems.org/..
Fetching dependency metadata from https://rubygems.org/.
Resolving dependencies...
The latest bundler is 1.16.4, but you are currently running 1.15.1.
To update, run `gem install bundler`
Bundler could not find compatible versions for gem "activesupport":
  In snapshot (Gemfile.lock):
    activesupport (= 4.1.1)

  In Gemfile:
    coffee-rails (~> 4.0.0) was resolved to 4.0.1, which depends on
      railties (< 5.0, >= 4.0.0) was resolved to 4.1.1, which depends on
        actionpack (= 4.1.1) was resolved to 4.1.1, which depends on
          actionview (= 4.1.1) was resolved to 4.1.1, which depends on
            activesupport (= 4.1.1)

    coffee-rails (~> 4.0.0) was resolved to 4.0.1, which depends on
      railties (< 5.0, >= 4.0.0) was resolved to 4.1.1, which depends on
        actionpack (= 4.1.1) was resolved to 4.1.1, which depends on
          actionview (= 4.1.1) was resolved to 4.1.1, which depends on
            activesupport (= 4.1.1)

    coffee-rails (~> 4.0.0) was resolved to 4.0.1, which depends on
      railties (< 5.0, >= 4.0.0) was resolved to 4.1.1, which depends on
        actionpack (= 4.1.1) was resolved to 4.1.1, which depends on
          actionview (= 4.1.1) was resolved to 4.1.1, which depends on
            activesupport (= 4.1.1)

    rails (= 4.1.2) was resolved to 4.1.2, which depends on
      activesupport (= 4.1.2)

Running `bundle update` will rebuild your snapshot from scratch, using only
the gems in your Gemfile, which may resolve the conflict.

というエラー。
よくよく考えればupdateだよな。
とりあえずupdate。

/book_admin $ bundle update
Fetching gem metadata from https://rubygems.org/...........
Fetching version metadata from https://rubygems.org/...
Fetching dependency metadata from https://rubygems.org/..
Resolving dependencies...
Using rake 12.3.1
Using concurrent-ruby 1.0.5
Using json 1.8.6
Using minitest 5.11.3
Using thread_safe 0.3.6
Using builder 3.2.3
Using erubis 2.7.0
Using rack 1.5.5
Using mime-types 1.25.1
Using polyglot 0.3.5
Using arel 5.0.1.20140414130214
Using bundler 1.15.1
Using thor 0.20.0
Using hike 1.2.3
Using multi_json 1.13.1
Using tilt 1.4.1
Using sqlite3 1.3.13
Using sass 3.2.19
Using execjs 2.7.0
Using coffee-script-source 1.12.2
Using turbolinks-source 5.2.0
Using rdoc 4.3.0
Using spring 1.7.2
Using i18n 0.9.5
Using tzinfo 1.2.5
Using rack-test 0.6.3
Using treetop 1.4.15
Using sprockets 2.12.5
Using uglifier 4.1.18
Using coffee-script 2.4.1
Using turbolinks 5.2.0
Using sdoc 0.4.2
Using activesupport 4.1.2 (was 4.1.1)
Using mail 2.5.5
Using actionview 4.1.2 (was 4.1.1)
Using activemodel 4.1.2 (was 4.1.1)
Using jbuilder 2.6.4
Using actionpack 4.1.2 (was 4.1.1)
Using activerecord 4.1.2 (was 4.1.1)
Using actionmailer 4.1.2 (was 4.1.1)
Using railties 4.1.2 (was 4.1.1)
Using sprockets-rails 2.3.3
Using coffee-rails 4.0.1
Using jquery-rails 3.1.5
Using rails 4.1.2 (was 4.1.1)
Using sass-rails 4.0.5
Bundle updated!
The latest bundler is 1.16.4, but you are currently running 1.15.1.
To update, run `gem install bundler`

言われてみればそりゃそうだ。
さあ確認。

ここからコピペした際にwordpressにタグと認識されたレコードたちが消滅していますが、問題なく動作しております。ご了承ください。

irb(main):002:0> matz =  Author.create name: "松本ゆきひろ", penname: "Matz"
   (0.1ms)  begin transaction
  SQL (0.4ms)  INSERT INTO "authors" ("created_at", "name", "penname", "updated_at") VALUES (?, ?, ?, ?)  [["created_at", "2018-08-25 07:21:29.809364"], ["name", "松本ゆきひろ"], ["penname", "Matz"], ["updated_at", "2018-08-25 07:21:29.809364"]]
   (7.2ms)  commit transaction
=> #
irb(main):003:0> matz.books << Book.find(1) Book Load (0.1ms) SELECT "books".* FROM "books" WHERE "books"."id" = ? ORDER BY price DESC LIMIT1 [["id", 1]] (0.0ms) begin transaction SQL (0.2ms) INSERT INTO "book_authors" ("author_id", "book_id", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["author_id", 6], ["book_id", 1], ["created_at", "2018-08-25 07:21:53.782789"], ["updated_at", "2018-08-25 07:21:53.782789"]] (9.3ms) commit transaction Book Load (0.2ms) SELECT "books".* FROM "books" INNER JOIN "book_authors" ON "books"."id" = "book_authors"."book_id" WHERE "book_authors"."author_id" = ? ORDER BY price DESC [["author_id", 6]] => #]>
irb(main):004:0> matz.books
=> #]>
irb(main):005:0> book = Book.find(1)
  Book Load (0.2ms)  SELECT  "books".* FROM "books"  WHERE "books"."id" = ?  ORDER BY price DESC LIMIT1  [["id", 1]]
=> #
irb(main):006:0> book.authors
  Author Load (0.2ms)  SELECT "authors".* FROM "authors" INNER JOIN "book_authors" ON "authors"."id" ="book_authors"."author_id" WHERE "book_authors"."book_id" = ?  [["book_id", 1]]
=> #]>
irb(main):007:0>

なんとできてしまった。

うっそだろまじかよ

authorのIDからこれまでの苦労が垣間見える…

とりあえずうまく動いたので成功。
これ最初に発見した人はさぞ苦労したのだろうなあ。

モデルを操作する

操作しましょう。

irb(main):007:0> book = Book.new
=> #
irb(main):008:0> book.name = "Perfect RoR"
=> "Perfect RoR"
rb(main):009:0> book.save
   (0.1ms)  begin transaction
  SQL (0.4ms)  INSERT INTO "books" ("created_at", "name", "updated_at") VALUES (?, ?, ?)  [["created_at", "2018-08-25 07:29:45.312039"], ["name", "Perfect RoR"], ["updated_at", "2018-08-25 07:29:45.312039"]]
   (9.1ms)  commit transaction
=> true
irb(main):010:0>

createメソッドが何してるのか確認し操作しました。
次へすすむ

バリデーション

本の指示通りやる
バリデーションをbook.rbにかいた。

class Book < ActiveRecord::Base scope :costly, -> { where("price > ?", 3000)}
    scope :written_about, -> (theme){ where("name like '#{theme}'")}
    default_scope -> { order("price DESC")}

    belongs_to :publisher
    
    has_many :book_authors
    has_many :authors, through: :book_authors

    validates :name, presence: true
    validates :name, length: { maximum: 15 }
    validates :price, numericality: { greater_than_or_equal_to: 0 }
end

バリデーターが動くか確認してみる

irb(main):001:0> Book.create name: "あいうえお" ,price: 400000
   (0.1ms)  begin transaction
  SQL (0.8ms)  INSERT INTO "books" ("created_at", "name", "price", "updated_at") VALUES (?, ?, ?, ?)  [["created_at", "2018-08-25 07:46:41.959090"], ["name", "あいうえお"], ["price", 400000], ["updated_at", "2018-08-25 07:46:41.959090"]]
   (6.3ms)  commit transaction
=> #
irb(main):002:0> Book.create name: "あいうえお" ,price: -1500
   (0.1ms)  begin transaction
   (0.1ms)  rollback transaction
=> #
irb(main):003:0>

確認done
エラーも確認

irb(main):004:0> book1 = Book.create name: "あいうえお" ,price: -1500
   (0.2ms)  begin transaction
   (0.1ms)  rollback transaction
=> #
irb(main):005:0> book1.errors
=> #, @messages={:price=>["must be greater than or equal to 0"]}>
irb(main):006:0>

done

save!やupdate!がついたメソッドとそうでないメソッドの違い

!つき=>バリデーション失敗時に例外を投げる(ActiveRecord::RecordInvalid)
!なし=>バリデーション失敗時に例外は投げない

様々なバリデーション

ふーん、こんなにあるんだー
formatだけ見覚えある気がする、最近rails触ってなかったのでよく覚えていない

validateブロックは一応試しておく

/book_admin $ ./bin/rails c
Running via Spring preloader in process 3244
Loading development environment (Rails 4.1.2)
irb(main):001:0> book = Book.create name: "おいしいピーポくん", price: 857
   (0.1ms)  begin transaction
   (0.1ms)  rollback transaction
=> #
irb(main):002:0> book.errors
=> #, @messages={:name=>["そんな本があるはずありません"]}>
irb(main):003:0>

validateブロック、結構便利そう。
ここはvalidatesじゃなくてvalidateなの注意。

またincrement,decrement,toggle!,touch,update_XXXXXはバリデーションが効きません。
要注意ですね。
こういう場合はコールバックで確認します。

自分でバリデーターを定義したい場合はActiverecord::Validatorを継承するなどします。
詳細はggってね

コールバック

レコードを作成したり保存したりする過程である処理をはさみたい!という場合はコールバックを使う。

本の通りに汎用性の高そうなメソッドを使ったやつだけ実装してみる

class Book < ActiveRecord::Base scope :costly, -> { where("price > ?", 3000)}
    scope :written_about, -> (theme){ where("name like '#{theme}'")}
    default_scope -> { order("price DESC")}

    belongs_to :publisher
    
    has_many :book_authors
    has_many :authors, through: :book_authors

    validates :name, presence: true
    validates :name, length: { maximum: 15 }
    validates :price, numericality: { greater_than_or_equal_to: 0 }
    validate do |book|
        if book.name.include?("ピーポくん")
            book.errors[:name] << "そんな本があるはずありません"
        end
    end

    before_validation :replace_vim

    def replace_vim
        book.name = book.name.gsub(/emacs/) do |matched|
            "vim"
        end
    end
end

そして結果を確認するも動かない。
すこし考えて本に書いてあるbookって何指してるの?となり、コードのbookをselfに変え不要なmatchedを消した

    before_validation :replace_vim

    def replace_vim
        self.name = self.name.gsub(/emacs/) do
            "vim"
        end
    end

そして確認してみる。
再起動したからか、railsだけでver4.1.2のコンソールが立ち上がっている。

/book_admin $ rails c
Running via Spring preloader in process 3854
Loading development environment (Rails 4.1.2)
irb(main):001:0> Book.create name: "emacs 実践入門", price: 1500
   (0.0ms)  begin transaction
  SQL (0.2ms)  INSERT INTO "books" ("created_at", "name", "price", "updated_at") VALUES (?, ?, ?, ?)  [["created_at", "2018-08-25 08:31:41.618246"], ["name", "vim 実践入門"], ["price", 1500], ["updated_at", "2018-08-25 08:31:41.618246"]]
   (9.3ms)  commit transaction
=> #
irb(main):002:0>

動作確認done
エディタ戦争はVimが制した、わっはっは

コールバックポイントの整理

ほーん、たくさんあるねえ

コールバックを起動する条件の設定

ほーんなるほど、ifとunlessつけるだけね

上のものだけやってみる

class Book < ActiveRecord::Base scope :costly, -> { where("price > ?", 3000)}
    scope :written_about, -> (theme){ where("name like '#{theme}'")}
    default_scope -> { order("price DESC")}

    belongs_to :publisher
    
    has_many :book_authors
    has_many :authors, through: :book_authors

    validates :name, presence: true
    validates :name, length: { maximum: 15 }
    validates :price, numericality: { greater_than_or_equal_to: 0 }
    validate do |book|
        if book.name.include?("ピーポくん")
            book.errors[:name] << "そんな本があるはずありません" end end def high_price? self.price >= 5000
    end
    
    before_validation :replace_vim
    before_validation :if => :high_price? do |book|
        puts "お前そんな高い本買ったの?"
    end

    def replace_vim
        self.name = self.name.gsub(/emacs/) do
            "vim"
        end
    end

end

コンソールで動作確認をする

ここでwordpressにレコードが全部タグと認識されていることに気づく…

/book_admin $ rails c
Running via Spring preloader in process 4180Loading development environment (Rails 4.1.2)
#irb(main):001:0> Book.create name: "ぼくのかんがえたさいきょうのemacs", price: 700000
   (0.0ms)  begin transactionお前そんな高い本買ったの?   
   (0.0ms)  rollback transaction
    => #<Book id: nil, name: "ぼくのかんがえたさいきょうのvim", published_on: nil, price: 700000, number_of_page: nil, created_at: nil, updated_at: nil, publisher_id: nil>
#irb(main):002:0>

やっほいうごいたー!

ActiveRecord enumsでenum型を扱う

本に従ってやってみる

/book_admin $ rails g migration AddStatusToBooks status:integer
Running via Spring preloader in process 3854
Expected string default value for '--helper'; got true (boolean)
      invoke  active_record
      create    db/migrate/20180829061352_add_status_to_books.rb
aporia2718@aporia2718 ~/perfect-ror-part1/book_admin (master) $ rake db:migrate
== 20180829061352 AddStatusToBooks: migrating =================================
-- add_column(:books, :status, :integer)
   -> 0.0020s
== 20180829061352 AddStatusToBooks: migrated (0.0020s) ========================

/book_admin $ rails c
Running via Spring preloader in process 3886
Loading development environment (Rails 4.1.2)
irb(main):001:0> Book.create(status: "now_on_sale", name: "Booooook", price: 4000)
   (0.0ms)  begin transaction
  SQL (0.2ms)  INSERT INTO "books" ("created_at", "name", "price", "status", "updated_at") VALUES (?, ?, ?, ?, ?)  [["created_at", "2018-08-29 06:15:00.116829"], ["name", "Booooook"], ["price", 4000], ["status", 0], ["updated_at", "2018-08-29 06:15:00.116829"]]
   (12.2ms)  commit transaction
=> #<Book id: 15, name: "Booooook", published_on: nil, price: 4000, number_of_page: nil, created_at: "2018-08-29 06:15:00", updated_at: "2018-08-29 06:15:00", publisher_id: nil, status: 0>
irb(main):002:0> Book.create(status: "end_of_print", name: "Booooook1", price: 4000)
   (0.2ms)  begin transaction
  SQL (0.4ms)  INSERT INTO "books" ("created_at", "name", "price", "status", "updated_at") VALUES (?, ?, ?, ?, ?)  [["created_at", "2018-08-29 06:15:48.756526"], ["name", "Booooook1"], ["price", 4000], ["status", 0], ["updated_at", "2018-08-29 06:15:48.756526"]]
   (5.1ms)  commit transaction
=> ##<Book id: 16, name: "Booooook1", published_on: nil, price: 4000, number_of_page: nil, created_at: "2018-08-29 06:15:48", updated_at: "2018-08-29 06:15:48", publisher_id: nil, status: 0>
irb(main):003:0>

あれ、statusが変わらないなあ

ということで調べて書き方を変える

class Book < ActiveRecord::Base
    enum status: [:reservation, :now_on_sale, :end_of_print]
(略)

そして検証

/book_admin $ rails c
Running via Spring preloader in process 4054
Loading development environment (Rails 4.1.2)
irb(main):001:0> Book.create(status: :now_on_sale, name: "book11", price: 1000)
   (0.0ms)  begin transaction
  SQL (0.2ms)  INSERT INTO "books" ("created_at", "name", "price", "status", "updated_at") VALUES (?, ?, ?, ?, ?)  [["created_at", "2018-08-29 06:24:19.010944"], ["name", "book11"], ["price", 1000], ["status", 1], ["updated_at", "2018-08-29 06:24:19.010944"]]
   (5.4ms)  commit transaction
=> ##<Book id: 17, name: "book11", published_on: nil, price: 1000, number_of_page: nil, created_at: "2018-08-29 06:24:19", updated_at: "2018-08-29 06:24:19", publisher_id: nil, status: 1>
irb(main):002:0> Book.create(status: :end_of_print, name: "book12", price: 1000)
   (0.1ms)  begin transaction
  SQL (0.2ms)  INSERT INTO "books" ("created_at", "name", "price", "status", "updated_at") VALUES (?, ?, ?, ?, ?)  [["created_at", "2018-08-29 06:24:40.672612"], ["name", "book12"], ["price", 1000], ["status", 2], ["updated_at", "2018-08-29 06:24:40.672612"]]
   (9.1ms)  commit transaction
=> #<Book id: 18, name: "book12", published_on: nil, price: 1000, number_of_page: nil, created_at: "2018-08-29 06:24:40", updated_at: "2018-08-29 06:24:40", publisher_id: nil, status: 2>
irb(main):003:0> book = Book.find(17)
  Book Load (0.4ms)  SELECT  "books".* FROM "books"  WHERE "books"."id" = ?  ORDER BY price DESC LIMIT 1  [["id", 17]]=> #
irb(main):004:0> book.status
=> "now_on_sale"
irb(main):005:0> Book.find(18).status
  Book Load (0.2ms)  SELECT  "books".* FROM "books"  WHERE "books"."id" = ?  ORDER BY price DESC LIMIT 1  [["id", 18]]
=> "end_of_print"
irb(main):006:0>

できてるー!
本の情報、ちょっと古いのかなあ

コントローラの役割

お次はコントローラの解説へ。

Railsはhttp://localhost:3000/books/1というURLにアクセスした際、books/1というアクセスしたパスの情報を取得する。
これを受け取って、config/route.rbにマッチするものを探す。

とりあえず本のとおりにやってみる。
中身のコメントを除去し一行書き加えた。

Rails.application.routes.draw do
  get "/books/:id" => "books#show"
end

これで、/book/:idをHTTP/HTTPSのGETメソッドでアクセスした際に、booksコントローラのshowメソッドが立ち上がります。
しかしまだコントローラは定義されていないので作ります。

/book_admin $ rails g controller books
Running via Spring preloader in process 4537
Expected string default value for '--helper'; got true (boolean)
Expected string default value for '--helper'; got true (boolean)
Expected string default value for '--assets'; got true (boolean)
      create  app/controllers/books_controller.rb
      invoke  erb
      create    app/views/books
      invoke  test_unit
      create    test/controllers/books_controller_test.rb
      invoke  helper
      create    app/helpers/books_helper.rb
      invoke    test_unit
      create      test/helpers/books_helper_test.rb
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/books.js.coffee
      invoke    scss
      create      app/assets/stylesheets/books.css.scss
/book_admin $

これでコントローラのファイルができたので、showメソッドを定義します。

class BooksController < ApplicationController
    def show
        @book = Book.find(params[:id])
        respond_to do |format|
            format.html
            format.csv
            format.json
        end
    end
end

Booksコントローラのpublicに定義されたメソッドをアクションと呼びます。
ここではshowアクションを定義しました。
params[:id]でidパラメーターを取得できます。

下部のrespond_toブロックは、要求される形式によって返し方を変える処理を書いています。

/books/1.html
/books/1.csv
/books/1.json

のようなパスを受け取るとそれぞれ対応する形式で返り値が帰ってきます。
APIサーバーだとjsonで返すことが多いですね。

最初にビューの所からパラメーターを取得し、モデルとの橋渡しをし、またビューに返していることが想像できると思います。

アクションコールバック

それぞれのアクションにコールバックが定義できる機能です。
見ればわかるので見ましょう。

class BooksController < ApplicationController
    before_action :set_book, only: [:show, :edit, :update, :destroy]

    def show
        @book = Book.find(params[:id])
        respond_to do |format|
            format.html
            format.csv
            format.json
        end
    end

    private
    def set_book
        @book = Book.find(params[:id])
    end
end

before_action

after_action

around_actionの3種類のアクションコールバックが存在します。
またコールバックのスキップもできます。

親クラスで定義したコールバックをスキップするときに使います。
skip_before_action, skip_after…という感じで上記コールバックと同様の形式で使います。

ルーティングとリソース

resourcesを使ってみます。
本に従ってやってみる….

Rails.application.routes.draw do
  get "/books/:id" => "books#show"
  resources :publishers
end

routesを設定し、rakeで確認
上が記述前、下が記述後です。

/book_admin $ rake routes
Prefix Verb URI Pattern          Controller#Action
       GET  /books/:id(.:format) books#show
/book_admin $ rake routes
        Prefix Verb   URI Pattern                    Controller#Action
               GET    /books/:id(.:format)           books#show
    publishers GET    /publishers(.:format)          publishers#index
               POST   /publishers(.:format)          publishers#create
 new_publisher GET    /publishers/new(.:format)      publishers#new
edit_publisher GET    /publishers/:id/edit(.:format) publishers#edit
     publisher GET    /publishers/:id(.:format)      publishers#show
               PATCH  /publishers/:id(.:format)      publishers#update
               PUT    /publishers/:id(.:format)      publishers#update
               DELETE /publishers/:id(.:format)      publishers#destroy
/book_admin $

一気に定義されたことが確認できましたね。
GET<->show
POST<->create
PUT(PATCH)<->update
DELETE<->destroy
に対応していることがわかります。

resourcesの拡張

resourcesのルーティングを拡張する場合は、親のresourcesブロックの中に子のresourcesブロックを記述します。

これも本に従ってやってみる…
routesに追記

Rails.application.routes.draw do
  get "/books/:id" => "books#show"
  resources :publishers do
    resources :books

    member do
      get "detail"
    end

    collection do
      get "search"
    end
  end
end

そしてrakeで確認

/book_admin $ rake routes
             Prefix Verb   URI Pattern                                        Controller#Action
                    GET    /books/:id(.:format)                               books#show
    publisher_books GET    /publishers/:publisher_id/books(.:format)          books#index
                    POST   /publishers/:publisher_id/books(.:format)          books#create
 new_publisher_book GET    /publishers/:publisher_id/books/new(.:format)      books#new
edit_publisher_book GET    /publishers/:publisher_id/books/:id/edit(.:format) books#edit
     publisher_book GET    /publishers/:publisher_id/books/:id(.:format)      books#show
                    PATCH  /publishers/:publisher_id/books/:id(.:format)      books#update
                    PUT    /publishers/:publisher_id/books/:id(.:format)      books#update
                    DELETE /publishers/:publisher_id/books/:id(.:format)      books#destroy
   detail_publisher GET    /publishers/:id/detail(.:format)                   publishers#detail
  search_publishers GET    /publishers/search(.:format)                       publishers#search
         publishers GET    /publishers(.:format)                              publishers#index
                    POST   /publishers(.:format)                              publishers#create
      new_publisher GET    /publishers/new(.:format)                          publishers#new
     edit_publisher GET    /publishers/:id/edit(.:format)                     publishers#edit
          publisher GET    /publishers/:id(.:format)                          publishers#show
                    PATCH  /publishers/:id(.:format)                          publishers#update
                    PUT    /publishers/:id(.:format)                          publishers#update
                    DELETE /publishers/:id(.:format)                          publishers#destroy
/book_admin $

黄色はresources :booksに、青色はmemberに、緑色はcollectionに、赤色はこれの1つ上で上げたものと同じものを指しています。

ここで重要なのは赤以外です。

黄色は各publisher_idの中のbooksのidについてresourcesを適用させています。

青色は親の個々のリソース(ここではpublisher)に対して適用する処理を書いています。

緑は親の全体のリソースに対して適用する処理を書いています。

searchyやdetailがどのようにpublishersに対して適用されるかを想像すると理解が早まると思います。

resources以外のルーティングパターン

本や著者は1つのアプリケーション上に、実際に利用しているユーザーから見て複数見えますよね?
このような場合にはresorucesを使いました。

しかし、自分自身のアカウントなどのように、実際に利用しているユーザーからみて1つしか存在しないようなリソースも確かに存在します。
このような場合はどうするか、resource(単数形)を使います。

本のとおりにやっていきます。

/book_admin $ rails g model User name:string password:string email:string admin: boolean
Running via Spring preloader in process 4921
Expected string default value for '--helper'; got true (boolean)
      invoke  active_record
      create    db/migrate/20180829074800_create_users.rb
      create    app/models/user.rb
      invoke    test_unit
      create      test/models/user_test.rb
      create      test/fixtures/users.yml
/book_admin $ rails g controller Profile show edit update
Running via Spring preloader in process 4943
Expected string default value for '--helper'; got true (boolean)
Expected string default value for '--helper'; got true (boolean)
Expected string default value for '--assets'; got true (boolean)
      create  app/controllers/profile_controller.rb
       route  get 'profile/update'
       route  get 'profile/edit'
       route  get 'profile/show'
      invoke  erb
      create    app/views/profile
      create    app/views/profile/show.html.erb
      create    app/views/profile/edit.html.erb
      create    app/views/profile/update.html.erb
      invoke  test_unit
      create    test/controllers/profile_controller_test.rb
      invoke  helper
      create    app/helpers/profile_helper.rb
      invoke    test_unit
      create      test/helpers/profile_helper_test.rb
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/profile.js.coffee
      invoke    scss
      create      app/assets/stylesheets/profile.css.scss
aporia2718@aporia2718 ~/perfect-ror-part1/book_admin (master) $ rake db:migrate
== 20180829074800 CreateUsers: migrating ======================================
-- create_table(:users)
   -> 0.0022s
== 20180829074800 CreateUsers: migrated (0.0022s) =============================

/book_admin $

モデルを作って、Profile関連のルーティングを設定し直します。

  get 'profile/show'

  get 'profile/edit'

  get 'profile/update'

のような記述を消しまして

Rails.application.routes.draw do
  resource :profile
  get "/books/:id" => "books#show"
  resources :publishers do
    resources :books

    member do
      get "detail"
    end

    collection do
      get "search"
    end
  end
end

のようにします。
そしてルーティングを確認
新しく追加されたものだけ表示します。

/book_admin $ rake routes
             Prefix Verb   URI Pattern                                        Controller#Action
            profile POST   /profile(.:format)                                 profiles#create
        new_profile GET    /profile/new(.:format)                             profiles#new
       edit_profile GET    /profile/edit(.:format)                            profiles#edit
                    GET    /profile(.:format)                                 profiles#show
                    PATCH  /profile(.:format)                                 profiles#update
                    PUT    /profile(.:format)                                 profiles#update
                    DELETE /profile(.:format)                                 profiles#destroy

resorucesと違ってidがなくなりました。#indexも無いですね。
onlyの指定も試してみます。

resoruce :profile, only [:show, :edit, :update]

と変更すると

/book_admin $ rake routes
             Prefix Verb   URI Pattern                                        Controller#Action
       edit_profile GET    /profile/edit(.:format)                            profiles#edit
            profile GET    /profile(.:format)                                 profiles#show
                    PATCH  /profile(.:format)                                 profiles#update
                    PUT    /profile(.:format)                                 profiles#update

こうなりました。
やったね!

例外処理

RailsではApplicationControllerにステータスコードに対応するエラーを記述していきます。

rescue_from エラー名, with: :メソッド名

という形式で記述します。

エラー名に対応するクラスをStandardErrorクラスを継承する形で定義しておくことが必要です。
詳しくは本読んでね。

StrongParamatersとは

モデルの生成や更新のときにMass Assignment(一括割り当て)を行う機能があります。
例は本見てね。

端的に言うならば、不正なパラメーターをモデルに適用されてしますことを防ぐ機構です。
脆弱性を埋め込まないように極力これを使うべし。

ビューの役割

とうとうMVCもラスト、Vの部分です。

テンプレートの検索

books_controllerのrenderメソッドを明示的に呼び出す。

class BooksController < ApplicationController
    before_action :set_book, only: [:show, :edit, :update, :destroy]

    def show
        @book = Book.find(params[:id])
        render :show
    end

    private
    def set_book
        @book = Book.find(params[:id])
    end
end

ここでは

  • 描画するために必要なテンプレートを探す
  • 見つかったテンプレートをもとに最終的なデータを生成する。

このテンプレートは

RAILS_ROOT/app/views (/api/vn(nはバージョン)) /コントローラ名/アクション名.フォーマット名.エンジン名

で探します。
例えば今回では、RAILS_ROOT/app/views/books/show.html.erbです。
ファイルは存在しないので写経です。

<!DOCTYPE HTML>
<html>
    <head>
        <title>Book admin</title>
    </head>
    <body>
        <h1>info</h1>
        <dl>
            <dt>書籍名</dt>
            <dd><%= @book.name %></dd>
            <dt>価格</dt>
            <dd><%= @book.price %></dd>
            <dt>発売日</dt>
            <dd><%= @book.published_on.to_s %></dd>
        </dl>
    </body>
</html>

そしてwebrickを立ち上げアクセスしてみます。

おおーできた!!!感動!!!

続いてrenderを省略してみましょう。

おー

明示的にrenderを呼ばなくても、render :メソッド名が自動的に呼ばれるんですね〜。

コンテンツのタイプによって表示を出し分ける

文字通りです。これにはさっき出てきたrespond_toブロックを利用します。
respond_toブロックを使って書き換えてみます。

    def show
        @book = Book.find(params[:id])
        respond_to do |format|
            format.html
            format.json
            format.csv
        end
    end

先走ってcsvを取得しようとするとこのようになります。

テンプレートが無いので作りましょう。
写経です。

書籍名,価格,発売日
<%= [@book.name, @book.price, @book.published_on.to_s].join(",") %>

そしてcurlで確認。

/book_admin $ curl http://localhost:3000/books/2.csv
書籍名,価格,発売日
Book 2,1000,2018-06-24
/book_admin $

おー、取得できてる〜
joinメソッド、シングルクォーテーションだとなぜか動かなかった。

ここらへんの処理はrailsをAPIサーバーとして運用しようとしている私には不要だったのでさらりと読み飛ばす。

APIサーバにとってのビューについて

jbuilderは標準でインストールされているので、早速使ってみます。
インストールされているかどうかはGemfileで確認が可能です。

まずshow.json.jbuilderを作ります。

json.extract! @book, :id, :name, :price, :created_at

 

showアクションを以下のように書き換えて、webrickを立ち上げアクセスしてみます。

    def show
        @book = Book.find(params[:id])
        respond_to do |format|
            format.json
        end
    end

 

おー、帰ってきました!やったね!

!のつかないものはそれ自体がjsonのキーとなります。
実際にみてみます。
name_with_idというキーで帰ってくるか検証します。

json.name_with_id "#{@book.id} - #{@book.name}"

name_with_idの後ろの箇所はただの文字列オブジェクトなので、#{}で囲んで、変数を含む文字列を返したりできますね!

jsonをネストさせる場合はブロックを使います。実際にやってみます。

json.publisher do
    json.name @book.publisher.name
    json.address @book.publisher.address
end

できたー!使うのすごい簡単!

unlessとかifとかも使えちゃいます。
返すjsonは数字、文字列の他にbooleanも取ることができます。

unless @book.high_price?
    json.low_price true
end

jsonで複数のオブジェクトを返すことも可能です。

json.books Book.all do |book|
    json.extract! book, :id, :name, :price, :created_at
end

うおー、壮観ですね。

第2章終了!

いやー、めちゃくちゃ長かった、これ書きながらだとまじで時間かかる。

総文字数53640文字は流石に笑う

というわけで次回は第3章に入っていきたいとおもいます!ふー疲れた

それではまた!

シェアする

コメントを残す

%d人のブロガーが「いいね」をつけました。