複数のモデルを横断したリストを作成する

タイトルの通り、Rails で複数のモデルを横断したリストを作る場合のやり方を調べたらできたので、メモ的に残しておく。

目次

参考

準備

とりあえず、rails の環境を用意します。
モデルは、scaffold を使用して user と food を作ります。
この user と food は持っている要素が異なっていることとします。

  • user は、名前(name)と年齢(age)
  • food は、名前(name)と価格(price)

以下のコマンドを実行します。

1
2
3
4
5
6
7
bundle init
bundle install --path vender/bundle
bundle exec rails new .
bundle exec rails db:create
bundle exec rails g scaffold user name:string age:integer
bundle exec rails g scaffold food name:string price:integer
bundle exec rails db:migrate

モデル横断リスト実装

データ準備

bundle exec rails sで起動し、localhost:3000/users localhost:3000/foodsそれぞれにアクセスして、一旦データを作成します。
user は 1 件、food は 3 件登録します。

一旦確認

そもそも、どうやってモデルを横断したリストを作れるのか。
コンソールで確認します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
irb(main):001:0> @users=User.all
User Load (1.6ms) SELECT "users".* FROM "users" LIMIT ? [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<User id: 1, name: "UserA", age: 29, created_at: "2020-02-28 14:59:43", updated_at: "2020-02-28 15:01:22">]>
irb(main):002:0> @foods=Food.all
Food Load (0.2ms) SELECT "foods".* FROM "foods" LIMIT ? [["LIMIT", 11]]
=> #<ActiveRecord::Relation [#<Food id: 1, name: "FoodB", price: 1000, created_at: "2020-02-28 15:00:01", updated_at: "2020-02-28 15:01:02">, #<Food id: 2, name: "FoodC", price: 2000, created_at: "2020-02-28 15:00:35", updated_at: "2020-02-28 15:00:35">, #<Food id: 3, name: "FoodD", price: 3000, created_at: "2020-02-28 15:00:51", updated_at: "2020-02-28 15:00:51">]>
irb(main):003:0> @users.length
User Load (0.2ms) SELECT "users".* FROM "users"
=> 1
irb(main):004:0> @foods.length
Food Load (0.2ms) SELECT "foods".* FROM "foods"
=> 3
# userを1件、foodを3件が取得できていることが確認できる

irb(main):005:0> @lists=@users+@foods
=> [#<User id: 1, name: "UserA", age: 29, created_at: "2020-02-28 14:59:43", updated_at: "2020-02-28 15:01:22">, #<Food id: 1, name: "FoodB", price: 1000, created_at: "2020-02-28 15:00:01", updated_at: "2020-02-28 15:01:02">, #<Food id: 2, name: "FoodC", price: 2000, created_at: "2020-02-28 15:00:35", updated_at: "2020-02-28 15:00:35">, #<Food id: 3, name: "FoodD", price: 3000, created_at: "2020-02-28 15:00:51", updated_at: "2020-02-28 15:00:51">]
irb(main):006:0> @lists.length
=> 4

# まさかのそのまま合体できる!!

irb(main):007:0> @lists[0].class
=> User(id: integer, name: string, age: integer, created_at: datetime, updated_at: datetime)
irb(main):008:0> @lists[1].class
=> Food(id: integer, name: string, price: integer, created_at: datetime, updated_at: datetime)
irb(main):009:0> @lists[2].class
=> Food(id: integer, name: string, price: integer, created_at: datetime, updated_at: datetime)
irb(main):010:0> @lists[3].class
=> Food(id: integer, name: string, price: integer, created_at: datetime, updated_at: datetime)

#それぞれの要素のクラスで区別できる

irb(main):011:0> @lists_f2=@lists.first(2)
=> [#<User id: 1, name: "UserA", age: 29, created_at: "2020-02-28 14:59:43", updated_at: "2020-02-28 15:01:22">, #<Food id: 1, name: "FoodB", price: 1000, created_at: "2020-02-28 15:00:01", updated_at: "2020-02-28 15:01:02">]
irb(main):012:0> @lists_f2.length
=> 2

#連結した状態で、先頭2件だけ取得できた!!

というわけで、それぞれの Active Record 取得結果は+で連結できた。
こちらを踏まえてコントローラとビュー、ルーティングを作成する。

実装

以下のファイルを作成する。
config\routes.rb

config\routes.rb
1
2
3
4
5
6
Rails.application.routes.draw do
resources :foods
resources :users
get '/mixlists', to: 'mix_lists#index' #<=ここを手で追加
# For details on the DSL available within this file, see http://guides.rubyonrails.org/routing.html
end

app\controllers\mix_lists_controller.rb

app\controllers\mix_lists_controller.rb
1
2
3
4
5
6
7
8
9
10
11
class MixListsController < ApplicationController
def index
foods = Food.all
users = User.all

# 二つのリストを合体
lists = foods + users
# 作成時刻で並べ替え
@lists_sorted = lists.sort{|a,b| a.created_at <=> b.created_at}
end
end

app\views\mix_lists\index.html.erb

app\views\mix_lists\index.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<table>
<thead>
<tr>
<th>Type</th>
<th>Name</th>
<th>age or price</th>
<th>created_at</th>
</tr>
</thead>
<tbody>
<% @lists_sorted.each do |c| %>
<tr>
<td><%= c.class %></td>
<td><%= c.name %></td>
<% if c.class == User %><!-- モデルを判定し、使用するプロパティを切り替える -->
<td><%= c.age %></td>
<td><%= link_to 'Edit', edit_user_path(c) %></td>
<% elsif c.class == Food %>
<td><%= c.price %></td>
<td><%= link_to 'Edit', edit_food_path(c) %></td>
<% end %>
</td>
<td><%= c.created_at %></td>
</tr>
<% end %>
</tbody>
</table>

確認

bundle exec rails sで起動し、localhost:3000/mixlistsにアクセスすると、以下の画面になります。

User と、Food を 1 つのリストで表示できました。
1 つのリストにしながらも、クラスを判定して user 向け food 向けそれぞれのリンクを作成することもできました。


今回はモデル横断したリストを作成しました。
最初は、「テーブルを join するとか、何かしなければいけないのでは?」などと考えて深いふっかーい泥沼にはまりましたが、問題は単純でした。
user と food を並べる状況はわかりませんが、例えば各モデルへの変更履歴を別途テーブルに作りすべての操作履歴を俯瞰で見たい場合は、利用価値がありそうです。

ではでは。