Ruby on Rails 事始めの第2弾となる記事。
その1は Ruby on Rails 事始め を参照のこと。
CSS, JavaScript に Sass, CoffeeScript 等のプリプロセッシングと、静的コンテンツ管理を行うアセットパイプライン
アセットパイプラインとは、CSS, JavaScript に Sass, CoffeeScript 等のプリプロセッシングを行うためのフレームワークである。アセットパイプラインは Sprockets Gem パッケージによる機能である。
アセットパイプラインは CSS, JavaScript, 画像等の静的コンテンツを Web アプリケーションが参照できるよう配置し、Sass, CoffeeScript, ERB 等のファイルに対してプリプロセッサ処理を行い、処理の結果をまとめた 1 つの CSS ファイル、JavaScript ファイルを生成する。
アセットパイプラインが処理を行う対象をアセットと呼び、処理を行うプロセッサをプリプロセッサエンジンと呼ぶ。
アセットは下記ディレクトリ内に保存する。
- app/assets/ … 現在のアプリケーション固有のアセット置き場
- lib/assets/ … 純正ライブラリのアセット置き場
- vendor/assets/ … サードパーティのアセット置き場
アセットに対してプリプロセッサエンジンは下記の処理を行う。
- 拡張子に対応するプリプロセッサエンジンを用いてプリプロセッサ処理を行う。
- プリプロセッサエンジンの例として、Sass, CoffeeScript, ERb がある。
- 対応する拡張子は Sass は .scss、CoffeeScript は .coffee、ERB は .erb である。
- ファイルに拡張子を複数指定することで複数のプリプロセッサ処理を行うことができる。
例:SOME.js.erb.coffee に対して CoffeeScript を実行した後、ERb が実行される。(右から左の順)
- プリプロセッサエンジンの例として、Sass, CoffeeScript, ERb がある。
- 複数の CSS ファイルを application.css ファイルにまとめて HTML から参照する CSS ファイルとする
- まとめる CSS ファイルはマニフェストファイル(
app/assets/stylesheets/application.css
)にて指定する。 - 指定する方法として、
*= require SOME.css
(SOME.css がまとめたい css ファイル) のように、*= require 〜
の形式で記述する。 - 尚、
*= require_tree .
は application.css と同ディレクトリ及びサブディレクトリ配下全ての CSS ファイルを読み込むよう指定している。
- まとめる CSS ファイルはマニフェストファイル(
- 複数の JavaScript ファイルを javascripts.js ファイルにまとめて HTML から参照する JS ファイルとする。
- まとめる JS ファイルはマニフェストファイル(
app/assets/javascripts/application.js
)にて指定する。 - 指定する方法は CSS と同様。
- まとめる JS ファイルはマニフェストファイル(
複数のファイルを1つにまとめる際、余分な空白が除去されてファイルサイズが軽くなり、かつブラウザとサーバ間の HTTP リクエスト数が減ることで高速な読み込みが期待できる。(複数ファイルに分割して HTTP リクエストを並列処理した方が高速であるとの議論もあるかと思うが実測していないので「期待できる」に留める)
関連して調べること(2017/12/30現在未調査)
- プリプロセッサを追加する方法(既存のプリプロセッサを調べる方法)
- プリプロセッシングを単体で実行する方法(単体テストとまで行かずとも開発中にテスト実行する方法)
- Sass, CoffeeScript の詳細
HTMLリンクを柔軟に指定する名前付きルート
HTML リンクを指定する際、絶対パスより相対パスを使った方が多くの場合に ドキュメント構成が変わった時に柔軟であるが、rails では名前付きルートを使うことで更に柔軟性を持たせることができる。
例えば、URL パス /static_pages/help
へのリンクを指定する場合、static_pages/home
から指定する場合は相対パスにより、
<a href="help">Help</a>
と指定するが、名前付きルートでは link_to ヘルパーメソッドを使い、
<%= link_to "Help", help_path %>
と指定できる。
link_to メソッドは ActionView::Helpers::UrlHelper で定義されるリンクタグを出力するメソッドである。
参考情報:http://api.rubyonrails.org/classes/ActionView/Helpers/UrlHelper.html#method-i-link_to
名前付きルートは rails ルーティング(rails.rb) で定義される。
root メソッドで static_pages#home
と指定するとブラウザが Web アプリケーションの /
へアクセスした時に StaticPagesController の home メソッドへルーティングすると共に、名前付きルートとして root_path, root_url が利用できるようになる。
# GET /
root 'static_pages#home'
上記を指定すると、名前付きルートの root_path は /
、root_url は http://SOMEDOMAIN/
として展開される。
定義された名前付きルート一覧は rails routes
を使って Prefix の内容から確認できる。
$ rails routes
Prefix Verb URI Pattern Controller#Action
root GET / static_pages#home
static_pages_home GET /static_pages/home(.:format) static_pages#home
static_pages_help GET /static_pages/help(.:format) static_pages#help
上記の結果の Prefix に _path
, _url
をつけた名前付きルートが利用できること、展開された結果は URI Pattern の (.:format) より左の部分になることが分かる。
名前付きルート | 展開される Path 又は URI |
---|---|
root_path | / |
root_url | http://SOMEDOMAIN/ |
static_pages_home_path | /home |
static_pages_home_url | http://SOMEDOMAIN/home |
static_pages_help_path | /help |
static_pages_help_url | http://SOMEDOMAIN/help |
リンク切れがないことをテストする
リンク切れがないことをテストするためには IntegrationTest を使って次の流れで確認する。
- ブラウザで任意のページを開く
- 出力された HTML DOM にリンク(a)タグが存在して、リンク先が意図した内容であることを確認する
IntegrationTest は rails generate
コマンドで作成できる。
作成するコマンドとその結果として作成されるファイルは次の通り。
$ rails generate integration_test site_layout
Running via Spring preloader in process 6261
invoke test_unit
create test/integration/site_layout_test.rb
require 'test_helper'
class SiteLayoutTest < ActionDispatch::IntegrationTest
# test "the truth" do
# assert true
# end
end
ここにリンク切れを確認するためには先に書いた通り、任意のページをブラウザで開き、リンクが意図どおり存在することを確認する動作をテストとして記述する。
- ブラウザで任意のページを開く(例:root)
- 例:
get root_path
- 例:
- 出力された HTML DOM にリンク(a)タグが存在して、リンク先が意図した内容であることを確認する(例:href 属性が contact_path の内容である a タグが 1 つ以上存在する)
- 例:
assert_select 'a[href=?]', contact_path
- 例:
ActiveRecord クラスでデータベースの O/R マッピングを行う
Rails ではデータベース操作を行うために SQL を隠蔽する O/R マッピングするためのクラスとして ActiveRecord が利用できる。
ActiveRecord によりモデルの追加・検索・削除等の操作ができる。
DB のテーブルを作成するためのマイグレーション操作
システムが利用するデータベースの構造は schema.rb
により表される。
rails generate
コマンドでモデルを作成すると、そのモデルをデータベースに作成するための操作を記述したマイグレーションファイルが作成される。
マイグレーションファイルの名前は db/migrate/${YYYYMMDDhhmmss}_${マイグレーション操作概要}.rb
で表される。これらのファイルはデータベースに対してテーブルの作成や編集、削除と言った操作方法が change メソッド等で記述され、rails db:migration
コマンドを実行することでマイグレーションファイルに記述された内容がデータベースに適用される。適用するごとにデータベースにバージョン番号がつけられる。
$ rails db:version
Current version: 0
$ rails db:migrate
== 20180108153653 CreateUsers: migrating ======================================
-- create_table(:users)
-> 0.0099s
== 20180108153653 CreateUsers: migrated (0.0101s) =============================
$ rails db:version
Current version: 20180108153653
上記コマンドによって下記のファイル db/schema.rb
が作成される。
ActiveRecord::Schema.define(version: 20180108153653) do
create_table "users", force: :cascade do |t|
t.string "name"
t.string "email"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
end
rails generate
コマンドを実行するごとにファイルが作成されるため、初期状態のデータベースに対しても、マイグレーションファイルを若い順に実行することで最新のデータベース構造を持つデータベースが構築できる。(データベース構造は復元できるがデータは db:migrate
コマンドでは復元できない)
また、データベースに変更を加えた内容によっては変更後のシステムが正常に動作しないことが考えられる。そういった場合に備えてマイグレーションファイルはロールバック(元戻し)ができるように記述することが求められる。change コマンドは自動的に逆操作を生成することができるが必ず成功するとは限らない。
$ cat db/migrate/20180108153653_create_users.rb
class CreateUsers < ActiveRecord::Migration[5.1]
def change
create_table :users do |t|
t.string :name
t.string :email
t.timestamps
end
end
end
$ cat db/schema.rb | grep -v '#'
ActiveRecord::Schema.define(version: 20180108153653) do
create_table "users", force: :cascade do |t|
t.string "name"
t.string "email"
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
end
end
$ rails db:rollback
== 20180108153653 CreateUsers: reverting ======================================
-- drop_table(:users)
-> 0.0083s
== 20180108153653 CreateUsers: reverted (0.0182s) =============================
$ cat db/schema.rb | grep -v '#'
ActiveRecord::Schema.define(version: 0) do
end
そういった場合は、変更操作と元戻し操作をそれぞれ明示的に up メソッド, down メソッドにより記述することになる。
ActiveRecord を使って DB のレコードを操作する
DB のテーブルが作成できたら ApplicationRecord クラスを継承したモデルクラスを通じてデータ操作が行えるようになる。
ActiveRecord を使ってモデルを作成・削除する
ApplicationRecord クラスには DB のレコード操作を行うメソッドが用意されている。このクラスを継承することで DB のレコード操作が行えるようになる。
メソッド名 | 説明 |
---|---|
.new |
モデルを作成する。戻り値は作成したモデルのオブジェクト。オブジェクトは永続化されていない状態。 |
.save |
モデルを DB に保存する。戻り値は保存成否。 |
.create |
新たにモデルを作成し、DBに保存する。戻り値は作成したモデルのオブジェクト。 |
.destroy |
DBに保存されたモデルを削除する。戻り値は削除したモデルのオブジェクト。(.new で作成された場合と同様にオブジェクトは永続化されていない状態。) |
上記のメソッドを利用した例は次の通り。
$ rails c --sandbox
Running via Spring preloader in process 29640
Loading development environment in sandbox (Rails 5.1.2)
Any modifications you make will be rolled back on exit
>> user = User.new(name: 'Taro Yamada', email: 'taro@example.com') # Userモデルを新規にインスタンス化してuser変数に代入
=> #<User id: nil, name: "Taro Yamada", email: "taro@example.com", created_at: nil, updated_at: nil>
>> user.save # 作成したUserモデルをDBへ追加
(0.1ms) SAVEPOINT active_record_1
SQL (5.2ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "Taro Yamada"], ["email", "taro@example.com"], ["created_at", "2018-01-08 17:49:50.330961"], ["updated_at", "2018-01-08 17:49:50.330961"]]
(0.1ms) RELEASE SAVEPOINT active_record_1
=> true
>> user # user変数内のオブジェクトを参照
=> #<User id: 1, name: "Taro Yamada", email: "taro@example.com", created_at: "2018-01-08 17:49:50", updated_at: "2018-01-08 17:49:50">
>> p "#{user.name} : #{user.email}" # Userオブジェクトのname,emailプロパティを整形して出力
"Taro Yamada : taro@example.com"
=> "Taro Yamada : taro@example.com"
>> user.destroy # user変数内のオブジェクトをDBから削除
(0.1ms) SAVEPOINT active_record_1
SQL (0.3ms) DELETE FROM "users" WHERE "users"."id" = ? [["id", 1]]
(0.1ms) RELEASE SAVEPOINT active_record_1
=> #<User id: 1, name: "Taro Yamada", email: "taro@example.com", created_at: "2018-01-08 17:49:50", updated_at: "2018-01-08 17:49:50">
>> jiro = User.create(name: 'Jiro Yamada', email: 'jiro@example.com') # 新たなUserモデルを作成してDBに保存。またjiro変数にオブジェクトを代入。
(0.1ms) SAVEPOINT active_record_1
SQL (0.1ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "Jiro Yamada"], ["email", "jiro@example.com"], ["created_at", "2018-01-08 17:51:14.545949"], ["updated_at", "2018-01-08 17:51:14.545949"]]
(0.0ms) RELEASE SAVEPOINT active_record_1
=> #<User id: 2, name: "Jiro Yamada", email: "jiro@example.com", created_at: "2018-01-08 17:51:14", updated_at: "2018-01-08 17:51:14">
ActiveRecord を使ってモデルを検索する
メソッド名 | 説明 |
---|---|
.find |
指定したIDと一致するモデルを返す。参考情報:Ruby on Rails API - findメソッド |
.find_by |
指定した値と一致するモデルを返す。参考情報:Ruby on Rails API - find_byメソッド |
.where |
指定した条件でモデルを検索する。参考情報:Ruby on Rails API - whereメソッド |
.all |
全てのモデルを返す。参考情報:Ruby on Rails API - allメソッド |
.first , .second , .third
|
1,2,3番めのモデルを返す。 |
$ rails c --sandbox
Running via Spring preloader in process 30180
Loading development environment in sandbox (Rails 5.1.2)
Any modifications you make will be rolled back on exit
>> User.create(name: 'Taro Yamada', email: 'taro@example.com')
(0.1ms) SAVEPOINT active_record_1
SQL (16.8ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "Taro Yamada"], ["email", "taro@example.com"], ["created_at", "2018-01-08 18:07:44.630941"], ["updated_at", "2018-01-08 18:07:44.630941"]]
(0.2ms) RELEASE SAVEPOINT active_record_1
=> #<User id: 1, name: "Taro Yamada", email: "taro@example.com", created_at: "2018-01-08 18:07:44", updated_at: "2018-01-08 18:07:44">
>> User.create(name: 'Jiro Yamada', email: 'jiro@example.com')
(0.1ms) SAVEPOINT active_record_1
SQL (0.3ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "Jiro Yamada"], ["email", "jiro@example.com"], ["created_at", "2018-01-08 18:07:52.914345"], ["updated_at", "2018-01-08 18:07:52.914345"]]
(0.1ms) RELEASE SAVEPOINT active_record_1
=> #<User id: 2, name: "Jiro Yamada", email: "jiro@example.com", created_at: "2018-01-08 18:07:52", updated_at: "2018-01-08 18:07:52">
>> User.create(name: 'Saburo Yamada', email: 'saburo@example.com')
(0.1ms) SAVEPOINT active_record_1
SQL (0.1ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "Saburo Yamada"], ["email", "saburo@example.com"], ["created_at", "2018-01-08 18:08:02.575078"], ["updated_at", "2018-01-08 18:08:02.575078"]]
(0.0ms) RELEASE SAVEPOINT active_record_1
=> #<User id: 3, name: "Saburo Yamada", email: "saburo@example.com", created_at: "2018-01-08 18:08:02", updated_at: "2018-01-08 18:08:02">
>> User.create(name: 'Shiro Yamada', email: 'shiro@example.com')
(0.1ms) SAVEPOINT active_record_1
SQL (0.1ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "Shiro Yamada"], ["email", "shiro@example.com"], ["created_at", "2018-01-08 18:08:12.164921"], ["updated_at", "2018-01-08 18:08:12.164921"]]
(0.1ms) RELEASE SAVEPOINT active_record_1
=> #<User id: 4, name: "Shiro Yamada", email: "shiro@example.com", created_at: "2018-01-08 18:08:12", updated_at: "2018-01-08 18:08:12">
>> User.create(name: 'Goro Yamada', email: 'goro@example.com')
(0.2ms) SAVEPOINT active_record_1
SQL (0.1ms) INSERT INTO "users" ("name", "email", "created_at", "updated_at") VALUES (?, ?, ?, ?) [["name", "Goro Yamada"], ["email", "goro@example.com"], ["created_at", "2018-01-08 18:08:20.394995"], ["updated_at", "2018-01-08 18:08:20.394995"]]
(0.1ms) RELEASE SAVEPOINT active_record_1
=> #<User id: 5, name: "Goro Yamada", email: "goro@example.com", created_at: "2018-01-08 18:08:20", updated_at: "2018-01-08 18:08:20">
>> User.find(3) # ユーザIDで検索する
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 3], ["LIMIT", 1]]
=> #<User id: 3, name: "Saburo Yamada", email: "saburo@example.com", created_at: "2018-01-08 18:08:02", updated_at: "2018-01-08 18:08:02">
>> User.find(6) # 存在しないユーザIDを検索するとRecordNotFound Exception が発生する
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ? [["id", 6], ["LIMIT", 1]]
ActiveRecord::RecordNotFound: Couldn't find User with 'id'=6
from (irb):19
>> User.find_by(name: 'Goro Yamada') # nameプロパティの値で検索する
User Load (0.2ms) SELECT "users".* FROM "users" WHERE "users"."name" = ? LIMIT ? [["name", "Goro Yamada"], ["LIMIT", 1]]
>> User.find_by_name('Goro Yamada') # nameプロパティの値で検索する
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."name" = ? LIMIT ? [["name", "Goro Yamada"], ["LIMIT", 1]]
=> #<User id: 5, name: "Goro Yamada", email: "goro@example.com", created_at: "2018-01-08 18:08:20", updated_at: "2018-01-08 18:08:20">
=> #<User id: 5, name: "Goro Yamada", email: "goro@example.com", created_at: "2018-01-08 18:08:20", updated_at: "2018-01-08 18:08:20">
>> User.find_by(name: 'Goro') # nameプロパティの値で検索するが部分一致ではヒットしない
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."name" = ? LIMIT ? [["name", "Goro"], ["LIMIT", 1]]
=> nil
>> User.find_by(name: 'Goro *') # nameプロパティの値で検索するが部分一致ではヒットしないし、アスタリスク使っても無駄
User Load (0.1ms) SELECT "users".* FROM "users" WHERE "users"."name" = ? LIMIT ? [["name", "Goro *"], ["LIMIT", 1]]
=> nil
>> User.where(["name LIKE 'Goro %%'"]) # nameプロパティの値で部分一致させるには where を使う
User Load (0.2ms) SELECT "users".* FROM "users" WHERE (name LIKE 'Goro %') LIMIT ? [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<User id: 5, name: "Goro Yamada", email: "goro@example.com", created_at: "2018-01-08 18:08:20", updated_at: "2018-01-08 18:08:20">]>
>> all_users = User.all # 全ユーザを取得する
User Load (0.2ms) SELECT "users".* FROM "users" LIMIT ? [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<User id: 1, name: "Taro Yamada", email: "taro@example.com", created_at: "2018-01-08 18:07:44", updated_at: "2018-01-08 18:07:44">, #<User id: 2, name: "Jiro Yamada", email: "jiro@example.com", created_at: "2018-01-08 18:07:52", updated_at: "2018-01-08 18:07:52">, #<User id: 3, name: "Saburo Yamada", email: "saburo@example.com", created_at: "2018-01-08 18:08:02", updated_at: "2018-01-08 18:08:02">, #<User id: 4, name: "Shiro Yamada", email: "shiro@example.com", created_at: "2018-01-08 18:08:12", updated_at: "2018-01-08 18:08:12">, #<User id: 5, name: "Goro Yamada", email: "goro@example.com", created_at: "2018-01-08 18:08:20", updated_at: "2018-01-08 18:08:20">]>
>> all_users.class
=> User::ActiveRecord_Relation
>> all_users[0]
User Load (0.2ms) SELECT "users".* FROM "users"
=> #<User id: 1, name: "Taro Yamada", email: "taro@example.com", created_at: "2018-01-08 18:07:44", updated_at: "2018-01-08 18:07:44">
>> all_users[3]
=> #<User id: 4, name: "Shiro Yamada", email: "shiro@example.com", created_at: "2018-01-08 18:08:12", updated_at: "2018-01-08 18:08:12">
>> all_users.length
=> 5
ActiveRecord を使ってモデルを更新する
モデルを更新するためのメソッド save, update が用意されている。
オブジェクトの内容を更新してから save により保存するか、 update により DB を更新する。
default では DB に書き込みを行う前に validate 処理が行われて値が正しいか検証が行われ、検証の結果が正常であった場合に DB に保存される。
メソッド名 | 説明 |
---|---|
create(attributes = nil, &block) |
DB にオブジェクトを作成してオブジェクトを生成する。戻り値は生成したオブジェクト。 参考情報:Ruby on Rails API - createメソッド |
save(*args) |
オブジェクトの内容を使って DB へ保存する。モデルが無い場合は DB に新規作成し、既にある場合は更新する。default では保存前に validate が実行され、成功した場合のみ保存が行われる。 参考情報:Ruby on Rails API - saveメソッド |
update(attributes) , update_attribute(name, value)
|
引数で指定した値で DB を更新する。update_attributeメソッドは検証を行わない等の特徴がある。 参考情報:Ruby on Rails API - updateメソッド |
ActiveRecord を使ってモデルを検証する
一般的にモデルが取るべき値には制約が加えられる。(例:name は空文字列をにすることができない)
制約条件として設定するのは次のような内容である。
- 存在性 (presence)
- 長さ (length)
- フォーマット (format)
- 一意性 (uniqueness)
これらの制約はモデルに対して validates メソッドを記述することで指定する。
class User < ApplicationRecord
before_save { email.downcase! }
has_secure_password
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-]+(\.[a-z\d\-]+)*\.[a-z]+\z/i
validates :name, presence: true, length: { maximum: 50 }
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
validates :password, presence: true, length: { minimum: 6 }
end
validates メソッドで、各プロパティが正しい状態を定義する。
-
presence: true
を指定すると blank? メソッドの結果が false になる (つまり空文字列を設定できなくなる) -
length: { maximum: 50 }
を指定すると最大 50 文字に制限できる。同様にlength: { minimum: 6 }
を指定すると最小 6 文字に制限できる -
format: { with: $REGULAR_EXPRESSION }
(with: の後に正規表現を記載する) と指定すると正規表現にマッチするフォーマットのみに制限できる -
uniqueness: true
と指定すると一意制約(同じ値を許さない)をつけられる。大文字・小文字を区別しないで一意制約をつける場合はuniqueness: { case_sensitive: false }
と指定する。
require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
@user = User.new(name: "Example User", email: "user@example.com")
end
test "should be valid" do
assert @user.valid?
end
test "name should be present" do
@user.name = ""
assert_not @user.valid?
end
test "email should be present" do
@user.email = ""
assert_not @user.valid?
end
end
- assert は続いて指定された処理の結果が true, not nil であることを期待する
- assert_not は続いて指定された処理の結果が false, nil であることを期待する
検証結果は valid? を実行したモデルの errors
に保存される。
>> u = User.new
=> #<User id: nil, name: nil, email: nil, created_at: nil, updated_at: nil>
>> u.valid?
=> false
>> u.errors.full_messages
=> ["Name can't be blank", "Email can't be blank"]
>> u.errors.messages
=> {:name=>["can't be blank"], :email=>["can't be blank"]}
>> u.errors.messages[:email]
=> ["can't be blank"]
参考情報
Rails における MVC の命名規則
- Model は単数形、Controller は複数形
- Model に紐づくリレーショナルデータベースのテーブル名は複数形
-
rails generate migration
コマンド実行時のマイグレーションファイル名は末尾に追加したいモデル名を_to_${model_name}
の形式で指定すると対象のテーブルを指定できる