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?

【Rails】階層構造のデータ作成

Posted at

記事概要

Railsで階層構造があるデータを保存する方法をまとめる。

言語やフレームワーク

使用技術
フロントエンド HTML
バックエンド Ruby 3.2.0
Ruby on Rails 7.0.8.6
データベース MySQL
インフラ -
API -
その他 ancestry(Gem)

前提

サンプルアプリ(GitHub)

処理画面

階層構造があるデータ(categories)

Image from Gyazo

トップページ

Image from Gyazo

データ保存

Image from Gyazo

データ編集

Image from Gyazo

手順(モデルの作成+設定)

categories

モデルを作成するため、ターミナルで下記を実行する

% rails g model category

app/models/category.rbを下記のように編集する

category.rb
class Category < ApplicationRecord
  validates :name, presence: true

  has_many :items
  has_ancestry
end

items

モデルを作成するため、ターミナルで下記を実行する

% rails g model item

app/models/item.rbを下記のように編集する

item.rb
class Item < ApplicationRecord
  validates :name         , presence: true
  
  # 孫カテゴリのみを保存対象とするため、id=11~46を保存可能とする
  validates :category_id  , presence: true, format: { with: /\A1[1-9]|[2-3][0-9]|4[0-6]+\z/, message: "can't save value" }
  
  belongs_to :category
end

テーブルの編集

categories

db/migrate/yyyymmddhhmmss_create_categories.rbを下記のように編集する

yyyymmddhhmmss_create_categories.rb
class CreateCategories < ActiveRecord::Migration[7.0]
  def change
    create_table :categories do |t|
      t.string :name, null: false
      t.string :ancestry
      t.timestamps
    end
  end
end

items

db/migrate/yyyymmddhhmmss_create_items.rbを下記のように編集する

yyyymmddhhmmss_create_items.rb
class CreateItems < ActiveRecord::Migration[7.0]
  def change
    create_table :items do |t|
      t.string     :name      , null: false
      t.references :category  , null: false, foreign_key: true
      t.timestamps
    end
  end
end

シード値の作成

db/seeds.rbを下記のように編集する

seeds.rb
# 親カテゴリー
ladies,mens,baby = Category.create([{name: "レディース"}, {name: "メンズ"},{name: "ベビー・キッズ"}])

# 子カテゴリー
tops_ladies,bottoms_ladies = ladies.children.create([{name: "トップス"},{name: "ボトムス"}])
tops_mens,bottoms_mens = mens.children.create([{name: "トップス"},{name: "ボトムス"}])
kidsw_baby,omutu_baby,toy_baby = baby.children.create([{name: "キッズウェア"},{name: "おむつ/トイレ/バス"},{name: "おもちゃ"}])

# 孫カテゴリー
## レディース
tops_ladies.children.create([{name: "Tシャツ/カットソー(半袖/袖なし)"}, {name: "Tシャツ/カットソー(七分/長袖)"},{name: "シャツ/ブラウス(半袖/袖なし)"},{name: "その他"}])
bottoms_ladies.children.create([{name: "デニム/ジーンズ"},{name: "ショートパンツ"},{name: "カジュアルパンツ"},{name: "キュロット"},{name: "スカート"},{name: "その他"}])

## メンズ
tops_mens.children.create([{name: "Tシャツ/カットソー(半袖/袖なし)"}, {name: "Tシャツ/カットソー(七分/長袖)"},{name: "シャツ/ブラウス(半袖/袖なし)"},{name: "その他"}])
bottoms_mens.children.create([{name: "デニム/ジーンズ"},{name: "ショートパンツ"},{name: "カジュアルパンツ"},{name: "キュロット"},{name: "スカート"},{name: "その他"}])

## ベビー・キッズ
["コート","トップス","スカート","パンツ","ワンピース","セットアップ"].each do |name|
  kidsw_baby.children.create(name: name)
end

["おむつ用品","おまる/補助便座","トレーニングパンツ","ベビーバス","お風呂用品","その他"].each do |name|
  omutu_baby.children.create(name: name)
end

["おふろのおもちゃ","がらがら","知育玩具","その他"].each do |name|
  toy_baby.children.create(name: name)
end

シード値を作成するため、ターミナルで下記を実行する

% rails db:seed

手順(ルーティングの設定)

config/routes.rbにルーティングを設定する

routes.rb
Rails.application.routes.draw do
  root 'items#index'
  resources :items, only:[:index, :new, :create, :edit, :update] do
    get 'search'
  end
end

手順(コントローラーの作成+設定)

コントローラーを作成するため、ターミナルで下記を実行する

% rails g controller items

app/controllers/items_controller.rbを下記のように編集する

items_controller.rb
class ItemsController < ApplicationController
  def index
    @items = Item.all
  end

  def new
    @item = Item.new
    set_categories
  end

  def create
    set_categories
    @item = Item.new(set_params)
    if @item.save
      redirect_to root_path
    else
      render :new, status: :unprocessable_entity
    end
  end

  def edit
    get_item
    set_categories
  end

  def update
    get_item
    set_categories
    if @item.update(set_params)
      redirect_to root_path
    else
      render :edit, status: :unprocessable_entity
    end
  end

  def search
    item = Category.find(params[:item_id])
    children_item = item.children
    render json:{ item: children_item }
  end

  private

  def set_params
    params.require(:item).permit(:name, :category_id)
  end

  def get_item
    @item = Item.find(params[:id])
  end

  def set_categories
    @maincategories = Category.all.order("id ASC").limit(3)
  end
end

手順(ビューファイルの作成+設定)

index

app/views/itemsフォルダにindex.html.erbを手動作成し、ビューファイルを編集する

【商品一覧】  <%= link_to '商品登録', new_item_path %></br>
</br>
<% @items.each do |i| %>
  商品名:<%= i.name %> <%= link_to '編集', edit_item_path(i.id) %></br>
  カテゴリー:<%= i.category.root.name %> > <%= i.category.parent.name %> > <%= i.category.name %></br>
  </br>
<% end %>

new

app/views/itemsフォルダにnew.html.erbを手動作成し、ビューファイルを編集する

【商品登録】  <%= link_to '戻る', root_path %></br>
<%= render 'shared/form' %>

edit

app/views/itemsフォルダにedit.html.erbを手動作成し、ビューファイルを編集する

<%= @item.name %>の商品編集】  <%= link_to '戻る', root_path %></br>
<%= render 'shared/form' %>

部分テンプレート(form)

app/viewsフォルダにsharedフォルダを手動作成後、フォルダ内に_form.html.erbを手動作成し、ビューファイルを編集する

<%= form_with(model: @item, local: true) do |f|%>
  <%= render 'shared/error_messages', model: f.object %>

  <div>
    商品名
    <div>
      <%= f.text_field :name %>
    </div>
  </div>
  <div>
    カテゴリーの選択
    <div id="select-wrap">
      <%= f.collection_select :category_id, @maincategories, :id, :name, {include_blank: "---"}, {id: "parent-category" } %>
    </div>
  </div>
  </br>
  <div>
    <%= f.submit '投稿する' %>
  </div>
<% end %>

部分テンプレート(error_messages)

app/views/sharedフォルダに_error_messages.html.erbを手動作成し、ビューファイルを編集する

<% if model.errors.any? %>
<div class="error-alert">
  <ul>
    <% model.errors.full_messages.each do |message| %>
    <li class='error-message'><%= message %></li>
    <% end %>
  </ul>
</div>
<% end %>

手順(JavaScriptの作成+設定)

jsファイルの設定

app/javascriptフォルダにcategory.jsを手動作成し、下記のように編集する

category.js
function category (){
  console.log("category.jsファイルの読み込み完了")
};

window.addEventListener('turbo:load', category);
window.addEventListener("turbo:render", category);

config/importmap.rbを下記のように編集する

importmap.rb
# 中略
pin "category", to: "category.js"

app/javascript/application.jsを下記のように編集する

application.js
// 中略
import "category"

jsファイルの読み込み

サーバーを起動し、ブラウザのコンソールに「category.jsファイルの読み込み完了」と表示されることを確認する

jsファイルの編集

app/javascript/category.jsを下記のように編集する

category.js
function category (){
  const parentCategory = document.getElementById('parent-category')
  const selectWrap = document.getElementById('select-wrap')
  if (!parentCategory){ return false;}

  // 選択フォームを繰り返し表示する
  const selectChildElement = (selectForm) => {
    if (document.getElementById(selectForm) !== null) {
      document.getElementById(selectForm).remove()
    }
  }

  // Ajax通信を可能にする
  const XHR = new XMLHttpRequest();
  const categoryXHR = (id) => {
    XHR.open("GET", `/items/${id}/search`, true);
    XHR.responseType = "json";
    XHR.send();
  }

  // 子カテゴリーの値を全て取得する関数
  const getChildCategoryData = () => {
    const parentValue = parentCategory.value
    categoryXHR(parentValue)

    // コントローラーからJSON形式でレスポンスの受信が成功した時に、onloadが発火する
    XHR.onload = () => {
      const items = XHR.response.item;
      appendChildSelect(items)
      const childCategory = document.getElementById('child-select')

      // 子カテゴリーのプルダウンの値が変化することによって孫カテゴリーのイベント発火する
      childCategory.addEventListener('change', () => {
        selectChildElement('grand-child-select-wrap')
        getGrandchildCategoryData(childCategory)
      })
    }
  }

  // 子カテゴリーの値を全て取得する関数
  const appendChildSelect = (items) => {

    const childWrap = document.createElement('div')
    const childSelect = document.createElement('select')

    childWrap.setAttribute('id', 'child-select-wrap')
    childSelect.setAttribute('id', 'child-select')

    //子カテゴリーの初期値設定
    const childOption = document.createElement('option')
    childOption.innerHTML = '---'
    childSelect.appendChild(childOption)

    // forEach文でitem(子カテゴリーの値)を繰り返す
    items.forEach(item => {
      const childOption = document.createElement('option')
      childOption.innerHTML = item.name
      childOption.setAttribute('value', item.id)
      childSelect.appendChild(childOption)
    });

    childWrap.appendChild(childSelect)
    selectWrap.appendChild(childWrap)
  }

  // 孫カテゴリーの値を全て取得する関数 
  const getGrandchildCategoryData = (grandchildCategory) => {
    const grandchildValue = grandchildCategory.value
    categoryXHR(grandchildValue)

    // コントローラーからJSON形式でレスポンスの受信が成功した時に、onloadが発火する
    XHR.onload = () => {
      const GrandChildItems = XHR.response.item;
      appendGrandChildSelect(GrandChildItems)
    }
  }

  // 孫カテゴリーのプルダウンを表示させる関数
  const appendGrandChildSelect = (items) => {

    const childWrap = document.getElementById('child-select-wrap')
    const grandchildWrap = document.createElement('div')
    const grandchildSelect = document.createElement('select')

    grandchildWrap.setAttribute('id', 'grand-child-select-wrap')
    grandchildSelect.setAttribute('id', 'grand-child-select')
    grandchildSelect.setAttribute('name', 'item[category_id]') // category_idの値を上書き

    //孫カテゴリーの初期値設定
    const grandchildOption = document.createElement('option')
    grandchildOption.innerHTML = '---'
    grandchildOption.setAttribute('value', 0)
    grandchildSelect.appendChild(grandchildOption)

    // forEach文でitem(孫カテゴリーの値)を繰り返す
    items.forEach(item => {
      const grandchildOption = document.createElement('option')
      grandchildOption.innerHTML = item.name
      grandchildOption.setAttribute('value', item.id)
      grandchildSelect.appendChild(grandchildOption)
    });

    grandchildWrap.appendChild(grandchildSelect)
    childWrap.appendChild(grandchildWrap)
  }

  // 親カテゴリーを選択した後の発火するイベント
  parentCategory.addEventListener('change', function () {
    selectChildElement('child-select-wrap')
    getChildCategoryData()
  })
};

window.addEventListener('turbo:load', category);
window.addEventListener("turbo:render", category);

備考

  • ancestry(Gem)を使用しない場合テーブル設計が複雑になるため、Gemを適用する
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?