【イベントレポート】『THE TEAM』×『エンジニアリング組織論への招待』コラボイベント
2019/05/09(木)に行われた『THE TEAM』×『エンジニアリング組織論への招待』コラボイベントにブログ執筆枠で参加して来たので、イベントレポートを書かせていただきます!
公式のイベントページはこちら connpass.com
今回のイベントは『THE TEAM 5つの法則』の著者である麻野耕司さん、そして『エンジニアリング組織論への招待』の著者である広木大地さんとのパネルディスカッションイベントです。
『THE TEAM』ではチームについての5つの法則が述べられているのですが、今回のイベントではこの中でも『Boarding(人員選定)の法則』、『Aim(目標設定)の法則』、『Decision(意思決定)の法則』の3つの議題について浅野さんと広木さんによるディスカッションが行われました。
他の参加者の方がイベントの全文書き起こし記事を上げていらっしゃるので、このブログでは各テーマでの要点を抽出した内容を書いていこうと思います。
全文書き起こし記事はこちら note.mu
[目次]
Boarding(人員選定)の法則
Boardingの法則とは?
- チームは環境の変化度合いと人材の連携度合いという2軸の大小で4つのタイプに分類される
- チームのタイプによって理想的な人員選定方法は異なる
- 環境の変化度合いが大きいチームではメンバーの流動性を高めるほうがいい
- 環境の変化度合いが小さいチームでは固定的なメンバーで行うほうがいい
- 人材の連携度合いが大きいチームでは多様性のあるメンバーを集めるほうがいい
- 人材の連携度合いが小さいチームでは均質性のあるメンバーを集めるほうがいい
自分のチームはどのタイプか
- 自分たちのチームがどのタイプに当てはまるか理解するのが難しい
- 『相手』チームが見えているか?
- 『自分』がどんな環境の中にいるか?
- 事業の形態と組織のタイプがリンクしていることが大事
- 経営者は「自分の事業に合った組織とはどのようなものか」を考えるのに苦労する
- 一般の人にも理解できるよう限りなくわかりやすくしたのがこの図
チーム間で文化衝突が発生する
- 同じ組織の中にも様々なタイプに属するチームが混在している
- サッカー型のエンジニアチーム、柔道団体戦型の営業チーム、野球型のバックオフィスチームなど
- 自分たちがどのタイプに属しているか、チームの中にいるだけでは気づきにくい
- 同じ組織内でも自分のチームやその他のチームがどのタイプに属するか互いに理解できていない
- 各タイプ、各チームでそれぞれの文化や常識がある
- 自分のチーム、他のチームがどのタイプに属しているかを理解せず接してしまうことで文化衝突が起きる
- 『THE TEAM』は自分のチームや他のチームがどこに属するのか理解し、話し合いをしていくための本なのではないか
自分の常識を疑う
- 自分の常識を疑うというのは『THE TEAM』における重要なポイント
- エンジニアはエンジニアの中のカルチャーや正義がすべての組織やビジネスモデルに通じると思いがち
- 「今までのチームとは違う新しいチーム、組織になるために最適なものは何か」という客観的な視点を身につける必要がある
- 事実としてチームによって文化や価値観のギャップはあるが、それは仕方のないこと
- どうやって互いの文化や価値観を擦り合わせて行くかを考えなければならないが、そもそも常識が異なると論理的な話になりにくい
- お互いのバックグラウンドを共有し、自分の価値観がどこにあるか整理しながら話し合う際の道具として『THE TEAM』は有効なのではないか
- 今までのチームのあり方は一つのパターンにすぎないことを認識する
Aim(目標設定)の法則
Aimの法則とは?
- 「目標をどのように達成するか?」も大切だが、そもそも「目標を適切に設定すること」が重要である
- 目標には意義目標、成果目標、行動目標の3種類がある
- 3種類のうちどれが良い/悪いではなく、3種類の目標にはそれぞれメリット/デメリットがある
- チームの状況に合わせてどの目標を設定するかが重要
近年のトレンドは意義目標
- 昔の日本の評価方法は全て行動目標だった
- 環境の変化が少ない場合は行動目標が活きる
- どんな行動をすれば成果が出るのかイメージしやすい
- 90年代ごろから環境の変化が早くなることで行動目標が活きにくくなり、成果目標が主流になった
- MBO(Management By Objectives)
- 数値を用いて目標を設定
- 近年はさらに環境の変化が早くなったため意義目標がトレンドに
- 環境変化によって数ヶ月前に立てた数値目標が役に立たない場面も
- OKR(Objectives and Key Results)
- 目的(Objectives)から認識を合わせ、成果目標(Key Results)にブレイクダウンする
具体と抽象を行き来する能力をどう養うか
- 目標を設定する意義の一つに「フォーカスを作る」というものもある
- 意義目標が抽象的すぎても成果目標や行動目標に落としづらい
- 意義目標を設定する上で具体化と抽象化のバランスが難しい
- 結果として目標が設定しやすい成果目標に留まりがち
- 抽象度の上げ下げは日常的に訓練する必要がある
- 日本人は学校で意義目標を立てることを習ってない
- 与えられた成果目標や行動目標をきちんとやることだけ学ばされる
- (麻野さんも)今でも目的の設定は難しく感じるので、日々鍛えられている
目標の納得感とフィット感
- 中には行動目標寄りの課題しか処理できないメンバーもいる
- いきなり意義目標から考えさせてしまうと逆に混乱を招いてしまいかねない
- 納得感とフィット感をどのように見極めていくのか?
- チームメンバーの能力レベルもしくはチームや事業の環境変化を見て考える
- 能力レベルを見る場合
- 「自ら考えて行動を起こす」能力のレベルが高ければ意義目標や成果目標を渡してあげたほう状況に合わせた動きができるので成果が出やすい
- 「自ら考えて行動を起こす」能力のレベルが低い場合は「これをやればうまくいく」という行動目標までブレイクダウンしないとずれた方向に向かってしまうことも
- 環境変化を見る場合
- 環境の変化が大きくないのであれば、具体的な勝ちパターンを行動目標レベルで定めてしまうのが強い
- 環境の変化が大きい場合は抽象度をあげて、意義目標で動いてもらったほうがいい
- この2つの観点の狭間にどのような目標設定が適切かというポイントがある
Decision(意思決定)の法則
Decisionの法則とは?
- 意思決定の方法には独裁、多数決、合議の3種類がある
- 目標と同様、3種類の意思決定方法にはそれぞれメリット/デメリットがある
- 意思決定の方法を意思決定する必要がある
どの方法で意思決定するかを決定する
- リーダーとメンバーの間で認識のずれが起きることも
- ある程度は独裁でスピード感を持って決めたいリーダーと、合議で自分も納得した上で決めたいメンバーの構図
- この方針のずれによって不和が生まれることもある
- 日本社会では合議が良しとされる風潮があるが、必ずしもそうとは限らない
- ずっと合議ばかりしていて何も決まらないチームもある
- 一人が状況も把握せず決めすぎて間違った方向へ行ってしまうこともある
- いま自分たちのチームはどの方法で意思決定すべきかを決めることが大事
組織構造と意思決定
- 組織が大きくなり階層構造が出て来た場合
- 全ての階層についてトップが独裁しようとしても回らない
- 環境変化の早さによる意思決定と、組織構造による意思決定のバランスはどのように考えるべき?
- 階層構造についてもどの階層のことはどのようしてに決める、という認識合わせが必要
- メンバーが増えてきたら、「独裁する範囲を決める」ということをしていかなければならない
- 組織サイズが大きくなると意思決定の量が増え、トップによる独裁には限界がくる
- 適切にデリゲーション(権限委譲)していく必要がある
納得感と決断の早さは常にトレードオフ?
- トレードオフになる場合とならない場合がある
- 合議でも納得感が生まれない場合もあるし、独裁でも納得感が生まれる場合もある
- 独裁で納得感が生まれる例
- 独裁する人の影響力が高い
- リーダーが現場の状況をよく理解している
- リーダーがメンバーから信頼を寄せられている
- 独裁する人の影響力が高い
- 合議で納得感が生まれない例
- 会議には意見をぶつけ合って意思決定することを目的とするものと、意思決定を遅らせることを目的としものが存在する
- 意見をぶつけ合って意思決定するための会議
- いわゆる「合議」のイメージ
- 納得感と時間がトレードオフの関係になる
- 意思決定を遅らせるための会議
- 責任の所在が曖昧であることが多い
- 意思決定されないため、時間をかけても納得感が生まれない
腐った会社では意思決定がされない
- 腐った会社では何を提案してもYesもNoも返ってこない状態で放置される
- 「この会社では何を言っても前に進まない」という気持ちを抱かせてしまう
- 企業再生ファンドは活きのいい社員を集めて提案をさせ、社長にその場でYes/Noを決定させる
- 9割Noであっても社員に活気が出る
- 「やらない」ことが決まることで、「この会社でも言えば何かが決まるんだ」という前向きな気持ちになる
質疑応答
開発組織で部下から上司への感謝の言葉が出てこない。どうすれば良い組織になる?
- 上司から部下、部下同士では感謝の言葉を伝えられている
- なぜ上司は感謝されない?
- 物事を決めないこと、部下から言われたことをやらないことが多い
- 組織サーベイで「即時の意思決定」の項目が低いマネージャーはその他の項目も低い傾向がある
- 学校教育でも100点を取るための教育が多い
- 間違うことが良しとされる経験を積んでいない
- 早めに間違うことは失敗じゃないという経験を積めると、意思決定が怖くなくなるのではないか
世の経営層の「環境変化に合わせて素早くアクション、意思決定すべき」という温度感はどの程度?
- 事業の拡大にはデリゲーションの観点は重要
- 「デリゲーションできない」 = 「全部自分で決めたい」だと思う
- 全てを一人で決定すると意思決定が遅くなるので、事業や組織は拡大しにくい
- 大きくなる会社は幹部や部下に判断を任せている
- 組織としての判断基準やバリュー、スタイルのようなものを一貫できていることが重要
- こういう意識を持った経営者の比率まではわからないが、組織を大きくする上ではここが一つの大きな転換点
- トヨタの豊田章男さん
- 環境変化のスピード感をアップデートするのは難しい
- あれだけ大きい規模の会社で環境変化に合わせた意思決定ができる豊田さんの感覚はすごい
まとめ
チーム間での文化衝突の話や目標設定・意思決定のトレーニングの話など書籍には書かれていないけど実際の現場では思い当たる話が多く、とても共感ポイントの多いイベントでした。どのチームも似た悩みを抱えてるんですね…。
会話から要点を抽出する作業の中で改めて気づかされることばかりで、とても内容の濃いイベントだったように思います。
僕も今回の学びを現場で活かせるよう精進します!
【ruby】エラーを想定した分岐処理
先日バッチ処理を書く際にエラーが発生することも想定して処理を書かないといけないシーンに遭遇しました。
今回はその時の方法を覚え書きとしてメモしておきます。
begin - rescue
たとえば
array = [1, 2, "hoge", 4] array.each do |elm| puts 1 + elm end
のような処理があった場合、実行結果は以下のように"hoge"
に対してのエラーで止まってしまい配列array
の4
まで実行することができません。
# 実行結果 2 3 TypeError: String can't be coerced into Fixnum from (pry):80:in `+'
こんな時に
- 正常に動作した場合の処理
- エラーが発生した際の処理
を分岐して実行してくれるのがbegin
- rescue
構文です。
array = [1, 2, "hoge", 4] array.each do |elm| begin puts 1 + elm rescue => error puts error end end
このようにすることでbegin
で実行中の処理でエラーが起きるとrescue
以下の処理が実行され、全体の処理を中断することなく処理を進めることができます。
結果、以下のようにエラーが発生しても最後まで実行することができます。
# 実行結果 2 3 String can't be coerced into Fixnum 5
ちなみにrescue
時にputs error.backtrace
とするとエラー出力のバックトレース情報を返すことができます。
begin - rescue - ensure
エラーが発生してもしなくても実行したい処理がある場合はensure
を使います。
array = [1, 2, "hoge", 4] array.each do |elm| begin puts 1 + elm rescue => error puts error ensure puts "elm = #{elm}" end end
この場合begin
もしくはrescue
を通過した処理は最終的にensure
を通過して終了します。
# 実行結果 2 elm = 1 3 elm = 2 String can't be coerced into Fixnum elm = hoge 5 elm = 4
新卒社員が入社半月でペアプロ・モブプロをやってみた
このblog記事は、株式会社アドウェイズの記事(https://blog.engineer.adways.net/entry/2018/09/14/173000)を許可を得て転載したものです。 (執筆者は当blog管理人自身です)
初めまして。2018年新卒の佐土原です。
10月で僕たち2018年新卒も入社半年となります。長かったような短かったような複雑な心境です…。
ちょうどいい節目にブログのお話がきたのでこの半年間を振り返ってみました。本日のブログではその中でも特に貴重な経験ができたと感じている、ペアプロ・モブプロについて書こうと思います。
ペアプロ・モブプロとは
僕の所属しているチームでは、春先から積極的にペアプロやモブプロを取り入れています。配属が決定した4月の半ばからは僕もそこに参加しながら業務を行なっていました。
ペアプロ・モブプロはそれぞれペアプログラミング、モブプログラミングの略称で、ペアでは2人1組、モブでは3〜5人1組でひとつのコードを記述していきます。いずれの方法でもドライバー(実際にコードを記述する人)を1人決め、それ以外の人は随時意見やアドバイスを投げかけ話し合いながらコーディングを行います。
複数人でひとつのコードを書く、と聞くと効率が悪そうに思えますが、ペアプロ・モブプロには以下のようなメリットがあると言われています。
メリット
- 一人で悩む時間が解消される
- 属人化している知見を共有できる
- コードレビューの時間を設けずにすむ
- チームメンバーのわからないことがわかる
確かにひとつのプログラム作成にかかる工数は増えますが、コードレビューや情報共有にかかる時間は削減できチーム全体のレベルも底上げしやすいという恩恵もあるようです。
また僕の所属するチームでは前述のペアプロ・モブプロの恩恵を最大限受けるために、以下のようなルールを設けて実施していました。
ルール
- 15〜30分程度を1タームとする
- タイマーで時間を計る
- 1タームごとにドライバーを交代する
- そのタームで何をするのか事前にゴールを決める
- ドライバーはいま何を考えているか随時口に出す
- わからないことがあればわからないと言う
- その他思ったことはその場で発言する
なにやらハイレベルそうな感じですが、そんな現場で入社半月の新人が感じたことを書いてみようと思います。
入社当時のレベル感
入社当時の僕のエンジニアとしてのレベル感です。
プログラム経験 ... C, Pythonでコードを書いた経験あり。大学の研究で必要となる統計処理系のプログラムがメイン。RubyやJavaScriptなどは触ったことがない。
Webサービスの知識 ... ほぼ皆無。サーバー側、フロント側どちらの知識もない状態。
やる気 ... あり。ここだけが取り柄。
ほぼ丸腰です。よろしくお願いします。
実際にやってみて
4月半ばから7月半ばまでの約3ヶ月間、いろいろな形のペアやモブを組ませていただきました。その感想です。
圧倒的インプット量
とにかくインプット量が多いです。新人がペアプロやモブプロから学べることは、
など上げればキリがないほどです。
先輩方と一緒に同じ作業をするので、実際に先輩方がどのようなことを考えながらコードを書いているのかをかなり網羅的に把握できるように思います。
情報過多になりやすい
一方で上手に時間を使わなければ情報過多になってしまう環境でもありました。
当時は現状把握や場の会話についていくことに必死で深く考えられていませんでしたが、いま思うと整理とアウトプットにあまり時間を割けていなかったように思います。
整理とアウトプットの時間が取れない結果、僕は以下のような悪循環にハマってしまいました。
- インプットによりたくさんの情報を入手する
- 入手した情報を整理する時間が取れない
- 情報と情報の繋がりを把握できず浅い理解に留まってしまう
- ドライバーでもアウトプットができず新たなインプットが増える
- 以下繰り返し...
自分がドライバーの時も、「自分の頭で考えアウトプットする時間」より「コーディングで考えるべきことを教わるインプットの時間」が多くなっていました。
一度立ち止まって、
- 知っている単語/知らない単語
- 自分が理解できていること/理解できていないこと
- 今の自分にできること/できないこと
- 新規の情報と既存の情報の関係性
などについてゆっくり考え整理することも重要だと感じました。
とにかく焦る
タイマーとプロジェクトは常に進み続けるため、置いていかれまいとひたすらに焦っていました。結果として上記のような悪循環にハマっていきます。
また焦る原因のひとつとして、周囲の先輩方の時間を奪っている感覚が強かったこともあります。素人同然の新人がペアプロ・モブプロに参加する以上、どうしても作業を止めて基礎知識を教わる時間の方が長くなってしまいます。「新人をペアプロ・モブプロに参加させるか?」という議論はチーム内で必要かと思いますが、参加した以上多少は仕方のないことだと割り切った方が良いかもしれません。
「他人の時間を奪わない」というのも大切な感覚ではありますが、最近では下手に意地を張らずに「できないこと/わからないこと」は先に伝えてしまった方が結果的に自分も周りも時間が奪われないように感じています。
コミュニケーション量が増える
これはチームに入りたての新人にはかなりありがたい要素でした。
ペアプロやモブプロではとにかく思っていることをその場で口にすることを求められるため、必然的にメンバー間での会話が増えます。
ペアプロ・モブプロに参加させてもらえたおかげでとてもスムーズにチームに馴染めたように思います。
まとめ
【新人がペアプロ・モブプロに参加すると】
- 学びだらけでとても刺激になる
- どんなことを意識して作業すべきなのか間近で学べる
- インプットを整理する時間を設けないと情報過多になりやすい
- 下手な意地やプライドは邪魔
- チームに馴染みやすくなる
僕の経験からはこのようなことが言えそうです。当然といえば当然なのですが、知識量や技術レベルに差がありすぎると議論というより講義に近い側面が強くなりやすいので、レベルの近い人同士だとより質の高いペアプロやモブプロが体験できるかもしれません。
入社後もう半年ですが、まだ半年でもあります。少しでも早くエンジニアとして戦力化するためにも、今後も張り切って業務に取り組んでいこうと思います!
【rails】viewのerbファイルをslim化パーシャル化
この記事を読んでいるということはみなさんrailsで何かしらのサービスを作っていることでしょう。どうでしょうか、進捗は順調でしょうか。
そしてどうでしょうか。そろそろerb
ファイル、読む気が失せてきたのではないでしょうか。僕は読みたくなくなりました。少なくとももう<>
を見たくない。
というわけで今回はviewの憎きerb
ファイルを撲滅し、シンプルで読みやすいslim
ファイルへ移行する方法について書こうと思います。
(slim
化が思いのほか簡単だったのでついでにパーシャルもやってみました。)
erbは目が痛い
htmlに準じた記法であるerb
。コードが少ないうちは特に気にならないですね。
<!DOCTYPE html> <html> <head> <title>About | Ruby on Rails Tutorial Sample App</title> </head> <body> <h1>TimeLine</h1> <% @posts.each do |post| %> <ul> <li>投稿ID:<%= post.id %></li> <li>投稿者:</li> <li>投稿:<%= post.text_content %></li> </ul> <% end %> </body> </html>
この頃はまだそんなに気になってなかったんですが、これが最近だとこうなりました。
<!DOCTYPE Html> <html> <head> <title>About | Ruby on Rails Tutorial Sample App</title> </head> <body> <% if !session[:login_id] %> <p>ログインしてください。</p> <p><%= link_to "ログイン", login_path %></p> <% else %> <h1>TimeLine</h1> <p><%= link_to "投稿する", new_post_path %></p> <ul> <% @posts.each do |post| %> <% if post.sharing.present? %> <%= User.find(post.user_id).account_id %>さんがシェア <% share_post_id = post.id %> <% post = post.sharing %> <% end %> <li> <%= link_to User.find(post.user_id).account_id, user_path(id: post.user_id) %> <%= post.created_at.strftime("%Y-%m-%d%H:%M:%S") %> </li> <p> <% if post.replying.present? %> to: <%= link_to User.find(post.replying.user_id).account_id, user_path(id: User.find(post.replying.user_id).id) %><br> <% end %> <%= post.content %><br> </p> <p> <%= link_to "詳細", post_path(id: post.id) %> <%= link_to "返信", new_post_path(reply: {replying_id: session[:login_id], main_post_id: post.id}) %>: <%= post.replied.length %><br> <% if post.shared.map(&:user_id).include?(session[:login_id]) %> <%= link_to "シェア", destroy_share_path(post: {id: post.id, share_post_id: share_post_id}) %>: <%= post.shared.length %><br> <% else %> <%= link_to "シェア", share_path(post: {id: post.id}) %>: <%= post.shared.length %><br> <% end %> <% if post.likes.find_by(user_id: session[:login_id]).present? %> <%= link_to "いいね", like_path(id: post.likes.find_by(user_id: session[:login_id]).id, like: {post_id: post.id}), method: :delete %>: <%= post.likes.length %> <% else %> <%= link_to "いいね", likes_path(like: {post_id: post.id}), method: :post %>: <%= post.likes.length %><br> <% end %> </p> <% end %> </ul> <% end %> </body> </html>
コードの拙さは一旦置いといて、流石にちょっと読みづらい。なんというか、全体的にトゲトゲしてて目が痛い。
というわけで早速slim
の方を導入していきたいと思います。
slim化する
やることは以下の2ステップです。
Gemfile
に記述を追加する- サーバーを再起動する
とてもシンプルですね。
1. Gemfile
に記述を追加する
Gemfile
に以下の2行を追加します。
gem 'slim-rails' gem 'html2silm'
僕はここでハイフン(-)をアンダースコア(_)で書いていて10分ほどハマったのでしっかり確認しましょう。
2. サーバーを再起動する
すでにサーバーを立ち上げている方はCtrl + c
で一度サーバーを止めましょう。
そしてサーバーを再起動します。
rails s
これで全てのerb
ファイルがslim
ファイルに変換されているはずです。
確認
早速さきほどの目が痛かったerb
ファイルを確認してみましょう。
doctype html html head title | About | Ruby on Rails Tutorial Sample App body - if !session[:login_id] p | ログインしてください。 p = link_to "ログイン", login_path - else h1 | TimeLine p = link_to "投稿する", new_post_path ul - @posts.each do |post| - if post.sharing.present? = User.find(post.user_id).account_id | さんがシェア - share_post_id = post.id - post = post.sharing li = link_to User.find(post.user_id).account_id, user_path(id: post.user_id) = post.created_at.strftime("%Y-%m-%d %H:%M:%S") p - if post.replying.present? | to: = link_to User.find(post.replying.user_id).account_id, user_path(id: User.find(post.replying.user_id).id) br = post.content br p = link_to "詳細", post_path(id: post.id) br = link_to "返信", new_post_path(reply: {replying_id: session[:login_id], main_post_id: post.id}) | : = post.replied.length br - if post.shared.map(&:user_id).include?(session[:login_id]) = link_to "シェア", destroy_share_path(post: {id: post.id, share_post_id: post.shared.find_by(user_id: session[:login_id]).id}) | : = post.shared.length br - else = link_to "シェア", share_path(post: {id: post.id}) | : = post.shared.length br - if post.likes.find_by(user_id: session[:login_id]).present? = link_to "いいね", like_path(id: post.likes.find_by(user_id: session[:login_id]).id, like: {post_id: post.id}), method: :delete | : = post.likes.length - else = link_to "いいね", likes_path(like: {post_id: post.id}), method: :post | : = post.likes.length br
はい、ちょっとシンタックスが効かないみたいなので色がつかないんですが、かなりすっきりしたような気がしますね。
これで晴れてerb
の目の痛さからも解放されました!
パーシャル化(おまけ)
あまりにも簡単にslim
化できてしまったのでこの際ついでにパーシャル化もしてしまおう!という雑な魂胆です。お付き合いください。
パーシャルをざっくり説明すると、viewファイルの一部を別ファイルに切り出し、それをテンプレートとして元のviewファイルからテンプレートファイルを呼び出して表示することです。
百聞は一見に如かず
自分でも何言ってるかよくわからなかったので実際のコードを書いてみます。
元のviewファイルは先ほどslim
化したapp/view/posts/index.html.slim
を例としてみていきます。
以下が元のapp/view/posts/index.html.slim
ファイルです。
※シンタックスが効いていないですがもうしばらくお付き合いください。
doctype html html head title | About | Ruby on Rails Tutorial Sample App body - if !session[:login_id] p | ログインしてください。 p = link_to "ログイン", login_path - else h1 | TimeLine p = link_to "投稿する", new_post_path ul - @posts.each do |post| - if post.sharing.present? = User.find(post.user_id).account_id | さんがシェア - share_post_id = post.id - post = post.sharing li = link_to User.find(post.user_id).account_id, user_path(id: post.user_id) = post.created_at.strftime("%Y-%m-%d %H:%M:%S") p - if post.replying.present? | to: = link_to User.find(post.replying.user_id).account_id, user_path(id: User.find(post.replying.user_id).id) br = post.content br p = link_to "詳細", post_path(id: post.id) br = link_to "返信", new_post_path(reply: {replying_id: session[:login_id], main_post_id: post.id}) | : = post.replied.length br - if post.shared.map(&:user_id).include?(session[:login_id]) = link_to "シェア", destroy_share_path(post: {id: post.id, share_post_id: post.shared.find_by(user_id: session[:login_id]).id}) | : = post.shared.length br - else = link_to "シェア", share_path(post: {id: post.id}) | : = post.shared.length br - if post.likes.find_by(user_id: session[:login_id]).present? = link_to "いいね", like_path(id: post.likes.find_by(user_id: session[:login_id]).id, like: {post_id: post.id}), method: :delete | : = post.likes.length - else = link_to "いいね", likes_path(like: {post_id: post.id}), method: :post | : = post.likes.length br
このファイルのul
行以下からは@posts
で渡された複数のpost
を一覧表示する処理になっています。
いまの状態だとposts.html.slim
ファイルがごちゃごちゃしてみづらいですし、ul
以下はテンプレート化しておけば他の部分でも使い回す機会があるかもしれません。
というわけで、この部分をテンプレート化(パーシャル化)してみましょう。
パーシャル化するには以下のステップを踏みます。
- パーシャルとして参照用のファイルを作成する
- (元ファイルに)パーシャルを参照する処理を書く
早速見ていきましょう。
1. パーシャルとして参照用のファイルを作成する
元のファイル(app/view/posts/index.html.slim
)のul
以下の処理を切り出し、app/view/posts/_posts_list.html.slim
として保存します。
以下がapp/view/posts/_posts_list.html.slim
です。
ul - @posts.each do |post| - if post.sharing.present? = User.find(post.user_id).account_id | さんがシェア - share_post_id = post.id - post = post.sharing li = link_to User.find(post.user_id).account_id, user_path(id: post.user_id) = post.created_at.strftime("%Y-%m-%d %H:%M:%S") p - if post.replying.present? | to: = link_to User.find(post.replying.user_id).account_id, user_path(id: User.find(post.replying.user_id).id) br = post.content br p = link_to "詳細", post_path(id: post.id) br = link_to "返信", new_post_path(reply: {replying_id: session[:login_id], main_post_id: post.id}) | : = post.replied.length br - if post.shared.map(&:user_id).include?(session[:login_id]) = link_to "シェア", destroy_share_path(post: {id: post.id, share_post_id: post.shared.find_by(user_id: session[:login_id]).id}) | : = post.shared.length br - else = link_to "シェア", share_path(post: {id: post.id}) | : = post.shared.length br - if post.likes.find_by(user_id: session[:login_id]).present? = link_to "いいね", like_path(id: post.likes.find_by(user_id: session[:login_id]).id, like: {post_id: post.id}), method: :delete | : = post.likes.length - else = link_to "いいね", likes_path(like: {post_id: post.id}), method: :post | : = post.likes.length br
パーシャルとなるファイルには、_
から始まる名前をつけるのがルールとなっています。
簡単ですが、これでテンプレートとなるパーシャルの準備は完了です。
2. (元ファイルに)パーシャルを参照する処理を書く
続いて元のファイル(app/view/posts/index.html.slim
)にパーシャルを参照する処理を書きます。
パーシャルを参照する元のファイルは以下のようになります。
doctype html html head title | About | Ruby on Rails Tutorial Sample App body - if !session[:login_id] p | ログインしてください。 p = link_to "ログイン", login_path - else h1 | TimeLine p = link_to "投稿する", new_post_path = render 'posts_list'
とても簡潔になりました。
最後の行に注目してください。
さっきまでul
が書いてあった部分に= render 'posts_list'
という処理が追加されています。
これがパーシャルを参照する処理になります。
_○○.html.slim
というパーシャルを呼び出す際には、= render '○○'
のように書きます。_
が省略されることに注意してください。
以上でパーシャルの呼び出しは完了です。
結果的には= render 'posts_list'
の行から先はapp/view/posts/_posts_list.html.slim
の処理が読み込まれるため、元のファイルと全く同じview画面が作成されます。
パーシャルまとめ
パーシャルとしてコードを分けることのメリットには以下のようなものがあると思います。
- コードが簡潔になることで可読性が上がる
- 機能単位でパーシャルを作成することで他のページでも簡単に機能を使い回すことができる
- 複数のページで利用される機能をパーシャルでまとめることで、修正時の修正箇所を抑えることができる
あまりにも細かく分けすぎるのもそれはそれでファイル管理が煩雑になってしまうため考えものですが、基本的に適度に使う分には便利な機能だと思います。
全体まとめ
気がつけばおまけのパーシャルについての方がボリュームが出てしまってますね。まぁそんなこともあります。
slim
もパーシャルもストレスなくコードを読むためには便利な機能ですので、ぜひ試してみてください。
【rails】Twitterのツイート機能のモデル構造を考えてみた
みなさん、Twitterやってますか?
自分はそこそこいい歳ですがそこそこのツイ廃具合を発揮させてもらっています。(もし興味あればフォローどうぞ さどはら (@sado_not_maso) | Twitter )
今日は、最近Twitterのツイート機能(つぶやきを投稿する機能)のモデル構成をRailsで考えてみたのでそのことについて簡単にまとめようと思います。
(今回記事内でやたらTwitter, Twitterと言っているためTwitter公式から怒られると怖いのでリンク貼っておきます。Twitterやったことないよ!という方は以下のリンクから簡単に始められるのでどうぞ)
ツイート機能の概要
まずはモデル構成(モデル間のアソシエーション)を考える上で押さえておきたいTwitterのツイート機能の特徴について一度まとめておきます。
- ひとつのツイートに紐づくユーザーはただ一人のみ
- ツイートに対してリプライ(返信)ができる
- 返信も1つのツイートとしてカウントされる
- 他人のツイートをリツイート(共有・拡散)できる
細かいことを言い出すと「いいね」機能や140字制限などの特徴もありますが、ここではあくまで「モデル構成を考える上で考慮する必要のある機能」を挙げています。
それでは早速これらの機能を備えるためのモデル構成を考えていきたいと思います。
登場するモデル
今回のアソシエーションで登場するモデルとそのカラムをまとめておきます。
(カラムは今回のツイート機能を実装する上で必要となるものだけを記載しています。)
User
モデル
User |
---|
id |
Tweet
モデル
Tweet |
---|
id |
user_id |
ReplyRelationship
モデル
ReplyRelationship |
---|
id |
main_tweet_id |
reply_tweet_id |
ShareRelationship
モデル
※Tweet
とRetweet
では少しややこしいためShare
としています。
ShareRelationship |
---|
id |
origin_tweet_id |
share_tweet_id |
実際のコード
User
モデル
# users.rb class User < ActiveRecord::Base has_many :tweets, dependent: :destroy end
Tweet
モデル
# tweets.rb class Tweet < ActiveRecord::Base belongs_to :user has_one :replying, through: :replying_relationships, source: :main_tweet has_one :replying_relationships, class_name: 'ReplyRelationship', foreign_key: 'reply_tweet_id', dependent: :destroy has_many :replied, through: :replied_relationships, source: :reply_tweet has_many :replied_relationships, class_name: 'ReplyRelationship', foreign_key: 'main_tweet_id', dependent: :destroy has_one :sharing, through: :sharing_relationships, source: :origin_tweet has_one :sharing_relationships, class_name: 'Share', foreign_key: 'share_tweet_id', dependent: :destroy has_many :shared, through: :shared_relationships, source: :share_tweet has_many :shared_relationships, class_name: 'Share', foreign_key: 'origin_tweet_id', dependent: :destroy end
ReplyRelationship
モデル
# reply_relationships.rb class ReplyRelationship < ActiveRecord::Base belongs_to :main_tweet, class_name: 'Tweet' belongs_to :reply_tweet, class_name: 'Tweet' end
ShareRelationship
モデル
# share_relationships.rb class ShareRelationship < ActiveRecord::Base belongs_to :origin_tweet, class_name: 'Tweet' belongs_to :share_tweet, class_name: 'Tweet' end
解説
Tweet
とUser
について
# users.rb class User < ActiveRecord::Base has_many :tweets, dependent: :destroy end # tweets.rb class Tweet < ActiveRecord::Base belongs_to :user end
これはシンプルなhas_many
とbelongs_to
の関係です。
あるuser
に対して、user.tweets
でそのユーザーのツイート一覧を参照できます。
Tweet
とReplyRelationship
について
# tweets.rb class Tweet < ActiveRecord::Base has_one :replying, through: :replying_relationships, source: :main_tweet has_one :replying_relationships, class_name: 'ReplyRelationship', foreign_key: 'reply_tweet_id', dependent: :destroy has_many :replied, through: :replied_relationships, source: :reply_tweet has_many :replied_relationships, class_name: 'ReplyRelationship', foreign_key: 'main_tweet_id', dependent: :destroy end # reply_relationships.rb class ReplyRelationship < ActiveRecord::Base belongs_to :main_tweet, class_name: 'Tweet' belongs_to :reply_tweet, class_name: 'Tweet' end
ツイートのアソシエーションを考える上で一番特徴的なのはTweet
モデルとReplyRelationship
モデルの関係性かなと思います。
Twitterをやっている方はご存知かと思いますが、Twitterでのリプライ(返信)は
- リプライは主となるツイートに付随するものである
- リプライそのものも一つのツイートである
という二つの性質を同時に持ちます。ちょうど以下の画像のような感じです。
リプライの例:@◯◯から始まる内容は特定のユーザーに向けた返信
これがFacebookなどになると、一つの投稿(Post
)に対するコメント(Comment
)は主従の関係がはっきりと成立するため
Post -- has_many --> Comments
Comment -- belongs_to --> Post
というわかりやすいアソシエーションとなります。
しかし今回リプライはツイートに付随するコメントとしての性質と、それ自体が一つのツイートであるという性質をあわせ持つため、Tweet
モデル間を繋げる役目を持つReplyRelationship
モデルを用意しています。
このようにすることで、以前ユーザーのフォロー機能を解説した記事のようにアソシエーションを考えることができます。
Tweet
とShareRelationship
について
# tweets.rb class Tweet < ActiveRecord::Base has_one :sharing, through: :sharing_relationships, source: :origin_tweet has_one :sharing_relationships, class_name: 'Share', foreign_key: 'share_tweet_id', dependent: :destroy has_many :shared, through: :shared_relationships, source: :share_tweet has_many :shared_relationships, class_name: 'Share', foreign_key: 'origin_tweet_id', dependent: :destroy end # share_relationships.rb class ShareRelationship < ActiveRecord::Base belongs_to :origin_tweet, class_name: 'Tweet' belongs_to :share_tweet, class_name: 'Tweet' end
リツイートについては以下の性質を持っています。
- 表示上は他人のツイート
- 所属は自分のTweets群
つまるところ、あるuser
がリツイートした他人のツイートもuser.tweets
で参照できるtweets
群の中に含めたい、という感じです。
これについてはいろいろ方法があるかもしれませんが、僕が考えた方法ではTweets
群の中のあるtweet
がもしリツイートであればtweet.sharing
として別のツイートを参照できる、というものでした。(もっといい方法があればアドバイスいただけると幸いです。)
それによって必要になったのがShareRelationship
というモデルですね。
アソシエーション自体はTweet
とReplyRelationship
の場合とほとんど変わりません。
まとめ
今回はTwitterのツイート機能のモデル構成について考えてみました。
個人的にツイート機能まわりはリプライの扱いが少し独特かなーという感じでしたが、なんとなく理解していただけたでしょうか。
とてもざっくりとした内容になりましたが、今回の記事が少しでも何かの参考になれば幸いです。
【ruby】ループ処理いろいろ
最近rubyのコードを触る中でいろんなループ処理を捌く機会があったので備忘録として書き留めておきます。
今回の記事で整理しているループ処理のメソッドは
each
map
inject
each_with_object
の4つです。
ループ処理は決してこれだけではないので、用途に合わない場合はいろいろ調べてみてください。
それでは早速見ていきます。
each
とmap
each
とmap
で同じコードを書くと以下のような違いが出ます。
pry(main)> [1, 2, 3, 4, 5].each do |elm| pry(main)* elm * 2 pry(main)* end => [1, 2, 3, 4, 5]
pry(main)> [1, 2, 3, 4, 5].map do |elm| pry(main)* elm * 2 pry(main)* end => [2, 4, 6, 8, 10]
each
では、ループ処理の中身に関係なく受け取った値をそのまま返します。
一方map
では、ループ処理の中の最後の評価値を配列の要素に代入した値を返します。
map
の場合「最後の評価値」ということですが、例えば処理の最後にputs
を使用していた場合は以下のようにnil
が入ることになります。(puts
は値を表示後nil
を返すため。)
pry(main)> [1, 2, 3, 4, 5].map do |elm| pry(main)* puts (elm * 2) pry(main)* end 2 4 6 8 10 => [nil, nil, nil, nil, nil]
これもeach
であれば最後の処理に関係なく受け取った配列と同じ配列が返ってきます。
pry(main)> [1, 2, 3, 4, 5].each do |elm| pry(main)* puts (elm * 2) pry(main)* end 2 4 6 8 10 => [1, 2, 3, 4, 5]
まとめると、
each
→ 与えたデータを単に参照したいだけの時(データに変更を加える必要のない時)map
→ 与えたデータにをもとに何らかの新しいデータを取得したい時
のように使い分けると便利です。
inject
とeach_with_object
inject
inject
では複数の変数を与えることで簡単に配列の計算などを行うことができます。
まずeach
を使った例を見てみます。
pry(main)> sum = 0 => 0 pry(main)> [1,2,3,4,5].each do |n| pry(main)* sum = sum + n pry(main)* end => [1, 2, 3, 4, 5] pry(main)> sum => 15
each
を使うとループの外でsum
を初期化するなど長くなってしまう計算も、inject
を使うことでシンプルにループの中で完結させることができます。
pry(main)> [1,2,3,4,5].inject do |sum, n| pry(main)* sum + n pry(main)* end => 15
さてこのinject
の繰り返し処理ですが、パイプ文字sum
とn
はそれぞれ以下のように値を受け取っています。
pry(main)> [1,2,3,4,5].inject do |sum, n| pry(main)* puts "sum = #{sum}" pry(main)* puts "n = #{n}" pry(main)* sum + n pry(main)* end sum = 1 n = 2 sum = 3 n = 3 sum = 6 n = 4 sum = 10 n = 5 => 15
1回目のループではsum
が初期値として配列の最初の値(1
)を受け取り、n
は二つ目の値(2
)を受け取ります。そして2回目以降のループになるとsum
は前回のループの最後の評価値(sum + n
)を受け取り、n
は順次配列の3つ目以降の値を受け取っていくという流れになります。
さらに、inject(初期値)
とすることで以下のようにsum
の初期値を指定することもできます。
pry(main)> [1,2,3,4,5].inject(0) do |sum, n| pry(main)* puts "sum = #{sum}" pry(main)* puts "n = #{n}" pry(main)* sum + n pry(main)* end sum = 0 n = 1 sum = 1 n = 2 sum = 3 n = 3 sum = 6 n = 4 sum = 10 n = 5 => 15
この場合、n
は1回目のループで配列の一つ目の値を受け取ることになります。
またinject
の引数にハッシュや配列を受け取ることも可能です。
pry(main)> [1,2,3,4,5].inject([]) do |array, n| pry(main)* array.push(n) pry(main)* end => [1, 2, 3, 4, 5]
しかしmap
同様2回目以降のループでは前回のループの最後の評価値が参照されるので使い方に注意が必要です。
each_with_object
ループのなかで任意のオブジェクトを作成したい時はeach_with_object
が便利です。
people = [ { id: 1, name: "tanaka", age: 21, home_town: "Tokyo"}, { id: 2, name: "suzuki", age: 30, home_town: "Osaka"}, { id: 3, name: "sato", age: 26, home_town: "Fukuoka"} ]
例として上のような人物情報から人の名前と年齢だけを抜き出したリストage_list
を作りたいときを考えてみます。
map
だと、
pry(main)> age_list = {} => {} pry(main)> people.map do |person| pry(main)* age_list[person[:name]] = person[:age] pry(main)* end => [21, 30, 26] pry(main)> age_list => {"tanaka"=>21, "suzuki"=>30, "sato"=>26}
となりループの外でハッシュの初期化や参照をしなければなりません。
これをinject
で扱うと以下のようにmap
より簡単に書けます。
pry(main)> age_list = people.inject({}) do |hash, person| pry(main)* hash[person[:name]] = person[:age] pry(main)* hash pry(main)* end => {"tanaka"=>21, "suzuki"=>30, "sato"=>26}
しかしinject
の2回目以降のループで変数hash
はループの中の最後の評価値を受け取るためループの最後にhash
を与えなければなりません。
ここでeach_with_object
ではループ処理とオブジェクトの初期化を同時に可能であるため、
pry(main)> age_list = people.each_with_object({}) do |person, hash| pry(main)* hash[person[:name]] = person[:age] pry(main)* end => {"tanaka"=>21, "suzuki"=>30, "sato"=>26}
となりmap
やinject
より簡潔に書くことができます。
inject
とeach_with_object
似てるところが多いですが、以下のような違いがあります。
それぞれ
hoge.inject(object) do |one, two| hoge.each_with_object(object) do |one, two|
とした場合、
inject
は一つ目のパイプ文字(one
)で、each_with_object
は二つ目のパイプ文字(two
)でそれぞれ引数(object
)を参照するinject
→ 1周目:one
=object
, 2周目以降:one
=前回の最終評価値each_with_object
→ 1周目:two
=object
, 2周目以降:two
=two
となります。
まとめ
使い分けがややこしいループ系メソッドの違いが理解できたでしょうか。
今回は備忘録記事になりましたが、少しでもメソッド理解の役に立てていれば幸いです。
(間違いや誤りがあればご指摘お願いします!)
【Railsチュートリアル】フォロー機能のアソシエーションを解説してみる
みなさん一度くらいユーザーのフォロー機能を実装したくなったことありません?ありますか。奇遇ですね僕もです。
と、いうわけで今回の記事ではフォロー機能の実装とその解説をしたいと思います。
今回は実際のコードを見ながら説明をしていこうと思うので、実際にrailsでコードを書いたことがある人向けの内容になります。
全くコードを書いたことないんだけど!アソシエーションって何やねん!という方、なんとびっくり偶然にも僕の前回の記事がたまたまコードを書かずにアソシエーションの概念について説明しているのでそちらを見ていただければ幸いです。是非。
今回の目的
さてさて今回の記事の目的ですが、
- フォロー機能のためのアソシエーション(とそのコードの意味)の理解
とします。機能や実装方法については調べればこれでもかと出てくるので色々調べてみてください。
また今回の記事ではコードの曖昧さ回避の意味も兼ねて Ruby on Rails チュートリアル:実例を使って Rails を学ぼう (version 5.1)の14章、 14.1.1 データモデルの問題 (および解決策) から 14.1.5 フォロワー についての説明に絞ろうと思います。
railsチュートリアルをやったことがない人にもわかりやすいよう書くつもりですが、もしよくわからない部分があれば本家サイトの方も参照してみてください。
コード
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
ただ実際にはUserモデルは一つだけなので、今回の記事では以下のような図を使って解説していきたいと思います。
図2
ちなみに今回の「フォロー機能」では図のように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モデルだけですよね。follower
やfollowing
といったモデルはありません。そこで活きてくるのが二つ目の情報、class_name:
です。
class_name:
をつけることで、関連先のモデルを参照する際の名前を変更することができます。
上のコードでのbelongs_to :follower, class_name: "User"
という記述がありますが、日本語的に意訳すると
「Userモデルに対してfollower
って名前でbelongs_to
の関連付けしときますねー」
という意味になります。
なぜこんなことをするのかというと、あとで解説しますが今回実はRelationshipモデルはUserモデルに対して二つの関連付けをしたいんです。それなのに関連先として使える名前が"User"だけだと、二つの関連付けの見分けがつきませんね。そこでclass_name
で本当の名前を指定することで関連先の名前を変更できるというわけです。
この関連付けを図にすると以下のようになります。
図3
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_many
とhas_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_key
とdependent
ですが、それぞれ以下のような意味になります。
foreign_key
関連先モデルの外部キーを指定する。dependent
自モデルと関連先モデルの依存性を指定する。
foreign_key
についてですが、Relationshipモデルに生成されたインスタンスはfollower_id
や followed_id
などいくつかのデータ(カラムといいます)を持ちます。その中で、アソシエーションによって参照するカラムのことをforeign_key
(外部キー)として指定しています。
dependent
では自モデルと関連先モデルの依存性を指定します。destroy
を指定すると自モデルのインスタンスが削除された際にその関連先モデルのインスタンスも自動的に削除されるようになります。
つまり、ここでのhas_many
アソシエーションを日本語的に意訳すると、
「Relationshipモデルに対してfollower_id
を見てactive_relathionships
って名前でアソシエーションしときますねー。あ、あと関連するUserインスタンスが消えた時は対応するactive_relationships
インスタンスも消しとくんでよろしくー。」
みたいな感じになります。
この関連付けを図にすると以下のようになります。
図4
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ではfollower
とfollowed
の2つの関連付けが行われています。そのような経緯で今回の場合だとsource
ではUser
ではなくfollower
かfollowed
を指定しなければならないため、follower
を指定しているということになります.
つまり、ここでのhas_many :through
アソシエーションを日本語的に意訳すると、
「active_relationships
モデルを経由した上でfollowed
モデルに対してfollowing
って名前でアソシエーションしときますねー」
といった感じになります。
この関連付けを図にすると以下のようになります。
図5
まとめ
最終的に今回のアソシエーションを全て一つの図に表すと以下のようになります。
図6
なんとなくでもユーザーをフォローするためのアソシエーションが理解できたでしょうか。
今回の例はアソシエーションを考える上でまだ比較的簡単な方かと思いうので、ぜひいろんなアソシエーションにチャレンジしてみてください。