やりたいこと
チーム開発で、一人のマッサージ師に対して複数の出張範囲を登録出来る機能を実装した。モデルの関係性など勉強になったのでメモ。完全自分用です。
モデルの関係性
マッサージ師
class Masseur < ApplicationRecord
has_many :business_trip_ranges, dependent: :delete_all
has_many :cities, through: :business_trip_ranges
accepts_nested_attributes_for :business_trip_ranges, reject_if: :reject_business_trip_range, allow_destroy: true
出張範囲
class BusinessTripRange < ApplicationRecord
belongs_to :masseur
belongs_to :city
end
市町村
class City < ApplicationRecord
belongs_to :prefecture
has_many :masseurs, through: :business_trip_ranges
has_many :business_trip_ranges
end
ややこしいですが図にするとこんな感じ。

作業の流れ
1、APIで取ってきた都道府県 + 市町村データをPrefectureモデルとCityモデルに保存する
2、保存したデータを使って都道府県チェックボックスを作成していきます
3、チェックボックスにチェックした出張範囲はBusinessTripRangeに保存される
ざっくりしすぎか。
APIでデータを取ってくる
このサイトから都道府県 + 市町村データがAPIで取ってこれます。APIが使えるならこっちの方が手っ取り早いです。なおAPIキーが必要になるのでサイト内で申請してください。
https://opendata.resas-portal.go.jp/
まずは以下のようにしてAPIデータを取得し
module StoreManager::BusinessTripRangesHelper
require 'net/http'
require 'uri'
require 'json'
# 都道府県のAPIを叩き、データを出力
def prefectures_api(url)
uri = URI.parse(URI.escape(url))
https = Net::HTTP.new(uri.host, uri.port)
https.use_ssl = true
param = {}
param['X-API-KEY'] = '123hsydeusnsindidkm' # 自分が取得したAPI-KEY
req = Net::HTTP::Get.new(uri.request_uri, param)
res = https.request(req)
json = res.body
result = JSON.parse(json)
end
end
seeds.rbファイルで取得したデータをPrefectureモデルとCityモデルのカラムに入れて都道府県 + 市町村データを格納します
include StoreManager::BusinessTripRangesHelper
# 都道府県データを取得
prefectures = prefectures_api("https://opendata.resas-portal.go.jp/api/v1/prefectures")
# 東京都の市/区データを取得
cities = prefectures_api("https://opendata.resas-portal.go.jp/api/v1/cities?prefCode=13")
prefectures["result"].each do |value|
prefecture_name = value["prefName"]
Prefecture.find_or_create_by(name: prefecture_name)
end
cities["result"].each do |value|
city_name = value["cityName"]
City.find_or_create_by(name: city_name, prefecture_id: 13)
end
$ rails db:seed
これでデータがPrefectureとCityに入りました。
都道府県 + 市町村だからとんでもなくデータ多いです。写真はPrefectureモデルに入っているデータですがCityモデルにも市町村データはちゃんと入ってるぞい。
保存したデータを元にチェックボックス作る
今回使ったのはcolection_check_boxesというメソッド。なんか最初はややこしかったけど分かると使いやすいかも。
<%= form_with(model: @current_masseur, url: store_manager_business_trip_ranges_update_path(@current_masseur), method: :patch, local: true) do |f| %>
<table class="prefecture table table-hover">
<thead>
# Prefectureモデルに格納した47都道府県データを全てブロック変数prefectureに代入。
<%= collection_check_boxes :prefecture, :prefecture_ids, Prefecture.all, :id, :name, include_hidden: false do |prefecture| %>
<tr>
<th>
#ブロック変数.textで都道府県名、ブロック変数.checkboxでチェックボックスを作成
<%= prefecture.check_box + prefecture.text %>
</th>
</tr>
</thead>
<tbody>
<tr>
<td class="city-check-boxes">
<div id="city-list">
#市町村も同様です
<%= f.collection_check_boxes :city_ids, City.all, :id, :name, include_hidden: false do |city| %>
<ul class="city-check-box">
<% if prefecture.object.id == city.object.prefecture_id %>
<div id="boxes">
<li class="city-check-box-item"><%= city.check_box + city.text %></li>
</div>
<% end %>
</ul>
<% end %>
</div>
</td>
</tr>
<% end %>
</tbody>
</table>
<div class="actions text-center">
<%= f.submit "登録する", class: "btn btn-primary mt-5 w-50" %>
</div>
<% end %>
こうなります。

ちなみにコントローラーはこうなってます。
class StoreManager::BusinessTripRangesController < StoreManager::Base
before_action :set_masseur, only: [:edit, :update, :show]
def edit
@prefectures = Prefecture.all
@ranges = @current_masseur.business_trip_ranges
end
def update
# チェックがあった場合
if params[:masseur].present?
@current_masseur.update(city_business_trip_range_params)
flash[:success] = "出張範囲を更新しました。"
redirect_to store_manager_masseurs_business_trip_ranges_url
# 一つもチェックがなかった場合
else
flash[:danger] = "出張範囲を選択してください。"
render :edit
end
end
private
def set_masseur
@current_masseur = Masseur.find(params[:masseur_id])
end
def city_business_trip_range_params
if params[:masseur].present?
params.require(:masseur).permit(city_ids: [])
end
end
end
重要なのはストロングパラメーターで配列を受け取る時に
このように書く↓
params.require(:masseur).permit(city_ids: [])
もう一つはupdateアクションの@current_masseur.update(city_business_trip_range_params)
このように書くことで「作成」「編集」「削除」が同時にできてしまうこと。
故に「new」「create」「destroy」は必要ありません。
途中で眠くなったわ。。