娯楽開発

娯楽開発

開発は娯楽。遊び心で開発をすすめるブログ。

【Railsチュートリアル】フォロー機能のアソシエーションを解説してみる

みなさん一度くらいユーザーのフォロー機能を実装したくなったことありません?ありますか。奇遇ですね僕もです。

と、いうわけで今回の記事ではフォロー機能の実装とその解説をしたいと思います。

今回は実際のコードを見ながら説明をしていこうと思うので、実際にrailsでコードを書いたことがある人向けの内容になります。

全くコードを書いたことないんだけど!アソシエーションって何やねん!という方、なんとびっくり偶然にも僕の前回の記事がたまたまコードを書かずにアソシエーションの概念について説明しているのでそちらを見ていただければ幸いです。是非。

sado-tech.hateblo.jp

今回の目的

さてさて今回の記事の目的ですが、

  • フォロー機能のためのアソシエーション(とそのコードの意味)の理解

とします。機能や実装方法については調べればこれでもかと出てくるので色々調べてみてください。

また今回の記事ではコードの曖昧さ回避の意味も兼ねて Ruby on Rails チュートリアル:実例を使って Rails を学ぼう (version 5.1)の14章、 14.1.1 データモデルの問題 (および解決策) から 14.1.5 フォロワー についての説明に絞ろうと思います。

railsチュートリアルをやったことがない人にもわかりやすいよう書くつもりですが、もしよくわからない部分があれば本家サイトの方も参照してみてください。

railstutorial.jp

コード

Railsチュートリアルに沿っていればおおよそこんな感じになってるはずです。

# app/models/relationship.rb
class Relationship < ApplicationRecord
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"
end

# app/models/user.rb
class User < ApplicationRecord
  has_many :active_relationships,  class_name:  "Relationship",
                                   foreign_key: "follower_id",
                                   dependent:   :destroy
  has_many :passive_relationships, class_name:  "Relationship",
                                   foreign_key: "followed_id",
                                   dependent:   :destroy
  has_many :following, through: :active_relationships,  source: :followed
  has_many :followers, through: :passive_relationships, source: :follower
end

今回はフォロー機能についての各モデルのアソシエーションを中心に解説するので一部余分なコード(Userモデルのmicropost関連など)は省いています。

解説

それでは早速見ていきましょう。

そもそも

はい、いきなりコードの話から脱線するんですけど、そもそも今回のフォロー機能ではUserモデルからUserモデルに対しての多対多のアソシエーションを行っています。 前回の記事で言う所の「A組の学生」モデルと「B組の学生」モデルがどっちもUserになっている状態です。

つまり、Userモデルは自分自身のインスタンスからその他の自分自身のインスタンスに対してアソシエーションを行わなければならないわけです。カオス。

これを解決する為に、has_many :throughアソシエーション(has_and_belongs_to_manyでも可)を使ってUserモデルの間にRelationshipモデル(中間モデル)を置きます。

前回の記事に習って書くと、今回実現されるモデル構造は以下のようになります。

図1 f:id:sado_tech:20180816233548p:plain

ただ実際にはUserモデルは一つだけなので、今回の記事では以下のような図を使って解説していきたいと思います。

図2 f:id:sado_tech:20180817193439p:plain

ちなみに今回の「フォロー機能」では図のように3つのアソシエーション(×2)が行われています。 ((×2)としたのはそれぞれに対して「フォローしている」アソシエーションと「フォローされている」アソシエーションの2種類が発生するからです。)

図の中の番号はこの後の解説の順番を示しているだけで深い意味はないのでそんなに気にしないでください。

コードの読み方

Relationshipモデル

それではまず比較的シンプルなRelationshipモデルから。図2で言う①にあたる部分です。

# app/models/relationship.rb
class Relationship < ApplicationRecord
  belongs_to :follower, class_name: "User"
  belongs_to :followed, class_name: "User"
end

図1からもわかるようにRelationshipモデルはUserモデルに対して常に1対1の関連を持っており、かつUserモデルに所属しているので使うアソシエーションはbelongs_toです。

ではこのbelongs_toですが、カンマ(,)区切りで見てみるとそれぞれ二つの情報を取得していることがわかります。

まず一つ目の情報(:follower, :followed)ですが、これらはRelationshipモデルが関連を持つ先のモデルを示します。しかし上の図を見るとRelationshipモデルが関連を持つ相手はUserモデルだけですよね。followerfollowingといったモデルはありません。そこで活きてくるのが二つ目の情報、class_name:です。

class_name:をつけることで、関連先のモデルを参照する際の名前を変更することができます。

上のコードでのbelongs_to :follower, class_name: "User"という記述がありますが、日本語的に意訳すると

「Userモデルに対してfollowerって名前でbelongs_toの関連付けしときますねー」

という意味になります。

なぜこんなことをするのかというと、あとで解説しますが今回実はRelationshipモデルはUserモデルに対して二つの関連付けをしたいんです。それなのに関連先として使える名前が"User"だけだと、二つの関連付けの見分けがつきませんね。そこでclass_nameで本当の名前を指定することで関連先の名前を変更できるというわけです。

この関連付けを図にすると以下のようになります。

図3 f:id:sado_tech:20180818105933p:plain

Userモデル

続いてUserモデルについてです。

# app/models/user.rb
class User < ApplicationRecord
  has_many :active_relationships,  class_name:  "Relationship",
                                   foreign_key: "follower_id",
                                   dependent:   :destroy
  has_many :passive_relationships, class_name:  "Relationship",
                                   foreign_key: "followed_id",
                                   dependent:   :destroy
  has_many :following, through: :active_relationships,  source: :followed
  has_many :followers, through: :passive_relationships, source: :follower
end

Relationshipモデルより少し複雑に見えます。いや実際複雑なんですけども。

ここではhas_manyhas_many :throughという大きく二つの関連付けがなされています。ひとつずつ見ていきましょう。

has_many

図2で言う②の部分ですね。以下の一文について考えてみましょう。

has_many :active_relationships,  class_name:  "Relationship",
                                 foreign_key: "follower_id",
                                 dependent:   :destroy

has_manyはカンマ区切りで見ると四つの情報を取得していることがわかります。このうちclass_name: "Relationship"まではRelationshipモデルのbelongs_toで説明したものと同じ内容になります。

ではforeign_keydependentですが、それぞれ以下のような意味になります。

  • foreign_key 関連先モデルの外部キーを指定する。
  • dependent 自モデルと関連先モデルの依存性を指定する。

foreign_keyについてですが、Relationshipモデルに生成されたインスタンスfollower_idfollowed_idなどいくつかのデータ(カラムといいます)を持ちます。その中で、アソシエーションによって参照するカラムのことをforeign_key(外部キー)として指定しています。

dependentでは自モデルと関連先モデルの依存性を指定します。destroyを指定すると自モデルのインスタンスが削除された際にその関連先モデルのインスタンスも自動的に削除されるようになります。

つまり、ここでのhas_manyアソシエーションを日本語的に意訳すると、

「Relationshipモデルに対してfollower_idを見てactive_relathionshipsって名前でアソシエーションしときますねー。あ、あと関連するUserインスタンスが消えた時は対応するactive_relationshipsインスタンスも消しとくんでよろしくー。」

みたいな感じになります。

この関連付けを図にすると以下のようになります。

図4 f:id:sado_tech:20180818110003p:plain

has_many :through

最後に図2で言う③の部分です。以下の一文について考えてみます。

has_many :following, through: :active_relationships,  source: :followed

ここで設定されている情報は以下のようになっています。

  • through 経由するモデル名を指定する。
  • source 関連先モデル名を指定する。

前回の記事でも触れているんですが、has_many :throughではアソシエーションを行うモデル間を中継する中間モデルが存在します。その中間モデル名をthroughで指定していますね。今回の例だと、has_manyで関連付けしたactive_relationshipsが指定されています。

またこのアソシエーションでも最終的な関連先モデルの名前をfollowingに変更していますが、has_manyで指定していたclass_nameが見当たりません。代わりにsourceで関連先のモデルを指定するんですが、ここで指定するのはUserではなくfollowedであることに注意が必要です。

このアソシエーションによって行う関連付けはUser→Relationship→Userですが、Relationshipモデルでのアソシエーションで説明した通りRelationship→Userではfollowerfollowedの2つの関連付けが行われています。そのような経緯で今回の場合だとsourceではUserではなくfollowerfollowedを指定しなければならないため、followerを指定しているということになります.

つまり、ここでのhas_many :throughアソシエーションを日本語的に意訳すると、

active_relationshipsモデルを経由した上でfollowedモデルに対してfollowingって名前でアソシエーションしときますねー」

といった感じになります。

この関連付けを図にすると以下のようになります。

図5 f:id:sado_tech:20180818110654p:plain

まとめ

最終的に今回のアソシエーションを全て一つの図に表すと以下のようになります。

図6 f:id:sado_tech:20180818110109p:plain

なんとなくでもユーザーをフォローするためのアソシエーションが理解できたでしょうか。

今回の例はアソシエーションを考える上でまだ比較的簡単な方かと思いうので、ぜひいろんなアソシエーションにチャレンジしてみてください。

【初心者向け】アソシエーションって何?というあなたへ【rails】

webサービスを作っているとモデルがたくさん生成されてしんどくなってくるんですが、それらのアソシエーションをしたくて解説記事を探してもなかなかわかりづらくてよりしんどみが増してくる今日この頃です。

そんな迷える子羊な僕のために僕自身が僕自身の手で僕の解釈を解説してみました。そんな内容です。

今回の記事は下記サイトの内容を踏襲してます。

railsguides.jp

僕の記事は基本そうですが、参考サイト以上の情報は出てこないのでこれらのサイトを難なく理解できる方はそちらを読んだ方が早いと思います。

(書いてる僕自身rails初心者ですので間違っている箇所等あれば遠慮なくご指摘いただけると幸いです。)

アソシエーションとは

アソシエーション(association)を日本語にすると「「関連付け」だそうです。 railsでコードにするとだいたいこんな感じになります。

f:id:sado_tech:20180810084718p:plain

では関連付けとはどういうことかというと、簡単に言うと「こっちのモデルはあっちのモデルとこういう関係性にありますよ〜」という説明書きのことです。

これをしておくことでrailsのシステムがモデルの関係性を把握してくれて、モデル間のデータのやりとりなどをなんかこう、スムーズに行うことができたりします。

モデルってなんやねん

こんな感じです。

f:id:sado_tech:20180810220434p:plain

この場合だと黄色がモデル、青がインスタンスというように呼ばれます。

モデルは性質や特性、できることなどのインスタンスに共通する内容が書かれた説明書のようなイメージです。

一方インスタンスはモデルという説明書に沿った特徴・性質を持つ具体的なモノのことを指します。それぞれのインスタンスはモデルに沿った同一の特徴を持ちますが、インスタンスごとに異なった特徴も持っています。

この例だと「車」モデルの中に「プリウス」「タント」「NOTE」といったインスタンスが含まれてますね。いずれのインスタンスも「車」モデルに属してはいますがいずれもメーカーや搭載シート数など固有の特徴を持っています。

例えば

それではここにもし新たに「タイヤ」モデルができた場合、「車」モデルと「タイヤ」モデルの間にはどういう関係性があるんでしょう?

そんな時にモデルで事前に関係性を示しておくのがアソシエーションです。

このように、railsを使ったwebサービスではこういったモデルをたくさん用意して、それらの関連付け(アソシエーション)をする工程が必要となります。今回はその工程でのお話というわけですね。

アソシエーションの種類

さて本題です。

railsでサポートされているRelationは以下の6種類です。

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

早速解説していきましょう。

belongs_to と has_many

has_many

belongs_to

具体例

f:id:sado_tech:20180810120345p:plain

先ほども登場した「車」モデルと「タイヤ」モデルのアソシエーションの例ですね。

「車」モデルのインスタンスであるプリウスは「タイヤ」モデルのインスタンスであるタイヤを必ず4つ所有します。

これはプリウスに限った話じゃなくて「車」モデルに属するインスタンスであればどんなものにでも当てはまることなので、「車」モデルのインスタンスは「タイヤ」モデルに対して常に「1対多」の関係が成り立つことになります。

つまり「車」モデルは「タイヤ」モデルに対してhas_manyの関係を持っていると言えます。

一方タイヤ1に注目して見るとどうでしょう。プリウスは複数のタイヤを持っていますが、タイヤ1が所有されている「車」モデルのインスタンスプリウスだけですね。タイヤ1がその他の「車」インスタンスに所有されることはまずありえません。

このように各「タイヤ」インスタンスから見ると、「タイヤ」モデルと「車」モデルの間には常に「1対1」の関係性が成り立つことになります。つまり「タイヤ」モデルは「車」モデルに対してbelongs_toの関係を持っていると言えます。

has_one

has_one

具体例

f:id:sado_tech:20180810222743p:plain

続いてhas_oneの例です。

これは前述のhas_manyが持つ相手方インスタンスが1つに限られる場合の話だと思ってください。

今回はマイナンバーを例に出しますが、マイナンバーは政府によって例外なく日本国民全員に1つずつ与えられますね。1人が複数のマイナンバーを持つことはありえません。常に1対1の関係性です。

ここでhas_onebelongs_toは同じ1対1に対して使うアソシエーションですが、その違いは「所有している」か「所属している(所有されている)」かです。今回の例では「人物」が「マイナンバー」を所有し、「マイナンバー」は「人物」に所有されています。ということで「人物」モデルから見た「マイナンバー」モデルとの関係性はhas_oneとなります。

この辺りは曖昧な部分も多いので、詳細について知りたい方は主キー(primary key)と外部キー(foreign key)などについて調べてみるといいかもしれません。

www.pc-master.jp

has_many :through と has_and_belongs_to_many

has_many :through, has_and_belongs_to_many

  • 中間モデルを経由することで、注目するインスタンスと相手となるインスタンスの間で多対多の関係性が構築されることを示す
  • 中間モデルとなる3つ目のモデルを必要とする

具体例

f:id:sado_tech:20180810120425p:plain

「1対1」、「1対多」ときたところで「多対多」です。上の図では学校などでの友人関係のつもりで例に出しています。深い意図はないので純粋な目で見てください。

少し複雑になりました。A男から見てF助は友達ですが、F助もまたA男以外にたくさんの友達を持ちます。つまりどのインスタンスから見ても相手方のインスタンスを複数所有しているわけですね。車とタイヤのように明確な所有と所属の関係が成立しないためhas_manybelongs_toでは表現できません。

このような場合にhas_many :throughhas_and_belongs_to_manyを用います。(図では例としてhas_many :throughを出していますがhas_and_belongs_to_manyでも代用可能です。)

この例だと一見して2つのモデルが直接関係を持ち合っているように見えますが、実際は以下の図のように2つのモデルを中継する3つ目のモデルが存在します。

f:id:sado_tech:20180810120452p:plain

このように中間モデルを使うことによって両サイドのモデルは中間モデルに対してhas_manyを、中間モデルは各モデルに対してbelongs_toを持つことになり、多対多の関係性をhas_manybelongs_toの関係に置き換えられるという仕組みなわけですね。

で、どっちを使えばいいの?

お察しの通り、基本的な働きは同じなhas_many :throughhas_and_belongs_to_manyです。どのような判断基準のもとどちらを使えばいいんでしょう?

参照元のサイトには以下のように書いてありましたので、参考にしてみてください。

どちらを使用するかについてですが、経験上、リレーションシップのモデルそれ自体を独立したエンティティとして扱いたい(両モデルの関係そのものについて処理を行いたい)のであれば、中間に結合モデルを使用するhas_many :throughリレーションシップを選ぶのが最もシンプルです。リレーションシップのモデルで何か特別なことをする必要がまったくないのであれば、結合モデルの不要なhas_and_belongs_to_manyリレーションシップを使用するのがシンプルです(ただし、こちらの場合は結合モデルが不要な代わりに、専用の結合テーブルを別途データベースに作成しておく必要がありますので、お忘れなきよう)。

Active Record の関連付け (アソシエーション) | Rails ガイド

has_one :through

has_one :through

  • 中間モデルを経由することで、注目するインスタンスと相手となるインスタンスの間で1対1の関係性が構築されることを示す
  • 中間モデルとなる3つ目のモデルを必要とする

具体例

f:id:sado_tech:20180810120542p:plain

こちらはhas_many :throughの1対1バージョンだと捉えていただけるとわかりやすいかなと。

正直これについては具体的な用途が思いつかなかったんですが、

  • モデル構造的には中間モデルを介して紐づけられるけど、参照する場合には直接的に(has_oneのように)参照したい

というような場合に使うことになるのかなぁと思っています。

何かいい例が見つかれば後日追記でもしようかと思います。

まとめ

今回はアソシエーションの種類を概念的な部分を中心にまとめてみました。なんとなくモデルのアソシエーションについて理解できたでしょうか。

いつか実際のコードなども使いながら具体的な用途も紹介できたらなーと思っているので気長に待っていていただけるとうれしいです。

【rails】sessionでログイン情報を管理したい

railsでログイン/ログアウトの機能を作ろうとしたところ素朴な疑問にぶつかりました。

  • ログイン/ログアウトの情報ってどこに保存するの?

ログイン情報っていちいちサーバー上に保存/削除して管理するほどのものでもないよなーと不思議に思いまして。

そこでいろいろ調べていたところ、やたらと「セッション(session)」という単語を見かけたので簡単にまとめてみました。

(だいたい引用です。)

セッションとは

そもそもセッションってなんやねん、ということでこちらを。

railstutorial.jp

HTTPはステートレスなプロトコルです。文字通り「ステート (state)」が「ない (less)」ので、HTTPのリクエストひとつひとつは、それより前のリクエストの情報をまったく利用できない、独立したトランザクションとして扱われます。HTTPは言ってみれば、リクエストが終わると何もかも忘れて次回最初からやり直す健忘症的なプロトコルであり、過去を捨てた旅から旅の流れ者的なプロトコルです (しかし、だからこそこのプロトコルは非常に頑丈なのです)。この本質的な特性のため、ブラウザのあるページから別のページに移動したときに、ユーザーのIDを保持しておく手段がHTTPプロトコル内「には」まったくありません。ユーザーログインの必要なWebアプリケーションでは、セッションと呼ばれる半永続的な接続をコンピュータ間 (ユーザーのパソコンのWebブラウザとRailsサーバーなど) に別途設定します。

qiita.com

セッションについてはまぁこれが全てで、要するにHTTPでは状態を記憶できないので、別レイヤーでそれを行いますよという話。 これをユーザーのログイン・ログアウトで考えると、一度ユーザーがログインやログアウトをしたはいいけど、ユーザーのログイン状態をHTTPでは記憶できないので、セッションでその情報を保持しましょうということ。 Railsでは基本的にsessionメソッドを使ってこの機能を実現する。sessionメソッドで作成されたセッションは自動で暗号化されるが、ブラウザを閉じると破棄される。

なるほど。どうやら状態(ステータス)を保持できないHTTPさんの代わりにセッションさんがステータスを記憶しておいてくれる、ということらしいです。

具体的な動き

セッションという言葉について大まかに理解できたところで次に湧いてくるのは具体的なプログラム上での疑問。

  1. どこにどういう形で記憶されるの?
  2. 記憶されたステータスにはどうやってアクセスするの?

ひとつずつ見ていきます。

1. どこにどういう形で記憶されるの?

結論としてはWebブラウザ上のcookieに保存されるようです。1

cookieについてはこちら。

クッキー(cookie)とは?初心者でも分かるように図解

クッキーとはサーバーから送られてくる会員証のようなもので、次にそのページにアクセスするときにサーバーに「私はこの会員IDですよ」と見せる。それにより、一人ひとりに違う内容のページを表示させたり、前訪れたときの続きとして訪問者に対応したりできる。

要はサーバー側ではなく、WebページにアクセスしているPC側のブラウザにcookieとして情報を保存するんですね。

2. 記憶されたステータスにはどうやってアクセスするの?

続いてrailsにおけるセッションへのアクセス方法。

railsではsessionというハッシュが用意されているので、そこに任意のキーに対して値を格納するだけでOKなようです。

session[キー] = 値

実際にログイン/ログアウトを管理する場面を想定した具体的な使い方は以下のようになります。

class LoginsController < ApplicationController
   def create
     session[:login_id]  = user.id
   end

   def destroy
     session.delete(:login_id)
   end
 end

ここではLoginsControllercreateアクションでログインを、destroyアクションでログアウトを管理しています。session:login_idキーでuser.idをそれぞれ保存・削除しています。

コントローラー名は任意なので管理しやすい名前のコントローラーを作成しましょう。

(※上記はセッションでステータスを管理することを簡潔に示した例なので、実際のシステムに組み込む場合にはバリデーションなどを必要とする場合が多くなることに気をつけてください。)

まとめ

  1. セッションではcookieでステータスを保存する。2
  2. railsでセッション管理するにはsessionハッシュを利用する。

ざっとこんな感じです。

今回書いたのは本当に簡潔なさわりの部分だけでセッションもまだまだ奥が深いようですので、気になる方は色々と調べてみてください。

注意事項

ページ最下段の注釈にも書いていますがセッションをcookieで管理するのはセッション管理方法の一つにすぎないみたいです。railsのセッションが必ずしもcookieで管理されているとは限らないので注意してください。

どんな管理方法が最適なのかは様々な議論があるようなので、調べてみると面白いかもしれませんね。

下記の記事なども参考にしてみてください。

shindolog.hatenablog.com

qiita.com


  1. セッションをcookieで管理するのはあくまで方法論の一つのようです。

  2. 同上

わかった気になりたい人のための正規表現 ~メタ文字の見分け方~

見事に3記事坊主をキメて以来久しぶりの更新になります。

今回の記事から僕の所属する開発チームの先輩方とローテーションで回しながらブログを更新していくことになりました。

経緯などはこちらの先輩の記事を参照していただけると。

nannannanan.hatenablog.com

そんなわけで僕は金曜日更新の担当になりました。

今後は(少なくとも)金曜日には記事を更新していくと思うのでよろしくお願いします。

正規表現とは

本題です。

今回は今まで直視せず避け続けてきた正規表現と向き合います。

正規表現といえば/\d{2,5}[-(]\d{1,4}[-)]\d{4}/みたいな呪文のことですが、プログラムの中でテキストデータから任意の文字列を抜き出す際に使います。

先ほど挙げた呪文では「03-****-****」や 「090-****-****」といった電話番号のような文字列を抜き出すことができます。

てなわけで今回は「正規表現を使いたい!」と思った時にだいたいこの辺り押さえとけばある程度読める/書けるんじゃね?っていうポイントをまとめておきます。他でもない未来の自分のために。

ここで書くのはほんとにポイントだけです。詳細についてはググれば山ほど出てくるので気になる人は調べてみてください。僕はこの辺りの記事を参考にしました。

qiita.com

ちなみに上の記事(全4回)の内容をすべて理解すれば今回の記事は全く読む必要がなくなるのでがっつり正規表現を勉強したい方は上記の記事を、それとなくわかった気になりたい方はこの記事を読みましょう。

正規表現を読む上で

正規表現にはメタ文字があります。メタ文字とは、普通の文字と違い正規表現としての何らかの意味を持つ文字/文字列のことです。

そもそも正規表現の文字列の中にメタ文字とそれ以外の文字が入り混じっていることが諸悪の根源ですよね。自分が初心者なのでわかるのですが、正規表現初心者の方が正規表現を読めないのは

  • メタ文字を見分けることができない
  • メタ文字の意味がわからない

からだと思っています。

逆に言うと正規表現の呪文からメタ文字を見分けることができ、かつメタ文字の意味を理解できさえすればある程度は正規表現を読めるのではないでしょうか。

というわけで

  • メタ文字の見分け方
  • メタ文字の意味を理解する

の2点について書こうかなと思ったんですが、「メタ文字の意味を理解する」について書き出すと1記事にまとまらないし上記の丁寧な記事もあるのでこの記事では「メタ文字の見分け方」に絞って書いていこうと思います。何事も棲み分けです。

メタ文字の話の前に

そもそも論になりますが基本的に正規表現/で始まり/で終わります。(※言語によっては/でない場合もあるかもしれません)

/の前後にオプションの文字をつけることもありますが、基本的には/で囲われてる部分が正規表現なんだな〜くらいの認識でいいと思います。

ですので冒頭で挙げた例/\d{2,5}[-(]\d{1,4}[-)]\d{4}/において/正規表現の始まりと終わりを示すだけのものなので、正規表現としての中身は\d{2,5}[-(]\d{1,4}[-)]\d{4}の部分になります。

※これ以降の正規表現の例では/は省略します。

メタ文字の見分け方

早速いくつかの見分け方を挙げていきます。

\ + アルファベット でメタ文字

アルファベットの前に\がついていたらそいつはメタ文字です。

同じアルファベットでもdは普通の文字だし\dはメタ文字です。

仮にabc\defgという文字列が並んでいた場合a,b, c, e, f, gは普通の文字ですが\d、お前はメタ文字だ。

これは\ + アルファベットというところがポイントで、\がついていれば全てメタ文字」というわけではないので注意が必要です。

後述しますが、逆に\*はメタ文字じゃなかったりします。闇が深い。

記号はだいたいメタ文字

記号はだいたいメタ文字です。警戒してください。

特に頻出なのは{}, [], (), +, *, ?, ., ^, $, |などです。正規表現においてこれらの記号には常にメタ文字として何らかの意味を持っています。

もちろん全ての記号文字がメタ文字というわけではなく、特に正規表現としての意味を持たない記号もあります。詳細についてはいろいろ調べてみてください。

しかし、?などは疑問文で多用しますし単に?という文字として扱いたい場面もあります。

そのような場合は対象となる記号の前に\をつけることで、正規表現としての?の意味をエスケープすることができます。

そのため先ほど挙げたように\*はメタ文字ではなく普通の文字として扱われるのです。

カッコで囲われていたらメタ文字のグループ

大抵の場合、メタ文字は複数の文字列である意味を持った正規表現として扱われます。むしろ1文字で独立して存在する場合の方が稀です。

複数の文字列がメタ文字として扱われる場合、(), {}, []などで囲われていることが多いです。

例えば[A-Z]abcとあった場合、[A-Z]正規表現として意味を持つメタ文字のグループであり abcは普通の文字です。

まとめ

  • \ + アルファベット
  • 記号
  • カッコ

この辺りはだいたいメタ文字だと思って文字列を眺めましょう。

ちなみに冒頭に挙げた\d{2,5}[-(]\d{1,4}[-)]\d{4}ですが、上の項目を意識して眺めるとざっくり\d, {2,5}, [-(], \d, {1,4}, [-)], \d, {4}のようにグルーピングできる気がしてきますね。

メタ文字をメタ文字として識別できるだけでグッと正規表現の文字列が読みやすくなるので、このように意識しながら正規表現を眺めてみてください。

あとはメタ文字を理解するだけ

正規表現、わかった気になれたでしょうか。

ここまで来ればあとはもうメタ文字の意味を理解してうまいこと使いこなすだけで正規表現マスターですね。

今回は詳しい説明はしません(できません)が各メタ文字の解説は下記サイトなどで調べられるので参考にしてみてください。

正規表現言語 - クイック リファレンス | Microsoft Docs

それでは素敵な正規表現ライフを!

【謎挙動】form_tagとtext_fieldで検索フォームを作成【rails】

検索フォームを作ろうとしてform_tagtext_fieldを使った際に謎の挙動が見られたのでメモ書き。(一応解決されました。)

環境は以下の通りです。

結論

まず結論。 form_tagtext_fieldを使って検索フォームを実装する場合、以下のように書くのが正解なようです。

# tasks_controller.rb

def index
  @tasks = Task.all
end

def search
  search_word = "content like '%" + params[:task][:content] + "%'"
  @tasks = Task.where(search_word)
end
<!-- index.html.erb -->

<body>
  <div>
    <%= form_tag search_path do %>
      <%= text_field :task, :content %>
      <%= submit_tag "検索" %>
    <% end %>
  </div>
...
...
</body>

これで検索フォームに入力した単語(search_word)を持つデータがTaskテーブルからsearchアクションの@tasksに入力されます。

問題の謎挙動

さて早速ですが問題の挙動です。

以下のようなコードを書いた時に起こりました。

# tasks_controller.rb

def index
  @tasks = Task.all
  @task = Task.new
end

def search
  search_word = "content like '%" + params[:content] + "%'"
  @tasks = Task.where(search_word)
end

まずコントローラーを上のように設定し、indexページにsearchアクションを仕込ませてparams[:content]を検索ワード(search_word)として抜き出そうという狙いでした。(Taskテーブルにcontentカラムを持たせています。)

そして実際にindexページに書いた間違いコードが以下の二つ。

<!-- index.html.erb 間違いその1 -->

<div>
  <%= form_tag search_path do %>
    <%= text_field @task, :content %>
    <%= submit_tag "検索" %>
  <% end %>
</div>
<!-- index.html.erb 間違いその2 -->

<body>
  <%= form_tag search_path @task do |f| %>
    <%= text_field f, :content %>
    <%= submit_tag "検索" %>
  <% end %>
</body>

これらはどちらもform_tagtext_fieldの用法としては間違いです。

しかしその2ではparams[:content]でうまく検索ワードを抜き出せてしまいました。

なぜ「その2」では間違った記法にも関わらず正しい挙動をしたのか、また「その1」と「その2」はほとんど等価なプログラムのように見えるのになぜ「その1」では正しく挙動しないのか、わかったことを簡単にまとめます。

paramsを見てみる

まず、これらの書き方によって得られるparamsを見てみます。

searchアクションにbinding.pryを仕込んでparamsの中身を調べます。

# index.html.erb 間違いその1

[1] pry(#<TasksController>)> params
=> <ActionController::Parameters {"utf8"=>"", "authenticity_token"=>"wB4+spI/twQq+OS7wGOkjP7rYuGlDN/kXYbeN9lESqFTHSU38EcWMmG00Z5cVGKl0EFcQ5HSf2qpijb8r+zCuA==", "#<Task:0x00007f9f327081d0>"=>{"content"=>"hoge"}, "commit"=>"検索", "controller"=>"tasks", "action"=>"search"} permitted: false>
# index.html.erb 間違いその2

[1] pry(#<TasksController>)> params
=> <ActionController::Parameters {"utf8"=>"", "authenticity_token"=>"IE7LzRafXk1idX7E5tHpQMsvs1WsRrnv1Udbcbm2aRCzTdBIdOf/eyk5S+F65i9p5YWN95iYGWEhS7O6zx7hCQ==", "content"=>"hoge", "commit"=>"検索", "controller"=>"tasks", "action"=>"search"} permitted: false>

その1ではparamsの中に"#<Task:0x00007f9f327081d0>"=>{"content"=>"hoge"}というハッシュがネストしており、params[:content]では"hoge"にアクセスできない状態になっています。

一方その2ではparamsハッシュの中にうまく"content"=>"hoge"が格納されており、params[:content]"hoge"にアクセスできていますね。

間違いその1について

<!-- index.html.erb 間違いその1 -->

<div>
  <%= form_tag search_path do %>
    <%= text_field @task, :content %>
    <%= submit_tag "検索" %>
  <% end %>
</div>

その1で間違っているのはtext_fieldの使い方です。

<%= text_field hoge, fuga %>
<%= submit_tag %>

とした場合、text_field文字列 "hoge"をキーとしたハッシュ"hoge"=>{"fuga"=>"入力文字"}paramsに返します。

この辺りがtext_fieldのよしなに力を感じる部分で、仮にhoge:hogeでも@hogeでも@@hogeであったとしてもいったん文字列"hoge"としてキーにするようです。

今回の間違いその1ではhogeに該当する部分に@taskが入っていました。 この@taskはコントローラーのindexアクションから空のクラスTask.newを受け取ってきます。 Task.newを文字列に変換すると

>> Task.new.to_s
=> #<Task:0x00007f9f327081d0>

となるので、結果としてparamsには"#<Task:0x00007f9f327081d0>"=>{"content"=>"hoge"}というハッシュが出来上がります。

ちなみにtext_fieldにはもうちょっとだけrailsのよしなに力が働いていて、キーとして与えられた文字列(例でいうhogefuga)が変数として使われていないかindexアクションまで確認に行ってくれているようです。

そのため今回の場合だと@tasksは複数のクラスを抱えていることを認知しているため

<%= text_field @tasks, fuga %>

のような使い方をすると、@tasksをキーとするには不適切と判断しエラーを出力します。

hogefugaは基本的にはどんな文字列でも大丈夫ですが、そのページで使用するクラス変数などは中身を参照してよしなに判断されてしまうので注意が必要です。

さすがはrailsさん、完全に初見殺し気が利いていますね。

間違いその2について

<!-- index.html.erb 間違いその2 -->

<body>
  <%= form_tag search_path @task do |f| %>
    <%= text_field f, :content %>
    <%= submit_tag "検索" %>
  <% end %>
</body>

続いてその2でparams[:content]にアクセスできている理由です。

そもそも論なんですが、form_tagではその2のようなブロック変数|f|は受け取らない設計になっています。

ですので「その2」のdo-endブロック内においてブロック変数fnilとして扱われています。

よって<%= text_field f, :content %>fは無視され paramsに直接"content"=>"hoge"が入力されることになります。

本来の用法ではありませんがエラーを吐かずに動いてしまうため、製作者の意図に反してこのような挙動をとってしまう可能性があります。 こちらも注意が必要です。

まとめ

text_field

  • 引数をいったん文字列としてparamsのキーにする
  • 使用できる文字列かをよしなに判断してくれる
    • 配列やハッシュなど複数の値を持つ場合はエラーを吐く可能性がある

form_tag

  • ブロック変数は取らない
  • ブロック変数を持たせても動作するが、ブロック変数はnil扱いとなる

思う存分にrailsマジックに翻弄された事例でした。 皆さんも十分にお気をつけください。

Ruby初心者によるRuby初心者のためのRuby基礎

Rubyはいろいろ柔軟すぎてまったくコードが読めなかったので備忘録も兼ねてざっと整理してみました。

まだRubyRailsを触り始めて2〜3週間程度なのでなにか間違っていた場合はご指摘お願いします。

※ちょくちょく余談で深めな部分に言及してますがあまり気にしないでください。

文字列

  • ダブルクォート(")、シングルクォート(')どちらでも括れる
  • シングルクォート(')では変数が文字列展開されない
  • カラ文字はカラ文字扱い
>>  "foo"
=> "foo"
>>  'foo'
=> "foo"
>>  ""
=> ""
  • +で結合できる
>>  "foo" + "bar"
=> "foobar"
>>  "foo" + ""
=> "foo"

変数

変数の初期化

  • 宣言 -> 必要なし。どこでも使い始められる。
  • 型の指定 -> 必要なし
  • 数値、文字列ともに代入可能
  • 命名規則 -> 小文字のみを使う。単語は_で繋ぐ(snake_case)
#変数の宣言、型の宣言は必要なし
#代入と共にいきなり使える
>> valu = 1
=> 1
>> char = "foo"
=> "foo"

変数の文字列展開

  • #{変数名}でダブルクォート内(" ")でも格納している文字列を展開できる
  • シングルクォート内(' ')では展開できない
  • 「#{変数名}」を出力したい場合は'#{変数名}'もしくは"\#{変数名}"
>> char = "foo"
=> "foo"
>> char + "bar"
=> "foobar"
>> "char" + "bar"
=> "charbar"
>> "#{char}" + "bar"
=> "foobar"
>> "#{char}bar"
=> "foobar"
>> '#{char}bar'
=> "\#{char}bar"
>> "\#{char}bar"
=> "\#{char}bar"

変数のスコープ

>> def puts_name
>>   name = "Tanaka Taro"
>>   puts name
>> end
=> :puts_name
>> puts_name
Tanaka Taro
=> nil
>> puts name
Traceback (most recent call last):
        1: from (irb):11
NameError (undefined local variable or method `name' for main:Object)

メソッド内で初期化された変数は初期化されたメソッド内からしか呼び出せない。

上の例ではputs_nameメソッドで初期化された変数nameputs_name内では呼び出せるが、puts_name外から呼び出そうとするとエラーになる。

※余談

  • printメソッドやputsメソッドでは"\n"が改行を意味する
  • 「\」もしくは「''」で普通表示されない特殊文字を表示できる
>> print "foo"
foo=> nil
>> print "foo\n"
foo
=> nil
>> print "foo\\n"
foo\n=> nil
>> print 'foo\n'
foo\n=> nil
>> puts "foobar"
foobar
=> nil
>> puts "foo\nbar"
foo
bar
=> nil
>> puts "foo\\nbar"
foo\nbar
=> nil
>> puts 'foo\nbar'
foo\nbar
=> nil

 配列

 配列の基本

  • 変数同様宣言や型の指定は必要ない
  • 配列の中にどんなオブジェクトも格納可能
    • 数値や文字列、nilを同時に格納可能
  • []Array.newで初期化できる
  • 要素への参照は要素番号(0~)を指定
>> []  #中身のないカラの配列
=> []
>> Array.new  #ArrayはArrayクラスのオブジェクト
=> []
>> [] == Array.new
=> true
>> array = [1, 2, "three", 4, "five", nil]
=> [1, 2, "three", 4, "five", nil]
>> array[0]
=> 1
>> array[2]
=> "three"
>> array[5]
=> nil

 配列の大きさ取得

  • 配列の大きさはlengthもしくはsizeで取得
>> array = [1, 2, "three", 4, "five", nil]
=> [1, 2, "three", 4, "five", nil]
>> array.length
=> 6
>> array.size
=> 6

 配列の各要素への繰り返し処理

  • eachもしくはmapを使う
  • 処理の書き方は{}もしくはdo-end
>> array = [1, 2, "three", 4, "five", nil]
=> [1, 2, "three", 4, "five", nil]
>>
>> # {}を使った記述
>> array.each{ |value| puts "value = #{value}"}
value = 1
value = 2
value = three
value = 4
value = five
value = nil
=> [1, 2, "three", 4, "five", nil]
>>
>> # do-end を使った記述
>> array.each do |value|
>>   puts "value = #{value}"
>> end
value = 1
value = 2
value = three
value = 4
value = five
value = nil
=> [nil, nil, nil, nil, nil, nil]

 ※余談

eachmapの違い

each

処理1

>> def hoge
>>   (1..10).to_a.each do |i|
>>     unk = i + 1
>>   end
>> end
=> :hoge
>> ihr = hoge
=> [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]

処理2

>> array = [1, 2, 3, 4, 5]
>> i = 0
>> array.each do |value|
>>   puts "value = #{value}"
>>   puts "i     = #{i}"
>>   i = value + 1
>>   puts "i     = value + 1"
>>   puts "i     = #{i}"
>>   puts"-------------------"
>> end
value = 1
i     = 0
i     = value + 1
i     = 2
-------------------
value = 2
i     = 2
i     = value + 1
i     = 3
-------------------
value = 3
i     = 3
i     = value + 1
i     = 4
-------------------
value = 4
i     = 4
i     = value + 1
i     = 5
-------------------
value = 5
i     = 5
i     = value + 1
i     = 6
-------------------
=> [1, 2, 3, 4, 5]

参考:each (Array) | Rubyリファレンス

参考:rubyのeach, mapの違い

eachメソッドは、配列の要素の数だけブロックを繰り返し実行します。繰り返しごとにブロック引数には各要素が順に入ります。戻り値はレシーバ自身です。

map

処理1

>> def huga
>>   (1..10).to_a.map do |i|
>>     unk = i + 1
>>   end
>> end
=> :huga
>> ihr2 = huga
=> [2, 3, 4, 5, 6, 7, 8, 9, 10, 11]

処理2

>> array = [1, 2, 3, 4, 5]
>> i = 0
>> array.map do |value|
>>   puts "value = #{value}"
>>   puts "i     = #{i}"
>>   i = value + 1
>>   puts "i     = value + 1"
>>   puts "i     = #{i}"
>>   puts"-------------------"
>> end
value = 1
i     = 0
i     = value + 1
i     = 2
-------------------
value = 2
i     = 2
i     = value + 1
i     = 3
-------------------
value = 3
i     = 3
i     = value + 1
i     = 4
-------------------
value = 4
i     = 4
i     = value + 1
i     = 5
-------------------
value = 5
i     = 5
i     = value + 1
i     = 6
-------------------
=> [nil, nil, nil, nil, nil]

参考:map, map! (Array) | Rubyリファレンス

参考:rubyのeach, mapの違い

mapメソッドは、要素の数だけ繰り返しブロックを実行し、ブロックの戻り値を集めた配列を作成して返します。collectメソッドの別名です。

eachでは配列の要素(処理1のi, 処理2のvalue)を返す。処理1では配列の要素iを返し、処理2ではvalueを返す。いずれの処理においても処理内で要素自身を更新していないため処理後に返る値は入力された配列のまま。

一方mapではブロックの戻り値を返す。処理1ではhogeメソッドが返す値unkを返し、処理2ではブロックをメソッド化していないためブロックは値を返さない。よって処理1では元の配列に1が足され、処理2ではnilが返る。

ちなみにmapはどうやらメソッドの最後に評価した値を返すようで、上記処理2の例ではputs "-------------------"の評価値を返している。putsnilを返すため、配列はnilが入った状態で返ってくる。 よってmap処理の最後の行にj = 1という処理を追加した場合、返る配列は以下のように[1, 1, 1, 1, 1]になる。

>> array = [1, 2, 3, 4, 5]
>> i = 0
>> array.map do |value|
>>   puts "value = #{value}"
>>   puts "i     = #{i}"
>>   i = value + 1
>>   puts "i     = value + 1"
>>   puts "i     = #{i}"
>>   puts"-------------------"
>>   j = 1
>> end
...
...
(省略)
...
=> [1, 1, 1, 1, 1]

メソッド

メソッドの定義

  • クラス内で定義されたメソッドはそのクラスから作成されたオブジェクト(インスタンス)に対してのみ使用できる。
  • 引数の()は省略可能
def メソッド名(引数1, 引数2, ..., 引数n)
  実行する処理1
  実行する処理2
  ...
end

メソッドの呼び出し

  • オブジェクトに続いて.で繋ぐことで、そのオブジェクトのクラスで定義されているメソッドを呼び出すことができる。
  • 引数の()は省略可能
メソッド名(引数1, 引数2, ..., 引数n)
メソッド名 引数1, 引数2, ..., 引数n

オブジェクト名.メソッド名(引数1, 引数2, ..., 引数n)
オブジェクト名.メソッド名 引数1, 引数2, ..., 引数n
  • メソッド命名規則
    • 一般的に全て小文字
    • 単語間は_で繋ぐ(snake_case)
>> def sum_puts(num1, num2)
>>   puts(num1 + num2)
>> end
=> :sum_puts
>> sum_puts(1, 2)
3
=> nil
>> sum_puts 1, 2
3
=> nil
  • メソッドの定義位置はメソッドの呼び出し前
  • 定義前に呼び出すとエラーになる
>> sum_puts(1, 2)
Traceback (most recent call last):
        1: from (irb):1
NoMethodError (undefined method `sum_puts' for main:Object) 
>> def sum_puts(num1, num2)
>>   puts num1 + num2
>> end
=> :sum_puts
>> sum_puts(1, 2)
3
=> nil

※余談

値渡しと参照渡し

要はrubyではメソッド引数での値の渡し方は値渡しである、ということ。

値渡しと参照渡しについては以下の記事が詳しく解説している。

参考:値渡しと参照渡しの違いを理解する | Rubyist Magazine

簡単にサンプルコードを書いてみた。

>> def sample(num_method)
>>   i = num_method
>>   puts "i = num_method"
>>   puts "---------------------"
>>   puts "num_method    = #{num_method}"
>>   puts "i             = #{i}"
>>   puts "---------------------"
>>   puts "num_method id = #{num_method.object_id}"
>>   puts "i id          = #{i.object_id}"
>>   puts "---------------------"
>>   i = i + 1
>>   puts "i = i + 1"
>>   puts "---------------------"
>>   puts "num_method    = #{num_method}"
>>   puts "i             = #{i}"
>>   puts "---------------------"
>>   puts "num_method id = #{num_method.object_id}"
>>   puts "i id          = #{i.object_id}"
>>   puts "---------------------"
>>   num_method = num_method + 3
>>   puts "num_method = num_method + 3"
>>   puts "---------------------"
>>   puts "num_method    = #{num_method}"
>>   puts "i             = #{i}"
>>   puts "---------------------"
>>   puts "num_method id = #{num_method.object_id}"
>>   puts "i id          = #{i.object_id}"
>> end
=> :sample
>> num = 10
=> 10
>> num.object_id
=> 21
>> sample(num)
i = num_method
---------------------
num_method    = 10
i             = 10
---------------------
num_method id = 21
i id          = 21
---------------------
i = i + 1
---------------------
num_method    = 10
i             = 11
---------------------
num_method id = 21
i id          = 23
---------------------
num_method = num_method + 3
---------------------
num_method    = 13
i             = 11
---------------------
num_method id = 27
i id          = 23
=> nil
>> num
=> 10
>> num.object_id
=> 21

sampleメソッド内の num_method,iともに始めのうちは引数numと同じ値、同じobject_idを持っている。

その後メソッド内で変数の中身が書き換わると同時にそれぞれの変数のobject_idが変化している。

最終的にどちらも元の変数numと違うobject_idとなり、numの値は元のものから変化しない。

破壊的メソッドによる値の渡し方

強制的にレシーバーの値を変更する破壊的メソッドではどのように値を渡しているのかを検証。

破壊的メソッドを使用する前後でのobject_idを調べる。

>> # sample メソッド
>> def sample(str)
>>   puts "str           = #{str}"
>>   puts "str id        = #{str.object_id}"
>> end 
=> :sample
>> 
>> # sample_upcase! メソッド
>> def sample_upcase(str)
>>   puts "str            = #{str}"
>>   puts "str id         = #{str.object_id}"
>>   puts "str.upcase     = #{str.upcase}"
>>   puts "str.upcase id  = #{str.upcase.object_id}"
>>   i = str.upcase
>>   puts "i              = #{i}"
>>   puts "i id           = #{i.object_id}"
>>   puts "str            = #{str}"
>>   puts "str id         = #{str.object_id}"
>> end
=> :sample_upcase
>>
>> # sample_upcase! メソッド
>> def sample_upcase!(str)
>>   puts "str            = #{str}"
>>   puts "str id         = #{str.object_id}"
>>   puts "str.upcase!    = #{str.upcase!}"
>>   puts "str.upcase! id = #{str.upcase!.object_id}"
>>   i = str.upcase!
>>   puts "i              = #{i}"
>>   puts "i id           = #{i.object_id}"
>>   puts "str            = #{str}"
>>   puts "str id         = #{str.object_id}"
>> end
=> :sample_upcase!
>>
>> # str 変数に "hello" を代入、object_id 確認
>> str = "hello"
=> "hello"
>> puts "str id         = #{str.object_id}"
str id         = 70154707881220
=> nil
>>
>> # sample メソッド呼び出し
>> sample(str)
str           = hello
str id        = 70154707881220
=> nil
>>
>> # str 変数に格納されている文字列と object_id の確認
>> puts "str            = #{str}"
str            = hello
=> nil
>> puts "str id         = #{str.object_id}"
str id         = 70154707881220
=> nil
>>
>> #sample_upcase メソッド呼び出し
>> sample_upcase(str)
str            = hello
str id         = 70154707881220
str.upcase     = HELLO
str.upcase id  = 70154697977560
i              = HELLO
i id           = 70154719397600
str            = hello
str id         = 70154707881220
=> nil
>>
>> # str 変数に格納されている文字列と object_id の確認
>> puts "str            = #{str}"
str            = hello
=> nil
>> puts "str id         = #{str.object_id}"
str id         = 70154707881220
=> nil
>>
>> # sample_upcase! メソッド呼び出し
>> sample_upcase!(str)
str            = hello
str id         = 70154707881220
str.upcase!    = HELLO
str.upcase! id = 8
i              = 
i id           = 8
str            = HELLO
str id         = 70154707881220
=> nil
>>
>> # str 変数に格納されている文字列と object_id の確認
>> puts "str            = #{str}"
str            = HELLO
=> nil
>> puts "str id         = #{str.object_id}"
str id         = 70154707881220
=> nil

この例からわかること

  • strの参照するobject_idは破壊的メソッド(upcase!)使用前後で変わらない。
  • 破壊的でないメソッドではレシーバー(str)とは違うobject_idに値を返す。

肝心のsample_upcase!メソッドの挙動ですが、iに空文字かnilが入っているなど少し挙動がおかしいようなのでもう少し検証。

>> str = "hello"
=> "hello"
>> str.object_id
=> 70287705130100
>> str.upcase!
=> "HELLO"
>> str.upcase!
=> nil
>> str
=> "HELLO"
>> str.object_id
=> 70287705130100
>> str.upcase!.object_id
=> 8
>> nil.object_id
=> 8

破壊的メソッドupcase!によって大文字変換を行う際に、対象の文字列がすでにすべて大文字だとnilが返ってくる。

そしてnilオブジェクトのobject_idが8。

つまり先ほどの例ではsample_upcase!メソッド内のputs "str.upcase! = #{str.upcase!}"処理によってstr内の文字列が大文字に変換され、それ以降の処理puts "str.upcase! id = #{str.upcase!.object_id}", i = str.upcase!ではnilが参照されていたことになる。

それでは肝心の破壊的メソッドupcase!が実行される瞬間はどこを参照しているのかというと、

>> str = "hello"
=> "hello"
>> str.object_id
=> 70287705130100
>> str.upcase!.object_id
=> 70287705130100
>> str
=> "HELLO"
>> str.object_id
=> 70287705130100

きちんとレシーバーであるstrobject_idを参照している。

結論、

  • 非破壊的メソッドupcaseはレシーバーと異なるobject_idで文字列変換を行う
  • 破壊的メソッドupcese!はレシーバーと同じobuject_idで文字列変換を行う
  • 破壊的メソッドを使用してレシーバーの値から変更がない場合、破壊的メソッドはnilを返す

参考:Rubyの破壊的メソッドについて

クラス

クラスの定義

  • 定義位置はクラスの呼び出し前
  • クラスの命名規則
    • 頭文字は大文字
    • 単語の頭文字を大文字にして単語を区切る(CamelCase)
# クラスの定義
class クラス名
end
>> JapanesePeople.class # classメソッド = オブジェクトのクラスを返すメソッド
Traceback (most recent call last):
        1: from (irb):1
NameError (uninitialized constant JapanesePeople) # クラスの定義前なのでエラー出力
>> class JapanesePeople
>> 
>> end
=> nil
>> JapanesePeople.class
=> Class   # JapanesePeopleクラスが作成されていることが確認された

オブジェクトの生成

  • オブジェクトを生成するとクラス内のinitializeメソッドが実行される。
  • initializeメソッドはクラス定義時に省略可能。
  • initializeメソッドを使うことでオブジェクト生成時に必ず行うべき処理を実行することができる。
# オブジェクトの生成
オブジェクト名 = クラス名.new()
>> class JapanesePeople
>>   def initialize(name, age, from)
>>     @name = name
>>     @age  = age
>>     @from = from
>>     puts "name: #{@name}, age: #{@age}, from: #{@from}"
>>   end
>> end
=> :initialize
>> tanaka = JapanesePeople.new("Tanaka", 25, "Tokyo")
name: Tanaka, age: 25, from: Tokyo
=> #<JapanesePeople:0x00007fcd2640abc8 @name="Tanaka", @age=25, @from="Tokyo">

インスタンス変数

  • クラス内の全てのメソッドで共有できる変数。
  • 変数名の頭に@をひとつ付ける
  • クラスから作成されるインスタンスごとに独立した変数
>> class JapanesePeople
>>   def initialize(name, age, from)
>>     @name = name
>>     @age  = age
>>     @from = from
>>     puts "name: #{@name}, age: #{@age}, from: #{@from}"
>>   end
>>   def introduction
>>     puts "#{@from}出身の#{@name}です。#{@age}歳です。"
>>   end
>> end
=> :introduction
>> tanaka = JapanesePeople.new("Tanaka", 25, "Tokyo")
name: Tanaka, age: 25, from: Tokyo
=> #<JapanesePeople:0x00007fcd2640abc8 @name="Tanaka", @age=25, @from="Tokyo">
>> suzuki = JapanesePeople.new("Suzuki", 30, "Fukuoka")
name: Suzuki, age: 30, from: Fukuoka
=> #<JapanesePeople:0x00007fcd2642b620 @name="Suzuki", @age=30, @from="Fukuoka">
>> tanaka.introduction
Tokyo出身のTanakaです。25歳です。
=> nil
>> suzuki.introduction
Fukuoka出身のSuzukiです。30歳です。
=> nil

インスタンス変数はtanakasuzukiで独立しているが、どちらもJapanesePeopleクラスなのでintroductionメソッドは共通。

クラス変数

  • 同じクラスであればインスタンスを超えて参照可能な変数。
    • もちろんクラス内のどのメソッドからも参照可能。
  • 変数名の頭に@@を付ける。
>> class JapanesePeople
>>   def initialize(name, age, from)
>>     @name = name
>>     @age  = age
>>     @from = from
>>     @@like = "kome"
>>     puts "name: #{@name}, age: #{@age}, from: #{@from}, like: #{@@like}"
>>   end
>>   def introduction
>>     puts "#{@from}出身の#{@name}です。#{@age}歳です。#{@@like}が好きです。"
>>   end
>>   def like_change_kome
>>     @@like = "kome"
>>   end
>>   def like_change_sakura
>>     @@like = "sakura"
>>   end
>> end
=> :like_change_sakura
>> tanaka = JapanesePeople.new("Tanaka", 25, "Tokyo")
name: Tanaka, age: 25, from: Tokyo, like: kome
=> #<JapanesePeople:0x00007fcd26b9a118 @name="Tanaka", @age=25, @from="Tokyo">
>> suzuki = JapanesePeople.new("Suzuki", 30, "Fukuoka")
name: Suzuki, age: 30, from: Fukuoka, like: kome
=> #<JapanesePeople:0x00007fcd28c5e2c8 @name="Suzuki", @age=30, @from="Fukuoka">
>> tanaka.introduction
Tokyo出身のTanakaです。25歳です。komeが好きです。
=> nil
>> suzuki.introduction
Fukuoka出身のSuzukiです。30歳です。komeが好きです。
=> nil
>> tanaka.like_change_sakura
=> "sakura"
>> tanaka.introduction
Tokyo出身のTanakaです。25歳です。sakuraが好きです。
=> nil
>> suzuki.introduction
Fukuoka出身のSuzukiです。30歳です。sakuraが好きです。
=> nil
>> suzuki.like_change_kome
=> "kome"
>> tanaka.introduction
Tokyo出身のTanakaです。25歳です。komeが好きです。
=> nil
>> suzuki.introduction
Fukuoka出身のSuzukiです。30歳です。komeが好きです。
=> nil

クラス変数である@@likeJapanesePeopleクラスのtanaka, suzukiどちらにも常に共通であり、どちら側のメソッドからも参照して値を変更することが可能。

クラスの継承

  • クラスの継承方法
class クラス名 < 継承したいクラス名
end

クラスを継承することで、継承元のクラスで定義したメソッドなどを継承先のクラスでも使用することができる。

>> class Person
>>   def introduction
>>     puts "私は人間です。"
>>   end
>> end
=> :introduction
>> class Japanese < Person
>> end
=> nil
>> Tanaka = Japanese.new()
=> #<Japanese:0x00007ffb0099f3b0>
>> Tanaka.introduction
私は人間です。
=> nil

JapaneseクラスはPersonクラスを継承しているため、JapaneseクラスのインスタンスであるTanakaJapaneseクラスで定義されていなくてもPersonクラスのintroductionメソッドを使用することができる。

シンボル

:symbolなど、:が前置された文字列のこと。

変数と違い、シンボル自身に値や文字列を格納することはできない。

シンボルを表すクラス。シンボルは任意の文字列と一対一に対応するオブジェクトです。

文字列の代わりに用いることもできますが、必ずしも文字列と同じ振る舞いをするわけではありません。 同じ内容のシンボルはかならず同一のオブジェクトです。

参考:Ruby 2.3.0 リファレンスマニュアル

文字列との違い

文字列は、同じ文字列でも参照するたびにobject_idの違うオブジェクトが参照されている。

何度参照しても表面上の文字列としては同じものだが、参照するたびにオブジェクトとしては違うものという判定になる。

>> "symbol".object_id
=> 70193690175060
>> "symbol".object_id
=> 70193690258700
>> "symbol".object_id
=> 70193693840280
>> "symbol" == "symbol"
=> true
>> "symbol".object_id == "symbol".object_id
=> false

一方シンボルは文字列の組み合わせに対して常に同じobject_idをとる。

何度参照しても常に同じobject_idとなり、同じオブジェクトとみなされる。

>> :symbol.object_id
=> 392008
>> :symbol.object_id
=> 392008
>> :symbol.object_id
=> 392008
>> :symbol == :symbols
=> true
>> :symbol.object_id == :symbol.object_id
=> true

ハッシュ

ハッシュの定義

  • 配列と似た構造
  • キーを指定してオブジェクト(値)を格納する。
    • 配列番号のみでなく任意の数値や文字列でオブジェクトを取り出せる。
  • {}は省略可能
  • キーには数値、文字列、シンボルなどを始めあらゆるオブジェクトを採用可能。
    • niltrue, false, クラス, モジュールなど
# キーが数値、文字列の場合
ハッシュ名{キー1 => オブジェクト1, キー2 => オブジェクト2, ...}
ハッシュ名 キー1 => オブジェクト1, キー2 => オブジェクト2, ...

# キーがシンボルの場合
ハッシュ名{:キー1 => オブジェクト1, :キー2 => オブジェクト2, ...}
ハッシュ名 :キー1 => オブジェクト1, :キー2 => オブジェクト2, ... 

ハッシュ名{キー1: オブジェクト1, キー2: オブジェクト2, ...}
ハッシュ名 キー1: オブジェクト1, キー2: オブジェクト2, ...

# ハッシュにオブジェクトを追加
ハッシュ名[キー] = オブジェクト

# ハッシュの値を参照
ハッシュ名[キー]
  • 変数や配列、ハッシュは中身のオブジェクトがキーとして割り当てられる
    • よって中身が変わるとエラーとなる
>> greet = "hello"
=> "hello"
>> array = [1, 2, 3, 4, 5]
=> [1, 2, 3, 4, 5]
>> person = {name: "tanaka", age: 25}
=> {:name => "tanaka", :age => 25}
>> hash = {greet => "hoge", array[0] => "fuga", person[:name] => "hogefuga"}
=> {"hello"=>"hoge, "1=>"fuga", "tanaka"=>"hogefuga"}

ハッシュをハッシュにネストする

ハッシュのオブジェクトにハッシュを持っている状態

>> family = {
>>   name: family_name = {  
>>     dat: "toshio",    
>>     mam: "kaori",    
>>     son: "yuta",    
>>     daughter: "saori"    
>>   },  
>>   num: 4
>> }  
=> {:name=>{:dat=>"toshio", :mam=>"kaori", :son=>"yuta", :daughter=>"saori"}, :num=>4}

モジュール

モジュールの定義

  • 定義位置はモジュールの呼び出し前
  • モジュールの命名規則
    • 頭文字は大文字
    • 単語の頭文字を大文字にして単語を区切る(CamelCase)
module モジュール名
  処理
end
>> module Person
>>   def introduction
>>     puts "私は人間です。"
>>   end
>> end
=> :introduction

モジュールについてはこちらの記事がわかりやすかったのでここでは簡単な紹介程度で。

参考:【Ruby入門】Rubyのモジュールの使い方

上記の記事で、

  • モジュールはインスタンスを作れない
  • モジュールは継承できない

とあったので、これだけ確認しておきます。

モジュールはインスタンス(オブジェクト)を生成できない

  • そもそもModuleクラスにはインスタンスを生成するnewメソッドは用意されていない。
>> module Person
>>   def introduction
>>     puts "私は人間です。"
>>   end
>> end
=> :introduction
>> Person.new()
NoMethodError: undefined method `new' for Person:Module
from (pry):10:in `__pry__'  

モジュールは継承できない

  • モジュールを継承しようとするとエラーが出力される。
>> module Person
>>   def introduction
>>     puts "私は人間です。"
>>   end
>> end
=> :introduction
>> module Japanese < Person
SyntaxError: unexpected '<'
module Japanese < Person
                ^
>> class Japanese < Person
>> end
TypeError: superclass must be a Class (Module given)
from (pry):6:in `__pry__'

おわりに

何かご指摘やご要望があれば遠慮なくコメントお願いします。

【初心者】Ruby on Rails での開発環境を整える

筆者はRuby on Railsの勉強を始めたばかりの新米エンジニアです。

自分なりにプロジェクト(なにか適当なWebページらしきもの)を作る過程を適当に書き起こしていきます。

完全なる備忘録ですが、ついでにこれからRailsを始める方の参考にでもなれば幸いです。

この記事のゴール

Railsでプロジェクトをスタートできる環境を作る

初心者が無駄に高い目標を設定したところでどうせ挫折するので、ゆっくり地道にやります。

具体的には以下のものをインストールする過程を書いています。

この記事の対象者

  • Rubyを触ったことがない
  • Railsも触ったことがない
  • ターミナルは触ったことがあってほしい

ターミナル触ったことない方はこの辺りの記事を参考にしていただけると。

参考:今さら聞けない!ターミナルの使い方【初心者向け】

ちなみに今回はMacBookProにRailsを入れていきます。

さっそく始めてみる

今回はこの辺りの記事を参考に環境を整えていこうと思います。

参考:Rails Girls インストール・レシピ    rbenv を利用した Ruby 環境の構築

みなさん片っ端からrbenvなるものを導入しているようなので調べて見たんですが、どうやらRubyのバージョンを簡単に切り替えられるようになるみたいです。

参考:rbenvとは、gemとは何か?

入れておいて邪魔になるものでもなさそうなので今回はrbenvを入れる方向で進もうと思います。

コマンドラインツールのインストール

まずはコマンドラインツールのインストールから。

必要な方は以下のコマンドでインストールできます。

僕の環境ではすでにインストール済みだったのでここはスルー。

$ xcode-select --install

インストールされていれば以下のコマンドでバージョンが表示されるはずです。

$ xcode-select -v
xcode-select version 2349.

Homebrewのインストール

次にHomebrewのインストール。

HomebrewはMac OS Xのパッケージマネージャーで、導入することでソフトウェアの入手が楽になるらしい。

Homebrewやパッケージマネージャーについてはこの辺りの記事がわかりやすかったです。

参考:パッケージ管理システム Homebrew

   「パッケージ管理システムとは」by Windowsユーザー

   Homebrew macOS 用パッケージマネージャー

Homebrew公式を参考に以下のコマンドを実行。

$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

インストールの確認のためバージョンを表示させます。

$ brew -v
Homebrew 1.6.4

表示されましたね。これでHomebrewをインストール完了。

rbenvのインストール

続いてrbenvをインストール。

$ brew install rbenv
$ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile
$ source ~/.bash_profile

ここでも確認のためバージョンを表示します。

$ rbenv -v
rbenv 1.1.1

無事インストールされているようです。

また、このときrbenv単体だけだとこの後のRubyのインストールができないので同時にruby-buildもインストールしておきます。

$ brew install ruby-build

Rubyのインストール

先ほどインストールしたrbenvでRubyをインストールしていきます。

$ rbenv install --listでインストール可能なバージョンの一覧が出てくるので目当てのバージョンをインストールします。

ここでは最新バージョンのバージョン2.5.1を指定しています。

$ rbenv install 2.5.1

インストールが完了すると下記コマンドのいずれかでインストールしたRubyのバージョンが確認できます。

$ rbenv versions
$ ruby -v
ruby 2.5.1

バージョンが表示されたら無事Rubyのインストールは完了。

※余談ですが、rbenvを使わずにHomebrewのみでも下記コマンドでインストール可能です。

$ brew install ruby

参考:Rubyのインストール

Bundlerのインストール

続いてBundlerをインストールしていきます。

Bundlerやgemについては以下の記事がわかりやすかったです。

参考:Bundlerとは?

   Ruby on Rails 初心者必見!パッケージ管理ツール『gem』を徹底解説

下記コマンドでBundlerをインストールします。

$ gem install budler

インストールを確認するためバージョンを表示します。

$ bundler -v
Bundler version 1.16.1

バージョンが表示されました。これでBundlerのインストールは完了です。

Railsのインストール

いよいよRailsをインストールします。

$ gem install rails

実行が終了したら確認のためバージョンを表示します。

$ rails -v

これまで通りバージョンが表示されると思いきや、

$ rails -v
Rails is not currently installed on this system. To get the latest version, simply type:

    $ sudo gem install rails

You can then rerun your "rails" command.

変なエラーが出ました。

どうやら$ sudo gem install railsのコマンドでrailsをインストールしろ、ということなので実行しバージョンを確認。

$ sudo gem install rails
...(略)...

$ rails -v
Rails is not currently installed on this system. To get the latest version, simply type:

    $ sudo gem install rails

You can then rerun your "rails" command.

ハマった


これ以降ずっと同じことの繰り返し。

というわけでググり申してみると、以下のような記事に行き着いた。

参考:MacOSX-Lionでrbenvを使ってRubyOnRailsの環境構築すると、以下のようなメッセージが出てドハマリしたけど、なんとか解決したことについてのメモ(推理なので正しいかどうかの確証持てない)

参考:無限 sudo gem install rails にハマりました

つまるところこのエラーの原因は、

gemとはrubyそれぞれがもつツールである。しかし、rbenvをいれてもMacOSXはデフォルトでrubyとgemを持っているから、いくらgemコマンドを打ち込んでもデフォルトのrubyのgemが起動してしまう。gemコマンドが自分が使いたいrbenvのrubyで動くように、きちんとPATHを指定する必要がある。そうすれば、railsだって動く

ということだそうです。

そして具体的な解決策はexport PATH="$HOME/.rbenv/shims:$PATH"を追記することだそうですけど、その記入先が.bashrc,.bash_profileとそれぞれの記事で微妙に違う。

というわけで.bashrc,.bash_profileについて調べてみると下記の記事が。

sshでログインすると、.bash_profile -> bashrcの順に実行されます。

ログイン時に一回だけ実行したい時

-> .bash_profileに記載

シェルを起動する度に実行したい時(一般的な設定)

-> .bashrcに記載

参考:.bash_profileと.bashrcについて

とのことなのでvimを起動して.bashrcに記入。

$ vim ~/.bashrc
export PATH="$HOME/.rbenv/shims:$PATH"

その後ターミナルを再起動させて、もう一度Railsをインストール。

$ gem install rails
...(略)...

$ rails -v
Rails 5.2.0

バージョン表示されました!

これにてやっとこさRailsをインストール完了。

おわりに

以上がRailsで開発を始めるための下準備かと思います。

また足りないものがあったら随時追記していきます。