娯楽開発

娯楽開発

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

【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

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

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

【初心者向け】アソシエーションって何?というあなたへ【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のように)参照したい

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

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

まとめ

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

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