記事概要
Railsで階層構造があるデータを保存する方法をまとめる。
言語やフレームワーク
使用技術 | |
---|---|
フロントエンド | HTML |
バックエンド | Ruby 3.2.0 Ruby on Rails 7.0.8.6 |
データベース | MySQL |
インフラ | - |
API | - |
その他 | ancestry(Gem) |
前提
- Gemのancestryを適用している
サンプルアプリ(GitHub)
処理画面
階層構造があるデータ(categories)
トップページ
データ保存
データ編集
手順(モデルの作成+設定)
categories
モデルを作成するため、ターミナルで下記を実行する
% rails g model category
app/models/category.rb
を下記のように編集する
class Category < ApplicationRecord
validates :name, presence: true
has_many :items
has_ancestry
end
items
モデルを作成するため、ターミナルで下記を実行する
% rails g model item
app/models/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
を下記のように編集する
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
を下記のように編集する
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
を下記のように編集する
# 親カテゴリー
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
にルーティングを設定する
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
を下記のように編集する
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
を手動作成し、下記のように編集する
function category (){
console.log("category.jsファイルの読み込み完了")
};
window.addEventListener('turbo:load', category);
window.addEventListener("turbo:render", category);
config/importmap.rb
を下記のように編集する
# 中略
pin "category", to: "category.js"
app/javascript/application.js
を下記のように編集する
// 中略
import "category"
jsファイルの読み込み
サーバーを起動し、ブラウザのコンソールに「category.jsファイルの読み込み完了」と表示されることを確認する
jsファイルの編集
app/javascript/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を適用する