Day 9 ~ 14:大学生データ操作 App
- 9日目:大学生データ(マスターデータ
- 10日目(1):マスターデータ(DBへ情報入力、ページに出力
- 11日目(1)、12日目:マスターデータ。ページUIの編集、ページャ導入
- 14日目:Scaffoldで作成したサイトにgem devise等を組み合わせていく
9日目
- rails new で動かない不具合
- stop spring で解消
- Ruby 側で整数型を int と書き間違えることによるエラー
- Rubyの整数型はinteger
- MySQL は int(C++経験上、int の方が馴染み深い)
- rails db:migrate コマでのエラー
- 中間テーブルを先に作ってしまったため
- 中間テーブルは主テーブルの id 等参照するので、作成は一番後
- 最適なデータ型を選択できなかった
不具合改善のなかで、Vagrantfile で、使用できる RAM のサイズを 8GB に変更
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
gem 'mini_racer', platforms: :ruby
bundle install
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:textrails g scaffold Subject name:string max_score:integerrails g scaffold Club name:stringrails 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_studentsend
# Subject modelclass Subject < ApplicationRecord has_many :exam_results has_many :students, through: :exam_resultsend
# ExamResult modelclass ExamResult < ApplicationRecord belongs_to :student belongs_to :subjectend
# Club modelclass Club < ApplicationRecord has_many :club_students has_many :students, through: :club_studentsend
# ClubStudent modelclass ClubStudent < ApplicationRecord belongs_to :student belongs_to :clubend
マスターデータ作成
# 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 tableClub.create(name: '自転車')Club.create(name: 'サッカー')Club.create(name: 'バスケットボール')# ...
# subject tableSubject.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日目
今回の流れ
- 中間テーブルにデータ入力
- 性別の 0 or 1 の表記を、male or female に変更
- Student の show ページに、生徒ごとの試験結果など、データを出力
実段階
Students の show ページの、前回までの状態

生徒データと関連付けするときは
student1 = Student.firststudent1.clubs << Club.firststudent1.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 endend
生徒の試験結果情報 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 endend
Studentsのindexページの表記を変更
enum gender: { male: 0 ,female: 1}enum age: {"teen": 0, "twenty": 1}
<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_scoreFROM studentsINNER JOIN exam_results ON students.id = exam_results.student_idINNER JOIN subjects ON exam_results.subject_id = subjects.idGROUP 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アクション編集
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 endend
showページのviewを編集
<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日目
今回の流れ
- ExamResults の index ページのデータ出力を編集
- ExamRusult の新規作成ページの UI を変更
- gem kaminari でページャー追加
実段階a
modify index page
※app/views/exam_results/show.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ページにセレクトボックス
<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>
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
編集後


pagination by kaminari
student と ExamResult の index ページを、数ページに区切って表示させたい。 今回は gem の kaminari を用いる。
インストール
gem 'kaminari'
bundle install
studentのindexページから変更
index アクションを編集
def index # 編集前:@students = Student.all @students = Student.page(params[:page]).per(20)end
view を編集
# ファイル先頭行に追加<div class="page-header"># ファイル最終行に追加<%= paginate @students %></div>

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 対応のページャテーマもある。
# frozen_string_literal: trueKaminari.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 = falseend
12日目
kaminariの別のファイル設定
- models に paginates_per 30 と記述
- controller の index アクションの末尾にある、per()を削除
- (ビューファイルは同じ)
exam_result も編集は同じ。
paginates_per 30
@students = Student.page(params[:page])
# ファイル先頭<div class="page-header"># ファイル末尾 <%= paginate @students %></div>
studentのindexページに、exam_resultのnewへのリンク作成
リンクを作成
<td><%= link_to 'New Exam Result', new_exam_result_path(student_id: student.id) %></td>
def new if params[:student_id] @student = Student.find(params[:student_id]) @selected_student = [@student.name, @student.id] end @exam_result = ExamResult.newend
<%= 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” とミスタイプしていた。
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 endend
なお、ALTTER TABLE コマンドを使って、あとから修正する方法は DB 内のデータを書き換えるだけで、アプリ自体のファイル等は編集されない。
-- ALTER TABLE テーブル名 MODIFY COLUMN カラム名 新しい定義ALTER TABLE ClubStudent MODIFY COLUMN student references
つまり、原因の根本的な部分を修正できないので、駄目
render partial: 部分テンプレ
すべてのページのヘッダーに、ログアウトや他の student や clubs などのリンクを乗せる
共通して表示させるので、/app/views/layouts/application.html.erb を編集する。 なお、部分テンプレファイル名は『_』アンダースコア始まり
<body> <%= render :partial => 'shared/header' %></body>
表示させたいリンクを書きこむ。
<%= 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
は
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 に変更した
# (該当部分だけ抜き出し).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 下にファイルが生成される
class AddPasswordToStudents < ActiveRecord::Migration[5.2] def change add_column :students, :password, :integer endend
これで、パスワード情報入りの生徒データを 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 endend