1
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で階層構造があるカテゴリーを作成する方法をまとめる。

前提

  • Ruby on Railsでアプリケーションを作成している
  • ライブラリ「pry-rails」をインストール済みである

サンプルアプリ(GitHub)

階層構造のデータベースについて

1つのデータが複数のデータに対して、「親子関係」を持っている

イメージ

各カテゴリーは複数存在するので、3世代それぞれの関係性は全て「多対多」になる
Image from Gyazo

テーブル設計

テーブル同士の関係性が「多対多」のときは、「中間テーブル」を作成するが、テーブル設計が非常に複雑になる
Image from Gyazo

ancestry

階層構造の編成を可能にするGem
ancestryを導入することにより「親・子・孫の関係」を簡単に紐付けることができる
Image from Gyazo

手順1(ancestryを導入する)

  1. Gemancestryを追加する
    詳細は、こちらを参照

手順2(モデルを生成する)

  1. Categoryモデルを作成する
    % rails g model category
    
  2. マイグレーションファイルを編集する
    db/migrate/20**********_create_categories.rb
    class CreateCategories < ActiveRecord::Migration[7.1]
     def change
       create_table :categories do |t|
         t.string :name
         t.string :ancestry
         t.timestamps
       end
     end
    end
    
  3. マイグレーションファイルを実行する
    % rails db:migrate
    
  4. Categoryモデルにancestryを追加するため、category.rbを編集する
    has_ancestryを記述することにより、モデルが階層構造になる
    app/models/category.rb
    class Category < ApplicationRecord
     has_ancestry
    end
    

手順3(データベースに値を保存する)

  1. シードファイルdb/seeds.rbを更新する
    1. シードファイルに記述されている内容を削除する
    2. ActiveRecordのcreateメソッドを使用し、categoriesテーブルに親要素を追加する
      db/seeds.rb
      # 親カテゴリー
      ladies,mens,baby = Category.create([{name: "レディース"}, {name: "メンズ"},{name: "ベビー・キッズ"}])
      
    3. ancestryを導入しているので、createメソッドで生成したオブジェクトに対して、childrenメソッドが使用できる
      変数.childrenと記述することで、子要素として扱える
      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: "おもちゃ"}])
      
    4. childrenメソッドを使用し、孫要素を追加する
      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
      
  2. categoriesテーブルにシード値を追加する
    % rails db:seed
    
  3. Sequel Proを開き、レコードが追加されていることを確認する
    Image from Gyazo
    id name ancestry 備考
    1 レディース NULL
    4 トップス 1 親カテゴリーのid
    13 シャツ/ブラウス(半袖/袖なし) 1/4 親カテゴリーと子カテゴリーのid

手順4(親カテゴリーを表示する)

  1. コントローラーとビューを作成する
    % rails g controller categories new
    
  2. ルーティングを設定する
    config/routes.rb
    Rails.application.routes.draw do
      root 'categories#new'
      resources :categories, only:[:new]
    end
    
  3. categries_controller.rbを編集する
    app/controllers/categries_controller.rb
    class CategoriesController < ApplicationController
      def new
        @categories = Category.new
        # 親カテゴリーが3つなので、limitは3とし、昇順に並び替え
        @maincategories = Category.all.order("id ASC").limit(3)
      end
    end
    
  4. app/views/categories/new.html.erbを編集し、初期の親カテゴリーのidを選択できるように記述する
    <%= form_with model: @categories, url: '/categories/new' do |f|%>
      <div>
        カテゴリーの選択
        <div id="select-wrap">
          <%#= セレクトボックスの選択肢は、初期の親カテゴリーのidを選択できるように記述 %>
          <%= f.collection_select :ancestry, @maincategories, :id, :name, {include_blank: "---"}, {id: "parent-category" } %>
        </div>
      </div>
    <% end %>
    
  5. ブラウザにて、親カテゴリが選択できることを確認する
    Image from Gyazo

手順5(子カテゴリーを表示する)

  1. jsファイルを作成する
    1. app/javascript/category.jsを手動作成する
    2. importmapを編集する
      config/importmap.rb
      # 最終行に追記
      pin "category", to: "category.js"
      
    3. application.jsを編集する
      app/javascript/application.js
      // 最終行に追記
      import "category"
      
    4. jsファイルが読み込まれるように記述する
      category.js
      function category (){
        console.log("category.jsファイルの読み込み完了")
      };
      
      window.addEventListener('turbo:load', category);
      window.addEventListener("turbo:render", category);
      
  2. jsファイルが読み込まれたことを、ブラウザにて確認する
    Image from Gyazo
  3. 子カテゴリーが表示される土台を作る
    category.js
    function category (){
      // セレクトボックスの選択肢を取得する
      const parentCategory = document.getElementById('parent-category')
    
      // selectChildElementという関数を定義
      const selectChildElement = (selectForm) => {
    
      }
    
      // parentCategoryという要素の値を選択が変更された時に、changeイベント発火
      parentCategory.addEventListener('change', function () {
        // (selectForm)へ要素が格納される
        selectChildElement('child-select-wrap')
        console.log('add child category')
      })
    };
    
    window.addEventListener('turbo:load', category);
    window.addEventListener("turbo:render", category);
    
  4. ブラウザにて、親カテゴリをクリックした際にイベント発火していることを確認する
    Image from Gyazo
  5. ルーティングを設定する
    config/routes.rb
      # 省略
      
      get '/category/:id', to: 'categories#search'
    end
    
    • 上記のように記述することで、http://localhost:3000/category/1のようなURLを生成できる
    • URLにidを付与することで、paramsにidを含め、コントローラーでidの取得が可能になる
  6. 子カテゴリーの値全て取得するための関数を記述する
    category.js
    // 省略
    
    // selectChildElementという関数を定義
    const selectChildElement = (selectForm) => {
    
    }
    
    // 子カテゴリーの値を全て取得する関数を定義
    const getChildCategoryData = () => {
    }
    
    // parentCategoryという要素の値を選択が変更された時に、changeイベント発火
    parentCategory.addEventListener('change', function () {
    
    // 省略
    
  7. 親カテゴリーの値を取得するように記述する
    category.js
    // 省略
    
    // 子カテゴリーの値を全て取得する関数を定義
    const getChildCategoryData = () => {
      // 親カテゴリーの値を取得する
      const parentValue = parentCategory.value
    }
    
    // 省略
    
  8. 非同期通信でリクエストを送信する
    category.js
    // 省略
    
    // selectChildElementという関数を定義
    const selectChildElement = (selectForm) => {
    
    }
    
    // JSON形式でレスポンスを設定し、コントローラーにリクエストを送信
    const XHR = new XMLHttpRequest();
    const categoryXHR = (id) => {
      // /category/:idというエンドポイントへのリクエストを記述
      XHR.open("GET", `/category/${id}`, true);
      XHR.responseType = "json";
      XHR.send();
    }
    
    // 子カテゴリーの値を全て取得する関数を定義
    const getChildCategoryData = () => {
      // 親カテゴリーの値を取得する
      const parentValue = parentCategory.value
      // 子カテゴリーの値を取得するためには親カテゴリーと紐付ける必要があるため、親カテゴリーの値を引数とする
      categoryXHR(parentValue)
    }
    
    // parentCategoryという要素の値を選択が変更された時に、changeイベント発火
    parentCategory.addEventListener('change', function () {
      // (selectForm)へ要素が格納される
      selectChildElement('child-select-wrap')
      // getChildCategoryData()の処理を呼び出す
      getChildCategoryData()
    })
    
    // 省略
    
  9. searchアクションを定義し、レスポンスを返す
    app/controller/categories_controller.rb
      # 省略
      
      def search
        # idをもとに、該当する親カテゴリーのレコードを取得しitemに代入
        item = Category.find(params[:id])
        # 子カテゴリーの要素を取得し、children_itemに代入
        children_item = item.children
        # JSON形式でレスポンスを返す
        render json:{ item: children_item }
      end
    end
    
  10. 値が送られているかを確認する
    1. 値が送られているか確認するため、binding.pryを追記する
      app/controller/categories_controller.rb
        def search
          # idをもとに、該当する親カテゴリーのレコードを取得しitemに代入
          item = Category.find(params[:id])
          # 子カテゴリーの要素を取得し、children_itemに代入
          children_item = item.children
          binding.pry
          
          # JSON形式でレスポンスを返す
          render json:{ item: children_item }
        end
      end
      
    2. ブラウザで親カテゴリーを選択する
    3. ターミナルを開き、変数item children_itemに正しく値を取得できているか確認
      Image from Gyazo
    4. binding.pryを削除する
  11. レスポンス後の処理を実行する
    category.js
    // 省略
    
    // 子カテゴリーの値を全て取得する関数を定義
    const getChildCategoryData = () => {
      // 親カテゴリーの値を取得する
      const parentValue = parentCategory.value
      // 子カテゴリーの値を取得するためには親カテゴリーと紐付ける必要があるため、親カテゴリーの値を引数とする
      categoryXHR(parentValue)
    
      // コントローラーからのレスポンスの受信に成功した場合の処理
      XHR.onload = () => {
        // searchアクションから返却したitemは、「XHR.response.item」で取得できる
        const items = XHR.response.item;
      }
    }
    
    // 省略
    
  12. 子カテゴリーのプルダウンを表示する
    category.js
    function category (){
      // セレクトボックスの選択肢を取得する
      const parentCategory = document.getElementById('parent-category')
      // 選択フォーム画面を取得
      const selectWrap = document.getElementById('select-wrap')
    
      // 中略
    
      const appendChildSelect = (items) => {
    
        // div要素とselect要素を生成する
        const childWrap = document.createElement('div')
        const childSelect = document.createElement('select')
    
        // 「div要素」に、('id', 'child-select-wrap')を追加
        childWrap.setAttribute('id', 'child-select-wrap')
        // 「select要素」に('id', 'child-select')を追加
        childSelect.setAttribute('id', 'child-select')
    
        // forEach文でitems(コントローラーから返却された、子カテゴリーの値)を繰り返し表示
        items.forEach(item => {
          const childOption = document.createElement('option')
          childOption.innerHTML = item.name
          childOption.setAttribute('value', item.id)
          childSelect.appendChild(childOption)
        });
    
        // childSelectの中に、子要素として追加
        childWrap.appendChild(childSelect)
        // childWrapの中に、子要素として追加
        selectWrap.appendChild(childWrap)
      }
    
      // parentCategoryという要素の値を選択が変更された時に、changeイベント発火
      parentCategory.addEventListener('change', function () {
        // (selectForm)へ要素が格納される
        selectChildElement('child-select-wrap')
        // getChildCategoryData()の処理を呼び出す
        getChildCategoryData()
      })
    };
    
    window.addEventListener('turbo:load', category);
    window.addEventListener("turbo:render", category);
    
  13. 下記のような子カテゴリーの選択フォームを表示することができる
    <div id = "child-select-wrap">
     <select id = "child-select">
       <option value="item.id">item.name</option>
       <option value="item.id">item.name</option>
       <option value="item.id">item.name</option>
       <option value="item.id">item.name</option>
       <option value="item.id">item.name</option>
       <option value="item.id">item.name</option>
    
  14. 子カテゴリーの値を取得する
    category.js
    // 省略
    
    // コントローラーからのレスポンスの受信に成功した場合の処理
    XHR.onload = () => {
      // searchアクションから返却したitemは、「XHR.response.item」で取得できる
      const items = XHR.response.item;
      // appendChildSelectで定義した処理を実行し、子カテゴリーの選択フォームを取得する
      appendChildSelect(items)
      const childCategory = document.getElementById('child-select')
    }
    
    // 省略
    
  15. ブラウザにて、子カテゴリーが選択できることを確認する
    Image from Gyazo

手順6(孫カテゴリーを表示する)

  1. 子カテゴリーのプルダウンの値が変化することによって、孫カテゴリーの選択フォームが表示されるイベントを定義する
    category.js
    // 省略
    
    // コントローラーからのレスポンスの受信に成功した場合の処理
    XHR.onload = () => {
      // searchアクションから返却したitemは、「XHR.response.item」で取得できる
      const items = XHR.response.item;
      // appendChildSelectで定義した処理を実行し、子カテゴリーの選択フォームを取得する
      appendChildSelect(items)
      const childCategory = document.getElementById('child-select')
    
      // 子カテゴリーのプルダウンの値が変化することによって、孫カテゴリーの選択フォームが表示されるイベントを定義
      childCategory.addEventListener('change', () => {
        selectChildElement('grand-child-select-wrap')
        getGrandchildCategoryData(childCategory)
      })
    }
    
    // 省略
    
  2. 孫カテゴリーの値を取得する関数を定義する
    category.js
      // 省略
      
      // 孫カテゴリーの値を取得する関数を定義
      const getGrandchildCategoryData = (grandchildCategory) => {
        const grandchildValue = grandchildCategory.value
        categoryXHR(grandchildValue)
    
        XHR.onload = () => {
          const GrandChildItems = XHR.response.item;
          appendGrandChildSelect(GrandChildItems)
        }
      }
    
      // parentCategoryという要素の値を選択が変更された時に、changeイベント発火
      parentCategory.addEventListener('change', function () {
        // (selectForm)へ要素が格納される
        selectChildElement('child-select-wrap')
        // getChildCategoryData()の処理を呼び出す
        getChildCategoryData()
      })
    };
    
    window.addEventListener('turbo:load', category);
    window.addEventListener("turbo:render", category);
    
  3. 孫カテゴリーのプルダウンを表示させる
    category.js
      // 省略
      
      // 孫カテゴリーのプルダウンを表示させる
      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')
    
        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という要素の値を選択が変更された時に、changeイベント発火
      parentCategory.addEventListener('change', function () {
        // (selectForm)へ要素が格納される
        selectChildElement('child-select-wrap')
        // getChildCategoryData()の処理を呼び出す
        getChildCategoryData()
      })
    };
    
    window.addEventListener('turbo:load', category);
    window.addEventListener("turbo:render", category);
    
  4. ブラウザにて、孫カテゴリーが選択できることを確認する
    Image from Gyazo

手順7(選択フォームを繰り返し表示する)

  1. 現状、再度ジャンルの違うカテゴリーを選択しようとすると、選択フォームが下に連なってしまう
    Image from Gyazo
  2. 孫カテゴリーまで選択した時に、再度違うジャンルのカテゴリーが選択できるように記述する
    category.js
    // 省略
    
    // selectChildElementという関数を定義
    const selectChildElement = (selectForm) => {
      // 再度違うジャンルのカテゴリーが選択できるようにする
      if (document.getElementById(selectForm) !== null) {
        document.getElementById(selectForm).remove()
      }
    }
    
    // 省略
    
  3. ブラウザを確認する
    Image from Gyazo
1
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
1
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?