RailsでCRUDアプリ作ってみる

こんにちは。
この記事はRailsを触り始めたので何かWebアプリを作ってみた軌跡を残してみたというものです。
手探りで進めたものなのでこのやり方だと効率が悪い、などあると思いますが同じく初心者のための助けとなれば幸いです。
環境構築手順は[こちら](https://qiita.com/ave/items/650ef08c3e4261cf62bc)に記載しています

## 現在の状況
- Vagrant + VirtualBoxでとりあえず仮想環境作ってみた
- Rails(5.1)インストール完了、画面も表示できた

## 作りたいもの
単純なCRUDアプリを作成してみるということで、タスク管理をするアプリを作成します。
ざっくり要件としては以下のようなものです。

- 一覧
    - タスクの一覧を実施する期限とともに表示
    - タスクを終えたら「実施済み」に変更できる
    - 「削除」で未実施/実施済みに関わらず一覧から表示されなくなる（論理削除する）
- 作成
- 編集（更新）
    - タスク名と実施期限のみ設定可能（実施済みへの変更は一覧からのみ行える）
- 削除
    - 一覧画面より「削除」から実施できる

# この記事のゴール
- Railsの環境にMySQLをインストールする
- migration機能を使ってみる
- DB接続してデータの一覧を表示する

# 1. MySQLをインストール
RailsのデフォルトのDBはSQLiteですが、今回はMySQLを使用することにします。　　
そのため構築したRailsの環境にMySQLをインストールします。

- 新規アプリケーション作成時にオプションでDBを指定する　　
<pre>
$ rails new project --database=mysql
</pre>
`project`の部分はアプリケーション名を指定してください。
なお新規アプリケーション作成時と記載していますが、私の場合はDBを指定する前にすでに`rails new`していたので
その時と同じアプリ名で上書きして作成し直しました。
その際は以下のように上書きの警告が途中出てくるかと思います。

```ruby:上書き時
$ rails new project --database=mysql
       exist
   identical  README.md
   identical  Rakefile
   identical  config.ru
    conflict  .gitignore
Overwrite /***/.gitignore? (enter "h" for help) [Ynaqdh] y
       force  .gitignore
    conflict  Gemfile
Overwrite /***/Gemfile? (enter "h" for help) [Ynaqdh] y
       force  Gemfile
         run  git init from "."
Reinitialized existing Git repository in /***/.git/
       exist  app
   identical  app/assets/config/manifest.js
   identical  app/assets/javascripts/application.js
```
アプリケーションが作成されると、`Gemfile`に以下のように記述が増え
> gem 'mysql2', '>= 0.3.18', '< 0.5'

また`config/database.yml`が以下のように記載されていると思います。

```yml:database.yml
# MySQL. Versions 5.1.10 and up are supported.
#
# Install the MySQL driver
#   gem install mysql2
#
# Ensure the MySQL gem is defined in your Gemfile
#   gem 'mysql2'
#
# And be sure to use new-style password hashing:
#   http://dev.mysql.com/doc/refman/5.7/en/old-client.html
#
default: &default
  adapter: mysql2
  encoding: utf8
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  username: root
  password:
  host: localhost

development:
  <<: *default
  database: project_development

# Warning: The database defined as "test" will be erased and
# re-generated from your development database when you run "rake".
# Do not set this db to the same as development or production.
test:
  <<: *default
  database: project_test

```
defaultのadapterが`mysql2`になっています。

- MySQLをサーバにインストール
VagrantにSSH接続した状態で、以下を実行。
<pre>
$ sudo rpm -ivh http://dev.mysql.com/get/mysql57-community-release-el7-8.noarch.rpm
$ sudo yum update
$ sudo yum install mysql-community-server
</pre>
続けて起動
<pre>
// 注：CentOS7の場合
$ sudo systemctl start mysqld.service
// 自動起動設定も
$ sudo systemctl enable mysqld.service
</pre>

MySQLにログインしてみます。
<pre>
$ mysql -u root
ERROR 1045 (28000): Access denied for user 'root'@'localhost' (using password: NO)
</pre>
エラー出力。
調べるとログファイルにパスワードが吐かれているため、それを使用して一時ログインするのが良さそう
<pre>
$ cat /var/log/mysqld.log | grep 'password is generated'
2018-04-06T05:39:08.571786Z 1 [Note] A temporary password is generated for root@localhost: ********
</pre>
`var/log/mysqld.log`に出力されていたためそれを使用して再ログイン。
<pre>
$ mysql -u root -p
Enter password:
Welcome to the MySQL monitor.  Commands end with ; or \g.
Your MySQL connection id is 6
Server version: 5.7.21

Copyright (c) 2000, 2018, Oracle and/or its affiliates. All rights reserved.
</pre>
成功。`set password`でパスワードは新規に設定し直しておきます。
<pre>
mysql> select version();
+-----------+
| version() |
+-----------+
| 5.7.21    |
+-----------+
1 row in set (0.02 sec)
</pre>
サーバにインストールされたバージョンは5.7。
ログインまで完了したため次の工程に進みます。

# 2. DB, table作成

- DB作成、マイグレーション実行
1でMySQLログイン時のパスワードを設定したため、`Gemfile`の`password`に記載します。

```yml:database.yml
# And be sure to use new-style password hashing:
#   http://dev.mysql.com/doc/refman/5.7/en/old-client.html
#
default: &default
  adapter: mysql2
  ...
  password: *******（こちらに記載）
  ...
```

次に`Gemfile`が配置されているディレクトリで以下のコマンドを実行します
<pre>
$ rails db:create
$ rails db:migrate
</pre>

上記、1行目ではDBを新規作成しています。
2行目の`migrate`とは何かというと、Railsのマイグレーション機能を使用するということを指します。
マイグレーションとは、マイグレーションスクリプトと呼ばれるスクリプトファイルを作成し実行することで、SQL文を直接実行することができるというものです。
この「スクリプトファイル」は決められたRailsnの記法で記述しますが、使用しているDBがMySQLであってもSQLiteであっても、同じ記述で対応が可能とのこと。
つまり今回はアプリケーション作成時からMySQLを指定して進めていますが、途中でSQLiteに切り替えることも、逆にSQLiteからMySQLへの切り替えも容易にできるというメリットがありますね。

- テーブル作成
以下、`rails db`コマンドを実行することによってMySQLにログインし対話モードに入ります。
<pre>
$ rails db
Enter password:
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A
Welcome to the MySQL monitor.  Commands end with ; or \g.
 ...
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql>
</pre>
このコマンドではRailsが現在接続しているDBを適切に選び、対話式モードへ接続してくれます。
このままSQL構文を書いてもいいのですが、railsコマンドを駆使して進めていくことにします。

今回、以下のようなcreate文で作成されるテーブルを作成したいと思います。

```ruby:発行したいcreate文
  CREATE TABLE `tasks` (
    `id` bigint(20) NOT NULL auto_increment,
    `task_name` varchar(255) NOT NULL,
    `term_at` datetime NOT NULL,
    `created_at` datetime NOT NULL default current_timestamp,
    `updated_at` datetime NOT NULL default current_timestamp on update current_timestamp,
    `disp_flag` tinyint(4) NOT NULL default '0',
    `delete_flag` tinyint(4) NOT NULL default '0',
    PRIMARY KEY (`id`)
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8;
```

`rails generate`コマンドを実行します。

<pre>
$ rails g model tasks
Running via Spring preloader in process 6639
[WARNING] The model name 'tasks' was recognized as a plural, using the singular 'task' instead. Override with --force-plural or setup custom inflection rules for this noun before running the generator.
      invoke  active_record
      create    db/migrate/20180406084039_create_tasks.rb
      create    app/models/task.rb
      invoke    test_unit
      create      test/models/task_test.rb
      create      test/fixtures/tasks.yml
</pre>
「モデル名は単数形で指定しろ」という旨のWARNINGが出ており、Modelのファイル名はtaskで生成されています。なるほど。　　
これによりモデルを作成すると同時にモデルに対応するテーブルを作成するためのマイグレーションスクリプトが自動的に作成され、　　
`db/migrate`下に以下のファイルが生成されています。

```ruby:20180408XXXXXX_create_tasks.rb
class CreateTasks < ActiveRecord::Migration[5.1]
  def change
    create_table :tasks do |t|

      t.timestamps
    end
  end
end
```
ここにデフォルトで生成されているカラム以外の情報を記載します。　　
こちらの各制約などの記述方法はここでは割愛。

```ruby:修正後
class CreateTasks < ActiveRecord::Migration[5.1]
  def change
    create_table :tasks do |t|
      t.string :task_name, :null => false
      t.datetime :term_at, :null => false
      t.datetime :created_at, default: -> { 'NOW()' }
      t.datetime :updated_at, default: -> { 'NOW()' }
      t.boolean :disp_flag, default:false
      t.boolean :delete_flag, default:false
    end
  end
end
```
試行錯誤しこのように落ち着きました。　　
さて再度`migrate`コマンドを実行します。
<pre>
$ rails db:migrate
== 20180406084039 CreateTasks: migrating ======================================
-- create_table(:tasks)
   -> 0.0476s
== 20180406084039 CreateTasks: migrated (0.0477s) =============================
</pre>
migrateは正常に実行されたため、
実際にテーブルがどのように作成されたか確認してみます。

<pre>
$ rails db
</pre>

```SQL:migrate後確認
mysql> show databases;
+---------------------+
| Database            |
+---------------------+
| information_schema  |
| mysql               |
| performance_schema  |
| project_development |
| project_test        |
| sys                 |
+---------------------+
6 rows in set (0.00 sec)

mysql> use project_development;
Database changed
mysql> show tables;
+-------------------------------+
| Tables_in_project_development |
+-------------------------------+
| ar_internal_metadata          |
| schema_migrations             |
| tasks                         |
+-------------------------------+
3 rows in set (0.00 sec)
```
DBが`project_development`と`project_test`が作成されています。
これは`database.yml`中の

```yml:database.yml
development:
  <<: *default
  database: project_development
```
こちらの記述に基づいてということになります。　　
続けて確認していくと、

```SQL:migrate後確認
mysql> desc tasks;
+-------------+--------------+------+-----+-------------------+----------------+
| Field       | Type         | Null | Key | Default           | Extra          |
+-------------+--------------+------+-----+-------------------+----------------+
| id          | bigint(20)   | NO   | PRI | NULL              | auto_increment |
| task_name   | varchar(255) | NO   |     | NULL              |                |
| term_at     | datetime     | NO   |     | NULL              |                |
| created_at  | datetime     | YES  |     | CURRENT_TIMESTAMP |                |
| updated_at  | datetime     | YES  |     | CURRENT_TIMESTAMP |                |
| disp_flag   | tinyint(1)   | YES  |     | 0                 |                |
| delete_flag | tinyint(1)   | YES  |     | 0                 |                |
+-------------+--------------+------+-----+-------------------+----------------+
7 rows in set (0.01 sec)

mysql> show create table tasks\G
*************************** 1. row ***************************
       Table: tasks
Create Table: CREATE TABLE `tasks` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `task_name` varchar(255) NOT NULL,
  `term_at` datetime NOT NULL,
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
  `updated_at` datetime DEFAULT CURRENT_TIMESTAMP,
  `disp_flag` tinyint(1) DEFAULT '0',
  `delete_flag` tinyint(1) DEFAULT '0',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.00 sec)
```

show create tableで確認しても、上で記載したcreate文とほぼ同じ構成で作成されているのが確認できます。　　
( ﾟ∀ﾟ )ｷﾀ━━

### ★注意点

#### idカラムを勝手に追加してはいけない
idカラムは自動生成されるため、追加しちゃうと以下のようにエラーが出力されるので注意。
<pre>
StandardError: An error has occurred, all later migrations canceled:

you can't redefine the primary key column 'id'. To define a custom primary key, pass { id: false } to create_table.
</pre>

#### migrationの変更が反映されない
マイグレーションスクリプトを変更して`rails db:migrate`を行なってもエラーや実行結果が出力されず、DBへの反映もされない場合があります。　　
これは一度実行したmigrationを書き換えても、すでに処理済みとみなされて、再度処理対象とはならないためです。　　
その場合は以下のように`rollback`後再度`migrate`を実行するか、
<pre>
$ rails db:rollback
== 20180406084039 CreateTasks: reverting ======================================
-- drop_table(:tasks)
   -> 0.0125s
== 20180406084039 CreateTasks: reverted (0.0174s) =============================

$ rails db:migrate
== 20180406084039 CreateTasks: migrating ======================================
-- create_table(:tasks)
   -> 0.0148s
== 20180406084039 CreateTasks: migrated (0.0148s) =============================
</pre>
または変更/追加を行うカラムの分だけ別にマイグレーションを行うことで回避できます。　　
その方法は今回は割愛します。

# 3. レコード作成
一覧画面でデータを扱うための用意をしておきます。　　
上で作成したテーブルにinsertを行うための手段はymlファイルを使うとか色々ありますが、今回は **Active Record** を使用します。
<pre>
$ rails console
</pre>
上記のように`rails console`で実行していきます。
これはRailsの環境を読み込んだ状態でrubyコードを実行できるコンソールツールで、読み込んでいるgemも実行可能だそう。
コンソールから以下を実行します。
<pre>
task = Task.new({task_name: '新札を用意', term_at: '2018-05-01 00:00:00'})
task.save
</pre>


```ruby:実行結果
$ rails console
Running via Spring preloader in process 9232
Loading development environment (Rails 5.1.5)
irb(main):001:0>
irb(main):002:0> task = Task.new({task_name: '新札を用意', term_at: '2018-05-01 00:00:00'})
   (0.5ms)  SET NAMES utf8,  @@SESSION.sql_mode = CONCAT(CONCAT(@@sql_mode, ',STRICT_ALL_TABLES'), ',NO_AUTO_VALUE_ON_ZERO'),  @@SESSION.sql_auto_is_null = 0, @@SESSION.wait_timeout = 2147483
=> #<Task id: nil, task_name: "新札を用意", term_at: "2018-05-01 00:00:00", created_at: nil, updated_at: nil, disp_flag: false, delete_flag: false>
irb(main):003:0> task.save
   (0.6ms)  BEGIN
  SQL (15.7ms)  INSERT INTO `tasks` (`task_name`, `term_at`, `created_at`, `updated_at`) VALUES ('新札を用意', '2018-05-01 00:00:00', '2018-04-09 06:36:57', '2018-04-09 06:36:57')
   (3.4ms)  COMMIT
=> true
irb(main):004:0>
```
commitまで完了していますね。
再度MySQLに入ってみるとレコードが追加されているのを確認できます。

# 4. 一覧画面を作成
上でmigrationを実行しているため、`generate`コマンドには`controller`を指定して作成します

```ruby
$ rails g controller Task list
Running via Spring preloader in process 9327
      create  app/controllers/task_controller.rb
       route  get 'task/list'
      invoke  erb
      create    app/views/task
      create    app/views/task/list.html.erb
      invoke  test_unit
      create    test/controllers/task_controller_test.rb
      invoke  helper
      create    app/helpers/task_helper.rb
      invoke    test_unit
      invoke  assets
      invoke    coffee
      create      app/assets/javascripts/task.coffee
      invoke    scss
      create      app/assets/stylesheets/task.scss
```
Controller, Model, Viewを以下のように修正。

- Controller

```ruby:task_controller.rb
class TaskController < ApplicationController
  def list
    @task = Task.all
  end
end
```
- Model

```ruby:task.rb
class Task < ApplicationRecord
  def disp_term_at
    # 期限日+残り日数を設定
    term_string = '期限日まであと' + ((Date.parse(term_at.to_s) - Date.today).to_i).to_s + '日'
    Date.today > Date.parse(term_at.to_s) ? '期限切れ' : term_at.to_s + term_string
  end
end
```

- View

```erb:list.html.erb
<h1>タスクリスト</h1>
<table border="0">
  <tr>
    <th>ID</th>
    <th>タスク名</th>
    <th>期限</th>
    <th></th>
    <th></th>
  </tr>
  <% @task.each do |item| %>
  <% if item.delete_flag == false %>
  <tr>
    <td><%= item.id %></td>
    <td><%= item.task_name %></td>
    <td><%= item.disp_term_at %></td>
    <td>
      <% if item.disp_flag == false %>
      <%= link_to "完了！" %>
      <% else %>
      実施済み
      <% end %>
    </td>
    <td>
      <%= link_to "削除する" %>
    </td>
  </tr>
  <% else %>
  <% end %>
  <% end %>
</table>
```

- initializers  
またdatetime型のカラムをそのまま表示しようとすると
`2018-04-10 23:00:00 UTC`
このような形式で表示されるため（Railsのデフォルト）、日時フォーマットのためのイニシャライザを
`config/initializers/time_formats.rb`に以下のように作成しました。

```ruby:time_formats.rb
# 日時フォーマット定義
Time::DATE_FORMATS[:default] = '%Y/%m/%d %H:%M'
Time::DATE_FORMATS[:datetime] = '%Y/%m/%d %H:%M'
```

# 5. 作成した画面を確認
http://192.168.33.10:3000/task/list にアクセスしてみると以下のようなタスク一覧が表示されます。
※レコードはいくつか追加しています
<img width="727" alt="スクリーンショット 2018-04-13 15.43.15.png" src="https://qiita-image-store.s3.amazonaws.com/0/49828/2dd56ba0-a197-09ac-aa59-804624a60316.png">

見せ方が今一つ＆まだ改良の余地はありますが一覧の作成が完了しました