前回までのまとめ
has_many throuのアソシエーションを設定できた! やったね!
あとはScaffold機能を使わなくてもCRUDを作ることが出来た。
記事一覧
- 初学者によるubuntuでのRails + React で駅案内システム作り①~下準備編~
- 初学者によるubuntuでのRails + React で駅案内システム作り②~Rails+ReactでAPI通信編~
- 初学者によるubuntuでのRails + React で駅案内システム作り③~Scaffold機能を使わずCRUD~
-
初学者によるubuntuでのRails + React で駅案内システム作り④~1:1、1:mで対応したモデルの登録更新、JSON出力~
- 今回はこちら!
アソシエーションつづき
さて、アソシエーションの続きになります。前回はm:n、間に中間テーブルをはさんだものを作りました。
ここで新規テーブルを考えます。具体的には駅情報の拡充を目指したいところです。
駅情報のカラムを既存のstationsテーブルに持たせるのも良いですが、カラム数がどれだけ増えるのか未知数なのと、stationsテーブルは最低限に留めておきたいので新規にstation_infosテーブルを作成し、こちらで沢山駅情報のカラムを持たせることにしました。
$ rails g model station_info station_id:integer longtitude:float latitude:float toilet_exist:boolean
今回は1:1、has_oneアソシエーションになるのでモデルファイルの中身もhas_many throughの時の記述と変わります。
class Station < ApplicationRecord
# has_many through の設定
has_many :station_line_relations
has_many :lines, through: :station_line_relations
# has_one(stationが1に対してstation_infoを1つ持つ)
has_one :station_info
end
class StationInfo < ApplicationRecord
# has_one(stationとstation_infoが1対1になる)
belongs_to :station
end
あとはこれを登録する画面を作りたいのですが、よくよく考えたら既存の駅登録画面で登録を行いたいですよね? ということでcontroller、viewを新規で作ることはせずに既存のものを改修していくことにしました。
accepts_nested_attributes_forの利用検討
始めはこれを使って駅テーブルと駅情報テーブルの両方を同時にレコード作成をすることを考えていました。ただ、これだと追加するformのメンテナンスが結構大変らしいのとActiveRecordから拡張したときに使えないとかなんとかであまり良くないらしい。いろんな記事を探してもaccepts_nested_attributes_for
を使っている記事ばかりなんですが……。色々影響範囲が大きいメソッドだから避けたい場合もあるという感じでしょうか。とはいえ今回は1:1でそれほ複雑にする予定は無いので使うことにしました。フラグにならないでくれと祈りつつ。伏線回収しなくていいからここで……
今回途中から1:1対応のテーブルを追加したので、そのことも考慮してControllerを改修したほうがよさそうです。
class StationsController < ApplicationController
# 改修した箇所だけ抜粋して表示
# GET /stations/new
def new
@station = Station.new
# 1:1になるのでbuild_(対応させるモデル)
@station.build_station_info
end
# GET /stations/1/edit
def edit
# 1:1になるのでbuild_(対応させるモデル)
# 今回途中で追加したのでstationモデルに対応したstation_infoが無いケースも考えうるので条件分岐を設定
unless StationInfo.find_by(station_id: @station.id)
@station.build_station_info
end
end
private
# Only allow a list of trusted parameters through.
def station_params
# station_info_attributes内ではidもpermitに追加しないとUnpermitted parameter: :idと怒られるので注意
params.require(:station).permit(:name, :name_kana, :name_english, :name_russian, :opening_date, :closing_date, station_info_attributes: [:id, :longtitude, :latitude, :toilet_exist])
end
end
viewも改修しちゃいます。
<%= form_with(model: station) do |form| %>
<!--以下改修箇所抜粋-->
<div>
<%= form.fields_for :station_info do |g| %>
<div class="col-2">
<%= g.label :longtitude, style: "display: block" %>
<%= g.text_field :longtitude, class: 'form-control' %>
</div>
<div class="col-2">
<%= g.label :latitude, style: "display: block" %>
<%= g.text_field :latitude, class: 'form-control' %>
</div>
<div class="col-2">
<%= g.label :toilet_exist, style: "display: block" %>
<%= g.select :toilet_exist, [['トイレあり',true],['トイレなし',false]], { include_blank: true } %>
</div>
<% end %>
</div>
<% end %>
トイレの有無、田舎の駅であればあるほど知りたいですよね。
あとほかにも情報を追加していきたいな、そんなときはカラムを追加してあげる必要があります。
$ rails generate migration <マイグレーションファイル名>
マイグレーションファイル名については良い感じに指定すれば自動でマイグレーションファイルの中身を書いてもらえるんですが、今回は中身についてうんうん悩みながら追加したいので自力で書くことにしました。
class AddColumn < ActiveRecord::Migration[7.0]
def change
add_column :station_infos, :ticket_office_exist, :boolean
add_column :station_infos, :ticket_office_start_1, :time
add_column :station_infos, :ticket_office_end_1, :time
add_column :station_infos, :ticket_office_start_2, :time
add_column :station_infos, :ticket_office_end_2, :time
add_column :station_infos, :ticket_office_start_3, :time
add_column :station_infos, :ticket_office_end_3, :time
add_column :station_infos, :reservedseat_ticket_machine, :boolean
add_column :station_infos, :reservedseat_ticket_machine_start, :time
add_column :station_infos, :reservedseat_ticket_machine_end, :time
end
end
これでrails db:migrate
してあげることでテーブルにカラムを追加反映させることが出来ました。
ちなみに追加したのはみどりの窓口の有無と営業時間、指定席券売機と稼働時間になります。
$ rails g model station_history station_id:inte
ger event_date:date event_title:string event_detail:string
ということで、動的なフォーム入力欄を設けることにしました。しかし、ここでまさかの難題が発生します。
app/assets/application.jsなんてファイルは無いが?
これ設定しないとフォームの動作が上手くいかないんですよね……
ということでGitを確認してみましたが
https://github.com/ncri/nested_form_fields
application.jsが無い問題は何も解決していませんね。ということで調べてみたところ、どうもRails6以降とRails5までとでアセットパイプラインが異なっている関係で違うらしい。
ということで別のgemを探してみましたが、どうもcocoonというgemがとても機能として似ていたのでこれを使うことにしました。
jQuery導入……したかった話
gemの取説を読んでみると
This gem depends on jQuery, so it's most useful in a Rails project where you are already using jQuery.
とのことなので、早速導入です!
$ yarn add jquery
をやったのはいいけれど、Rails7系にwebpackフォルダが無い……一旦力業でjQuery云々をapplication.hyml.erbに書くことにしました。
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"></script>
。。。動かない・・・これは困りました。
今回は仕方ない! フォームの自動追加は本題ではないのでこの課題は後回しにして別の方法を取ることにしました。
これで登録を押下すると登録が進み、下に新規で登録フォームが展開されるようにしました。
といっても今回は複雑な使用にはしていません。
改修
既存の駅テーブルに、駅の歴史テーブルを紐づけます。
class Station < ApplicationRecord
# 今回追加したのは下2つ
has_many :station_histories
accepts_nested_attributes_for :station_histories, update_only: true
end
class StationHistory < ApplicationRecord
belongs_to :station
end
Controllerはこんな感じに
class StationHistoriesController < ApplicationController
before_action :set_station_history, only: %i[ show edit update destroy ]
def new
@station_history = StationHistory.new
end
def edit
end
def create
@station_history = StationHistory.new(station_history_params)
respond_to do |format|
if @station_history.save
format.html { redirect_to station_url(@station_history.station_id), notice: "駅の歴史情報の登録に成功しました." }
else
format.html { render :new, status: :unprocessable_entity }
end
end
end
def update
respond_to do |format|
if @station_history.update(station_history_params)
format.html { redirect_to station_url(@station_history.station_id), notice: "駅の歴史情報の更新に成功しました." }
else
format.html { render :edit, status: :unprocessable_entity }
end
end
end
def destroy
@station_history.destroy
respond_to do |format|
format.html { redirect_to station_url(@station_history.station_id), notice: "駅の歴史情報の削除に成功しました." }
end
end
private
# Use callbacks to share common setup or constraints between actions.
def set_station_history
@station_history = StationHistory.find(params[:id])
end
# Only allow a list of trusted parameters through.
def station_history_params
params.require(:station_history).permit(:station_id, :event_date, :event_title, :event_detail)
end
end
今回viewは駅の歴史情報単独では作成しないことにしました
既存の駅情報画面の修正を実施
<!-- 追加実施 -->
<table class="table">
<thead>
<th>編集</th>
<th>年月日</th>
<th>出来事</th>
<th>詳細</th>
<th></th>
<th>削除</th>
</thead>
<tbody>
<% @station.station_histories.order(event_date: :asc).each do |station_history| %>
<tr>
<%= form_with(model: station_history) do |form| %>
<td><%= form.collection_select :station_id, Station.all, :id, :name, options = { selected: @station.id }, class: 'form-control' %></td>
<td><%= form.date_field :event_date, class: 'form-control' %></td>
<td><%= form.text_field :event_title, class: 'form-control' %></td>
<td><%= form.text_area :event_detail, class: 'form-control' %></td>
<td><%= form.submit '更新', class: 'btn btn-primary mx-1' %></td>
<% end %>
<td><%= button_to "削除", station_history, {method: :delete, class: "btn btn-danger mx-1"} %></td>
</tr>
<% end %>
<%= form_with(model: @station_history) do |form| %>
<tr>
<td><%= form.collection_select :station_id, Station.all, :id, :name, options = { selected: @station.id }, class: 'form-control' %></td>
<td><%= form.date_field :event_date, class: 'form-control' %></td>
<td><%= form.text_field :event_title, class: 'form-control' %></td>
<td><%= form.text_area :event_detail, class: 'form-control' %></td>
<td><%= form.submit '登録', class: 'btn btn-primary mx-1' %></td>
</tr>
<% end %>
</tbody>
</table>
あとは詳細画面で新しい定数を設定する必要があるのでcontrollerファイルを一部修正します
# GET /stations/1
def show
@station_history = StationHistory.new
end
リレーションを考慮したJSON出力
さて、話はいくつか前の記事にて取り扱ったjbuilderファイル作成によるAPI通信の諸々を作成します。
今現状のjbuilderファイルは
json.array! @stations, :name, :name_kana, :name_english, :name_russian, :opening_date, :closing_date
これではアソシエーション設定したテーブルをAPIで拾ってこれません。ということでちょいちょい編集していきます。
ここでのstation_info
はstation
と1対1、station_history
はstation
と1対多、ということでjbuilderの内容はこんな感じに。
json.array! @stations do |station|
json.extract! station,
:name,
:name_kana,
:name_english,
:name_russian,
:opening_date,
:closing_date
# station_infosを出力
json.station_info do
json.extract! station.station_info,
:longtitude,
:latitude,
:toilet_exist,
:ticket_office_exist,
:ticket_office_start_1,
:ticket_office_end_1,
:ticket_office_start_2,
:ticket_office_end_2,
:ticket_office_start_3,
:ticket_office_end_3,
:reservedseat_ticket_machine,
:reservedseat_ticket_machine_start,
:reservedseat_ticket_machine_end
end
# station_historiesを出力
json.station_histories do
json.array! station.station_histories do |station_history|
json.extract! station_history,
:event_date,
:event_title,
:event_detail
end
end
end
ポイントは、1対1のものはarray!
で出力しないことでした。これでAPIを早速叩いてみたところ、
(下の出力結果は省略表示です)
[
{
name: "根室",
name_kana: "ねむろ",
station_info: {
longtitude: 145.582929,
latitude: 43.326793,
}
station_histories: [
{
event_date: "1921-08-05",
event_title: "開業",
event_detail: "西和田駅から延伸開業"
}
]
}
]
良い感じですね! これでフロント側で取得した際に良い感じに加工して表示できそうです
次からは恐らく! フロント側の諸々を! 書きます! たぶん!