2014年6月26日木曜日

2-2 モデル

 いきなり何の説明も無くActiveRecordという単語が出てきた。索引見ても、これが最初に出てきたページだ。なんだよ。いじめかよ。 とりあえず、言われた通りモデルを作る。書籍を管理するカラムを同時に指定しています。えっと、書籍名と発売日と価格とページ数ですか。
$ rails g model Book name:string published_on:date price:integer number_of_page:integer
  本にうるさい人間としては、正直ISBN的なユニークな記号が無くては管理に行き詰まりそうな気がして心配です。まぁ、そんなことはどうでもいいです。
 /app/models/book.rbがモデルで、/db/migrate/〜がテーブル生成用のマイグレーションファイル。
 マイグレーションファイルについて、「ActiveRecord::MigrationのDSLで記述したものです」と書かれているけど、どちらの意味も全くわからない。解説も無い。

参考:
5. クラスとモジュール
第1回 DSLとは?

 要するにクラスMigrationがモジュールActiveRecordの中でネスト(入れ子)になっているということでいいのでしょうかね。いいのでしょうかと言っても、その説明自体の具体的な経験が無くてイメージわかず。言葉で覚えただけ。そのうちわかりそうな気はする。

 マイグレーションしてbook.rbを見るも、クラスが定義されているだけで、中身は空っぽ。
 ’<’はクラスの継承で、そのうち説明してくれるでしょう。つーか、Rubyを知らずにこの本読んじゃいけないんじゃなかろうか。ま、いいか。
$ cat app/models/book.rb
class Book < ActiveRecord::Base
end
でも、コンソールで確認するとフィールドが定義されていてdbの格好になっている。もちろんデータは何もありません。
$ rails c
Loading development environment (Rails 4.1.1)
irb(main):001:0> Book.new
=> #<Book id: nil, name: nil, published_on: nil, price: nil, number_of_page: nil, created_at: nil, updated_at: nil>
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> Book.all
  Book Load (0.2ms)  SELECT "books".* FROM "books"
=> #<ActiveRecord::Relation []> 
あ、ここで解説。
 ActiveRecordはdbと接続してレコードとエンティティを結ぶ。これはモデルクラスに何も記述しなくてもテーブルのカラム情報を取得してモデルのフィールド情報に自動反映するような動作を実現する。また、接続情報を隠蔽する。
 もう一つ、バリデーションやコールバック等のビジネスロジックを実行する役割を持つ。実際にはこの動作はモジュールActiveModelが担っている。まずは基本的なバリデーションやコールバックを覚えればそれで良い。
 全くわからない。言葉同士の関係性はなんとなくわかるけど、その他は全く。でも体で覚えられるポイントな気がするので、気に留めるだけにして先に進む。
動詞 : 具体的な内容 : ActiveRecordの対応するメソッド
Create : レコードを作る : Book.create / Book#save
Read : レコードを参照する : Book.find / Book.all
Update : レコードを更新する : Book#save / Book#update_attributes
Delete : レコードを削除する : Book#delete / Book#destroy
Readは読み出しだけでなので参照系、その他はデータの書き変わるので更新系と分類される。

 今後実際にデータを入力して検証することも出てくるそうで、コンソールで本のデータを少し入力しておく。
$ rails c
Loading development environment (Rails 4.1.1)
irb(main):001:0> (1..5).each do |i|
irb(main):002:1* Book.create(name: "Book #{i}", published_on: i.months.ago, price: (i * 1000))
irb(main):003:1> end #データ作成
   (0.1ms)  begin transaction
  SQL (0.6ms)  INSERT INTO "books" ("created_at", "name", "price", "published_on", "updated_at") VALUES (?, ?, ?, ?, ?)  [["created_at", "2014-06-25 07:04:24.117114"], ["name", "Book 1"], ["price", 1000], ["published_on", "2014-05-25"], ["updated_at", "2014-06-25 07:04:24.117114"]]
   (0.7ms)  commit transaction
   (0.1ms)  begin transaction
  SQL (0.2ms)  INSERT INTO "books" ("created_at", "name", "price", "published_on", "updated_at") VALUES (?, ?, ?, ?, ?)  [["created_at", "2014-06-25 07:04:24.124550"], ["name", "Book 2"], ["price", 2000], ["published_on", "2014-04-25"], ["updated_at", "2014-06-25 07:04:24.124550"]]
   (0.7ms)  commit transaction
   (0.1ms)  begin transaction
  SQL (0.2ms)  INSERT INTO "books" ("created_at", "name", "price", "published_on", "updated_at") VALUES (?, ?, ?, ?, ?)  [["created_at", "2014-06-25 07:04:24.127138"], ["name", "Book 3"], ["price", 3000], ["published_on", "2014-03-25"], ["updated_at", "2014-06-25 07:04:24.127138"]]
   (0.6ms)  commit transaction
   (0.0ms)  begin transaction
  SQL (0.2ms)  INSERT INTO "books" ("created_at", "name", "price", "published_on", "updated_at") VALUES (?, ?, ?, ?, ?)  [["created_at", "2014-06-25 07:04:24.129217"], ["name", "Book 4"], ["price", 4000], ["published_on", "2014-02-25"], ["updated_at", "2014-06-25 07:04:24.129217"]]
   (0.7ms)  commit transaction
   (0.0ms)  begin transaction
  SQL (0.2ms)  INSERT INTO "books" ("created_at", "name", "price", "published_on", "updated_at") VALUES (?, ?, ?, ?, ?)  [["created_at", "2014-06-25 07:04:24.131291"], ["name", "Book 5"], ["price", 5000], ["published_on", "2014-01-25"], ["updated_at", "2014-06-25 07:04:24.131291"]]
   (0.6ms)  commit transaction
=> 1..5
irb(main):004:0> Book.find(1) #id検索
  Book Load (0.2ms)  SELECT  "books".* FROM "books"  WHERE "books"."id" = ? LIMIT 1  [["id", 1]]
=> #<Book id: 1, name: "Book 1", published_on: "2014-05-25", price: 1000, number_of_page: nil, created_at: "2014-06-25 07:04:24", updated_at: "2014-06-25 07:04:24">
irb(main):005:0> Book.find_by(name: "Book 3") #書籍名、発売日で検索
  Book Load (0.2ms)  SELECT  "books".* FROM "books"  WHERE "books"."name" = 'Book 3' LIMIT 1
=> #<Book id: 3, name: "Book 3", published_on: "2014-03-25", price: 3000, number_of_page: nil, created_at: "2014-06-25 07:04:24", updated_at: "2014-06-25 07:04:24">
irb(main):006:0> Book.where("price > ?", 2000) #複数レコード検索
  Book Load (0.2ms)  SELECT "books".* FROM "books"  WHERE (price > 2000)
=> #<ActiveRecord::Relation [#<Book id: 3, name: "Book 3", published_on: "2014-03-25", price: 3000, number_of_page: nil, created_at: "2014-06-25 07:04:24", updated_at: "2014-06-25 07:04:24">, #<Book id: 4, name: "Book 4", published_on: "2014-02-25", price: 4000, number_of_page: nil, created_at: "2014-06-25 07:04:24", updated_at: "2014-06-25 07:04:24">, #<Book id: 5, name: "Book 5", published_on: "2014-01-25", price: 5000, number_of_page: nil, created_at: "2014-06-25 07:04:24", updated_at: "2014-06-25 07:04:24">]>
irb(main):007:0> Book.where("price > ?", 2000).limit(3).order(:name) #メソッドをチェーンさせることもできる(Query Interfaceという機能)
  Book Load (0.3ms)  SELECT  "books".* FROM "books"  WHERE (price > 2000)  ORDER BY "books"."name" ASC LIMIT 3
=> #<ActiveRecord::Relation [#<Book id: 3, name: "Book 3", published_on: "2014-03-25", price: 3000, number_of_page: nil, created_at: "2014-06-25 07:04:24", updated_at: "2014-06-25 07:04:24">, #<Book id: 4, name: "Book 4", published_on: "2014-02-25", price: 4000, number_of_page: nil, created_at: "2014-06-25 07:04:24", updated_at: "2014-06-25 07:04:24">, #<Book id: 5, name: "Book 5", published_on: "2014-01-25", price: 5000, number_of_page: nil, created_at: "2014-06-25 07:04:24", updated_at: "2014-06-25 07:04:24">]>
SQLに使えるActiveRecordのメソッドは、select / where / limit / offset / order / group / joins / includes / not がある。SQLの話だし、ここではメソッド名だけ。でも実際にはすごく使うんだろうな。使いながら覚える方向で。

 ActiveRecord::Relationクラスは、ActiveRecordのQuery Interfaceの操作結果をオブジェクトとしたもの。メソッドがチェーンされるごとにdbにアクセスするのではなく、チェーンした条件で問い合わせをするようなイメージかな。SQLの実行結果を配列の感覚で扱える、とか。よくわからん。

 突然scopeの話。よく利用する検索結果をひとまとめにしたもの、だそう。再利用性と可読性が向上してイイネということらしいです。
 /app/medel内で定義する。今回は/app/model/book.rb。
class Book < ActiveRecord::Base
  scope :costly, -> { where("price > ?", 3000 )}
  scope :written_about, ->(theme) { where("name like ?", "%#{theme}%") }
end
これで、Book.costlyでprice>3000の条件が抽出できる、と。
 複数のscopeをand処理することもできる。Book.costly.written_about("java")なら、書名にjavaとつく3千円より高い書籍を探すことができる。:written_aboutのように、パラメータをとるスコープも定義できる。
 でもググっていたらこういう書き方困るんだよねー、みたいな情報もあるので一応リンクだけ張っておきます。

参考:
ActiveRecord4でこんなSQLクエリどう書くの? Arel編

 デフォルトでどのスコープにも適用したいスコープについてはdefault_scopeで定義できる。忘れていると思わぬ挙動になるので注意。
default_scope -> { order("published_on desc") }
モデル同士のリレーションについて。Bookに続いてPublisherとAutherというモデルを作成。これらを関連づけるために下記でマイグレーションを作成すると言うんだけど、どこから出てきたんだ。丸暗記?
rails g migration AddPublisherIdToBooks publisher:references
これでBookにはPublisherモデルへの参照が追加された。この過程でBookにはpublisher_idというフィールドが追加されている。
 ところで、あるbookには一意にpublisherが決まる状態を/app/models内に記述しておくと、出版社と本がひもづいてお互いにメソッドして利用できるようになるようです。
 Publisher側は以下の通り。Bookについても同様にbelongs_to :publisherを書いておきます。
class Publisher < ActiveRecord::Base
has_many :books
end
下記がコンソールで確認した内容。
irb(main):015:0> publisher = Publisher.create name: 'gihyo', address: 'ichigaya'
   (0.1ms)  begin transaction
  SQL (0.4ms)  INSERT INTO "publishers" ("address", "created_at", "name", "updated_at") VALUES (?, ?, ?, ?)  [["address", "ichigaya"], ["created_at", "2014-06-26 01:23:58.851325"], ["name", "gihyo"], ["updated_at", "2014-06-26 01:23:58.851325"]]
   (2.9ms)  commit transaction
=> #<Publisher id: 2, name: "gihyo", address: "ichigaya", created_at: "2014-06-26 01:23:58", updated_at: "2014-06-26 01:23:58">
irb(main):016:0> publisher.books << Book.find(1)                                  Book Load (0.2ms)  SELECT  "books".* FROM "books"  WHERE "books"."id" = ? LIMIT 1  [["id", 1]]
   (0.1ms)  begin transaction
  SQL (0.3ms)  UPDATE "books" SET "publisher_id" = ?, "updated_at" = ? WHERE "books"."id" = 1  [["publisher_id", 2], ["updated_at", "2014-06-26 01:24:10.115361"]]
   (2.9ms)  commit transaction
  Book Load (0.1ms)  SELECT "books".* FROM "books"  WHERE "books"."publisher_id" = ?  [["publisher_id", 2]]
=> #<ActiveRecord::Associations::CollectionProxy [#<Book id: 1, name: "Book 1", published_on: "2014-05-25", price: 1000, number_of_page: nil, created_at: "2014-06-25 07:04:24", updated_at: "2014-06-26 01:24:10", publisher_id: 2>]>
irb(main):017:0> publisher.books.to_a
=> [#<Book id: 1, name: "Book 1", published_on: "2014-05-25", price: 1000, number_of_page: nil, created_at: "2014-06-25 07:04:24", updated_at: "2014-06-26 01:24:10", publisher_id: 2>]
irb(main):018:0> book = Book.find(1)
  Book Load (0.1ms)  SELECT  "books".* FROM "books"  WHERE "books"."id" = ? LIMIT 1  [["id", 1]]
=> #<Book id: 1, name: "Book 1", published_on: "2014-05-25", price: 1000, number_of_page: nil, created_at: "2014-06-25 07:04:24", updated_at: "2014-06-26 01:24:10", publisher_id: 2>
irb(main):019:0> book
=> #<Book id: 1, name: "Book 1", published_on: "2014-05-25", price: 1000, number_of_page: nil, created_at: "2014-06-25 07:04:24", updated_at: "2014-06-26 01:24:10", publisher_id: 2>
irb(main):020:0> book.publisher
  Publisher Load (0.1ms)  SELECT  "publishers".* FROM "publishers"  WHERE "publishers"."id" = ? LIMIT 1  [["id", 2]]
=> #<Publisher id: 2, name: "gihyo", address: "ichigaya", created_at: "2014-06-26 01:23:58", updated_at: "2014-06-26 01:23:58">
irb(main):021:0> book.publisher.name
=> "gihyo"
もう、頭が大混乱しています。
 上の例のように1対多ではなく、1対1で決まる場合には、has_oneというクラスメソッドが用意されているそうです。
 本と著者のように、多対多の関係もあります。この場合は一旦中間モデルを作成します。します、とか書いてますが全然わかっていません。
rails g model BookAuther book:references auther:references
そしてAutherに下記を追加。
class Auther < ActiveRecord::Base
has_many :book_authers
has_many :books, through: :book_authers
end 
どうしてBookAutherを挟まないといけないのか。よくわからない。わかりにくいと感じるけど、この本には中間テーブルの作成やこれをキーにしたデータの取得がActiveRecordのおかげで楽になっていると書かれている。そうなんだ…。
 Bookにも上に習って逆向きで宣言する。
 コンソールでの確認は面倒になってきたので、というか時間がないので、パス。こういうのがいけないんだよね。わかってはいるんだけどね…。

ActiveRecordを利用したデータの保存

 突然ですが、記事があまりに長くなってきたのでたまに見出しを入れることにします。
Book.create name: "Rails Book", published_on: 2.months.ago, price: 2980
上記は、下記に分解して入力したものと同じ意味を持ちます。
irb(main):002:0> book = Book.new
=> #<Book id: nil, name: nil, published_on: nil, price: nil, number_of_page: nil, created_at: nil, updated_at: nil, publisher_id: nil>
irb(main):003:0> book.name = "Rails Book"
=> "Rails Book"
irb(main):004:0> book.published_on = 2.months.ago
=> Sat, 26 Apr 2014 01:52:05 UTC +00:00
irb(main):005:0> book.price = 2980
=> 2980
irb(main):006:0> book.save
   (0.1ms)  begin transaction
  SQL (0.8ms)  INSERT INTO "books" ("created_at", "name", "price", "published_on", "updated_at") VALUES (?, ?, ?, ?, ?)  [["created_at", "2014-06-26 01:52:24.327140"], ["name", "Rails Book"], ["price", 2980], ["published_on", "2014-04-26"], ["updated_at", "2014-06-26 01:52:24.327140"]]
   (2.4ms)  commit transaction
=> true 
  データを更新する場合は、必要なレコードをfind等で取得し、特定のフィールドを更新してsaveするだけ。削除は特定のレコードにdestroyメソッドを呼び出すだけでいい。
 あ、書いててわかった。レコードってあるモデルのあるidにひもづいたデータをひとまとめにした単位か。今更。フィールドはカラムとほぼ同意みたい。エクセルで言う列。この本の中でもフィールドとカラムという言葉が出てきてややこしい。厳密には違うのかもしれないけど。

バリデーション

データの入力ルール。あるフィールドにデータが入るときに、適切であるデータ形式であるかチェックする。
 books.rbに以下を追記する。
  validates :name, presence:true #入力必須
  validates :name, length: { maximum: 15 } #15字以内
  validates :price, numericality: { greater_than_or_equal_to: 0} #0以上の数値
条件を満たしていないと、レコードの保存に失敗する。save時にfalseとなる。
 errorsメソッドでエラーの内容を確認することができる。
irb(main):005:0> book.errors
=> #<ActiveModel::Errors:0x007fcea720e950 @base=#<Book id: nil, name: "asdfajsdfaksfsladfalsdfjasldj", published_on: nil, price: nil, number_of_page: nil, created_at: nil, updated_at: nil, publisher_id: nil>, @messages={:name=>["is too long (maximum is 15 characters)"], :price=>["is not a number"]}>
saveせずにバリデーションだけを行う場合は、valid?が使える。book.valid?でtrue/falseの判定ができ、falseの場合はerrorsインスタンスにエラーが書き出される。便利そう。
 バリデーションの一覧がp57にあって、きっと今後よう使うことになりそう。値の存在、ユニークかどうか、数値かどうか等々、いろいろあるようです。
 あれ、と思ったら下記のように式とエラーを宣言して自分で条件が作れるんだって。そうすか。
validates do |book|
  if book.name.include?("exercise")
    book.errors[:name] << "I don't like exercise."
  end
end

コールバック

レコード作成から保存までの間に任意の処理を呼び出すことをコールバックという。
 レコード処理の前後処理や、保存後に必ず行う処理等、コールバックを利用することで実行漏れが防げる。"cat"が含まれていたら"lovely cat"におきかえる、とか。あー、わかりやすい。珍しい。
before_validation do |look|
  book.name = book.name.gsub(/Cat/) do |matched|
    "lovely #{matched}"
  end
end
before_validationはバリデーションの前にチェックするよってことかな。たまに出てくる"|"の意味がわからないけど、形式的なものなんだろうか。gsubは正規表現でマッチしたものを置換するんですって。
 メソッドを使うと下のように書ける、ときたけど、オブジェクト思考に慣れていないからかまだ違和感を感じる。
before_validation :add_lovely_to_cat
def add_lovely_to_cat
  book.name = book.name.gsub(/Cat/) do |matched|
    "lovely #{matched}"
  end
end
以下はレコード削除時にログに書き出す方法。
  after_destroy do |book|
  Rails.logger.info "Book is deleted: #{book.attributes.inspect}"
  end
確かにlog/development.logにBook is deleted:〜の表示が出ました。
 コールバックを行うタイミングはいろいろ用意されているようです。before_validationやらafter_saveやら、いろいろ。p59参照。

コールバックの起動に条件付けをする

def high_price?
  price >= 5000
end
after_destroy :if => :high_price? do |book|
  Rails.logger.warn "Book with high price is deleted: #{book.attributes.inspect}"
  Rails.logger.warn "Please check!"
end
 5千円以上の本のレコードを削除すると、メッセージを表示する。
 :ifや:unlessではProcオブジェクトも使えるという説明があるんだけど、procって?ブロックをオブジェクト化したもの。ブロックはメソッドの引数となるdo~endや{~}で囲われたかたまり。よくわかんないね。いいよ、もう。

参考:

 Rails4.1で追加されたActiveRecordの機能として、enum型(列挙型)という変数形式が追加されている。p61参照。
 コード上ではわかりやすい定数として扱えて、db上では数値として保存されるため、dbの空間効率がよくなる。

0 件のコメント:

コメントを投稿