娯楽開発

娯楽開発

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

【イベントレポート】『THE TEAM』×『エンジニアリング組織論への招待』コラボイベント

2019/05/09(木)に行われた『THE TEAM』×『エンジニアリング組織論への招待』コラボイベントにブログ執筆枠で参加して来たので、イベントレポートを書かせていただきます!

公式のイベントページはこちら connpass.com

今回のイベントは『THE TEAM 5つの法則』の著者である麻野耕司さん、そして『エンジニアリング組織論への招待』の著者である広木大地さんとのパネルディスカッションイベントです。

『THE TEAM』ではチームについての5つの法則が述べられているのですが、今回のイベントではこの中でも『Boarding(人員選定)の法則』、『Aim(目標設定)の法則』、『Decision(意思決定)の法則』の3つの議題について浅野さんと広木さんによるディスカッションが行われました。

他の参加者の方がイベントの全文書き起こし記事を上げていらっしゃるので、このブログでは各テーマでの要点を抽出した内容を書いていこうと思います。

全文書き起こし記事はこちら note.mu

[目次]

Boarding(人員選定)の法則

f:id:sado_tech:20190512163425j:plain

Boardingの法則とは?

  • チームは環境の変化度合い人材の連携度合いという2軸の大小で4つのタイプに分類される
  • チームのタイプによって理想的な人員選定方法は異なる
  • 環境の変化度合い大きいチームではメンバーの流動性を高めるほうがいい
  • 環境の変化度合い小さいチームでは固定的なメンバーで行うほうがいい
  • 人材の連携度合い大きいチームでは多様性のあるメンバーを集めるほうがいい
  • 人材の連携度合い小さいチームでは均質性のあるメンバーを集めるほうがいい

自分のチームはどのタイプか

  • 自分たちのチームがどのタイプに当てはまるか理解するのが難しい
    • 『相手』チームが見えているか?
    • 『自分』がどんな環境の中にいるか?
  • 事業の形態と組織のタイプがリンクしていることが大事
    • 経営者は「自分の事業に合った組織とはどのようなものか」を考えるのに苦労する
    • 一般の人にも理解できるよう限りなくわかりやすくしたのがこの図

チーム間で文化衝突が発生する

f:id:sado_tech:20190512163444j:plain

  • 同じ組織の中にも様々なタイプに属するチームが混在している
    • サッカー型のエンジニアチーム、柔道団体戦型の営業チーム、野球型のバックオフィスチームなど
    • 自分たちがどのタイプに属しているか、チームの中にいるだけでは気づきにくい
  • 同じ組織内でも自分のチームやその他のチームがどのタイプに属するか互いに理解できていない
    • 各タイプ、各チームでそれぞれの文化や常識がある
  • 自分のチーム、他のチームがどのタイプに属しているかを理解せず接してしまうことで文化衝突が起きる
  • 『THE TEAM』は自分のチームや他のチームがどこに属するのか理解し、話し合いをしていくための本なのではないか

自分の常識を疑う

  • 自分の常識を疑うというのは『THE TEAM』における重要なポイント
    • エンジニアはエンジニアの中のカルチャーや正義がすべての組織やビジネスモデルに通じると思いがち
  • 今までのチームとは違う新しいチーム、組織になるために最適なものは何か」という客観的な視点を身につける必要がある
    • 事実としてチームによって文化や価値観のギャップはあるが、それは仕方のないこと
    • どうやって互いの文化や価値観を擦り合わせて行くかを考えなければならないが、そもそも常識が異なると論理的な話になりにくい
  • お互いのバックグラウンドを共有し、自分の価値観がどこにあるか整理しながら話し合う際の道具として『THE TEAM』は有効なのではないか
    • 今までのチームのあり方は一つのパターンにすぎないことを認識する

Aim(目標設定)の法則

f:id:sado_tech:20190512163441j:plain

Aimの法則とは?

  • 「目標をどのように達成するか?」も大切だが、そもそも「目標を適切に設定すること」が重要である
  • 目標には意義目標成果目標行動目標の3種類がある
  • 3種類のうちどれが良い/悪いではなく、3種類の目標にはそれぞれメリット/デメリットがある
  • チームの状況に合わせてどの目標を設定するかが重要

近年のトレンドは意義目標

  • 昔の日本の評価方法は全て行動目標だった
  • 環境の変化が少ない場合は行動目標が活きる
    • どんな行動をすれば成果が出るのかイメージしやすい
  • 90年代ごろから環境の変化が早くなることで行動目標が活きにくくなり、成果目標が主流になった
    • MBO(Management By Objectives)
    • 数値を用いて目標を設定
  • 近年はさらに環境の変化が早くなったため意義目標がトレンドに
    • 環境変化によって数ヶ月前に立てた数値目標が役に立たない場面も
    • OKR(Objectives and Key Results)
    • 目的(Objectives)から認識を合わせ、成果目標(Key Results)にブレイクダウンする

具体と抽象を行き来する能力をどう養うか

  • 目標を設定する意義の一つに「フォーカスを作る」というものもある
    • 意義目標が抽象的すぎても成果目標や行動目標に落としづらい
    • 意義目標を設定する上で具体化と抽象化のバランスが難しい
    • 結果として目標が設定しやすい成果目標に留まりがち
  • 抽象度の上げ下げは日常的に訓練する必要がある
    • 日本人は学校で意義目標を立てることを習ってない
    • 与えられた成果目標や行動目標をきちんとやることだけ学ばされる
    • (麻野さんも)今でも目的の設定は難しく感じるので、日々鍛えられている

目標の納得感とフィット感

  • 中には行動目標寄りの課題しか処理できないメンバーもいる
    • いきなり意義目標から考えさせてしまうと逆に混乱を招いてしまいかねない
  • 納得感とフィット感をどのように見極めていくのか?
    • チームメンバーの能力レベルもしくはチームや事業の環境変化を見て考える
  • 能力レベルを見る場合
    • 「自ら考えて行動を起こす」能力のレベルが高ければ意義目標や成果目標を渡してあげたほう状況に合わせた動きができるので成果が出やすい
    • 「自ら考えて行動を起こす」能力のレベルが低い場合は「これをやればうまくいく」という行動目標までブレイクダウンしないとずれた方向に向かってしまうことも
  • 環境変化を見る場合
    • 環境の変化が大きくないのであれば、具体的な勝ちパターンを行動目標レベルで定めてしまうのが強い
    • 環境の変化が大きい場合は抽象度をあげて、意義目標で動いてもらったほうがいい
  • この2つの観点の狭間にどのような目標設定が適切かというポイントがある

Decision(意思決定)の法則

f:id:sado_tech:20190512163443j:plain

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"に対してのエラーで止まってしまい配列array4まで実行することができません。

# 実行結果
2
3
TypeError: String can't be coerced into Fixnum
from (pry):80:in `+'

こんな時に

  1. 正常に動作した場合の処理
  2. エラーが発生した際の処理

を分岐して実行してくれるのが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人決め、それ以外の人は随時意見やアドバイスを投げかけ話し合いながらコーディングを行います。

f:id:sado_tech:20180914180713p:plain

複数人でひとつのコードを書く、と聞くと効率が悪そうに思えますが、ペアプロ・モブプロには以下のようなメリットがあると言われています。

メリット

  • 一人で悩む時間が解消される
  • 属人化している知見を共有できる
  • コードレビューの時間を設けずにすむ
  • チームメンバーのわからないことがわかる

確かにひとつのプログラム作成にかかる工数は増えますが、コードレビューや情報共有にかかる時間は削減できチーム全体のレベルも底上げしやすいという恩恵もあるようです。

また僕の所属するチームでは前述のペアプロ・モブプロの恩恵を最大限受けるために、以下のようなルールを設けて実施していました。

ルール

  • 15〜30分程度を1タームとする
  • タイマーで時間を計る
  • 1タームごとにドライバーを交代する
  • そのタームで何をするのか事前にゴールを決める
  • ドライバーはいま何を考えているか随時口に出す
  • わからないことがあればわからないと言う
  • その他思ったことはその場で発言する

なにやらハイレベルそうな感じですが、そんな現場で入社半月の新人が感じたことを書いてみようと思います。

入社当時のレベル感

入社当時の僕のエンジニアとしてのレベル感です。

  • プログラム経験 ... C, Pythonでコードを書いた経験あり。大学の研究で必要となる統計処理系のプログラムがメイン。RubyJavaScriptなどは触ったことがない。

  • 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ステップです。

  1. Gemfileに記述を追加する
  2. サーバーを再起動する

とてもシンプルですね。

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. パーシャルとして参照用のファイルを作成する
  2. (元ファイルに)パーシャルを参照する処理を書く

早速見ていきましょう。

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.com

ツイート機能の概要

まずはモデル構成(モデル間のアソシエーション)を考える上で押さえておきたいTwitterのツイート機能の特徴について一度まとめておきます。

  • ひとつのツイートに紐づくユーザーはただ一人のみ
  • ツイートに対してリプライ(返信)ができる
  • 返信も1つのツイートとしてカウントされる
  • 他人のツイートをリツイート(共有・拡散)できる

細かいことを言い出すと「いいね」機能や140字制限などの特徴もありますが、ここではあくまで「モデル構成を考える上で考慮する必要のある機能」を挙げています。

それでは早速これらの機能を備えるためのモデル構成を考えていきたいと思います。

登場するモデル

今回のアソシエーションで登場するモデルとそのカラムをまとめておきます。

(カラムは今回のツイート機能を実装する上で必要となるものだけを記載しています。)

Userモデル

User
id

Tweetモデル

Tweet
id
user_id

ReplyRelationshipモデル

ReplyRelationship
id
main_tweet_id
reply_tweet_id

ShareRelationshipモデル

TweetRetweetでは少しややこしいため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

解説

TweetUserについて

# users.rb
class User < ActiveRecord::Base
  has_many :tweets, dependent: :destroy
end

# tweets.rb
class Tweet < ActiveRecord::Base
  belongs_to :user
end

これはシンプルなhas_manybelongs_toの関係です。

あるuserに対して、user.tweetsでそのユーザーのツイート一覧を参照できます。

TweetReplyRelationshipについて

# 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でのリプライ(返信)は

  • リプライは主となるツイートに付随するものである
  • リプライそのものも一つのツイートである

という二つの性質を同時に持ちます。ちょうど以下の画像のような感じです。

リプライの例:@◯◯から始まる内容は特定のユーザーに向けた返信 f:id:sado_tech:20180831133307p:plain

これがFacebookなどになると、一つの投稿(Post)に対するコメント(Comment)は主従の関係がはっきりと成立するため

Post -- has_many --> Comments

Comment -- belongs_to --> Post

というわかりやすいアソシエーションとなります。

しかし今回リプライはツイートに付随するコメントとしての性質と、それ自体が一つのツイートであるという性質をあわせ持つため、Tweetモデル間を繋げる役目を持つReplyRelationshipモデルを用意しています。

このようにすることで、以前ユーザーのフォロー機能を解説した記事のようにアソシエーションを考えることができます。

sado-tech.hateblo.jp

TweetShareRelationshipについて

# 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というモデルですね。

アソシエーション自体はTweetReplyRelationshipの場合とほとんど変わりません。

まとめ

今回はTwitterのツイート機能のモデル構成について考えてみました。

個人的にツイート機能まわりはリプライの扱いが少し独特かなーという感じでしたが、なんとなく理解していただけたでしょうか。

とてもざっくりとした内容になりましたが、今回の記事が少しでも何かの参考になれば幸いです。

【ruby】ループ処理いろいろ

最近rubyのコードを触る中でいろんなループ処理を捌く機会があったので備忘録として書き留めておきます。

今回の記事で整理しているループ処理のメソッドは

  • each
  • map
  • inject
  • each_with_object

の4つです。

ループ処理は決してこれだけではないので、用途に合わない場合はいろいろ調べてみてください。

それでは早速見ていきます。

eachmap

eachmapで同じコードを書くと以下のような違いが出ます。

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 → 与えたデータにをもとに何らかの新しいデータを取得したい時

のように使い分けると便利です。

injecteach_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の繰り返し処理ですが、パイプ文字sumnはそれぞれ以下のように値を受け取っています。

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}

となりmapinjectより簡潔に書くことができます。

injecteach_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でコードを書いたことがある人向けの内容になります。

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

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

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

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