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?

active_hashを使って何が作れるか知りたいあなたに

Posted at

はじめに

データベースやマイグレなしで実行される単純なテストクラスを作成したいとき、APIを使わずに変更されないデータをモデルファイル内に直接記述したいとき、どうやって実装するか困ることあるかと思います
今回はそれを解決するためのactive_hashのgemを用いた実装方法とactive_hashで何ができるのかについて説明いたします。

active_hashとは

ActiveRecord のようなモデルの読み取り専用データソースとして Ruby ハッシュを使用できるようにするシンプルな基本クラスです。

すべてのハッシュに:idキーがあることを前提としていますが、データベースに保存されるであろうキーです。これにより、アプリのコードやデータベースの外部キーを変更することなく、ActiveHashオブジェクトから完全なActiveRecordオブジェクトにシームレスでアップグレードできます。

また、AR オブジェクトで #has_many と #belongs_to (belongs_to_active_hash 経由) を使用することも可能です。

ActiveHash には以下のものも同梱されています:

  • 1:ActiveFile: ファイルデータソースを作成するために使用できる基本クラス
  • 2:ActiveYaml: YAMLをハッシュに変換し、そのデータをActiveHashオブジェクトにロードする基本クラス

どういうときに便利なのか

都道府県名やカテゴリー選択などの変更されないデータをモデルファイル内に直接記述することで、データベースへ保存せずにデータを取り扱うことができます。

商品を登録するフォームなどでcollection_selectを使うときの実装に便利だと考えます

対応方法

今回はトップページにクイズ機能をactive_hashを用いて実装していきます

Railsの実装なので、MVCモデルの形で進めていきます

下記の技術を用いて、環境構築しています
rails newの環境構築は各自で進めていきましょう

使用技術
ツール 内容
フロントエンド HTML,CSS,Node.js 20.17.0,esbuild
バックエンド Rails 7.2.1, (ruby 3.2.3 (2024-01-18 revision 52bb2ac0a6) [x86_64-linux])
データベース PostgreSQL
開発環境 Docker

1: ルートを設定

今回はトップページのルート側にリソースを追加します

root "static_pages#home"
# 以下省略
resources :quizzes, only: %i[index show new create destroy]

2: ActiveYamlのモデルをセットアップする

Gemfileに以下を追加して、ライブラリをインストールします

gem 'active_hash'

bash環境でインストールする場合

bundle install

Dockerの仮想環境でインストールする場合

docker compose run web bundle install

3: モデルを生成

今回はquizモデルとquestionモデルを生成します
active_hashを使って実装するので、マイグレは生成しないようコマンドに

--skip-migration

を付け加えるのを忘れないでください


Quizモデル

rails generate model Quiz --skip-migration

app/models/quiz.rb

class Quiz
  include ActiveModel::Model
  # ActiveModel::Modelモジュールには、
  # Action PackやAction ViewとやりとりするためのActiveModel::APIが
  #デフォルトで含まれており、モデル的なRubyクラスを実装する場合はこの方法が推奨されています。
  include ActiveModel::Attributes
  # ActiveModel::Attributesモジュールを利用することで、
  # データ型の定義、デフォルト値の設定、PORO(プレーンなRubyオブジェクト)のキャストや
  # シリアライズの処理が可能になります。
  # これは、フォームデータで通常のオブジェクトの日付やブーリアン値などに対して
  # Active Recordと同じような変換を行うときに便利です。
  
  attr_accessor :question_id, :selected_answers

  validates :selected_answers, presence: true
end

Questionモデル
rails generate model Question --skip-migration

app/models/question.rb
yamlで定義したデータを使用したいモデルに読み込みます

class Question < ActiveYaml::Base
    set_root_path "app/models/active_hash"
    # app/models/active_hash/questions.ymlのにつなげて、問題を表示できるようにしています
    set_filename "questions"
end

4: YAMLファイルの作成: ファイルを作成し、データを記述

ここで問題の素材を作ります
app/models/active_hash/questions.yml

- id: 1 # 問題数
  content: パンはパンでも食べられないパンは? # 問題のタイトル
  choices: # 選択肢
    - カトパン
    - フライパン
    - ナカパン
  answers: # 回答
    - フライパン
  explain: "つまりはおやじギャグ" # 解説
- id: 2
// 以下省略

5: コントローラー側の生成

modelとviewをつなぎ動かすためのコントローラを生成します
app/controllers/quizzes_controller.rb

class QuizzesController < ApplicationController
    skip_before_action :require_login, only: [:new, :create]
    # クイズの結果を表示するアクション
    def index
      # 正解した問題数をすべて表示する
      @correct_count = session[:correct_count]
      @questions = Question.all
    end
    # 問題画面フォームの表示サクション
    def new
      if !params[:count]
      # 最初に、まだ「正解数(score)」が設定されていなければ
        session[:correct_count] = 0
        #「正解数(score)」は0として設定します
      end
      
      quiz_count = params[:count] || 1
      @quiz = Quiz.new
      @question = Question.find(quiz_count)
      # 問題を取り出して、画面に表示する準備します
    end
    
    # クイズの答えを送信してくれた後に、その答えが合っているかどうかをチェックするアクション
    def create
      @question = Question.find(quiz_params[:question_id])
      @select_answers = quiz_params[:selected_answers]
      @is_correct = @question.answers.sort == @select_answers.sort
      @questions_count = Question.all.count
      session[:correct_count] = session[:correct_count].to_i + 1 if @is_correct
      # 例えば、選んだ答えが問題の答えと合っていたら「正解!」と言って正解数を1増やします。
      # 不正解なら「不正解!」と言って、そのまま次の問題を準備します。

      # コードの中では、quiz_paramsを使って送られてきた答え(選択肢)を取り出し、
      # それが正しい答えと一致しているかどうかを調べています。
      # そして、一致していれば正解数を更新する仕組みです。

    end
  
    private
    # ストロングパラメーターで問題数(:question_id)と選択肢(selected_answers:)の答えが
    # 格納されています
    def quiz_params
      params.require(:quiz).permit(:question_id, selected_answers: [])
    end
end

クイズで答えを選ぶときに「送信ボタン」をうまく動かす仕組みを作るため、jsを作成します
app/javascript/controllers/quiz_controller.js

import { Controller } from "@hotwired/stimulus"

export default class extends Controller {
  static targets = ["checkbox", "submitButton"];
  
  /*クイズの画面が開いたときに「まずボタンの状態を確認する」作業をしてくれます。*/
  connect() {
    this.toggleButton();
  }

  /*「答えを選んだかどうかを調べる魔法」です。
  全部の答え(チェックボックス)を見て、「どれか1つでも選ばれているか?」を確認します。*/
  toggleButton() {
     /*もし選んでいれば、「ボタンを押せるようにする」*/
    const anyChecked = this.checkboxTargets.some((checkbox) => checkbox.checked);
     /*何も選んでいなければ、「ボタンを押せないようにする」*/
    this.submitButtonTarget.disabled = !anyChecked;
  }

  /*答えをクリック(選択)したときに、もう一度ボタンの状態を確認してくれます。*/
  check() {
    this.toggleButton();
  }
}

6: view画面を生成

問題画面の表示フォーム、全問回答結果を表示させるためのフォーム、クイズの答えを確認して正解か不正解かを判定しているフォームを作成します
今回CSSは追加していないので、デザインについては各個人で任せることにします

問題画面フォーム

(コントローラー側のnewアクションに当たる部分で、問題が表示され、回答ができます。問題、設問番号はyamlファイルから紐づいています)
app/views/quizzes/new.html.erb

<div>
  <div>
    <div>
      <div>
        <div>
          <h2><%= @question.content %></h2>

          <div data-controller="quiz">
            <%= form_with model: @quiz, url: quizzes_path do |f| %>
              <%= f.hidden_field :question_id, value: @question.id %>

              <div>
                <% @question.choices.shuffle.each do |choice| %>
                  <div>
                    <label id="answers_<%= choice %>_label">
                      <%= f.check_box :selected_answers, { multiple: true, name: 'quiz[selected_answers][]', id: "answers_#{choice}", data: { action: "quiz#check", quiz_target: "checkbox" } }, choice, nil %>
                      <%= simple_format(choice) %>
                    </label>
                  </div>
                <% end %>
              </div>

              <div id="answer_submit">
                <%= f.submit '回答する', data: { quiz_target: "submitButton" } %>
              </div>
              <div id="answer"></div>
            <% end %>
          </div>
        </div>
      </div>
    </div>
  </div>
</div>

全問回答結果フォーム

(コントローラー側のindexアクションに当たる部分で、何問正解したかと、問題の振り返りができます。問題、設問番号、解説はyamlファイルから紐づいています)
app/views/quizzes/index.html.erb

<div>
  <div>
    <div>
      <div>
        <div>
          <h2>結果</h2>
          <p>正解数: <%= @correct_count %> / <%= @questions.count %></p>
        </div>
      </div>

      <% @questions.each_with_index do |question, index| %>
        <div>
          <div>
            <h3>問題 <%= index + 1 %>: <%= question.content %></h3>

            <div>
              <p>正解:</p>
              <ul>
                <% question.answers.each do |correct_answer| %>
                  <li><%= correct_answer %></li>
                <% end %>
              </ul>
            </div>

            <div>
              <p>解説:</p>
              <p><%= question.explain %></p>
            </div>
          </div>
        </div>
      <% end %>
    </div>
  </div>
</div>

クイズの答えを確認して正解か不正解かを判定しているフォーム

(コントローラー側のcreateアクションに当たる部分で、判定と一緒に解説が付属されます。
解説はyamlファイルから紐づいています)
app/views/quizzes/create.tarbo_stream.erb

<% @question.choices.each do |choice| %>
  <%= turbo_stream.replace "answers_#{choice}_label" do %>
    <div>
      <% if @select_answers.include?(choice) %>
        <span><%= choice %></span>
      <% else %>
        <span><%= choice %></span>
      <% end %>
    </div>
  <% end %>
<% end %>

<%= turbo_stream.remove "answer_submit" %>

<%= turbo_stream.replace 'answer' do %>
  <div>
    <% if @is_correct %>
      <h2>✔ 正解!</h2>
    <% else %>
      <h2>✖ 不正解</h2>
      <p>正解は:</p>
      <ul>
        <% @question.answers.each do |answer| %>
          <li><%= answer %></li>
        <% end %>
      </ul>
    <% end %>

    <div>
      <%= simple_format(@question.explain) %>
    </div>

    <div>
      <% if @question.id < @questions_count %>
        <%= link_to '次の問題', new_quiz_path(count: @question.id + 1) %>
      <% else %>
        <%= link_to '結果を見る', quizzes_path %>
      <% end %>
    </div>
  </div>
<% end %>
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?