0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

初学者によるubuntuでのRails + React で駅案内システム作り④~1:1、1:mで対応したモデルの登録更新、JSON出力~

Posted at

前回までのまとめ

has_many throuのアソシエーションを設定できた! やったね!
あとはScaffold機能を使わなくてもCRUDを作ることが出来た。

記事一覧

アソシエーションつづき

さて、アソシエーションの続きになります。前回はm:n、間に中間テーブルをはさんだものを作りました。
ここで新規テーブルを考えます。具体的には駅情報の拡充を目指したいところです。
image.png
駅情報のカラムを既存の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の時の記述と変わります。

app/models/station.rb
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
app/models/station_info.rb
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を改修したほうがよさそうです。

stations_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も改修しちゃいます。

app/views/stations/_form.html.erb
<%= 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 %>

image.png
登録後の駅詳細画面もとりあえず表示だけさせました。
image.png

トイレの有無、田舎の駅であればあるほど知りたいですよね。
あとほかにも情報を追加していきたいな、そんなときはカラムを追加してあげる必要があります。

ターミナル
$ rails generate migration <マイグレーションファイル名>

マイグレーションファイル名については良い感じに指定すれば自動でマイグレーションファイルの中身を書いてもらえるんですが、今回は中身についてうんうん悩みながら追加したいので自力で書くことにしました。

202301311144651_add_column.rb
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してあげることでテーブルにカラムを追加反映させることが出来ました。
ちなみに追加したのはみどりの窓口の有無と営業時間、指定席券売機と稼働時間になります。

あとは駅の歴史も情報として知れたら嬉しくないですか? 
image.png

ターミナル
$ rails g model station_history station_id:inte
ger event_date:date event_title:string event_detail:string

ということで、動的なフォーム入力欄を設けることにしました。しかし、ここでまさかの難題が発生します。

app/assets/application.jsなんてファイルは無いが?

これ設定しないとフォームの動作が上手くいかないんですよね……
動作確認1.gif
ということで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に書くことにしました。

app/view/layouts/application.html.erb
<script src="https://code.jquery.com/jquery-3.3.1.slim.min.js"></script>

。。。動かない・・・これは困りました。
今回は仕方ない! フォームの自動追加は本題ではないのでこの課題は後回しにして別の方法を取ることにしました。
image.png

これで登録を押下すると登録が進み、下に新規で登録フォームが展開されるようにしました。
といっても今回は複雑な使用にはしていません。

改修

既存の駅テーブルに、駅の歴史テーブルを紐づけます。

app/models/station.rb
class Station < ApplicationRecord
  # 今回追加したのは下2つ
  has_many :station_histories
  accepts_nested_attributes_for :station_histories, update_only: true
end
app/models/station_history.rb
class StationHistory < ApplicationRecord
  belongs_to :station
end

Controllerはこんな感じに

station_histories_controller.rb
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は駅の歴史情報単独では作成しないことにしました
既存の駅情報画面の修正を実施

app/views/stations/show.html.erb
<!-- 追加実施 -->
  <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ファイルを一部修正します

app/controllers/stations_controller.rb
# GET /stations/1
  def show
    @station_history = StationHistory.new
  end

ということで実装後このような感じで動くようになりました!
qiita説明用動画2.gif

リレーションを考慮したJSON出力

さて、話はいくつか前の記事にて取り扱ったjbuilderファイル作成によるAPI通信の諸々を作成します。
今現状のjbuilderファイルは

app/views/api/reactapi/stations/index.json.jbuilder
json.array! @stations, :name, :name_kana, :name_english, :name_russian, :opening_date, :closing_date

これではアソシエーション設定したテーブルをAPIで拾ってこれません。ということでちょいちょい編集していきます。
ここでのstation_infostationと1対1、station_historystationと1対多、ということでjbuilderの内容はこんな感じに。

app/views/api/reactapi/stations/index.json.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を早速叩いてみたところ、
(下の出力結果は省略表示です)

GET http://localhost:3000/api/reactapi/stations
[
  {
    name: "根室",
    name_kana: "ねむろ",
    station_info: {
      longtitude: 145.582929,
      latitude: 43.326793,
    }
    station_histories: [
      {
        event_date: "1921-08-05",
        event_title: "開業",
        event_detail: "西和田駅から延伸開業"
      }
    ]
  }
]

良い感じですね! これでフロント側で取得した際に良い感じに加工して表示できそうです

次からは恐らく! フロント側の諸々を! 書きます! たぶん!

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?