【Sinatra入門】Ruby+Sinatraで記事投稿アプリケーションを作ろう！（その１）

## はじめに

今年は就活なので、前々から開発&運営している[個人ブログ](https://knowledge-blog.net)の記事投稿システムをポートフォリオとして作ってみました。
この記事ではその記事投稿システムの簡易版を、解説しつつ一から構築していきたいと思います！

一応初心者向けという事で、細かいところもある程度詳しく書いてます。

※長いので3つに分けてます

## 対象読者

- Sinatra入門者
- Rails入門者
- 簡易的なブログシステムを作ってみたい人
- ある程度ちゃんとした構成でSinatraの環境構築をしたい人
- Railsがよく分からなくて、もう少し基礎からWebアプリを作りたい人

※RailsやSinatraなどのFWを一度は触った事がある前提としてます

## Sinatraとは

>Rubyで作成されたオープンソースのWebアプリケーションフレームワークである。
他の著名なRubyで作成されたWebアプリケーションフレームワークであるRuby on Railsなどは、Model View Controller（MVC）の考え方に基づいた設計となっている。一方SinatraはMVCに基づかない設計で作成されており、小さく、柔軟性があるプログラミングが可能となるよう意識されている。
[Sinatra - Wikipedia](https://ja.wikipedia.org/wiki/Sinatra)

Sinatraでの開発手法としてクラシックタイプとモジュラータイプというものがありますが、今回はクラシックタイプで進めます。

## なぜSinatra?

当初はRailsで作ろうと思っていたのですが、Railsがよく分からなかった&&もう少し基礎から勉強したい&&作るものに対してRailsは重装備すぎる、ということでSinatraにしました。

ほとんどの情報をネットで得たため、至らない点やベストじゃない点、間違ってる所があるかと思います。もしそういう所があればコメントで教えてくれると助かります。

## 目標

想定するシナリオとしては、カテゴリーを持った記事をブラウザから投稿できる記事投稿サイトを作成します。
要は管理画面のあるブログみたいな感じです。

- 記事投稿画面を作成する。
- 記事のサムネイル画像も投稿できるようにする。
- 記事はmarkdownで書き、それをHTMLに変換してプレビュー・投稿を行う。
- 簡単にバリデーションも追加する。
- ログイン画面を作成する。
- 記事を投稿できるのは決められたユーザ(今回は一人しか想定しないためユーザは一人)のみにし、ログイン処理を作る。
- ログアウト処理も作る。
- CSRF対策を実装する。
- 画面遷移やセッション、SQLやActiveRecord、そのほかGemやRack・Rakeなどの基本的な使い方や導入に関する理解などが深まるとうれしい。

公開するところまではやりません。

## 環境

- Mac 10.14.5
- Ruby 2.5.3
- MySQL 8.0.17
- Sinatra 2.0.7
- ActiveRecord 5.2.3

## 環境構築

最初の難関・環境構築です。
単に動かすだけでなく、実用を想定してある程度しっかりした構成を最初に組んでいきます。

今回はMySQLを使うのですが、自分は大昔に入れてしまったため、インストール手順は[Mac へ MySQL を Homebrew でインストールする手順 - Qiita](https://qiita.com/hkusu/items/cda3e8461e7a46ecf25d)などを見て入れてください。

次に、プロジェクトフォルダを作成し、Gemfileを生成します。

```terminal:ターミナル
mkdir Article-Post
cd ./Article-Post
bundle init　# Gemfileを生成
```

Gemfileができたら、今回使うgemたちを記述します。

```Ruby:Gemfile
# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) { |repo_name| "https://github.com/#{repo_name}" }

gem 'activerecord'
gem 'bcrypt'
gem 'mysql2'
gem 'rake'
gem 'redcarpet'
gem 'sinatra'
gem 'sinatra-activerecord'
gem 'sinatra-contrib'
gem 'slim'
```

Sinatra + Slim + MySQLという編成にActiveRecordを乗せてやって行きたいと思います。

説明が必要そうなやつだけ軽く紹介

-  **sinatra**　本体
-  **sinatra-contrib**　開発時に便利な機能とかを色々追加できる
  -  今回はファイルを変更した時にサーバーを再起動しなくてよくするreloaderというやつを使います
- **redcarpet**　markdown記法で入力されたものをHTMLに変換してくれるすごいやつ
- **Rake**　Rubyで処理内容を定義できるビルドツール(今回だとあんまり活用できてないかも)

Gemfileに記述したgemをインストールします。

```terminal:ターミナル
sudo bundle install --path vendor/bundle
```


オプションでpathを指定するとインストール場所をその指定したpath配下になります。
pathを指定する理由は、システムにインストールし過ぎるとgemがカオスになるらしく、bundlerの推奨環境としてvender/bundle以下に個別に管理するようにしたほうが良いらしいです。

## mysql2のインストールでエラーが起きた場合

```terminal:ターミナル
An error occurred while installing mysql2 (0.5.2), and Bundler cannot continue. Make sure that gem install mysql2 -v '0.5.2' --source 'https://rubygems.org/' succeeds before bundling.
```

というエラーが出たらとりあえず言われた通りに下を実行

```terminal:ターミナル
sudo gem install mysql2 -v '0.5.2' --source 'https://rubygems.org/'
```

今度は

```terminal:ターミナル
 Don't know how to set rpath on your system, if MySQL libraries are not in path mysql2 may not load

ld: library not found for -lssl clang: error: linker command failed with exit code 1 (エラー原因っぽい場所を抜粋) 
```

と出る

ぐぐるとこのサイトが出てきました。https://qiita.com/akito19/items/e1dc54f907987e688cc0

つまりパスを通せってことらしいです。(超解釈)
パスを通すために下のコマンドを実行

```terminal:ターミナル
bundle config --local build.mysql2 "--with-ldflags=-L/usr/local/opt/openssl/lib --with-cppflags=-I/usr/local/opt/openssl/include"
```

そしてもう一度インストールすると出来ます。

```terminal:ターミナル
sudo bundle install --path vendor/bundle
```

## app.rbを作成

次に、メインの処理などを記述するrubyファイルを作成します。

```terminal:ターミナル
touch app.rb
```

このapp.rbに様々な(今回では主にコントローラ)処理を書いて行きます。ファイル名は任意です。

SinatraはRailsなどと違ってMVCに基づいていないので、1つのファイルに全ての処理を記述することは問題ではありません。
が、コード量が多くなってくると流石に見通しが悪くなるので今回はMVCを分けてやって行きます。

まず動作確認のためのサンプルをapp.rbに記述しておきます。

```Ruby:app.rb
get '/' do
  'hello world by sinatra'
end
```

これは、webサーバーを立てた時に、例えばhttp//localhost:9292/にアクセスした場合のGETリクエストに対応する処理となります。

このへんの基本的な説明が分からない人は[README](http://sinatrarb.com/intro-ja.html)と[Sinatra 入門(Qiita)](https://qiita.com/kimioka0/items/751e460cbb59c70379c6#11-%E6%8E%B2%E7%A4%BA%E6%9D%BF%E3%82%A2%E3%83%97%E3%83%AA%E3%82%92%E4%BD%9C%E3%81%A3%E3%81%A6%E3%81%BF%E3%82%88%E3%81%86)に目を通しておくと良いです。

次に、config.ruファイルを用意します。

### config.ruとは
config.ruとは、Rackのrackupコマンドを使ってサーバ起動を行う場合の設定ファイルになります。
今回は、gemの読み込みやDBの設定ファイルの読み込みなどを記述しておきます。

### Rackとは
>RubyによるWebアプリケーション開発のHTTP送受信処理を担当するモジュール(gem)で、Ruby on Railsを始めとする多くのWebフレームワークの一番下のレベルで利用されているもの。
[Rack解説 - Rackの構造とRack DSL](https://qiita.com/higuma/items/838f4f58bc4a0645950a)

```terminal:ターミナル
touch config.ru
```

```ruby:config.ru
require 'rubygems'
# bundleに入れたgemを全てrequire
require 'bundler'
Bundler.require
# sinatra-contribのオートリロードしてくれるやつはrequireする必要がある
require 'sinatra/reloader'
# app.rbもrequireする
require './app.rb'

run Sinatra::Application
```

ここまでで実行できる最低限の環境が揃ったのでサーバーを立ち上げてみます。
ポート番号は9292です。

```terminal:ターミナル
bundle exec rackup config.ru
```

以下の表示が出ればOK
![スクリーンショット 2019-06-25 14.37.53.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/202058/5fd3881c-57c1-bb21-a058-50df0790560e.png)

### ここまでのディレクトリ構成

```terminal
.
├── Gemfile
├── Gemfile.lock
├── app.rb
├── config.ru
└── vendor
    └── bundle
        └── ruby
```

## ちなみに

sinatraを起動するだけならもっと簡単で、config.ruファイルを使わずにapp.rbファイルでsinatraをrequireし、

```Ruby:app.rb
require 'sinatra'

get '/' do
  'hello world by sinatra'
end
```

```terminal:ターミナル
bundle exec ruby app.rb
```

で起動できます。
このように、config.ruファイルを使わずapp.rbに直接gemをrequireしたりDB設定を丸ごと書いて動かす事はできるのですが、見にくいのであんまりオススメしません。

## データベース関連の作成

Active Recordの機能であるマイグレーションを用いてテーブル作成を行っていきます。

### マイグレーションとは
>マイグレーション (migration) はActive Recordの機能の1つであり、データベーススキーマを長期にわたって安定して発展・増築し続けることができるようにするための仕組みです。マイグレーション機能のおかげで、Rubyで作成されたマイグレーション用のDSL (ドメイン固有言語) を用いて、テーブルの変更を簡単に記述できます。スキーマを変更するためにSQLを直に書いて実行する必要がありません。
[Active Record マイグレーション](https://railsguides.jp/active_record_migrations.html)

要するにSQLを直に書かなくてもRubyとDSL(ドメイン固有言語)でテーブルの作成や変更ができるよ、ということです。
変更毎にその内容がファイルとして生成されるので、履歴確認としても便利だなーと思います(小並感)

今回、そのマイグレーション用ファイルを生成するためのRakefileを作成します。

(Rakefikeとはrakeタスクの為の定義を記述するファイル...という事ですが、自分もよく分かってません。今回はあくまでDBの設定を書いてマイグレーションを行えるようにする、という目的のために作成しています。)

```terminal:ターミナル
touch Rakefile
```

```Ruby:Rakefile
require 'sinatra/activerecord'
require 'sinatra/activerecord/rake'
```

Rakefileでこの二つのパッケージをrequireすることにより、rakeタスクというものが使えるようになります。

rakeタスクの確認は ```bundle exec rake -T ``` でできます。
このrakeタスクの中にある ``` rake db:create_migration``` を使ってテーブルの作成(マイグレーション)を行います。

ですが、まだそのテーブルを格納するためのデータベースの作成を行っていないので、そちらを先にやります。

## データベースを作成

MySQLで今回使うデータベースを作成します。

```terminal:ターミナル
mysql.server start # MySQL起動
mysql -u root # ログイン
```

起動後、以下のSQLを発行

```terminal:ターミナル
mysql> CREATE DATABASE articles4; # articles4という名前のDBを作成
mysql> USE articles4; # それを使うよーと宣言
```

## database.ymlを作成

次に、データベースとの接続情報を記述するdatabase.ymlを作成します。

```terminal:ターミナル
touch database.yml
```

```yml:database.yml
development:
  adapter: mysql2
  database: articles4
  host: localhost
  username: root
  password:
  encoding: utf8
```

database:という所に先程作成したデータベース名を入れます。
今回はとりあえずdevelopmentだけ設定します。ちなみにパスワードは無しです。

次に、今作ったymlファイルをconfig.ruとRakefileでActiveRecordに読み込ませます。
ついでに、DBのタイムゾーンをTokyoに変更。

```ruby:config.ru
# 〜省略〜
require './app.rb'

# 追加↓
# database.ymlを読み込んでくれ〜
ActiveRecord::Base.configurations = YAML.load_file('database.yml')
# developmentを設定
ActiveRecord::Base.establish_connection(:development)
# タイムゾーンを東京にする
Time.zone = 'Tokyo'
ActiveRecord::Base.default_timezone = :local

run Sinatra::Application
```

```ruby:Rakefile
require 'sinatra/activerecord'
require 'sinatra/activerecord/rake'

# 上と同じやつ
ActiveRecord::Base.configurations = YAML.load_file('database.yml')
ActiveRecord::Base.establish_connection(:development)
Time.zone = 'Tokyo'
ActiveRecord::Base.default_timezone = :local
```

## マイグレーション用ファイルを作成

諸々の設定は出来たので、テーブルを追加するためのマイグレーション用ファイルをrakeを用いて作成します。
今回作成するテーブルは投稿した記事を格納するためのpostテーブル、その記事のカテゴリーを分類するためのcategoryテーブルを作成します。

テーブル名は複数形にして作成します。
まずはcategoriesテーブルから

```terminal:ターミナル
bundle exec rake db:create_migration NAME=create_categories
```

実行すると、db/migrateディレクトリが作成され、その下にマイグレーション用ファイル ```日付_create_categories.rb``` が作成されています。

中身はこんな感じ

```Ruby:日付_create_categories.rb
class CreateCategories < ActiveRecord::Migration[5.2]
  def change
  end
end
```

changeメソッドの中に、テーブルに必要なカラムを追加していきます。
categoriesテーブルには、

- カテゴリー番号(自動生成)
- カテゴリー名

を格納するためのカラムを作ります。

```Ruby:日付_create_categories.rb
class CreateCategories < ActiveRecord::Migration[5.2]
  def change
    create_table :categories do |t|
      t.string :name, null: false # stringはVARCHAR(255), null: falseでNOT NULL制約
    end
  end
end
```

次にpostsテーブルも作成
このテーブルには、投稿された記事の

- 記事番号（自動生成）
- カテゴリー番号
- タイトル
- 本文
- 記事のサムネイルの画像名
- 投稿日（自動生成）
- 更新日（自動生成）

を格納するためのカラムを作ります。
記事のサムネイル画像は、画像を丸ごとDBに保存するのではなく、そのサムネイルの画像名だけ保存します。

```terminal:ターミナル
bundle exec rake db:create_migration NAME=create_posts
```

```Ruby:日付_create_posts.rb
class CreatePosts < ActiveRecord::Migration[5.2]
  def change
    create_table :posts do |t| # postsがテーブル名
      t.integer :category_id
      t.string  :title, null: false
      t.text    :body,  null: false
      t.string  :thumbnail, null: false

      t.timestamps # これを書くとcreated_atとupdated_atカラムが自動定義される
    end
  end
end
```

これらを記述したら、 ```bundle exec rake db:migrate``` を実行してマイグレーション（テーブル作成）します。
もしMySQLサーバーを起動していない場合は起動してください。

```terminal:ターミナル
# 起動していない場合
mysql.server start
bundle exec rake db:migrate
```

テーブルができているかMySQLで確認

```terminal:ターミナル
mysql -u root # ログイン

# ここからSQL
mysql> use articles4;
mysql> SHOW columns FROM categories;
+-------+--------------+------+-----+---------+----------------+
| Field | Type         | Null | Key | Default | Extra          |
+-------+--------------+------+-----+---------+----------------+
| id    | bigint(20)   | NO   | PRI | NULL    | auto_increment |
| name  | varchar(255) | NO   |     | NULL    |                |
+-------+--------------+------+-----+---------+----------------+
mysql> SHOW columns FROM posts;
+-------------+--------------+------+-----+---------+----------------+
| Field       | Type         | Null | Key | Default | Extra          |
+-------------+--------------+------+-----+---------+----------------+
| id          | bigint(20)   | NO   | PRI | NULL    | auto_increment |
| category_id | int(11)      | YES  |     | NULL    |                |
| title       | varchar(255) | NO   |     | NULL    |                |
| body        | text         | NO   |     | NULL    |                |
| thumbnail   | varchar(255) | NO   |     | NULL    |                |
| created_at  | datetime     | NO   |     | NULL    |                |
| updated_at  | datetime     | NO   |     | NULL    |                |
+-------------+--------------+------+-----+---------+----------------+
```

良い感じにできてます。

本当は、postsテーブルが持つcategory_idを外部キーにしたかったのですが、やり方がよく分からなかったので諦めました(´・ω・｀)

### ここまでのディレクトリ構成

```terminal:ターミナル
.
├── Gemfile
├── Gemfile.lock
├── Rakefile
├── app.rb
├── config.ru
├── database.yml
├── db
│   ├── migrate
│   │   ├── 日付_create_categories.rb
│   │   └── 日付_create_posts.rb
│   └── schema.rb
└── vendor
    └── bundle
        └── ruby
```

## モデルを分ける

次に、MVCやその他必要なファイルを作成していきます。

まずモデルのためのファイルを作成します。

```terminal:ターミナル
mkdir models
touch models/categories.rb
touch models/posts.rb
```

modelsディレクトリの中に先ほど作ったテーブル名を単数形にした名前のクラスを作成し、それにActiveRecord::Baseを継承させます。

ファイルの中身は以下のようにします。

```Ruby:categories.rb
class Category < ActiveRecord::Base
end
```

```Ruby:posts.rb
class Post < ActiveRecord::Base
end
```

ここにモデルで行うバリデーションなどの処理を記述します。
この辺はRailsと似たような感じです。

## ビューを分ける

ビューを作成していきます。

```terminal:ターミナル
mkdir views
touch views/layout.slim
```

layout.slimはビューのデフォルトテンプレートになります。(ファイル名はおそらくlayoutにしないといけない)
これを用意すると、他の全てのビューはこのlayout.slimを通してレンダリングされます。

## その他必要なディレクトリを作成

app.rbやビューで使うヘルパーメソッドをまとめるhelpersディレクトリを作成

```terminal:ターミナル
mkdir helpers
```

静的ファイル(html, css, jsなど)を置くためのpublicディレクトリを作成
静的ファイルは./publicディレクトリから配信されます。 (ファイル名はおそらくpublicにしないといけない)

```terminal:ターミナル
mkdir public
mkdir public/js
mkdir public/css
mkdir public/img
```

## 作成したディレクトリをconfig.ruに読み込み

modelsとhelpersはconfig.ruで読み込む必要があります。
読み込む順番をミスるとエラーが起きたりするので注意してください。

```ruby:config.ru
# 〜省略〜
require './app.rb'

# 追加↓
# /models,　/helpers配下のrubyファイルを全てrequireする
Dir[File.dirname(__FILE__) + '/models/*.rb'].each { |f| require f }
Dir[File.dirname(__FILE__) + '/helpers/*.rb'].each { |f| require f }

# database.ymlを読み込み
ActiveRecord::Base.configurations = YAML.load_file('database.yml')
# 〜省略〜
```

### ここまでのディレクトリ構成

```terminal:ターミナル
.
├── Gemfile
├── Gemfile.lock
├── Rakefile
├── app.rb
├── config.ru
├── database.yml
├── db
│   ├── migrate
│   │   ├── 日付_create_categories.rb
│   │   └── 日付_create_posts.rb
│   └── schema.rb
├── helpers
├── models
│   ├── categories.rb
│   └── posts.rb
├── public
│   ├── css
│   ├── img
│   └── js
├── vendor
│   └── bundle
│       └── ruby
└── views
    └── layout.slim
```

## ターミナルから色々な操作やデバッグをできるようにする

Rails consoleのようにターミナルからテーブルのカラムを追加したり、モデルのバリデーションを試したりできるようにします。
通常app.rbにgemやdbの設定を書いている場合は ```bundle exec irb -r './app.rb'``` でrails consoleのような操作ができるのですが、今回はその設定ファイルをconfig.ruに分離しているので、上のコマンドを打ってもできません。

なので、それを解決する為のgemであるrackshを導入します。

```termina:ターミナル
gem install racksh
```

起動は ```racksh``` と打つだけ

```terminal:ターミナル
racksh
Rack::Shell v1.0.0 started in development environment.
[1] pry(main)>
```

pryが立ち上がります。これにて、rails consoleのような操作が出来るようになります。

## その２へ

環境構築はこれにて終了です。長く苦しい戦いでした。

次の回からメインとなる記事投稿システムを作っていきます。（その２〜３は自分のブログに載せてます）

[【ポートフォリオ】Ruby+Sinatraで記事投稿システムを作ろう！（その２） - Knowledge-Blog](https://knowledge-blog.net/articles/30)
[【ポートフォリオ】Ruby+Sinatraで記事投稿システムを作ろう！（その３） - Knowledge-Blog](https://knowledge-blog.net/articles/31)

最終的な完成品は[こちら(GitHub)](https://github.com/Alt70155/Article-Post)