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

/
#cebu#rails#mysql

9日目

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

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

Vagrantfile
1
config.vm.provider "virtualbox" do |vb
2
vb.memory = "8192"
3
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

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

bundle install

app/config/database.yml
1
password:

rails db:create

scaffold(本段階

  • scaffold では controller と model が作成される
  • Ruby の整数型は integer
  • 中間テーブルは一番最後に作成
  • 主キーを参照する column を reference で指定
    • 自動で bigint に設定される

rails g scaffold

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

rails db:migrate

テーブル同士の relation 定義

undefined
1
# Studentモデル
2
class Student < ApplicationRecord
3
has_many :exam_results
4
has_many :subjects, through: :exam_results
5
has_many :club_students
6
has_many :clubs, through: :club_students
7
end
8
9
# Subject model
10
class Subject < ApplicationRecord
11
has_many :exam_results
12
has_many :students, through: :exam_results
13
end
14
15
# ExamResult model
16
class ExamResult < ApplicationRecord
17
belongs_to :student
18
belongs_to :subject
19
end
20
21
# Club model
22
class Club < ApplicationRecord
23
has_many :club_students
24
has_many :students, through: :club_students
25
end
26
27
# ClubStudent model
28
class ClubStudent < ApplicationRecord
29
belongs_to :student
30
belongs_to :club
31
end

マスターデータ作成

undefined
1
# student table
2
(1..100).each do |num|
3
if num % 2 == 0
4
gen = 0
5
ag = 0
6
at = 0
7
else
8
gen = 1
9
ag = 1
10
at = 1
11
end
12
13
op = (0..20).map{('あ'..'わ').to_a[rand(26)]}.join
14
user = Student.create(name: "taro-#{num}", email: "val-#{num}@gmail.com", gender: gen, age: ag, opinion: op)
15
end
16
17
# club table
18
Club.create(name: '自転車')
19
Club.create(name: 'サッカー')
20
Club.create(name: 'バスケットボール')
21
# ...
22
23
# subject table
24
Subject.create(name: '数学', max_score: 200);
25
Subject.create(name: '国語', max_score: 200);
26
Subject.create(name: '英語', max_score: 200);
27
# ...

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

範囲オブジェクト

文字も使える

map

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

undefined
1
# 配列の入った変数.map {|変数名| 処理内容 }
2
numbers = ["68", "65", "6C", "6C", "6F"]
3
p numbers.map {|item| item.to_i(16) }
4
[104, 101, 108, 108, 111]
5
#上では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 ページの、前回までの状態

Image from Gyazo

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

undefined
1
student1 = Student.first
2
student1.clubs << Club.first
3
student1.save

データ入力

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

undefined
1
(1..100).each do |i|
2
student = Student.find(i)
3
1.upto(rand(0..4)) do
4
student.clubs << Club.find(rand(1..13))
5
student.save
6
end
7
end

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

undefined
1
(1..100).each do |i|
2
student = Student.find(i)
3
1.upto(9) do |num|
4
sub = Subject.find(num)
5
exam_res = ExamResult.new
6
exam_res.name = "試験#{num}"
7
exam_res.score = rand(1..sub.max_score)
8
exam_res.subject = sub
9
student.exam_results << exam_res
10
student.save
11
end
12
end

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

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

出力を考える

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

MySQL上の出力

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

ページ上の出力

students_controllerのshowアクション編集
app/controllers/studetns_controller.rb
1
def show
2
@students =
3
Student.joins(:subjects)
4
.select('students.name, students.email, students.age, students.gender, students.opinion, subjects.id as subject_id')
5
.select('exam_results.name as exam_result_name, subjects.name as subject_name, exam_results.score')
6
.select('CAST((exam_results.score / subjects.max_score) * 100 as unsigned) as ratio')
7
.where(id: params[:id])
8
9
avg_result =
10
Student.joins(:subjects)
11
.select('subjects.id as subject_id')
12
.select('CAST(AVG(exam_results.score) as unsigned) as avg_score')
13
.select('MAX(exam_results.score) as max_score')
14
.select('MIN(exam_results.score) as min_score')
15
.group('subjects.id')
16
.order('subjects.id')
17
@score_hash = {}
18
avg_result.each do |avg_res|
19
h = Hash.new
20
h[:avg_score] = avg_res.avg_score
21
h[:max_score] = avg_res.max_score
22
h[:min_score] = avg_res.min_score
23
@score_hash[avg_res.subject_id] = h
24
end
25
end
showページのviewを編集
app/views/students/show.html.erb
1
<table border="1">
2
<tr>
3
<th>科目名</th>
4
<th>点数</th>
5
<th>平均</th>
6
<th>最高</th>
7
<th>最小</th>
8
</tr>
9
<% @students.each do |student| %>
10
<tr>
11
<td><%= student.subject_name %></td>
12
<td><%= student.score %></td>
13
<td><%= @score_hash[student.subject_id][:avg_score] %></td>
14
<td><%= @score_hash[student.subject_id][:max_score] %></td>
15
<td><%= @score_hash[student.subject_id][:min_score] %></td>
16
</tr>
17
<% end %>
18
</table>

11日目

今回の流れ

  1. ExamResults の index ページのデータ出力を編集
  2. ExamRusult の新規作成ページの UI を変更
  3. gem kaminari でページャー追加

実段階a

modify index page

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

app/views/exam_results/index.html.erb
1
# before edit
2
# <td><%= exam_result.student %></td>
3
# <td><%= exam_result.subject %></td>
4
5
# after
6
<td><%= exam_result.student.name %></td>
7
<td><%= exam_result.subject.name %></td>

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

app/views/exam_results/_form.html.erb
1
<div class="field">
2
<%= form.label :student_id %>
3
<%= form.select :student_id, @students %>
4
</div>
5
<div class="field">
6
<%= form.label :subject_id %>
7
<%= form.select :subject_id, @subjects %>
8
</div>
app/controllers/exam_results_controller.rb
1
before_action :set_students_subjects, only: [:new, :edit]
2
3
def set_students_subjects
4
@students = Student.all.pluck(:name, :id)
5
@subjects = Subject.all.pluck(:name, :id)
6
end

編集後

Image from Gyazo
Image from Gyazo

pagination by kaminari

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

インストール

Gemfile
1
gem 'kaminari'

bundle install

studentのindexページから変更

index アクションを編集

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

view を編集

app/views/students/index.html.erb
1
# ファイル先頭行に追加
2
<div class="page-header">
3
# ファイル最終行に追加
4
<%= paginate @students %>
5
</div>
Image from Gyazo

ExamResultのindexページ編集

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

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

undefined
1
rails g kaminari:views default
2
3
# 実行結果
4
# create app/views/kaminari/_next_page.html.erb
5
# create app/views/kaminari/_gap.html.erb
6
# create app/views/kaminari/_prev_page.html.erb
7
# create app/views/kaminari/_last_page.html.erb
8
# create app/views/kaminari/_first_page.html.erb
9
# create app/views/kaminari/_paginator.html.erb
10
# create app/views/kaminari/_page.html.erb

ページャの設定を変える

undefined
1
rails g kaminari:config
2
3
#実行結果
4
# create config/initializers/kaminari_config.rb
5
# ここで作成されたファイルに設定がある。

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

config/initializers/kaminari_config.rb
1
# frozen_string_literal: true
2
Kaminari.configure do |config|
3
# config.default_per_page = 25
4
# config.max_per_page = nil
5
# config.window = 4
6
# config.outer_window = 0
7
# config.left = 0
8
# config.right = 0
9
# config.page_method_name = :page
10
# config.param_name = :page
11
# config.params_on_first_page = false
12
end

12日目

kaminariの別のファイル設定

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

exam_result も編集は同じ。

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

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

リンクを作成

app/views/student/index.html.erb
1
<td><%= link_to 'New Exam Result', new_exam_result_path(student_id: student.id) %></td>
app/controllers/exam_results_controller.rb
1
def new
2
if params[:student_id]
3
@student = Student.find(params[:student_id])
4
@selected_student = [@student.name, @student.id]
5
end
6
@exam_result = ExamResult.new
7
end
app/views/exam_result/_form.html.erb
1
<%= 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 名を指定する際に、混乱した。

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

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

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

db/migrate/20190326030303_create_club_students.rb
1
class CreateClubStudents < ActiveRecord::Migration[5.2]
2
def change
3
create_table :club_students do |t|
4
# t.refereces :student
5
# =>
6
t.references :student
7
t.references :club, foreign_key: true
8
t.timestamps
9
end
10
end
11
end

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

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

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

render partial: 部分テンプレ

すべてのページのヘッダーに、ログアウトや他の student や clubs などのリンクを乗せる

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

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

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

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

validation

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

validate条件

undefined
1
# 空でないこと
2
validates :name, presence: true
3
4
# 因みに、空が条件ならば
5
# validates :name, absence: true
6
7
#入力文字の長さ
8
# **文字の最大長は、データ型を要参照。varcharなら255文字まで**
9
# 2文字以上
10
validates :name, length:{minimum:2}
11
# 255文字以上
12
validates :name, length:{maximum:255}
13
14
# exclusion含まない
15
validates :name, exclusion: { in: %w(部 サークル) }
16
# 『含む』ならinclusion

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

type “unsigned” does not exist (※Postgresql)

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

undefined
1
# ActiveRecord::StatementInvalid in StudentsController#show
2
3
# 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
1
def show
2
@students =
3
Student.joins(:subjects)
4
.select('students.name, students.email, students.age, students.gender, students.opinion, subjects.id as subject_id')
5
.select('exam_results.name as exam_result_name, subjects.name as subject_name, exam_results.score')
6
.select('CAST((exam_results.score / subjects.max_score) * 100 as unsigned) as ratio')
7
.where(id: params[:id])
8
9
avg_result =
10
Student.joins(:subjects)
11
.select('subjects.id as subject_id')
12
.select('CAST(AVG(exam_results.score) as unsigned) as avg_score')
13
.select('MAX(exam_results.score) as max_score')
14
.select('MIN(exam_results.score) as min_score')
15
.group('subjects.id')
16
.order('subjects.id')
17
# (以下略)

因みに、この 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
1
# (該当部分だけ抜き出し)
2
.select('CAST((exam_results.score / subjects.max_score) * 100 as int) as ratio')
3
.select('CAST(AVG(exam_results.score) as int) as avg_score')

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

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

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

passwordカラムの追加

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

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

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

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

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

input data

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

undefined
1
(1..100).each do |num|
2
if num % 2 == 0 && num % 3 ==0
3
gen = 0
4
ag = 1
5
elsif num % 2 == 0
6
gen = rand(2)
7
ag = rand(3)
8
else
9
gen = 1
10
ag = 0
11
end
12
13
op = (1..10).map{('あ'..'わ').to_a[rand(26)]}.join
14
nm = (1..3).map{('あ'..'わ').to_a[rand(26)]}.join
15
16
user = Student.create!(name: "#{nm}", email: "#{nm}-#{rand(98)}@gmail.com", gender: gen, age: ag, opinion: op,password: 'password')
17
end
undefined
1
(1..100).each do |i|
2
student = Student.find(i)
3
1.upto(rand(0..4)) do
4
student.clubs << Club.find(rand(1..14))
5
student.save
6
end
7
end