2011年11月15日火曜日

Railsを使ったアプリケーション開発 (その3 応用編)


Railsを使ったアプリケーション開発 (その1 事前準備編)
Railsを使ったアプリケーション開発 (その2 基礎編)
ときて、いよいよ3回目である。

いよいよ書籍を管理するアプリケーションを作る。

手動でコントローラ、ビュー、モデルを作成してきたがもっと楽な手段がある。
Scaffolding機能を使う方法である。コマンド一発で、特定のテーブルをCreate、Read、Update、Delete操作できる、簡易アプリケーションが作れてしまう。

この機能を利用して書籍管理アプリケーションを作ってみる。



テーブルの設計
実体と属性
()はrailsが自動で作成する。
赤は主キー、青は非主キーである。
*が外部キーを表す。

books
 (id) title price (created_at) (updated_at)

users
 (id) name password (created_at) (updated_at)

authors
 (id) name (created_at) (updated_at)

reviews
 (id) bookd_id* user_id* body (created_at) (updated_at)

books_authors
 book_id* author_id*



ER図もどき
    books 1 - N reviews N 1 users
       1
       |
       N
author_books
       N
       |
       1
    authors



Scaffoldingを利用して関連ファイルを一括作成

今はここにいる。
# pwd
/var/www/rails/bookapp

モデルクラス名は単数なので注意を。複数形にするのはテーブルだけである。
# rails generate scaffold book title:string price:integer

# rails generate scaffold user name:string password:string

# rails generate scaffold author name:string

# rails generate scaffold review book:references user:references body:text

# rails generate model authors_book book:references author:references

author_booksだけscaffoldで作らなかった。純粋な中間テーブルをCRUDに従った操作をする必要はないと判断したためである。もちろん作ってもかまわない。



マイグレーションファイルによるテーブルの作成
自動でマイグレーションファイルはできているが、主キー列、created_at列、updated_at列が不要な中間テーブルの作成前には一部編集が必要である。主キーを無効化し、かつタイムスタンプ列を削除する。

# vi db/migrate/20111113040505_create_author_books.rb
def change
  create_table :author_books, :id=>false do |t|
    t.references :book
    t.references :author
    t.timestamps ←削除
  end
  add_index :author_books, :book_id
  add_index :author_books, :author_id
end


テーブルを作成する。
# rails db:migrate RAILS_ENV=development



テストデータの流し込み
自動でテストデータが作られている。流し入れるデータを変えたければymlファイルを編集すればよい。

# vi test/fixtures/books.yml
book1:
 title: book1
 price: 1000

book2:
 title: book2
 price: 2000

book3:
 title: book3
 price: 3000

上記のように記載していくと手間がかかるためブロックを使った式をymlファイルに記載してもよい。
<% 1.upto(3) do |n| %>
book<%= n %>:
  title: book<%= n %>
  price: <%= n * 1000 %>
<% end %>


# vi test/fixtures/users.yml
<% 1.upto(3) do |n| %>
user<%= n %>:
  name: name<%= n %>
  password: password<%= n %>
<% end %>


# vi test/fixtures/authors.yml
<% 1.upto(3) do |n| %>
author<%= n %>:
  name: name<%= n %>
<% end %>


reviwesテーブルはカラムにbookd_idとuser_idをもち、それぞれbooksテーブル、usersテーブルの外部キーになっているはずである。しかしbook_id、user_idにidを入れて参照元のテーブルと紐づけるのは手間である。そこでラベル名で参照先を識別させることもできる。
# vi test/fixtures/author_books.yml
one:
  book: book1
  author: author2

two:
  book: book3
  author: author1


上と同じく、book_id、user_idは使わずラベル名を利用する。
# vi test/fixtures/reviews.yml
one:
  book: book1
  user: user2
  body: fantastic

two:
  book: book2
  user: user3
  body: interesting



ymlの試験データを元にデータをテーブルへ流し込む。
# rails db:fixtures:load FIXTURES=books RAILS_ENV=development

# rails db:fixtures:load FIXTURES=users RAILS_ENV=development

# rails db:fixtures:load FIXTURES=authors RAILS_ENV=development

# rails db:fixtures:load FIXTURES=reviews RAILS_ENV=development

# rails db:fixtures:load FIXTURES=author_books RAILS_ENV=development

同じコマンドを2回打っても追記はされない。ymlファイルに記載されているものだけが挿入される。既に存在している行があってもymlになければその行は削除される。


データベースを初期化するにはこうである。
# rails db:reset



ルーティングの作成
ルーティングも自動で入っている。
# cat config/routes.rb
(略)
  resources :reviews
  resources :authors
  resources :users
  resources :books
(略)


# rails routes
ヘルパー名    HTTPメソッド URLパターン                 ルートパラメータ
(略)
books       GET         /books(.:format)           {:action=>"index", :controller=>"books"}
            POST        /books(.:format)           {:action=>"create", :controller=>"books"}
new_book    GET         /books/new(.:format)       {:action=>"new", :controller=>"books"}
edit_book   GET         /books/:id/edit(.:format)  {:action=>"edit", :controller=>"books"}
book        GET         /books/:id(.:format)       {:action=>"show", :controller=>"books"}
            PUT         /books/:id(.:format)       {:action=>"update", :controller=>"books"}
            DELETE      /books/:id(.:format)       {:action=>"destroy", :controller=>"books"}
(略)


ヘルパーとはテンプレートファイルを記述する際に役立つメソッドの総称である。

パスヘルパー          得られるパス
books_path         /books/
book_path(id)      /books/:id
new_book_path      /books/new
edit_book_path(id) /books/:id/edit


<%= link_to 'Edit', edit_book_path(book) %>
<%= link_to 'New',  new_book_path(book) %>

XXX_path 部分を XXX_url に変えると相対パスから、絶対パスになる。


さて、RESTfulなインターフェースになっていることに気がつくだろうか。RESTfulであるとは一意なURLに対して、CRUD(Create、Read、Update、Delete)を使ってアクセスできるということである。CRUDをHTTPプロトコルにあてはめると、POST、GET、PUT、DELETEになる。URLを一意にすることでインターフェースを統一し、プロトコルにしたがってルートパラメータでリソースを制御するのである。



確認
テーブルを作成し、テストデータを流し込んだので以下にアクセスして確認してみる。
Create、Read、Update、Delete操作ができるので触ってみてほしい。
http://x.x.x.x/books
http://x.x.x.x/users
http://x.x.x.x/authors
http://x.x.x.x/reviews



ついでに検証機能もいれておこう。
モデルファイルの編集(検証機能の設定)
bookモデルに検証機能を入れる。
タイトルは必須、また長さは1~100。
価格はintegerのみで価格は100000以下。

# vi app/models/book.rb
validates :title,
          :presence => true,
          :length => { :minimum => 1, :maximum => 100 }

validates :price,
          :numericality => { :only_integer => true, :less_than => 100000 }


再度アクセスして、書籍を新規に登録してみよう。
http://x.x.x.x/books
検証機能に違反する操作を行うとエラーが出るはずである。


これはどうやってエラーを表示させているのであろうか。
# cat app/views/books/new.html.erb
<%= render 'form' %>
部分テンプレートになっている

# cat app/views/books/_form.html.erb
  <% if @book.errors.any? %>
    <div id="error_explanation">
      <h2><%= pluralize(@book.errors.count, "error") %> prohibited this book from being saved:</h2>

      <ul>
      <% @book.errors.full_messages.each do |msg| %>
        <li><%= msg %></li>
      <% end %>
      </ul>
    </div>
  <% end %>

CSSを変えたければ以下のファイルのfield_with_errorsを編集すればよい。
# cat ./app/assets/stylesheets/scaffolds.css.scss
.field_with_errors {
(略)
}


最後に、複数のオブジェクトモデル(リレーショナルテーブル)を作ったので
それらを連携させたアプリケーションを作成する。
http://x.x.x.x/store/search
にアクセスして書籍を検索できるサイトを作る。



モデルファイルの編集(アソシエーションの設定)
" テーブルの設計"で作成したER図を見ながら考える。
再掲

    books 1 - N reviews N 1 users
       1
       |
       N
author_books
       N
       |
       1
    authors



# vi app/models/book.rb ※下記コードを挿入
has_many :authors
has_many : author_books
has_many :authors, through: :author_books

# vi app/models/user.rb ※下記コードを挿入
has_many :reviews
has_many :books, through: reviews

# vi app/models/author.rb ※下記コードを挿入
has_many :author_books
has_many :books

# vi app/models/review.rb ※下記コードを挿入
# booksテーブルとusersテーブルのM:Nひもづけ用中間テーブル
belongs_to :book
belongs_to :user


純粋な中間テーブル用のモデルは不要である
# rm app/models/books_author.rb



コントローラとビューの作成
# rails generate controller store

# vi app/views/store/search.html.erb
<%= form_tag :action => 'scan' do %>
  <div class="field">
    <%= label_tag 'title', 'book title:'  %><br />
    <%= text_field_tag 'title' %>
  </div>
  <%= submit_tag 'search' %>
<% end %>


# vi app/controllers/store_controller.rb ※下記コードを挿入
def scan
  @books = Book.where('title = ?', params[:title])
  render 'list'
end


# vi app/views/store/list.html.erb
<% @books.each do |book| %>
<h1>TITLE : <%= book.title %></h1>
<hr />

<ul>
  <li>PRICE : <%= book.price %></li>
  <% book.authors.each do |author| %>
  <li>AUTHOR : <%= author.name %> </li>
  <% end %>
</ul>

<h1>REVIEW</h1>
<hr />
<ul>
<% book.reviews.each do |review| %>
<li><%= review.body %>(<%= review.updated_at %>)</li>
<% end %>
</ul>
<% end %>



ルーティングの編集
"http://localhost/bookapp/store/search"としてアクセスさせたい。RESTfulインターフェースにはsearch、scan、listのアクションはないので、自前で用意する。

# vi config/routes.rb
resources :store do
  collection do
    get 'search'
    post 'scan'
    get 'list'
  end
end

# rails routes
search_store_index GET    /store/search(.:format {:action=>"search", :controller=>"store"}
  scan_store_index POST   /store/scan(.:format) {:action=>"scan", :controller=>"store"}
  list_store_index GET    /store/list(.:format) {:action=>"list", :controller=>"store"}

collectionは複数のオブジェクトを扱うが、単一のオブジェクト、例えば、GET時に、/store/:id/hoge のようなルーティングを作りたければ、memberブロックを使う。

member :store do
  member do
    get 'hoge'
  end
end



サービス確認
書籍名に"book1"などを入れて検索結果が出てくれば成功である。
http://x.x.x.x/store/search


Next ⇒ Railsを使ったアプリケーション開発 (その4 応用編 認証機能の追加)