Day 9 ~ 14:大学生データ操作 App

post on
post cover image

from Qiita

9日目

  • rails newで動かない不具合
    • stop springで解消
  • ruby側で整数型をintと書き間違えることによるエラー
    • rubyの整数型はinteger
    • mysqlはint (C++経験上、intの方が馴染み深い
  • rails db:migrateコマでのエラー
    • 中間テーブルを先に作ってしまった為。
    • 中間テーブルは主テーブルのid等参照するので、作成は一番後。
  • 最適なデータ型を選択できなかった

不具合改善の中で、Vagrantfileで、使用できるRAMのサイズを8GBに変更

# Vagrantfile
config.vm.provider "virtualbox" do |vb
  vb.memory = "8192"
end

Environment

  • 仮想環境OS: Ubuntu 18.04
  • Ruby:2.51
  • Rails:5.2.2

作成データ

  • テーブル
    • student (id, name,email, gender, age, opinion, updated_at, created_at)
    • ExamResult (id, student, subject, name, score, updated_at, created_at)
      • 中間テーブル
    • Subject (id, name, max_score, updated_at, created_at)
      • ※教科の意
    • ClubStudent (id, student, club, name, updated_at, created_at)
      • 中間テーブル
    • Club (id, name, updated_at, created_at)

準備

rails new

rails new self_univ -d mysql
# Gemfile
gem 'mini_racer', platforms: :ruby

bundle install

# qpp/config/database.yml
password: 

rails db:create

scaffold(本段階

  • scaffoldでは controller と model が作成される
  • rubyの整数型はinteger(他言語の知識とごっちゃになった
  • 中間テーブルは一番最後に作成
  • 主キーを参照するcolumnをreferenceで指定
    • 自動でbigintに設定される

rails g scaffold

rails g scaffold Student name:string email:string gender:integer age:integer opinion:text
rails g scaffold Subject name:string max_score:integer
rails g scaffold Club name:string
rails g scaffold ExamResult student:references subject:references name:string score:integer
# ClubStudentテーブル(中間テーブルなので最後
rails g scaffold ClubStudent student:references club:references name:string

rails db:migrate

テーブル同士の relation 定義

# Studentモデル
class Student < ApplicationRecord
  has_many :exam_results
  has_many :subjects, through: :exam_results
  has_many :club_students
  has_many :clubs, through: :club_students
end

# Subject model
class Subject < ApplicationRecord
  has_many :exam_results
  has_many :students, through: :exam_results
end

# ExamResult model
class ExamResult < ApplicationRecord
  belongs_to :student
  belongs_to :subject
end

# Club model
class Club < ApplicationRecord
  has_many :club_students
  has_many :students, through: :club_students
end

# ClubStudent model
class ClubStudent < ApplicationRecord
  belongs_to :student
  belongs_to :club
end

マスターデータ作成

# student table
(1..100).each do |num|
  if num % 2 == 0
    gen = 0
    ag = 0
    at = 0
  else
    gen = 1
    ag = 1
    at = 1
  end

  op = (0..20).map{('あ'..'わ').to_a[rand(26)]}.join
  user = Student.create(name: "taro-#{num}", email: "val-#{num}@gmail.com", gender: gen, age: ag, opinion: op)
end

# club table
Club.create(name: '自転車')
Club.create(name: 'サッカー')
Club.create(name: 'バスケットボール')
# ...

# subject table
Subject.create(name: '数学', max_score: 200);
Subject.create(name: '国語', max_score: 200);
Subject.create(name: '英語', max_score: 200);
# ...

(0..20).map{('あ'..'わ').to_a[rand(26)]}.join

範囲オブジェクト

文字も使える

map

要素の数だけ繰り返しブロックを実行し、ブロックの戻り値を集めた配列を作成して返す。
collectメソッドの別名です。

# 配列の入った変数.map {|変数名| 処理内容 }
numbers = ["68", "65", "6C", "6C", "6F"]
p numbers.map {|item| item.to_i(16) }
[104, 101, 108, 108, 111]
#上では16進数を10進数に変換

to_a(Array)

Arrayオブジェクトを返す

rand(max)

max が 0 の場合は 0.0 以上 1.0 未満の実数を、正の整数の場合は 0 以上 max 未満の整数を返す

join(sep =)

joinメソッドは、配列の各要素を文字列に変換し、引数sepを区切り文字として結合した文字列を返します。
引数を省略すると区切り文字なしで要素を結合した文字列になる


10日目

今回の流れ

  1. 中間テーブルにデータ入力
  2. 性別の0 or 1の表記を、male or femaleに変更
  3. Studentのshowページに、生徒ごとの試験結果等、データを出力

実段階

Studentsのshowページの、前回までの状態

student show

生徒データと関連付けするときは

student1 = Student.first
student1.clubs << Club.first
student1.save

データ入力

生徒の部活情報
id1からid100までの生徒に、0から4個の部活(選択肢は13部)に入ってもらう。

(1..100).each do |i|
  student = Student.find(i)
  1.upto(rand(0..4)) do
    student.clubs << Club.find(rand(1..13))
    student.save
  end
end

生徒の試験結果情報
id100までの生徒に、9科目の試験を受けてもらう。
なお、点数は0点から各教科ごとに設定の最大点までのランダム

(1..100).each do |i|
  student = Student.find(i)
  1.upto(9) do |num|
    sub = Subject.find(num)
    exam_res = ExamResult.new
    exam_res.name = "試験#{num}"
    exam_res.score = rand(1..sub.max_score)
    exam_res.subject = sub
    student.exam_results << exam_res
    student.save
  end
end

Studentsのindexページの表記を変更

# app/models/studetns.rb
enum gender: { male: 0 ,female: 1}
enum age: {"teen": 0, "twenty": 1}
# app/views/_form.html.erb
<div class="field">
  <%= form.label :gender %>
  <%= form.radio_button :gender, 'male' %>男性
  <%= form.radio_button :gender, 'female' %>女性
</div>
<div class="field">
  <%= form.label :age %>
  <%= form.radio_button :age, '20代' %>20代
  <%= form.radio_button :age, '30代' %>30代
</div>

出力を考える

  • 学生ごとのshowページで表示したいもの
    • 生徒のデータ(name, mail, gender, age, opinion)
    • 生徒の教科ごとの試験結果点数
    • 性と全体の試験結果の平均点、最大点、最小点

MySQL上の出力

SELECT
    subjects.name,
    CAST(AVG(exam_results.score) as unsigned) as avg_score,
    MAX(exam_results.score) as max_score,
    MIN(exam_results.score) as min_score
FROM
    students
INNER JOIN exam_results
    ON students.id = exam_results.student_id
INNER JOIN subjects
    ON exam_results.subject_id = subjects.id
GROUP BY subjects.id, subjects.name
-- 出力結果
+--------+--------------+-----------+-------+-------+
| name   | name         | name      | score | ratio |
+--------+--------------+-----------+-------+-------+
| taro-1 | 一次試験     | 数学      |   181 |    91 |
| taro-1 | 試験1        | 数学      |    61 |    31 |
| taro-1 | 一次試験     | 国語      |   146 |    73 |
-- ...

ページ上の出力

students_controllerのshowアクション編集
# app/controllers/studetns_controller.rb
def show
  @students = 
    Student.joins(:subjects)
            .select('students.name, students.email, students.age, students.gender, students.opinion, subjects.id as subject_id')
            .select('exam_results.name as exam_result_name, subjects.name as subject_name, exam_results.score')
            .select('CAST((exam_results.score / subjects.max_score) * 100 as unsigned) as ratio')
            .where(id: params[:id])

  avg_result = 
    Student.joins(:subjects)
            .select('subjects.id as subject_id')
            .select('CAST(AVG(exam_results.score) as unsigned) as avg_score')
            .select('MAX(exam_results.score) as max_score')
            .select('MIN(exam_results.score) as min_score')
            .group('subjects.id')
            .order('subjects.id')
  @score_hash = {}
  avg_result.each do |avg_res|
    h = Hash.new
    h[:avg_score] = avg_res.avg_score
    h[:max_score] = avg_res.max_score
    h[:min_score] = avg_res.min_score                                                                                                                                     
    @score_hash[avg_res.subject_id] = h
  end
end
showページのviewを編集
# app/views/students/show.html.erb
<table border="1">
  <tr>
    <th>科目名</th>
    <th>点数</th>
    <th>平均</th>
    <th>最高</th>
    <th>最小</th>
  </tr>
  <% @students.each do |student| %>
    <tr>
      <td><%= student.subject_name %></td>
      <td><%= student.score %></td>
      <td><%= @score_hash[student.subject_id][:avg_score] %></td>
      <td><%= @score_hash[student.subject_id][:max_score] %></td>
      <td><%= @score_hash[student.subject_id][:min_score] %></td>
    </tr>              
  <% end %>
</table>

11日目

今回の流れ

  1. ExamResultsのindexページのデータ出力を編集
  2. ExamRusultの新規作成ページのUIを変更
  3. gem kaminariでページャー追加(授業内では時間足らず)

実段階

modify index page

※app/views/exam_results/show.html.erbも同様にやる

# app/views/exam_results/index.html.erb
# before edit
# <td><%= exam_result.student %></td>
# <td><%= exam_result.subject %></td>

# after
<td><%= exam_result.student.name %></td>
<td><%= exam_result.subject.name %></td>

newページにセレクトボックス

Action View Form Helpers

# app/views/exam_results/_form.html.erb
<div class="field">
    <%= form.label :student_id %>
    <%= form.select :student_id, @students %>
 </div>
<div class="field">
    <%= form.label :subject_id %>
    <%= form.select :subject_id, @subjects %>
</div>
# app/controllers/exam_results_controller.rb
before_action :set_students_subjects, only: [:new, :edit]

def set_students_subjects
  @students = Student.all.pluck(:name, :id)
  @subjects = Subject.all.pluck(:name, :id)
end

編集後

exam result index exam new

pagination by kaminari

studentとExamResultのindexページを、数ページに区切って表示させたい。
今回はgemの kaminari を用いる。

インストール

# Gemfile
gem 'kaminari'

bundle install

studentのindexページから変更

indexアクションを編集

# app/controllers/students_controller.rb
def index
  # 編集前:@students = Student.all
    @students = Student.page(params[:page]).per(20)
end

viewを編集

# app/views/students/index.html.erb
# ファイル先頭行に追加
<div class="page-header">
# ファイル最終行に追加
<%= paginate @students %>
</div>
pagination

ExamResultのindexページ編集

app/controllers/exam_result_controller.rbのindexアクションと
app/view/exam_results/index.html.erbを同様に編集

ページャの見た目を変える

rails g kaminari:views default

# 実行結果
  # create  app/views/kaminari/_next_page.html.erb
  #     create  app/views/kaminari/_gap.html.erb
  #     create  app/views/kaminari/_prev_page.html.erb
  #     create  app/views/kaminari/_last_page.html.erb
  #     create  app/views/kaminari/_first_page.html.erb
  #     create  app/views/kaminari/_paginator.html.erb
  #     create  app/views/kaminari/_page.html.erb

ページャの設定を変える

rails g kaminari:config

#実行結果
create  config/initializers/kaminari_config.rb
# ここで作成されたファイルに設定がある。

Bootstrap対応のページャテーマもある.

# config/initializers/kaminari_config.rb
# frozen_string_literal: true
Kaminari.configure do |config|
  # config.default_per_page = 25
  # config.max_per_page = nil
  # config.window = 4
  # config.outer_window = 0
  # config.left = 0
  # config.right = 0
  # config.page_method_name = :page
  # config.param_name = :page
  # config.params_on_first_page = false
end

12日目

kaminariの別のファイル設定

  • modelsにpaginates_per 30と記述
  • controllerのindexアクションの末尾にある、per()を削除
    • (ビューファイルは同じ)

exam_resultも編集は同じ。

# app/models/student.rb
paginates_per 30
# app/controllers/students_controller.rb
@students = Student.page(params[:page])
# app/views/students/index.html.erb
# ファイル先頭
<div class="page-header">
# ファイル末尾
<%= paginate @students %>
</div>

studentのindexページに、exam_resultのnewへのリンク作成

リンクを作成

# app/views/student/index.html.erb
<td><%= link_to 'New Exam Result', new_exam_result_path(student_id: student.id) %></td>
# app/controllers/exam_results_controller.rb
def new
  if params[:student_id]
    @student = Student.find(params[:student_id])
    @selected_student = [@student.name, @student.id]
  end
  @exam_result = ExamResult.new
end
# app/views/exam_result/_form.html.erb
<%= form.select :student_id, options_for_select(@students, @selected_student) %>

studentのindexから'New Exam Result'リンクを押すと、exam_resultのnewページに飛び、
フォームのセレクトボタンのうち、生徒が自動で選択されるようになった。


14日目

今週からは、scaffoldで作成した大学データと、gemのdevise、Bootstrap等を組み合わせる。

What I did

  • Railsの命名規則(単数形と複数形)
  • DBのカラム定義を後から変更
  • render partial: 部分テンプレの参照
  • validation
  • UNSIGNEDという型が存在しないPostgreSQL

Railsの命名規則(単数形と複数形)

rails gコマンドで、controller名やmodel名を指定する際に、混乱した。

# rails generate scaffold model名の単数形 フィールド名の型と並び
# rails g controller controller名の複数形
# カラムの追加
# rails generate migration AddカラムToモデル名の複数形 フィールド名と並び
  • model は単数形で、頭文字を大文字
    • scaffoldの場合、modelが基準
  • controller名は複数形、頭文字を大文字
    • 1つのcontrollerに複数のactionが含まれるため

DBのカラム定義を後から変更

rails g scaffold 時に "refereces" とミスタイプしていた。

# db/migrate/20190326030303_create_club_students.rb
class CreateClubStudents < ActiveRecord::Migration[5.2]
  def change
    create_table :club_students do |t|
     # t.refereces :student
     # =>
      t.references :student
      t.references :club, foreign_key: true
      t.timestamps
    end
  end
end

なお、ALTTER TABLEコマンドを使って、あとから修正する方法は
DB内のデータを書き換えるだけで、アプリ自体のファイル等は編集されない。

-- ALTER TABLE テーブル名 MODIFY COLUMN カラム名 新しい定義
ALTER TABLE ClubStudent MODIFY COLUMN student references

つまり、原因の根本的な部分を修正できないので、駄目

render partial: 部分テンプレ

全てのページのヘッダー(上部)に、ログアウトや他のstudentやclubs等のリンクを乗せる

共通して表示させるので、/app/views/layouts/application.html.erb を編集する。
なお、部分テンプレファイル名は『_』アンダーバー始まり

# /app/views/layouts/application.html.erb
<body>
  <%= render :partial => 'shared/header' %>
</body>

表示させたいリンクを書きこむ。

# /app/views/shared/_header.html.erb
<%= link_to 'Student list', students_path %> 
<%= link_to 'subjects list', subjects_path %> 
<%= link_to 'clubs list', clubs_path %> 
<%= link_to 'exam_result list', exam_results_path %>
<%= link_to 'club_stdent list', club_students_path %>
<%= link_to 'Log Out', destroy_student_session_path, method: :delete %>

validation

バリデーションは有効なデータだけをDBに保存するのを確実にするための最善策。

validate条件

# 空でないこと
validates :name, presence: true

# 因みに、空が条件ならば
# validates :name, absence: true

#入力文字の長さ
# **文字の最大長は、データ型を要参照。varcharなら255文字まで**
# 2文字以上
validates :name, length:{minimum:2}
# 255文字以上
validates :name, length:{maximum:255}

# exclusion含まない
validates :name, exclusion: { in: %w(部 サークル) }
# 『含む』ならinclusion

空白や文字列長、『サークル』という語には、validatesが発動するが、『テニスサークル』だと発動しないので、正規表現等を使う必要がある。

type "unsigned" does not exist (※Postgresql)

validatesの実装していく最中に、エラーに気づいた
studentのeditページで更新すると、

# ActiveRecord::StatementInvalid in StudentsController#show

# PG::UndefinedObject: ERROR: type "unsigned" does not exist LINE 1: ...id as subject_id, CAST(AVG(exam_results.score) as unsigned) ... ^ : SELECT subjects.id as subject_id, CAST(AVG(exam_results.score) as unsigned) as avg_score, MAX(exam_results.score) as max_score, MIN(exam_results.score) as min_score FROM "students" INNER JOIN "exam_results" ON "exam_results"."student_id" = "students"."id" INNER JOIN "subjects" ON "subjects"."id" = "exam_results"."subject_id" GROUP BY subjects.id ORDER BY subjects.id

と、エラーを吐き、因みに、ブラウザの戻るボタンで戻ると、更新されている。
また、エラー原因であると思わる、StudentController#show

# app/controllers/students_controller.rb
def show
  @students = 
    Student.joins(:subjects)
            .select('students.name, students.email, students.age, students.gender, students.opinion, subjects.id as subject_id')
            .select('exam_results.name as exam_result_name, subjects.name as subject_name, exam_results.score')
            .select('CAST((exam_results.score / subjects.max_score) * 100 as unsigned) as ratio')
            .where(id: params[:id])

  avg_result = 
    Student.joins(:subjects)
            .select('subjects.id as subject_id')
            .select('CAST(AVG(exam_results.score) as unsigned) as avg_score')
            .select('MAX(exam_results.score) as max_score')
            .select('MIN(exam_results.score) as min_score')
            .group('subjects.id')
            .order('subjects.id')
# (以下略)

因みに、このcontrollerは、以前の大学データのcontrollerからコピーしてきたものだ。
つまり、MySQLで動くアプリのcontroller。

unsigned (MySQL)

  • MySQLにおいては正と負の整数を扱うことができる。
  • unsignedを指定すると、正の数しか格納できなくなり、代わりに範囲が2倍になる。
  • unsignedにした値が負になると、エラーを起こす
    • UNSIGNEDは、マイナス値が入らないだけでなく、マイナスになる計算もできない。
    • CASTで一時的に型を変える事で回避は可能。

Postgresqlにはunsined型は存在しない(最重要)

対応するには

  • unsignedをint等の型に置き換える
    • 今回は試験点数を扱っていて、intで事足りると思われる。
    • ただ、MySQLでint unsignedだと、範囲が正の方向に2倍になっている。
    • 扱う数によっては、intより1つ上ののbigintに変える必要がある
  • CAST as unsignedの部分を消す
    • MySQLでCAST as unsingedは、一時的に型を指定している

前回の大学データに倣って、今回はcast as intに変更した

# app/controllers/students_controller.rb
# (該当部分だけ抜き出し)
.select('CAST((exam_results.score / subjects.max_score) * 100 as int) as ratio')
.select('CAST(AVG(exam_results.score) as int) as avg_score') 

正常に、studentデータのedit、updateが機能した。

データ入力にはpassword情報が必要

deviseの関係上、パスワード情報入りのデータでないと、コンソールから入力できない。

passwordカラムの追加

deviseのモデル等がある、Studentテーブルに、パスワードカラムを追加した。

# rails generate migration AddカラムToモデル名の複数形 フィールド名と並び
rails g migration AddPasswordToStudents password:string

db/migrate下にファイルが生成される

# /db/migrate/20190327144825_add_password_to_students.rb
class AddPasswordToStudents < ActiveRecord::Migration[5.2]
  def change
    add_column :students, :password, :integer
  end
end

これで、パスワード情報入りの生徒データをDBに入力できる。

input data

未だデータの無い、生徒データと試験結果データをコンソールで入力した。

(1..100).each do |num|
  if num % 2 == 0 && num % 3 ==0
    gen = 0
    ag = 1
    elsif num % 2 == 0
    gen = rand(2)
    ag = rand(3)
  else
    gen = 1
    ag = 0
  end
  
  op = (1..10).map{('あ'..'わ').to_a[rand(26)]}.join
  nm = (1..3).map{('あ'..'わ').to_a[rand(26)]}.join

  user = Student.create!(name: "#{nm}", email: "#{nm}-#{rand(98)}@gmail.com", gender: gen, age: ag, opinion: op,password: 'password')
  end
  (1..100).each do |i|
  student = Student.find(i)
  1.upto(rand(0..4)) do
    student.clubs << Club.find(rand(1..14))
    student.save
  end
end