Railsアプリ開発の豆知識 その1
こんにちは。エンジニアの内山です。
今回は社内で個人課題としてRailsサンプルアプリを開発することになりましたので、その様子を書きます。 現在も引き続き取り組んでいますが、その途中経過を書きたいと思います。
私はRuby on Rails初心者の状態(前職で多少使っていましたがほぼ独学)で今年の6月にCluexにジョインし、 弊社の運営するwebサービス「mamanoko」に関する簡単なタスク対応をしていたのですが、 Railsに関する基本的な知識やベストプラクティス、その他周辺技術(特にフロント技術が苦手です・・・)についてまだまだ実力不足を痛感しており 実装で壁にぶち当たることもありました。
そういうわけで、「スクラッチでアプリを開発し、開発のアンチパターン・ベストプラクティスを知る」ことを目的に サンプルアプリを開発することにしました。 Rails開発に関するtips、チュートリアルやブログなどはとても参考になるものが多数ありますが、 レビューで受けたアドバイスや調べたこと(=今回学んだこと)を元に豆知識としてまとめたいと思います。 特に開発初心者の方の役に立つと私としてはとても嬉しいです。
作るもの
- ブログアプリ:ユーザを作成してログインすると、記事の閲覧・作成などができる。また、記事には複数のキーワード(タグ)を設定することができる。
- Rails, Gem, js, jQuery, bootstrapを使い、ユーザ認証や画像アップロード機能を実装し、スタイルも整える。
- テストも書く。 ※学習時間も含め、約1ヶ月で実装。毎週開発リーダーのレビューを受ける。
設計
基本を押さえて、以下の方針で設計しました。
DOA
DOA(Data Oriented Approach)とは、サービスを開発する際に、データの設計から行うアプローチです。
webサービスでは機能変更などが度々発生しますが、データの構造の変更は頻繁にあるわけではないので、 設計段階でデータの構造をしっかり決めておくことで効率よく開発ができるという考え方です。 複数のシステムを開発する際にDBを共有するときにも重要です。 今回は以下のようなデータ構造にしました。
REST
RESTの基本アクションを利用した設計です。例えば記事機能(articles)はこのような形で構成します。 この説明が分かりやすいと感じました。(http://igarashikuniaki.net/rails_textbook/crud.html)
action | 処理 |
---|---|
#index | 全記事を表示する画面 |
#show | 選択した記事を表示する画面 |
#new | 記事を新規作成する画面 |
#create | #newからpostされた記事を作成し、#indexにリダイレクトする。画面は無し。 |
#edit | 選択した記事を修正する画面 |
#update | #editからpostされた記事を更新し、#indexにリダイレクトする。画面は無し。 |
#destroy | 選択された記事を削除し、#indexにリダイレクトする。画面は無し。 |
実装開始
ということで実装に入っていきます。まっさらな状態で一瞬迷ってしまいますが、 DOAの考え方に従って、変更の少ないModel→Controller→Viewの順で作っていきます。 機能としては大きく記事機能(articles)、記事へのタグ付け機能(keywords)、ユーザ認証(users)がありますが、スモールステップでまずはarticlesの機能から作っていくことにしました。
「スモールステップでつくる」といっても「articlesのmodel、controller、viewからつくる」という単位ではおそらくダメで、DOAの考え方から、先に必要なモデル/DBはすべて作っておくべきです。「変更の可能性の少ないDB周りから実装を固める」というのが肝です。あとからモデルを追加してリレーションを設定したりするのは混乱の元なので・・・ このため、articles、keywords、articles-keywords(中間テーブル)のモデルとDBを先に作ります。(ただし、usersについてはdeviseというGemを利用して後ほど作ります。) なお、この記事で全てのリソースを取り上げるのは時間がかかるので、今回は記事機能(articles)を中心に見ていきたいと思います。
Model
このように実装し、マイグレーションしました。なお、本記事では明示していませんが、文字数制限等を設けたいのでそれらの設定を行なっています。
Modelです。入力文字数によってバリデーションをかけ、各テーブル間のアソシエーションを設定しました。
[app/models/article.rb(記事)] class Article < ApplicationRecord validates :title, presence: true, length: { maximum: 30 } validates :text, presence: true, length: { in: 10..1000 } attachment :image has_many :articles_keywords has_many :keywords, through: :articles_keywords end
[app/models/articles_keyword.rb(中間テーブル)] class ArticlesKeyword < ApplicationRecord belongs_to :article belongs_to :keyword end
[app/models/keyword.rb(キーワード)] class Keyword < ApplicationRecord validates :keyword, presence: true, uniqueness: true, length: { maximum: 20 } has_many :articles_keywords has_many :articles, through: :articles_keywords end
migration
migrationです。各DBカラムに対して、必要に応じてオプションを設定しました。
[db/migrate/yyyymmddhhmmss_create_article.rb(記事)] class CreateArticles < ActiveRecord::Migration[5.0] def change create_table :articles do |t| t.string :title, null: false t.text :text, null: false t.string :image t.references :user, index: true, foreign_key: true t.timestamps null: false end end end
[db/migrate/yyyymmddhhmmss_keyword_article.rb(中間テーブル)] class CreateArticlesKeywords < ActiveRecord::Migration[5.0] def change create_table :articles_keywords do |t| t.references :article, index: true, foreign_key: true t.references :keyword, index: true, foreign_key: true t.timestamps null: false end end end
[db/migrate/yyyymmddhhmmss_keyword.rb(キーワード)] class CreateKeywords < ActiveRecord::Migration[5.0] def change create_table :keywords do |t| t.string :name, null: false t.timestamps null: false end end end
Model/Migrationに関する学び
【Gemについてあらかじめ調べておく】 ここまで、あたかも想定通りに進んでいるかのように書いたのですが(その方が分かりやすいという判断もありましたが・・・)、実はUserモデルをいったん作成したものの削除したりしました。ユーザー認証機構としてdeviseというGemを使用する予定だったため、機能を実装しながら平行してdeviseの知識をインプットしていた折、必要な機能(ファイル)のほとんどがdeviseを導入することで簡単に生成できることが分かったためです。Gemやプラグインを利用するのがあらかじめ分かっている場合は事前に調べて手戻りを防ぐべきだなと、反省です。今後の実装の教訓としたいと思います。
【DBのカラムにオプションをつける】 データ操作に関するバリデーションはActiveRecordによって実現しますが、予期せぬDB不整合を防ぐために、マイグレーションファイルのDBカラムにも各種オプションを設定すべきです。null値を許容しない「null: false」以外にも、以下のようなオプションがあります。DBの整合性を維持するために設定しておきましょう。
option | 内容 |
---|---|
string | 文字列 |
text | 長い文字列 |
integer | 整数 |
float | 浮動小数 |
decimal | 精度の高い小数 |
datetime | 日時 |
timestamp | タイムスタンプ |
time | 時間 |
date | 日付 |
binary | バイナリデータ |
boolean | Boolean |
Controller
このような実装にしました。CRUDの考え方に基づいた標準的な作りです。
[app/controller/articles_controller.rb] class ArticlesController < ApplicationController before_action :set_article, except: [:index, :new, :create] # GET /articles def index @articles = Article.order(id: :desc) end # GET /articles/:id def show end # GET /articles/new def new @article = Article.new end # POST /articles def create @article = Article.new(article_params) if @article.save redirect_to articles_path else render :new end end # GET /articles/:id/edit def edit end # PATCH /articles/:id def update if @article.update(article_params) redirect_to articles_path else render :edit end end # DELETE /articles/:id def destroy @article.destroy redirect_to articles_path end private def article_params params.require(:article).permit(:title, :text, :image) end def set_article @article = Article.find(params[:id]) end end
Controllerに関する学び
- 【generatrorで作成されるファイル】 ジェネレータを使うと様々なファイルが自動的に作成できて便利ですよね。でもどのようなファイルが作られるかご存知でしょうか?このようなファイルが作られます。
$rails generate controller articles Running via Spring preloader in process 44 create app/controllers/articles_controller.rb invoke erb create app/views/articles invoke test_unit create test/controllers/articles_controller_test.rb invoke helper create app/helpers/articles_helper.rb invoke test_unit invoke assets invoke coffee create app/assets/javascripts/articles.coffee invoke scss create app/assets/stylesheets/articles.scss
せっかく作成されたファイルですが、例えば「helperはいらないなぁ」というようなことがあると思います。そういうときは、config/applicaiton.rb
を以下のように編集することで、不要なファイルが作成されなくなります。
[config/application.rb] module Sample class Application < Rails::Application config.generators do |g| g.helper false end end end
すると、指定したとおりhelperが作られなくなります。
$rails generate controller articles Running via Spring preloader in process 108 create app/controllers/articles_controller.rb invoke erb create app/views/articles invoke test_unit create test/controllers/articles_controller_test.rb invoke assets invoke coffee create app/assets/javascripts/articles.coffee invoke scss create app/assets/stylesheets/articles.scss
めったに使わないならrails generate controller articles --no-helper
でいいんですけどね。。。私の場合、実際はcontrollerが必要になったらファイルを新規追加で作ってしまったりすることもありますが、generatorよく使う方は試してみてください。
- 【deleteとdestroyの違い】 destroyアクションの中では、指定した記事を削除するという処理を行います。 削除にはdeleteメソッドまたはdestroyメソッドが使用可能ですが、その違いは「ActiveRecordオブジェクトを介する削除にはdestroy、そうでない削除にはdeleteを使う」と説明されます。ActiveRecordオブジェクトを介して処理を行うとは、「コールバックを通りバリデーションが行われてから削除される」ということを意味します。
ActiveRecordを利用すると「あるオブジェクトを削除したら、関連するオブジェクトも同時に削除する」という処理をコールバックによって実現することができます。複数のオブジェクト間で依存関係がある場合、コールバックを通ることでその依存関係も一緒に削除できます。
VIew
indexだけ載せます。若干スタイル等入っていますが、まだまだシンプルな状態です。
[app/views/articles/index.html.erb] <div class='container'> <h1>#index</h1> <div class='center-block'> <% @articles.each do |article| %> <div class='well well-lg'> <h3><%= article.title %></h3> <%= simple_format(article.text) %> <%= link_to 'show', article_path(article) %> <%= link_to 'edit', edit_article_path(article) %> <%= link_to 'destroy', article_path(article), method: :delete %> </div> <% end %> </div> </div>
Viewに関する学び
- 【view側でソートしない】 今回のアプリでは最新の記事が一番上に来るように表示したかったので、当初はviewで以下のように実装していました。
… <% @articles.reverse_each do |article| %> …
が、必要な処理はviewに渡す前に行うべきであるという指摘に納得し、以下の通り書き直しました。 前掲のコントローラ内で以下の通り処理し、viewでは余計な処理をしないように修正しました。
[app/controller/articles_controller.rb] def index @articles = Article.includes(:user).order(id: :desc) end
[app/view/articles/index.html.erb] … <% @articles.each do |article| %> …
ソートに限らず、Rails内でロジックを書くべき場所は常に意識しようと思います。
routing
articlesのみ設定した状態なのでこんな感じです。
[config/routes.rb] Rails.application.routes.draw do resources :articles end
次回予告
サンプルアプリ開発は引き続き取り組んでおり、現在、キーワード設定機能やユーザログイン機能などを実装した段階です。 最低限のCSSで整え、現在はこんな感じです。見た目についてもbootstrapを利用してさらにリッチにしていく予定です! 続編のブログでは、Gemの導入などに関する豆知識などをお伝えできればと思います。
それではまた次回!
We’re hiring!!
弊社ではWeb / ネイティブアプリエンジニアを募集しております。
ご興味がありましたらお気軽にご連絡下さいませ!
エンジニアの方、ぜひ情報交換しましょう!