Web備忘録

雑多に書いています

Ruby on Railsガイドを通読してまとめる Part.3

第三弾です。前回はこちら。Ruby on Railsガイドを通読してまとめる Part.2 - エンジニア備忘録

railsguides.jp

なんだかんだPart.3まで続けられました。

これもひとえに自分の努力の成果です。自分すげえ。(ただまとめてるだけなので大した努力ではない)

読みやすいようにまとめたいと思うんですが、やはり量が膨大になってしまうので、さらっと流し見して気になるところがあれば本家Railsガイドさんを読んでいただければと思います。

モデル

ActiveRecord クエリインターフェイス

Active Recordは、ユーザーに代わってデータベースにクエリを発行できる。勉強初期の頃は全くピンとこない概念。(今は何とかわかる)

発行されるクエリは多くのデータベースシステム (MySQLMariaDBPostgreSQLSQLiteなど) と互換性がある。

ActiveRecordはORマッパーとしてとても評判がいい。(と聞いたことがある。ソースはないです)

以下、様々なメソッドについての説明。

DBからオブジェクトを取り出す(1)

ActiveRecord::Baseを継承したクラスからオブジェクトを取り出すためのメソッド。たくさんありますね。

find
create_with
distinct
eager_load
extending
from
group
having
includes
joins
left_outer_joins
limit
lock
none
offset
order
preload
readonly
references
reorder
reverse_order
select
where

気になったやつだけ抜粋。

単一のオブジェクトを取り出す(1.1)

take

ランダムに1つ取り出す。(例:Client.take)

SQLとしては以下。

SELECT * FROM clients LIMIT 1

take(2)といった形にすれば取り出す数を増やせる。

firstとかはこのSQLにORDER BY idついてるだけなので、そっちでもいい。

(idにインデックス貼ってないことはないだろしtakeとfirstで早さほぼ変わらないはず)

複数のオブジェクトをバッチで取り出す(1.2)

find_each

DBから取り出したデータをメモリを圧迫しないサイズにして、ブロック引数に「取り出したレコード1つ」を格納して処理する。

User.find_each do |user|
  NewsMailer.weekly(user).deliver_now
end

デフォルトでは1000件がバッチとなり、|user|に1つずつレコードが格納されている(はじめに1000回繰り返し、終わったら次。)

オプションによってバッチとする件数を3000とかにカスタマイズ出来たり、どのidからどのidまでで処理を止めるか設定できる。

find_in_batches

DBから取り出したデータをメモリを圧迫しないサイズにして、ブロック引数に「取り出したレコード全て」を格納して処理する。

# 1回あたりadd_invoicesに納品書1000通の配列を渡す
Invoice.find_in_batches do |invoices|
  export.add_invoices(invoices)
end

invoicesというブロック引数に1000件分のレコードが詰まっている。その1000件分の情報はたぶんActiveRecord::Relationオブジェクトという形にして保持されていると思うけど、書いていないので詳細は謎。

気になる人は調べてね! 

文字列だけで表した条件で取り出す(2.1)

SQLインジェクションを発生させないため、パラメーターを使うときは配列(?を使った表現)を使用しましょう。

Client.where("orders_count = ?", params[:orders])

パラメータを使わないなら可読性上げるために文字列でもいいと思います。

プレースホルダーを使用した条件で取り出す(2.2.1)

配列(引数の順番で渡す)以外にも、ハッシュを使える。

Client.where("created_at >= :start_date AND created_at <= :end_date",
  {start_date: params[:start_date], end_date: params[:end_date]})

読みづらくね?

条件の上書き(8)

unscope

条件を外せる。カスタムしたスコープから一つだけ条件外したいときとかに使うのかな?

Article.where('id > 10').limit(20).order('id asc').unscope(:order)

reorder

デフォルトスコープの並び順を上書きできる。

class Article < ApplicationRecord
  has_many :comments, -> { order('posted_at DESC') }
end
 
Article.find(10).comments.reorder('name')
#上書きしたSQL
SELECT * FROM articles WHERE id = 10
SELECT * FROM comments WHERE article_id = 10 ORDER BY name

#上書きする前のSQL

SELECT * FROM articles WHERE id = 10
SELECT * FROM comments WHERE article_id = 10 ORDER BY posted_at DESC

基本unscopeでいけそうだけど、デフォルトスコープに他の条件がたくさんあってorderだけ変えたいときに使える。(あるのか?)

Nullリレーション

noneメソッドはチェイン可能なリレーションオブジェクトを生み出すことができる。

# visible_articles メソッドはリレーションを返すことが期待されている
@articles = current_user.visible_articles.where(name: params[:name])
 
def visible_articles
  case role
  when 'Country Manager'
    Article.where(country: country)
  when 'Reviewer'
    Article.published
  when 'Bad User'
    Article.none # => []またはnilを返すと、このコード例では呼び出し元のコードを壊してしまう
  end
end

nilじゃなくてActiveRecord::Relation(中身無し)を返したいときに使う。便利そう。

楽観的ロック(optimistic lock)(11.1)

複数のユーザーが同じレコードを編集することを認める。 このロックを使用するためには、テーブルに「lock_version」という名前のInteger型カラムが必要である。

レコードが更新されるたび、lock_versionカラムの数値が1ずつ増える。 ユーザーがレコードを更新するとき、DBのlock_versionが自分が編集しているlock_versionより大きかったらエラー。

楽観的と言うとるわりに普通にエラーは出す。

悲観的ロック(pessinistic lock)(11.2)

DBのロック機構を使用する。 絶対に他のユーザーにUPDATEさせないという前提のもと、「読み出し許可」するかしないか設定できる。

# MySQLの場合は、lockメソッドに「LOCK IN SHARE MODE」を与えれば読み出しは許可できる。
Item.transaction do
  i = Item.lock("LOCK IN SHARE MODE").find(1)
  i.increment!(:views)
end

たくさんのユーザーが1つのリソースを触るんじゃなく、決まったチームメンバーが1つのリソースを丁寧に時間かけて編集するときは悲観的ロックのほうが親切だろう。(楽観的ロックだと「え?hogeさんも編集してたんすか?あーだったら俺は編集しなかったのに」みたいなのが起き得る)

joinによる結合(12)

内部結合。SQL文を引数に渡すこともできるが、関連付けしてあれば結合クエリを簡単に作れる。

left_outer_joins

外部結合。関連レコードがない場合でもレコードをセットを取得できる。

関連付けを一括読込する(13)

eager loading! ググってもはっきり出てこないやつ。

日本語には「一括読み込み」と訳すようですね。

N+1クエリ問題を解決する

includeで予め読み込んでおく解決策

clients = Client.includes(:address).limit(10)
 
clients.each do |client|
  puts client.address.postcode
end

この場合は、addressesテーブルの外部キーをINで指定しておくことで、先に読み込んでいるようです。

SELECT * FROM clients LIMIT 10
SELECT addresses.* FROM addresses
  WHERE (addresses.client_id IN (1,2,3,4,5,6,7,8,9,10))

複数の関連付けを予め読み込む(13.1)

2つ引数を指定すればオーケー。

Article.includes(:category, :comments)

ネストした関連付けハッシュ(13.1.2)

Category.includes(articles: [{ comments: :guest }, :tags]).find(1)

上のコードは、id=1のカテゴリを検索し、関連付けられたすべての記事とそのタグやコメント、およびすべてのコメントのゲスト関連付けを一括読み込みする。

「先の先」を取りに行く場合は、ハッシュ構造を使うということですね。「Category.include({article: commets})」で、「categoryに紐づくarticlesを全てとる。そして取ってきたarticleにcommentsを紐づけておく」という動き。

関連付けの一括読み込みで条件を指定する(13.2)

ガイドに書いてあった日本語が難解だった。

「Active Recordでは、joinsのように事前読み込みされた関連付けに対して条件を指定することができますが、joins という方法を使用することをお勧めします。」とのこと。

おそらく意図は「通常、内部結合しにかかりたいならjoinsを使うのが一番いい」ってことかな…?

ただjoin使ってダメなときはincludesにwhereを使って外部結合表現することも可能、とのこと。

Article.includes(:comments).where(comments: { visible: true })

上のコードは下のクエリを発行する。外部結合していますね。

SELECT "articles"."id" AS t0_r0, ... "comments"."updated_at" AS t1_r5 
FROM "articles" 
LEFT OUTER JOIN "comments" ON "comments"."article_id" = "articles"."id" 
WHERE (comments.visible = 1)

ハッシュを渡さず文字列でwhereの条件を指定した時、リファレンスを指定して強制的にテーブルをjoinし、SQL断片化を防ぐ必要があるとのこと。

Article.includes(:comments).where("comments.visible = true").references(:comments)

SQL断片化…ってなにって思って調べてもよくわからん…。親が消えたのに残った外部キーのインデックスのことですかねえ。

そしてreferences(:commnets)を付けることでクエリにどう変化がうまれるのかサッパリ。

ここは宿題にさせて下さい(涙目)

スコープ(14)

scopeメソッドにシンボルとlambdaブロックを渡すことで、カスタムクエリ(スコープ)を作れる。

条件を組み合わせてあなただけのクエリを発行しよう!

class Article < ApplicationRecord
  scope :published, -> { where(published: true) }
end

上のコードは、下と同義らしいです。

class Article < ApplicationRecord
  def self.published
    where(published: true)
  end
end

つまりscopeなんてかっこつけたところで本質は「ただのクラスメソッド」ってことですね!!なんだビビらせやがって…。(ただ、戻り値の観点で少し差が生まれることがある。少し下の#14.2参照)

スコープ内でスコープをチェインすることも可能。

引数を渡す(14.1)

scopeでも引数を渡せるらしい。

class Article < ApplicationRecord
  scope :created_before, ->(time) { where("created_at < ?", time) }
end

ただ引数を渡す場合は、クラスメソッドとして定義することが推奨されているだとか。「scopeはクラスメソッドを複製した機能である」ということを強調するためらしいですが、scopeの方が明示的に「クエリ用」ってわかりやすいから、こっちでも良い気がしますけど、どうなんでしょう。

条件文を使う(14.2)

scopeとクラスメソッドで挙動が違う。

# これの戻り値は常に「ActiveRecord::Relationオブジェクト」
class Article < ApplicationRecord
  scope :created_before, ->(time) { where("created_at < ?", time) if time.present? }
end

# これは条件が「false」のとき、戻り値はnil
class Article < ApplicationRecord
  def self.created_before(time)
    where("created_at < ?", time) if time.present?
  end
end

デフォルトスコープ(14.3)

前回の記事で、「関連付けのさいの(相手側への)デフォルトスコープ」を学んだが、これは「自分に設定するデフォルトスコープ」

class Client < ApplicationRecord
  default_scope { where("removed_at IS NULL") }
end

こうすると、このモデルへのクエリに「WHERE removed_at IS NULL」がデフォルトで設定される。

デフォルトスコープの条件を複雑に設定したいなら、「self.default_scope」で定義してもOK。

スコープの解除(14.5)

デフォルトスコープ等を完全に解除したいときはunscopedしましょう。

Client.unscoped.all
# SELECT "clients".* FROM "clients"
 
Client.where(published: false).unscoped.all
# SELECT "clients".* FROM "clients"

大事なことなので2回言いました。

Enums(16)

整数型のカラムを設定可能な値の集合にマッピングしてくれる上に、対応するスコープまで自動的に作成してくれる。

class Book < ApplicationRecord
  enum availability: [:available, :unavailable]
end

0で:available、1で:unavailable。

メソッドの例が以下。

# 下の両方の例で、利用可能な本を問い合わせている
Book.available
# または
Book.where(availability: :available)
 
book = Book.new(availability: :available)
book.available?   # => true
book.unavailable! # => true
book.available?   # => false

詳細はRailsAPIへ!

ActiveRecord::Enum

メソッド

find_or_create_by(18.1)

レコードを検索し、なければ作成するメソッド。

レコード作成時の各カラムの値については、ブロックを渡すか、create_withメソッドでの指定で対応できる。ブロックのほうが勝手が効きそうなので、こちらだけ紹介。

Client.find_or_create_by(first_name: 'Andy') do |c|
  c.locked = false
end

Clientモデルのlockedカラムをfalseにする処理ができます。

find_or_initialize_by

レコードを検索し、なければ「new」でインスタンスを作成するメソッド。保存処理はしない。

find_by_sql

引数に文字列を渡すと、それをSQLとして実行してくれる。戻り値は常に配列(内部にオブジェクト格納)

select_all

引数のSQLを実行し、結果としてActiveRecord::Resultオブジェクトを返す。to_hashを打つとハッシュが格納された配列が得られる。

pluck

英語の意味は「摘む」。

テーブルから特定のカラムの値だけを配列等で取り出したいときに使える。

mapなどで無理やり取り出すより、pluckで取り出したほうがパフォーマンスが優れている。

Client.select(:id).map { |c| c.id }
# または
Client.select(:id).map(&:id)
# または
Client.select(:id, :name).map { |c| [c.id, c.name] }

上の例はAvtiveRecordオブジェクトへの#mapだが、下のようにpluckを使えば、ActiveRecordオブジェクトを準備しなくてもいいし、その上スマートに書ける。最高ですね。

Client.pluck(:id)
# または
Client.pluck(:id, :name)

ただし、オブジェクトを介していないため、オーバーライドは出来ない。

class Client < ApplicationRecord
  def name
    "私は#{super}"
  end
end
 
# Client.select(:name)で、Clientクラスのインスタンスが生まれている。それにnameを打つためオーバーライド出来る。
Client.select(:name).map &:name
# => ["私はDavid", "私はJeremy", "私はJose"]
 
# pluckはインスタンス作成を行わない。
Client.pluck(:name)
# => ["David", "Jeremy", "Jose"]

他にも、あくまで配列で取り出すため、limitなどを打つことはできないといった注意点がある。

# Arrayにlimitは打てない
Client.pluck(:name).limit(1)
# => NoMethodError: undefined method `limit' for #<Array:0x007ff34d3ad6d8>
 
# 先にlimitしてればOK
Client.limit(1).pluck(:name)
# => ["David"]

exists?

引数がDBに存在するか調べる。1つでもあればtrue、なければfalse

ライバルとして「present?」、「any?」が存在する。そしてそれぞれパフォーマンスが異なるそう。

そして「そのスコープによって検索に引っかかるレコードが存在するか?」という条件分岐だけをしたい際は、下の記事いわくパフォーマンス最強が「exists?」とのこと。

参考記事: Present? Vs Any? Vs Exists? - The Lean Software Boutique

この参考記事だとpresent?は内部結合したあと、レコード自体を取得しにいってるので、「存在するかどうか」だけを調べる場合パフォーマンスが悪いとのこと。

any?、exists?は内部結合するところまでは同じだけど、COUNT(*)でレコード数を取りに言ってるのでパフォーマンス的にpresent?に勝ってる。記事ではany?はLIMIT(1)を付けてないのでその分遅いらしいが、調べてたらRails5.2(?)からLimit付けてて同着らしい。どっちや。

ここまできたら内部のコード見に行った方がいいけどActiveRecord潜りにいく元気は今はねえ!! ということで、これも要検証認定です。おめでとう!!

count, average, minimum, maximum, sum(21)

SQLの集計関数を呼び出す者たち。

EXPLAIN

explainメソッドにより、標準出力などで以下のようなものが得られる

# これを打てば…
User.where(id: 1).joins(:articles).explain

↓が得られるそうです

EXPLAIN for: SELECT `users`.* FROM `users` INNER JOIN `articles` ON `articles`.`user_id` = `users`.`id` WHERE `users`.`id` = 1
+----+-------------+----------+-------+---------------+
| id | select_type | table    | type  | possible_keys |
+----+-------------+----------+-------+---------------+
|  1 | SIMPLE      | users    | const | PRIMARY       |
|  1 | SIMPLE      | articles | ALL   | NULL          |
+----+-------------+----------+-------+---------------+
+---------+---------+-------+------+-------------+
| key     | key_len | ref   | rows | Extra       |
+---------+---------+-------+------+-------------+
| PRIMARY | 4       | const |    1 |             |
| NULL    | NULL    | NULL  |    1 | Using where |
+---------+---------+-------+------+-------------+
 
2 rows in set (0.00 sec)

使うのかな…?

ビュー

ActionViewの概要

ビュー生成を担当するライブラリ。

Jbuilder(3.1.3)

Railsにデフォルトで含まれるGemの1つ。JSONを生成するのに使用する。

.jbuilderという拡張子を持つテンプレートでは、jsonという名前のJbuilderオブジェクトが自動的に利用できるようになる。

詳しい挙動は公式ドキュメントで。

GitHub - rails/jbuilder: Jbuilder: generate JSON objects with a Builder-style DSL

パーシャルレイアウト(4)

全体のレイアウトとは異なり、ローカル変数を渡せる。パーシャルテンプレートをyieldで評価しながらテンプレートを作る。

articles/show.html.erb

<%= render partial: 'article', layout: 'box', locals: { article: @article } %>

artcicles/_box.html.erb

<div class='box'>
  <%= yield %>
</div>

これでパーシャルテンプレートである_articleを、パーシャルレイアウトである_box内で使ってテンプレートを作成できる。

ActionViewのヘルパーメソッド(6)

大量にありますが、必要に応じて参照すれば良さそうです。(Rails側でテンプレートを作る技術が、今後フロントエンドのフレームワークに置き換わっていく可能性も考え、ここは一旦スルーします。)

レイアウトとレンダリング

コントローラからビューへの結果の渡し方について解説するトピック。

レスポンスを作成する(2)

コントローラー側から見ると、HTTPレスポンスの作成方法は以下の3通り。

  • renderを呼び出し、ブラウザに返す完全なレスポンスを作成する
  • redirect_toを呼び出し、HTTPリダイレクトコードステータスをブラウザに送信する
  • headを呼び出し、HTTPヘッダーのみで構成されたレスポンスを作成してブラウザに送信する

設定より規約(2.1)

コントローラーはrenderを指定しなくても命名規則に従ったものをrenderする。

そういえば、この規約って、「convention」の訳だと思うんですが、どちらかと言えば「慣習」という意図のほうが意図にマッチしてる気がする

別のコントローラからアクションのテンプレートを出力する(2.2.1)

app/views以下のパスを指定すればOK

render "products/show"

appディレクトリ外のテンプレートを使いたい場合は、"/"から始まるフルパスで指定する。

renderのオプション(2.2.12)

以下の5つが一般的。

  • content_type
  • layout
  • location
  • status
  • formats

content_type

レスポンスのcontent-typeを指定できる。:jsonを渡せば、application/json、:xmlを渡せばapplication/mlなど。

layout

現在のアクションに対して、特定のファイルをレイアウト指定できる。

出力時に、デフォルトのレイアウトを使用しないよう設定も可能。

render layout :false

location

HTTPのlocationヘッダーを設定できる。300などで飛ばす場所のこと。

render xml: photo, location: photo_url(photo)

status

HTTPステータスコードを明示的に指定したい場合に使用する。

formats

リクエストで指定されたフォーマットに応じてレスポンスを返す。

render formats: [:json, :xml]

レイアウトの探索経路(2.2.13)

レイアウトの探索経路は、「そのコントローラ管轄のviewsにlayoutディレクトリがあるか探し、なければ共通のレイアウトを使用する」という流れです。

たとえば、PhotosControllerクラスのアクションから出力するのであれば、app/views/layouts/photos.html.erbまたはapp/views/layouts/photos.builderを探します。該当のコントローラに属するレイアウトがない場合、app/views/layouts/application.html.erbまたはapp/views/layouts/application.builderを使用します。

設定次第で、任意のレイアウトを指定できる。

例えば、そのコントローラーのデフォルトのレイアウトを変更したい場合は

class ProductsController < ApplicationController
  layout "inventory"
  #...
end

シンボルで指定すれば、レイアウトを遅延させて決定できる。

class ProductsController < ApplicationController
  layout :products_layout
 
  def show
    @product = Product.find(params[:id])
  end
 
  private
    def products_layout
      @current_user.special? ? "special" : "products"
    end
 
end

メソッドの戻り値に文字列をセットしておいて、あとで評価するということですね。

Procオブジェクトを渡せば、リクエストの内容に応じてレイアウトを指定することもできる。

class ProductsController < ApplicationController
  layout Proc.new { |controller| controller.request.xhr? ? "popup" : "application" }
end

コントローラーインスタンスがリクエスト内容を保持しているようです。

二重レンダリングエラーを避ける(2.2.14)

and returnを使えば、レンダー先を指定したところで処理を終えるのが簡単。

def show
  @book = Book.find(params[:id])
  if @book.special?
    render action: "special_show" and return
  end
  render action: "regular_show"
end

redirect_toを使用する(2.3)

redirect_toメソッドは、別のURLに対して改めてリクエストを再送信するよう、ブラウザに指令を出すためのもの。

redirect_backを使う場合、戻り先の保証はHTTP_REFERERヘッダーによって行われるが、ブラウザが常に保証しているとは限らないので以下のようにして、失敗時どこに飛ぶかの設定は必要。

redirect_back(fallback_location: root_path)

headを使用する(2.4)

headを指定し、bodyなしのレスポンスを返すことが出来る。

head :bad_request

以下のようにヘッダーが生成される。

HTTP/1.1 400 Bad Request
Connection: close
Date: Sun, 24 Jan 2010 12:15:53 GMT
Transfer-Encoding: chunked
Content-Type: text/html; charset=utf-8
X-Runtime: 0.013483
Set-Cookie: _blog_session=...snip...; path=/; HttpOnly
Cache-Control: no-cache

感想

ActiveRecordについてのトピックが終わって、ActionViewに関するトピックに入りました。

ビューに関するトピックはもう1つだけあるのですが、飛ばします!!

というのが、次に作ろうとしてるアプリはRailsAPI+Vue.jsの予定なので、ビューの知識がすぐに活きないからです。

そして、知識としても結構細々してるので、必要に応じて参照すればいいかな、と。

コントローラーはガッツリ使うので、読んでいこうと思います。

要検証

  • present?, any?, exists? の違い
  • where説を文字列で指定するincludeについて、referencesメソッドを打つか打たないかでのSQL文の違い
Article.includes(:comments).where("comments.visible = true").references(:comments)

Ruby on Railsガイドを通読してまとめる Part.2

第二弾です。前回はこちら。

fujiten3.hatenablog.com

railsguides.jp

これのコールバック〜関連付け(アソシエーション)を読んでまとめました。

めちゃくちゃ長いです。

モデル

AvtiveRecord コールバック

ActiveRecordコールバックとは、ActiveRecordを継承したクラスのインスタンス(User.new等)について、それにsaveやupdateを実行したさいにトリガーされる処理のこと。

before_saveといったトリガと、それに呼び出されるメソッドのことですね。

ActiveRecordのオブジェクトライフサイクルへのフック」と言ってもよい。

コールバックの登録(2.1)

一般的に以下のような記述でコールバックを定めておく。

class User < ApplicationRecord
  #バリデーションの前にコールバックメソッドを実行
  before_validation :normalize_name,
 
  # :onは配列を取ることもできる。この場合はcreateアクションとupdateアクションのみにトリガし、destroyではスルー。
  after_validation :set_location, on: [ :create, :update ]
 
  # コールバックはprivateにしておくことが望ましい
  private
    def normalize_name
      self.name = name.downcase.titleize
    end
 
    def set_location
      #selfはUserクラスのインスタンス自身を表す。
      self.location = LocationService.query(self)
    end
end

個人的に気になった細かい注意点としては、コールバックはUserクラスの特異メソッドとしてでなく、インスタンスメソッドとして定めるというところかな。バリデーションをするかしないか決定するのはUserクラスだけど、その処理自体の内容はインスタンス自身が知っているように定めておく、 ということらしい。

利用可能なコールバック(3)

before_validation、before_saveといったもの。

結構量があるけど、挙動が複雑なのはaround_saveやaround_createといったaround_*系のみだろうか。ガイドで詳しい説明はなかったけど、ググってみた所どうやらサンドイッチ的に処理を書けるようです。

以下、実際に挙動を確認してはいないけど、コードに起こしてみました。たぶんちゃんと動いてくれる。

class User < ApplicationRecord
  around_save :do_around_save
  
  def do_around_save
     #save前の処理を記述
     yield #saves
     #save後の処理を記述
  end

end

この場合は yield によってインスタンスにsaveが実行されるってことですね。

参考記事:

ruby - Rails: around_* callbacks - Stack Overflow

ActiveRecord::Callbacks

after_initializeとafter_find

初めて見たコールバック。こんなのあるんですね。

after_initializeコールバックは、Active Recordオブジェクトが1つインスタンス化されるたびに呼び出される。インスタンス化は、直接newを実行する他にデータベースからレコードが読み込まれるときにも行われる。

インスタンス自体が生まれたときに使えるわけだから、インスタンスに対して最初に実行しておきたい処理とかを記述できるということだろう。

いやあActive_Record先輩すごいぜ。何でもやってくれるな。

挙動がそのまま乗ってたので掲載します。

class User < ApplicationRecord
  after_initialize do |user|
    puts "オブジェクトは初期化されました"
  end
 
  after_find do |user|
    puts "オブジェクトが見つかりました"
  end
end
 
>> User.new
オブジェクトは初期化されました
=> #<User id: nil>
 
>> User.first
オブジェクトが見つかりました
オブジェクトは初期化されました
=> #<User id: 1>

after_touch(3.5)

touchメソッドの後に実行。touch自体知らなかったけど、update_atを更新したい場合に使えるらしい。そのインスタンスにタッチするってことですね。

挙動例にスマートなコードがあったので掲載。親子関係のモデルで、子どもにタッチしたときの動き。

class Employee < ApplicationRecord
  belongs_to :company, touch: true
  after_touch do
    puts 'Employeeモデルにtouchされました'
  end
end
 
class Company < ApplicationRecord
  has_many :employees
  after_touch :log_when_employees_or_company_touched
 
  private
  def log_when_employees_or_company_touched
    puts 'Employee/Companyにtouchされました'
  end
end
 
>> @employee = Employee.last
=> #<Employee id: 1, company_id: 1, created_at: "2013-11-25 17:04:22", updated_at: "2013-11-25 17:05:05">
 
# @employee.company.touchをトリガーする
>> @employee.touch
Employeeにtouchされました
Employee/Companyにtouchされました
=> true

子どもにタッチしたとき、子ども側で「belongs_to :parent, touch: true」といったオプションがあれば、親側もタッチされる。(そして親のupdated_atも更新される)。親側がタッチされたとき、それにコールバックする処理を書いておくことも可能ということですね。連鎖するタッチ。優しさの連鎖。

コールバックの実行(4)

コールバックを実行するメソッドのうち「toggle!」だけ知らなかったので調べたら、true/falseを反転させて保存という処理を行うそうです。保存処理(save)にトリガしてコールバックがかかるんでしょうね。「toggle」単体だとただtrue/falseを変えるだけ。

リレーションシップのコールバック(7)

インスタンスのdestroyにフックして子インスタンスのdestroyを呼び出す処理にしていたとき、子側でコールバックを定められる。

class User < ApplicationRecord
  has_many :posts, dependent: :destroy
end
 
class Post < ApplicationRecord
  after_destroy :log_destroy_action
 
  def log_destroy_action
    puts 'Post destroyed'
  end
end
 
>> user = User.first
=> #<User id: 1>
>> user.posts.create!
=> #<Post id: 1, user_id: 1>
>> user.destroy
Post destroyed
=> #<User id: 1>

コールバックで複数の条件と指定する(8.3)

記事にコメントをするさい、記事の作者が「メール希望」であり、かつ「その記事がコメント無視する設定にしていない」ときにだけ作者にメールを送りたいときのコードが以下。

class Comment < ApplicationRecord
  after_create :send_email_to_author, if: :author_wants_emails?,
    unless: Proc.new { |comment| comment.post.ignore_comments? }
end

Proc.new{}の処理はメソッドにしてもよい。

コールバッククラス(9)

この章で一番難しいな、これな。

バリデーションをクラスで切り取れるように、コールバックの処理自体もクラスで切り取り、いろいろなモデルに流用できるよという概念。

ただコードへの解説の少なさがハンパなかったので自分で調べました。

# コールバッククラスに処理を委譲する(今回の場合はPictureFileCallbacks.newに委譲)
class PictureFile < ApplicationRecord
  after_destroy PictureFileCallbacks.new
end


class PictureFileCallbacks
  #トリガした側が「after_destroy」で委譲した場合、委譲された側では「after_destroy(record)」が実行される。
  #recordとはつまり、トリガした側のインスタンスのこと。ここでは「PictureFile.find(id: n)」が渡されるようになっている。
  def after_destroy(picture_file)
    if File.exist?(picture_file.filepath)
      File.delete(picture_file.filepath)
    end
  end
end

参考記事読みながら頑張って理解しましょう! 実務でもガンガン出てきそうです(想像です)

てめえらのRailsはオブジェクト指向じゃねえ!まずはCallbackクラス、Validatorクラスを活用しろ! - Qiita

トランザクションコールバック(10)

もっと難しいとこきた!!

after_crate_commit と after_createの違いについて話してくれていますが…かなり難解。

自分の解釈を書きますが、怪しいところもあるので自分で確認してください。

2つの違いはトランザクションの範囲(例外が出たさい、ロールバックする範囲)がポイントに見える。「after_crate_commit」の場合は、それが指定するコールバック内で例外が起きても、コミットした内容までロールバックすることはなさそう。

つまり、「Aを作成したあと、Bを削除する処理」を書く場合、after_crate_commit でAを作成したときは、Bの削除で例外が起きるとAの作成自体までロールバックされることはない。一方、after_createの場合は一つのトランザクション内に存在している動作だから、Bの削除における例外でAの作成自体もロールバックする(Aは生まれない)という処理になる、ということかなあ。

正直、難しい。これは自分でコード起こさないと理解できなさそうです。またのちほど検証します。

参考記事:

ruby - Difference between after_create, after_save and after_commit in rails callbacks - Stack Overflow

ruby on rails - after_create :foo vs after_commit :bar, :on => :create - Stack Overflow

ActiveRecord アソシエーション

関連付けの種類(2)

Railsでサポートされている関連付けは以下の6種類。

  • belongs_to
  • has_one
  • has_many
  • has_many :through
  • has_one :through
  • has_and_belongs_to_many

6種類しかなかったのね。もっとあるかと思ってた。

belongs_to と has_one どちらを選ぶか(2.7)

区別の決め手の一つは「外部キー」をどちらに置くのか、ということ。ほかにも、どちらがどちらに所属するのか考えるのも大事。たとえば供給者とアカウントの関係を考えたとき、アカウントが供給者に属すると見たほうが自然。ということで、以下のようなマイグレーションファイルが望ましい。

マイグレーションファイルの例。


class CreateSuppliers < ActiveRecord::Migration[5.0]
  def change
    create_table :suppliers do |t|
      t.string :name
      t.timestamps
    end
 
    create_table :accounts do |t|
      t.integer :supplier_id
      t.string  :account_number
      t.timestamps
    end
 
    add_index :accounts, :supplier_id
  end
end

t.integer :supplier_id は t.references :supplier と書いてもよい。同じ意味。 indexは自分で貼りましょう。

referencesで関連付けると、foreign_key: trueオプションをつかって、DB自体に参照整合性を設定できる。強い。大事ですね。

参考記事:

Railsの外部キー制約とreference型について - Qiita

has_many :throughとhas_and_belongs_to_manyのどちらを選ぶか(2.8)

似てるように見えて、違いがあります。どちらも中間テーブルが必要ですが、has_and_belongs_to_manyはその中間テーブルをモデルとして扱わない。「ただつなげる」だけ。

has_many :throughは、結合モデルを作るので、バリデーションやコールバック、追加の属性の設定などができる。

DBは一度作ると変更コストが大きいので、しっかり考えて作りたいですね。ここテキトーに選ぶと負債が大きそう。

ポリモーフィック関連付け(2.9)

高等テクニック。1つのモデルが他の複数のモデルに属すことを、1つの関連付けだけで表現できる。

SQLアンチパターン読んでたときにアンチパターン気味に紹介されてはいたけど、「のちのちへの対応力が高い」ということで、たしか拡張性の面では評価されてた。

従業員、製品から「Pictureモデル」にアクセスしたいときのポリモーフィック関連の記述法。

class Picture < ApplicationRecord
  belongs_to :imageable, polymorphic: true
end
 
class Employee < ApplicationRecord
  has_many :pictures, as: :imageable
end
 
class Product < ApplicationRecord
  has_many :pictures, as: :imageable
end

これで@employee.picturesや@product.picturesで写真を引っ張ってこれるし、逆に@picture.emageableで親モデルのタイプを知ることなく親を引っ張ってこれる。あとはEmployeeとProductに同名のメソッドを作って、それぞれの振る舞いさえ記述して上げれば、まさにオブジェクト指向的なコードになりそう。

つまりこんな感じですね。

class Employee < ApplicationRecord
  has_many :pictures, as: :imageable

  def greet
    puts "こんにちは!"
  end

end
 
class Product < ApplicationRecord
  has_many :pictures, as: :imageable

  def greet
    puts "コンニチハ"
  end

end

マイグレーションファイルはこのようにしましょう。

class CreatePictures < ActiveRecord::Migration[5.0]
  def change
    create_table :pictures do |t|
      t.string  :name
      t.references :imageable, polymorphic: true, index: true
      t.timestamps
    end
  end
end

これで、picturesテーブルのカラムに「imageable_type」と「imageable_id」カラムが追加されます。 imageable_typeはモデル名がString型で格納されてて、それでどの親モデルのidに属してるか判断します。

自己結合(2.10)

1つのテーブル内での関連付け。Employeeテーブルがマネージャと部下をもつ場合などに使う。

関連付けで知っておくべき注意事項(3)

  • キャッシュ制御
  • 名前衝突の回避
  • スキーマの更新
  • 関連付けのスコープ制御
  • 双方向関連付け

キャッシュ制御(3.1)

オブジェクトは実行したクエリの結果を保持する。再読込したい場合はreloadメソッドを実行しよう。

author.books                 # データベースからbooksを取得する
author.books.size            # booksのキャッシュコピーが使用される
author.books.reload.empty?   # booksのキャッシュコピーが破棄される
                             # その後データベースから再度読み込まれる

名前衝突の回避(3.2)

関連付け名はメソッドとして使われるため、ActiveRecord::Baseが持つメソッドと被らないように。「belongs_to :save」みたいな関連付けしたらダメだよってことですね。

スキーマの更新(3.3)

コード上で関連付けてもDB自体に必要な設定を施してないとうまく働かない。

  1. belongs_to関連付けを使用する場合は、外部キーを作成する
  2. has_and_belongs_to_many関連付けを使用する場合は、適切な結合テーブルを作成する

2について、中間テーブルにはプライマリキーを設定しない。設定すると動作不良を起こします。

関連付けのスコープ制御(3.4)

関連付けのさい、オブジェクトは現在のモジュールのスコープ内で探索を行う。 別の名前空間同士での関連付けの場合はclass_nameとして、完全なクラス名を宣言しなければならない。

module MyApplication
  module Business
    class Supplier < ApplicationRecord
      has_one :account,
      class_name: "MyApplication::Billing::Account"
    end
  end
 
  module Billing
    class Account < ApplicationRecord
      belongs_to :supplier,
      class_name: "MyApplication::Business::Supplier"
    end
  end
end

双方向の関連付け

基本的に、親モデル側からのあるインスタンスに対して、子側からアクセスしにかかっても、同じインスタンスを参照するようにActiveRecordは働く。これが双方向の関連付け。

しかし子側で:through、または:foreign_keyオプションを使ったさいは、inverse_ofオプションを使って親側からも子側を指定しないと、双方向にならない。

class Author < ApplicationRecord
  has_many :books, inverse_of: 'writer'
end
 
class Book < ApplicationRecord
  belongs_to :writer, class_name: 'Author', foreign_key: 'author_id'
end

詳しい説明見たい場合はガイドへGo!

アソシエーションの詳細情報

アソシエーションすることで得られるメソッドの解説。

AvtiveRecordっていろんなことしてくれるんですね(ActiveRecordへの感謝定期)

belongs_toで得られるメソッド(4.1.1)

全部で6つ。よく見るのばかり。

#association という言葉は、関連付け相手のテーブル名に動的に代わります

association
association=(associate)
build_association(attributes = {})
create_association(attributes = {})
create_association!(attributes = {})
reload_association

build_associationだけ解説。(associationという言葉はbelongs_to :author といった関連付けをすればauthorに置き換わる)

@author = @book.build_author(author_number: 123,
                                  author_name: "John Doe")

子側から親のインスタンスを作る動きですね。作った時点ではDBに保存されない。

create_asociationで親のインスタンスを作れば、作ったときに保存処理が行われる(なのでもちろんバリデーション等に弾かれる可能性もある)

belongs_toのオプション(4.1.2)

全部で11個!  多いぜ…。

  • :autosave
  • :class_name
  • :counter_cache
  • :dependent
  • :foreign_key
  • :primary_key
  • :inverse_of
  • :polymorphic
  • :touch
  • :validate
  • :optional

知らなかったもの、かつ重要そうなものをまとめていきます。

:autosave

APIガイド読みましたが、挙動の詳細がいまいちつかめません。 Railsガイドといろいろなサイト見たところ、つまりこういうことかもしれない。

  • autosave: true → 親オブジェクトのsaveで、「子モデルの作成・更新」をともに保存する。
  • autosave: false → 親オブジェクトのsaveで、「子モデルの作成・更新」をともに保存しない。
  • autosave未設定 →親オブジェクトのsaveで、「子モデルの作成」は保存し、「子モデルの更新」は保存しない。

挙動チェックまではしてないので、実装の際はご注意を!

参考記事: ActiveRecord::AutosaveAssociation

:class_name

実際のモデル名を指定するオプション。 メソッドとしてauthorで呼びたいが、モデル名がPersonのときは以下の設定。

class Book < ApplicationRecord
  belongs_to :author, class_name: "Patron"
end

:counter_cache

親モデルが持つ子モデルの数をキャッシュしておく設定。

詳細、結構調べましたが、古い記事が多く難しかったです。とりあえずクエリ発行の結果をキャッシュするという挙動はわかったんですが、クエリ結果のIntegerを普通に変数に保持しておいてアレコレするっていう動作の方が安全に思えるんですけどどうなんでしょうね。

実務で見かけたらまた調べようと思います。

counter_cacheを強化したcounter_cultureというgemもあるみたいです。

Rails tips: ActiveRecord count系機能の基本と応用(翻訳)|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社

Railsで関連レコード数の集計(カウンターキャッシュ) | 株式会社ランチェスター

:dependent

:destroyで関連付けのdestroyメソッドを実行。:deleteだと直接DBから削除。

子側の削除で親側の削除をフックすることはあまりなさそう。

:foreign_key

自分が持つ外部キーのカラム名を指定。

:primary_key

親側のprimary_keyがself.primary_key=hogeで変更されてる場合は、子側でも明示しないと参照できない。

:inverse_of

前述した通り。inverseは逆側という意味で、関連付けしているモデル目線から見た自分の呼び出し方を記述する。

:polymorphic

前述した通り。

:touch

上でも解説したが、足りないので補足。

関連しているオブジェクトの保存または削除に合わせて自分のupdated_atを更新する。

:validateと:optional

説明が薄かったので割愛。

belongs_toのスコープ(4.1.3)

アソシエーションが自動的につくるクエリをカスタマイズしたいとき、独自に設定することが出来る。

class Book < ApplicationRecord
  belongs_to :author, -> { where active: true },
                        dependent: :destroy
end

この記述をしておけば、book.authorで発行するクエリの条件に、active: trueが付随してくれる。 他にも状況に応じて3つほどオプションがあったけど、このwhereの例が一番頻出しそう(想像)。

has_oneで得られるメソッド(4.2.1)

belongs_toとほとんど同じ。

has_oneのオプション(4.2.2)

belongs_toとほぼ同じだが、大きく違うものだけ見ていく。

:dependent

親側オブジェクトの削除に対して、それに紐づく子側の挙動を設定できる。

  • :destroyを指定すると、関連付けられたオブジェクトも同時にdestroyされます。
  • :deleteを指定すると、関連付けられたオブジェクトはデータベースから直接削除されます。このときコールバックは実行されません。
  • :nullifyを指定すると、外部キーがNULLに設定されます。このときコールバックは実行されません。
  • :restrict_with_exceptionを指定すると、関連付けられたレコードがある場合に例外が発生します。
  • :restrict_with_errorを指定すると、関連付けられたオブジェクトがある場合にエラーがオーナーに追加されます。

:nullifyはDBのNOT NULL制約に弾かれるので注意。というか:nullifyオプション自体使用頻度低そう。

:primary_key

詳細が書いてなかったけど、子側のprimary_keyが変わってたらこのオプションで明示しないとダメなんだろう。 おそらくそういうことは滅多にない。

has_manyで追加されるメソッド(4.3.1)

全部で17個!!?マジ?? belongs_toは6個だったのに… 

#collection という単語は関連付け先のテーブル名に動的に代わります。

collection
collection<<(object, ...)
collection.delete(object, ...)
collection.destroy(object, ...)
collection=(objects)
collection_singular_ids
collection_singular_ids=(ids)
collection.clear
collection.empty?
collection.size
collection.find(...)
collection.where(...)
collection.exists?(...)
collection.build(attributes = {}, ...)
collection.create(attributes = {})
collection.create!(attributes = {})
collection.reload

collection.delete(object, ...)とcollection.destroy(object, ...)の違い

うーん、ここガイドが逆なように感じた。

ただAPIリファレンスからすぐに見つけられなかったから、一旦そのまままとめておきます。

書いてあることとしては「collection.delete(obj,...)はデフォルトでは外部キーにNULLセットで子モデルを削除扱い、親モデルにdependentがあればそれに応じてdestroyなりdelete」。一方「collection.destroy(obj,...) は問答無用でDBから削除」という挙動だそうです。

deleteがモデル層見つつ削除して、destroyが直接クエリ打つって、なんか逆に感じる。。

うーむ、また調べておきたい。

collection.clear(4.3.1.8)

親モデルの:dependentオプションに応じながら全削除。

@author.books.clear

@authorをdestroyするわけじゃないけど、まるでdestroyしたような挙動になりますね。 親モデルを一旦論理削除しておくけど、それに紐づく子モデルはDBから物理削除しておきたい、みたいなときに使うのかな。

collection.where(...)(4.3.1.12)

条件を指定して検索できるが、遅延読み込みされる点に注意。

#クエリはまだ発行されない
@available_books = @author.books.where(available: true) 

#クエリが発行される
@available_book = @available_books.first

has_manyのオプション(4.3.2)

has_oneとほぼ同じ。

has_manyのスコープ

has_oneより種類がある。重要そうなものを抜粋。

includes(4.3.3.4)

第2関連付けをeagar_loadしたいときに使う。

class Author < ApplicationRecord
  has_many :books, -> { includes :line_items }
end
 
class Book < ApplicationRecord
  belongs_to :author
  has_many :line_items
end
 
class LineItem < ApplicationRecord
  belongs_to :book
end

@author.books.line_itemsを取り出す頻度が多いときに予めincludeしておける、とのこと。

@author.booksでクエリ発行したときに、line_itemsのことも考えてincludeしてキャッシュしておくということでしょうかね。

@author.books.line_items単体のクエリのパフォーマンスが上がる(SQLの効率化)ってわけではなさそうなんだけどどうなんだろう? 要検証。

limitやoffsetやorderも

あらかじめ設定しておけるみたいだけど、author.booksに暗黙的にlimit(100)させたりするより、コントローラー側でlimit(100)するか、クラスで使用頻度高いクエリをまとめたscopeを作っておいた方が良さげに思えますね。

まあ、あくまで状況に応じてということでしょうが。

distinct

中間テーブルを介して多:多で、相手のレコードを探しにいくとき、中間テーブルの一意性を検証して、「参照前と参照後が同じであるレコード」が合った場合に、それが1つになるよう弾いてくれる。

DB自体で「その中間テーブルの一意性」を保証した場合は、以下の設定をしておこう。

add_index :readings, [:person_id, :article_id], unique: true

これをしておけば、「著者と記事のidが全く同じ中間テーブルが2つ以上ある」という状況が起きない。

include?などで一意性チェックしにかかるのはアンチパターンなのでやめましょう。

シングルテーブル継承 (STI

あるテーブルにtypeカラムを作り、そのテーブルを他に継承させる。 そうすることで、共通のメソッドやテーブルのカラムを再利用できる。

が…アンチパターン気味にしか見えない…。

現場Railsでも「STIはよっぽど同じ条件のモデルたちがあるときじゃないと使うべきじゃない(モジュールとかで対応できるよね)」って書いてた気がするし、安易に作らないようにしたい。

感想

大事だなあと思う所が多くて長くなりました。ぜんぜんまとめれてねえ。

内容はめちゃくちゃ為になるので、Part.3も引き続きやっていこうと思います。

要検証

  • トランザクションコールバック
  • collection.delete(object, ...)とcollection.destroy(object, ...)の違い
  • has_manyのオプションにおけるincludesのクエリの発行の仕方

Ruby on Railsガイドを通読してまとめる Part.1

なかなかボリューミーなことで有名なRuby on Railsガイド。

railsguides.jp

実務未経験、Rails触り初めて4ヶ月の人間が気になった所だけ抜き出ししてまとめます。 基本的に、「理解できなかった所」や「大事だなと思った所」を中心にしてまとめようと思います。

はじめに

Rails をはじめよう

ネストさせたモデルに値を送る(7.2)

このコードの1行目が初見だったので掲載。

<%= form_with(model: [ @article, @article.comments.build ], local: true) do |form| %>
  <p>
    <%= form.label :commenter %><br>
    <%= form.text_field :commenter %>
  </p>
  <p>
    <%= form.label :body %><br>
    <%= form.text_area :body %>
  </p>
  <p>
    <%= form.submit %>
  </p>
<% end %>

大まかな挙動はわかるけど、体系的にはどう動くのかなって思ってたらスタックオーバーフローで質問がありました。

Ruby on Rails Form For with two parameters - Stack Overflow

ネストさせたモデルに値を送りたいときの書き方のようですね。(実際の挙動見てないので、導入するときは要確認) 結構色々なところで頻出してそうなのに初対面だったのは驚き。@articleはArticle.find(params[:id])等してキャッチしてるのが前提です。

Basic認証(9.1)

検証環境のサーバー等で制限をかけたい場合によく使われる基本的な認証の1つ、BASIC認証Railsでの実装方法は以下の通り。

class ArticlesController < ApplicationController
 
  http_basic_authenticate_with name: "dhh", password: "secret", except: [:index, :show]
 
  def index
    @articles = Article.all
  end
 
  # (以下省略)

http_basic_authenticate_withメソッドにこのように引数を与えてあげれば、「Articleコントローラーのindexとshowアクション以外でBasic認証必要」という処理になります。

Qiitaに、アプリケーション全体に設定してる方がいました。環境変数でnameとpasswordを決めておいて、ソースコードで見れないようにしてますね。こっちのほうがより安全そうです。

RailsでBasic認証の導入 〜完全版〜 - Qiita

モデル

Active Record の基礎

命名ルール(2.1)

モデル名が単数形、テーブル名が複数形という慣習があるけど、英単語によってはただ「s」をつければ複数形になるわけではないので注意が必要。

モデル名→ テーブル名
Mouse → mice
Person → people

この挙動はActiveSupportによって、Stringが拡張されて#pluralizeメソッドを貰うことで保証しているようです。

[1] pry(main)> require 'active_support'
=> true
[2] pry(main)> require 'active_support/core_ext'
=> true
[3] pry(main)> 'cat'.pluralize
=> "cats"
[4] pry(main)> 'person'.pluralize
=> "people"

(require 'active_support' だけじゃNoMethodError出たので、'active_support/core_ext'もrequireしました。参考記事:Qiita:https://qiita.com/seri_k/items/4818af527bd0e94cc860

スキーマのルール(2.2)

テーブルのカラム名について、created_atなどは予約語なので独自に定めるべきではない。他の予約語として、以下のものがある。

  • 楽観的ロックのための lock_versionカラム。
  • 単一継承テーブルのための typeカラム。
  • ポリモーフィック関連のための「関連付け名_typeカラム」
  • 親が持つ子の数をキャッシュしておくための「テーブル名_countカラム」

これらはRailsルールがあるので、意図せずカラム名にしないよう注意。

命名ルールを上書きする(4)

以下のように記述することで、命名ルールを上書きできる。

class Product < ApplicationRecord
  self.table_name = "PRODUCT"
end
class Product < ApplicationRecord
  self.primary_key = "product_id"
end

Active Record マイグレーション

マイグレーションの概要(1)

マイグレーションがエラーを吐いたさい、全てがロールバックされるとは限らないので注意が必要。

(引用)スキーマ変更のステートメントを利用できるトランザクションがデータベースでサポートされている場合、マイグレーショントランザクションの内側にラップされて実行されます。これらがデータベースでサポートされていない場合は、マイグレーション中に一部が失敗した場合にロールバックされません。その場合は、変更を手動でロールバックする必要があります。

ん、これって…どこかで聞いたような…

ワイや!

スキーマ変更(カラム追加等)のDDL文がエラー吐いたらこうなるのはやはり一部RDBMSでは仕様のようです。 おとなしく手動でロールバックしましょう!

テーブルを作成する(3.1)

テーブルを作るさい:commentオプションというものがあるらしい。初めて見た。 db/schema.rbを直接見るか、DB用のGUIツールを使えば確認できるそうです。 使い方としては、「nameってカラム名だけど何のことだっけ?」みたいなものに、注釈を付けておける、といった効果がある。

案外使いやすそう?

class CreatePatients < ActiveRecord::Migration[5.0]
  def change
    create_table :patients do |t|
      t.string :name, comment: "患者のニックネーム"
      t.string :first_name, comment: "患者の下の名前"
      t.string :last_name, comment: "患者の名字"

      t.timestamps
    end
  end
end

参考記事: [Rails 5] マイグレーション時にデータベースのカラムにコメントを追加する|TechRacho(テックラッチョ)〜エンジニアの「?」を「!」に〜|BPS株式会社

テーブル結合を作成する(3.2)

中間テーブルをサクッと作りたいあなたへ。

create_join_table :products, :categories

productテーブルとcategoryテーブルの中間テーブル(外部キー保証)が出来ます。 インデックスや、独自のテーブル名を設定することも可能。詳細はRailsガイドへどうぞ。

テーブルを変更する(3.3)

完全に知らなかったので全て引用してます。

既存のテーブルを変更するchange_tableは、create_tableとよく似ています。基本的にはcreate_tableと同じ要領で使いますが、ブロックに対してyieldされるオブジェクトではいくつかのテクニックが利用できます。たとえば次のようになります。

change_table :products do |t|
  t.remove :description, :name
  t.string :part_number
  t.index :part_number
  t.rename :upccode, :upc_code
end

上のマイグレーションではdescriptionとnameカラムが削除され、stringカラムであるpart_numberが作成されてインデックスがそこに追加されます。そして最後にupccodeカラムをリネームしています。

ただ、ここまで一気にテーブル構造変えるのは保守性落ちそうですね。 作り直しがどうしても出来ない場合の策か。

データベースをリセットする(4.3)

bin/rails db:resetは一度テーブルをドロップしたあと、db/schema.rbに従って再びテーブルを作成しているだけで、マイグレーションをもう一度実行しているわけではないらしい。知らなかった。(db/schema.rbをスキーマダンプというらしい。後で説明がある)

既存のマイグレーションを変更する(5)

テーブルのカラム名などを変更したいときに、既存のマイグレーションファイルに手を加えて再実行するのはNG。コミットしてマージされてしまったマイグレーションファイルに対する変更をしてしまうとチームメンバーに影響がある。

新しいマイグレーションファイルを作成して対応しましょう。

スキーマダンプの種類(6.2)

db/schema.rbとして保存されているものを「スキーマダンプ」と呼ぶ。これは現在のDBの状態をダンプ(出力)したもの。 設定次第でdb/structure.sqlというsql文で書かれたスキーマダンプを手に入れることも出来る。(DBの高度な機能を使うためにはそうした方がいい。)

マイグレーションファイルとスキーマダンプとの関係を表す参考記事置いてあったので、貼っておきます。 schema.rbとmigrationファイルの関係をまとめてみたよ - Qiita

ActiveRecordバリデーション

バリデーション(1.1)

データをデータベースに保存する前に行えるバリデーションの種類は大別すると4つである。

  1. クライアントサイド(JS等)でのバリデーション

    • バリデーションとしての信頼性は低いが、ユーザーへの即座なフィードバックが可能である。モダンなアプリとかだったら、「そのパスワードの強度は低いですよ」みたいなのをリアクティブに教えてくれるものが多い気がする。もちろん、JSでの実装。
  2. コントローラーレベルでのバリデーション

    • 非推奨。テストと保守が困難になるため、避けたほうがいい。
  3. モデルレベル

    • 最も重要。さまざまなバリデーションが元々アクティブレコードに用意されている。また、自分でカスタムしたものを使うことも可能。テスト・メンテナンスも容易。バリデーションを行いたいときの基本である。
  4. データベースレベル

    • テストや保守を行うのは難しくなるが、Rails以外のアプリケーションからそのDBにアクセスするようなことがあるのなら、DB自体にバリデーションを付与しておくのは良い方法である。また、ユニークネス制約など、他の層のバリデーションでは実装が難しく、かつ使用頻度が高いものもあるので、覚えておこう。

モデル層での色々なバリデーション

acceptance(2.1)

class User < ActiveRecord::Base
  validates_acceptance_of :terms_of_service
end

> user = User.new(terms_of_service: true)
> user.valid?
#=> true

> user.terms_of_service = '1'
> user.valid?
#=> true

validates_acceptance_ofバリデーションは、利用規約同意のチェックボックス等に使える。 チェックボックスがチェックされて、その値が「true」か「"1"」なら通る(試してないけどInteger型の「1」でも通りそう) DBに存在していないカラムについても弾ける。「利用規約に同意しているかどうかカラム」なんて作ったところで、全部「true」に決まってるので、モデル層で弾く設計にした方が良いということでこのバリデーションは良さげですね。

参考記事: Rails 5 accepts 1 or true for acceptance validation | BigBinary Blog

confirmation(2.3)

パスワード確認用に使えるバリデーション。

class Person < ApplicationRecord
  validates :email, confirmation: true
  validates :email_confirmation, presence: true  #2行目のバリデーションを働かせるためnil不許可
end

これでビューテンプレートに「email」と「email_confirmation」に関するフィールドを作ればOK。同じ値であることを保証する。

format(2.5)

withオプションで与えられた正規表現とマッチするものだけを通す。

class Product < ApplicationRecord
  validates :legacy_code, format: { with: /\A[a-zA-Z]+\z/,
    message: "英文字のみが使えます" }
end

メールアドレスのバリデーションでよく出てくる気がする。

uniquness(2.11)

モデル側で定める一意性制約。どうやって挙動を保証してるのかな〜と思ったらSQLクエリを発行しているようですね。まあ、DB見ないとユニークネスかどうかわからないのは当たり前か。(もしかして何か裏技あるのかと思ってた)

validates_with(2.12)

カスタムバリデーションを使う際の記載。

class GoodnessValidator < ActiveModel::Validator
  def validate(record)
    if options[:fields].any?{|field| record.send(field) == "Evil" }
      record.errors[:base] << "これは悪人だ"
    end
  end
end
 
class Person < ApplicationRecord
  validates_with GoodnessValidator, fields: [:first_name, :last_name]
end

ちょっと複雑なコード。 言葉にしてみると、「Personモデルのインスタンスが保存されるさい、GoodnessValidatorにバリデーション処理が移譲される。移譲先ではvalidateメソッドが打たれることが保証され、引数としてインスタンス自身(record)が渡される。」ということ。

 if options[:fields].any?{|field| record.send(field) == "Evil" } 

この部分は、「もしオプションにfiledsという項目があったなら、そのfieldsという項目の数だけ、後ろのブロック{ record.send(field) =="Evil" }を評価し、その戻り値が結果とせよ。」という感じかな。record.send(field)は、recordにfield(:first_nameとか)のメソッドを打てということ。

ここ解説少なかったので掘り下げました。

(補足)

sendの挙動

[1] pry(main)> str = "str"
=> "str"
[2] pry(main)> str.send(:upcase)
=> "STR"

validates_each(2.13)

カスタムバリデーションを使うほどではないけど、既存のバリデーションじゃ不十分なときに使う。

class Person < ApplicationRecord
  validates_each :name, :surname do |record, attr, value|
    record.errors.add(attr, 'must start with upper case') if value =~ /\A[a-z]/
  end
end

属性ごとに後ろのブロックを評価。ブロック引数として、「インスタンス自身(record)」、「インスタンスの属性名(attr)」、「属性の値(value)」が固定で渡される。順番大事。

:ifや:unlessでバリデージョンの前に呼び出すメソッドを指定

一番良く使うオプションらしい。

class Order < ApplicationRecord
  validates :card_number, presence: true, if: :paid_with_card?
 
  def paid_with_card?
    payment_type == "card"
  end
end

支払い方法がカードのときのみカードナンバーを入力させたいときは上のような書き方。 if:にProcオブジェクトを渡すことも可能。そうすると、そのオブジェクトの処理はバリデーションの前に呼び出されて評価される(ブロックの評価値はtrueかfalseにしておく。)

カスタムバリデータ(6.1)

個別の要素を検証したいときは、カスタムバリデータに「ActiveModel::EachValidator」を継承させるのが一番手っ取り早い。

(2.12で示したように、「ActiveModel::Validator」を継承させたあと、処理を委譲させてもよい。)

class EmailValidator < ActiveModel::EachValidator
  def validate_each(record, attribute, value)
    unless value =~ /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i
      record.errors[attribute] << (options[:message] || "はメールアドレスではありません")
    end
  end
end
 
class Person < ApplicationRecord
  validates :email, presence: true, email: true
end

呼び出す側のモデルで「validates :email, email:true」とするだけで、簡単にカスタムバリデータさんを呼び出せる。

バリデーションエラーについて

valid?だったりsaveを打ったオブジェクトはActiveModel::Errorsクラスのインスタンスを一つ手に入れる。それはrecord.errorsで参照できる。

record.errorsに打てるメソッドとしては、[:attribute]、add、size、full_messages等…

詳しくはAPIガイドへ!

ActiveModel::Errors

感想

思ってたより読みやすい…すげえよ公式ガイド…。

「どういう挙動をするか」というコード例が助かります。

あと解説も想像してたよりは丁寧でありがたい(とはいえ、Rails学んだばかりの人だと難しいと思う。)

基礎部分が15記事あって、今回4つまとめたからPart.4までは続けようと思います。

Rails勉強してる方は、一度読んでみて「理解できるな」と思ったら読んでみると良いと思います!(ただ、作りながら覚える形式じゃないため、挙動を想像できない場合は非推奨かも。)

ゼロからVue.jsを学んでいる(用語整理)

以下の2冊で勉強しています。

Vue.js入門 基礎から実践アプリケーション開発まで

Vue.js入門 基礎から実践アプリケーション開発まで

基礎から学ぶ Vue.js

基礎から学ぶ Vue.js

Vue.js、学習コストが低いという噂を聞きつけてノコノコとやってきたんですが。

別に簡単ではない!!

未知の概念の連続っ…!! 

しかし、漠然とフロントエンドエンジニアという分野に持っていた誤った考え、つまり「フロントエンジニアとは、マークアップがちゃんとしているオシャレなサイトを作る分野だ」という誤解が氷解していき、一気にフロントにも興味が持て始めてきました。

見通しのよい設計でコードを書きつつ、レンダリングのパフォーマンス等を加味しながらUI/UXの向上を図る…みたいな感じやろ!?知らんけど…。

自分自身のキャリアとして完全なフルスタックを目指すつもりはないですが、チームに参画したときにパフォーマンスを出せる人間になりたい(「 出来るFWは他のポジションの役割も理解している」ように。)っていうのと、個人としての開発力を上げたいっていうのがフロントを学んでいる動機です。

とりあえず用語だけまとめながら本を読んでコード書こうと思います。

用語まとめ

リアクティブデータとは?

Vue.jsのgetまたはsetにフックして反応できるデータのこと。

ディクレティブとは?

Vue.jsの場合だとv-bindなどがそれにあたるが、コンパイルしてテンプレートを作るさいの補足情報のこと。 ディレクティブは指令という意味。Vue.jsへの指令。

v-showとv-ifの違い

v-showはCSSのdisplayプロパティを切り替えているだけで、DOM要素自体は残っている。 v-ifはDOM要素自体の削除。そのため描画コストがかかる。

this.$setの使い所

名前を持たない配列の値に直接オブジェクトを置きたい場合、またはそのオブジェクトのプロパティに新しいものを追加する場合に使う。

this.$set(更新する対象, インデックス番号 or 新しいプロパティ名, 新たなオブジェクト or 新たな値) 

イベントハンドラとは?

Vue.jsの場合だと、v-on:で紐付けるイベント対象によって実行される処理のこと。例: v-on:click="eventHandler"

算出プロパティとは?

computedで定義しておく関数。先に定義しておくと「this.definedProperty(定義しておいたプロパティ名)」で呼べるから便利。 メソッドと違い一度処理した値をキャッシュしておくため、呼び出すたびに処理が走ることはない。 (依存するdataの値が書き換わると再処理) 基本的にはdataの値を貰っておくゲッタープロパティだが、セッターとしての処理を書けばdataの値を書き換えることも出来る。

ウォッチャーとは?

特定のデータまたは算出プロパティを監視しておき、その変化にトリガーして実行される処理のこと。 監視しながらDOM操作したいならカスタムディレクティブの方がよい。

カスタムディレクティブとは?

v-bindのようなディレクティブを自作するための機能。direvtivesで定義する。ただし、カスタムディレクティブで受け取った値は$elや$refsと同じで、仮想DOMではないため、描写の最適化は行われない。 要素や値を監視しながらDOM操作したいときに使おう。

$el、$refsとは?

$elがidと結びついているDOM要素すべて。 $refsは$elが結びついてるDOM要素の中で、ref="foo"といった要素について$refs.fooで結びつけるために使う。

nextTickとは?

監視先のDOM要素について、データがバインディグされて処理されたあとのものをとってこれる。

<div id="app">
  <button v-on:click="list.push(list.length+1)">追加</button>
  <ul ref="list">
    <li v-for="item in list">{{ item }}</li>
  </ul>
</div>
new Vue({
  el: '#app',
  data: {
    list: []
  },
  watch: {
    list: function () {
      // 更新後のul要素の高さを取得できない…
      console.log('通常:', this.$refs.list.offsetHeight)
      // nextTickを使えばできる!
      this.$nextTick(function () {
        console.log('nextTick:', this.$refs.list.offsetHeight)
      })
    }
  }
})

コンポーネントとは?

JSとテンプレートをセットにしたもの。コンポーネントを使えばコードの再利用性が高い。 コンポーネントはそれぞれスコープを持っている。コンポーネントコンポーネントを呼び出すことも可能(親子関係になる)

コンポーネント間の通信方法

親から子へ渡す

子どもは親の属性(val='hoge')をprops: ['val']といったように指定すれば、そのhogeを扱うことができる 親のデータを渡したいときは親側にv-bind:val="message"といったふうに記述し、子側でprops:['val']と指定すれば、親側のdataに入っているmessageの値を貰えるといった動きになる。

子から親へ渡す

カスタムイベントと、$emitを使う。 親側でv-on:custom-event='eventHandler'とテンプレートにあった場合、子のコンポーネント側はthis.$emit('custom-event')で発火しにいける。(コンポーネント側でthis.$emitを呼ぶためにメソッドなど必要)。これでthis.$emit('custom-event')で親側のメソッドeventHandlerを実行させることが出来る。 emitは発火させるって意味。

親子関係でないコンポーネント間はイベントバスを使う

イベントバスは通信用のためだけに作られるVueインスタンス。そいつをフックにコンポーネント間をやりとりする。 状態管理がややこしくなるので非推奨っぽい。

スロットとは?

親となるコンポーネント側から子ども側のコンポーネントのテンプレートの一部を差し込む機能のこと。

<div class="comp-child">
   <slot>ここに親側のテンプレートが入る</slot>
</div>

スコープ付きスロット

スロットは定義した側(親側)のスコープが優先されるが、slot-scopeを使えば子側でのスコープでデータを受け取れる。

例:

子側のテンプレート

<ul class="comp-child">
  <slot v-for="item in list"  v-bind:item="item"></slot>
</ul>

親側のテンプレート

<comp-child>
  <li slot-scope="props">{{ props.item }}</li>
</comp-child>

propsという変数名を指定しているが、これは任意でいい。子側のコンポーネントのスコープを表す。

トランジションとは?

Vue.jsにおいては、CSSトランジション/アニメーション効果をより簡単に使いやすくサポートする機能のこと。

<div id="app">
  <p><button v-on:click="show=!show">切り替え</button></p>
  <transition>
    <div v-show="show">
      トランジションさせたい要素
    </div>
  </transition>
</div>
/* 1秒かけて透明度を遷移 */
.v-enter-active, .v-leave-active {
  transition: opacity 1s;
}
/* 見えなくなるときの透明度 */
.v-enter, .v-leave-to {
  opacity: 0;
}

webpackとは?

モジュール化した複数のファイルをまとめるバンドルツールのこと。

まだ多分続きますが

一旦、ここで終わろうと思います。 覚えることがたくさんありますねえ。

作りたいアプリの開発環境としては、RailsAPI+Vue.js(+もしいければ開発中のLINE用APIアプリケーション)って感じなので、もう少しVue.js触ってからRails動かしていこうかな、と思います。

がんばります!

「EFFECTIVE TESTING WITH RSPEC 3: BUILD RUBY APPS WITH CONFIDENCE」の1章を読んだ

あとから読みなおせるようにブログにまとめつつ読んでいます。

Effective Testing with RSpec 3: Build Ruby Apps with Confidence

Effective Testing with RSpec 3: Build Ruby Apps with Confidence

EFFECTIVE TESTING WITH RSPEC 3: BUILD RUBY APPS WITH CONFIDENCE 

は、直訳すると「RSpec3による効率的なテスト: 自信をもってRubyのアプリケーションを組み立てる」みたいな感じでしょうか。

英語版で読む理由

  1. 翻訳されてない
  2. 英語学習のため
  3. 英語で体系的な学習をする練習をするため

注意点

自分の解釈だったり誤訳も含んで日本語にしています。

序文(Foreword)

ソフトウェア開発は覚えることがたくさんあって大変だ。テストを書くこともそのうちの一つかもしれない。

しかし、ソフトウェア開発にテストを書くことは、欠かせない。

そして(Railsで)よいテストコードを書くためにRSpecは欠かせない。

RSpecは単なるテストフレームワークでなく、批判的に、根気よく、体系的に自分のコードの設計について考える方法と、体系的な手法でソフトウェアを開発する方法を学ぶためのツールでもある。

はじめに(Introduction)

「俺らのテストがまた落ちたよ!」

「なんでこの一連のコードが通るまでにこんなに時間がかかるんだ?」

「こんなテストになんの価値があるんだろう?」

テクノロジーは変わったけどテストへの不満の内容は変わらない。

この本では「効果的な(EFFECTIVE)」なテストの書き方を教えるよ。

効果的っていうのは、「わざわざ時間を割いて書く意味がある」ってことだ。

1章 RSpecをはじめよう
(Chp1 Getting Started With RSpec)

RSpecは生産的なテストフレームワークだ。スタイル、API、ライブラリ、設定、それら全てが最高のソフトウェア開発を手助けしてくれる。

いいテストから得られる効果は少なくともこれだけある。

最初のSpec

サンドウィッチを例にあげて考える。

RSpecはdescribeとitによって、まるで会話文のように表現することができる。

"Describe an ideal sandwich" (理想的なサンドウィッチについて説明してくれ)

"First, it is delicious" (はじめに、それはおいしい)

RSpecではこう書く。

RSpec.describe 'An ideal sandwich' do
  it 'is delicious' do
  end
end

テストケースを加えたものが以下だ。

RSpec.describe 'An ideal sandwich' do
  it 'is delicious' do
    sandwich = Sandwich.new('delicious', [])

    taste = sandwich.taste

    expect(taste).to eq('delicious')
  end
end

specはspecification(仕様書)の短縮形だ。

RSpec.describe ブロックは「例の集合*1」をつくる。

例の集合が何をテストしているか明らかにして、(今回だとサンドウィッチ)、関連するspecsを一つにまとめるんだ。

ネストされたブロック 'is delicious' はサンドウィッチの使い方の例だ。(他のフレームワークだとテストケースって呼ぶかもしれない)

コラム: テスト対スペック対イグザンプル (Tests vs. Specs vs. Examples)

テストはコードが正確に動くかvalidate(検証)する

スペックはコードに振る舞ってほしい動きをdescribe(説明)する

イグザンプルは特定のAPIがどのように使われるかを明らかにする

さて、ここでおさらいしておこう。

  • RSpec.describe が例の集合をつくる
  • it が例単体をつくる
  • expect が期待される結果をあらわす

この3つがRSpecの核だ。どんなに長い道のりも、この3つを使えば前には進むことが出来る。

また、サンドウィッチspecから得られる目的は2つ。

  • サンドウイッチに振る舞ってほしい挙動のドキュメント化
  • サンドウィッチがあるべき姿かどうかのチェック

サンドウィッチspecのよいところは、「あたらしくプロジェクトに参画した人」のドキュメントになるってことだ。 specを読めば、サンドウィッチはおいしくなきゃならないってのがすぐにわかる。

セットアップの共有(Sharing Setup)

  • RSpecフックはテストの間、自動的に特定の回数実行される。
  • ヘルパーメソッドはRubyの標準メソッドで、いつ実行されるかコントロールできる。
  • letは必要に応じてデータを初期化する。

一つずつ順に見ていこう。

RSpecフック(before)

beforeは、itごとに準備されるRSpecフックだ。

Sandwich = Struct.new(:taste, :toppings)
RSpec.describe 'An ideal sandwich' do

  before { @sandwich = Sandwich.new('delicious', [])}

  it 'is delicious' do
    taste = @sandwich.taste

    expect(taste).to eq('delicious')
  end

  it 'let me add toppings' do
    @sandwich.toppings << 'cheese'
    toppings = @sandwich.toppings

    expect(toppings).not_to be_empty
  end

end

ここでは、@sandwichを使ったけど、インスタンス変数には実は欠点もある。

  1. もし@sandwichをミススペルして参照しても、Rubyは暗黙的にnilを返す。このことが、エラーの原因が単なるタイポであることをエラー文から読み取りづらくする。
  2. もしsandwichという変数を、@sandwichというインスタンス変数に置き換えたい場合は、ファイル全体で置き換える必要がある。
  3. beforeフックでインスタンス変数を使うと、必要ないときも@sandwichを生み出し、セットアップのためにコストがかかる。これは大規模プロジェクトではときに見逃せない不効率性になる。

別のアプローチを試してみよう。

ヘルパーメソッド

RSpecは便利だから、ときにそれがRuby支配下であることを忘れるけど、メソッドを定義することだって可能なんだ。

def sandwich
  @sandwich ||= Sandwich.new('delicious', [])
end

このようなテクニックはメモ化(memoization)*2と呼ぶ。

この手法は便利だけど、落とし穴がないわけじゃない。||=は@sandwichがnilかfalseのときに働くから、@sandwichにnilかfalseを格納することはできなくなる。

こういった問題を解決する手段として、RSpecにはletが用意されている。

letでオブジェクトを共有する(Sharing Objects with let)

さきほどのsandwichメソッドを以下のコードに置き換えてみよう。

let(:sandwich) { Sandwich.new('delicious', [])}

letは「sandwichという名前と、ブロック{}を結びつけるもの」と考えてもいい。 このletによって、ちょうど先程のメモ化したヘルパーメソッドのように、それぞれのイグザンプルはsandwichが呼ばれた「最初のとき」だけブロックを評価するんだ。

コード共有テクニックを使うさいの注意点

これらのテクニックは、保守性を向上させ、ノイズを減らし、コードを明瞭にするために使おう。

あなたの番(Your Turn)

エクササイズ

  1. RSpecにおけるコードの重複を防ぐための3つの主要な手法(フック、ヘルパーメソッド、let)を紹介したけど、今回のケースだとどれが一番よいと思う? そしてそれはなぜだろう? そして、あなたが選択しなかった手法が、他の状況では望ましくなるのはどんなときだろう?

  2. rspec --help を実行し、いくつかsandwichイグザンプルで試してみてほしい。

さて、RSpecのお気に入りの使い方と出会う準備は出来てる? 

サンドウィッチ休憩をとったら、次のチャプターで会おう。

エクササイズと自分の感想

エクササイズ1をやってみた

1はbeforeでよさそうだな〜。@sandwichの各属性に対するテストだからitごとにインスタンス変数を準備しておくことに違和感を感じない。

複数のインスタンスを複合的に使ったりする場面ではletのがいいのかも?

エクササイズ2をやってみた

rspec -e STRINGを実行した。 STRINGに指定した文字列に対して、itの後ろの文字列( 'is delicious'等)がマッチしたものだけテストが走る。 ヘルプに「何回も使うかもね」って書いてた。

感想

読んで訳してブログにするのに4時間ぐらいかかったーー。 作業自体は(疲れるけど)楽しいから、ちょっと頭の体操したいときの息抜きかなあ。 そして、著者がRSpec大好きなことが伝わってきて読んでて面白い。

頑張って2章もやりたい…!!

*1:example group

*2:Wikipedia: メモ化(英: Memoization)とは、プログラムの高速化のための最適化技法の一種であり、サブルーチン呼び出しの結果を後で再利用するために保持し、そのサブルーチン(関数)の呼び出し毎の再計算を防ぐ手法である。