Cluex Developersブログ

株式会社Cluexでは、子育てをするママのためのメディア - mamanoko(ままのこ)をRuby on Railsで運営しております。

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を共有するときにも重要です。 今回は以下のようなデータ構造にしました。 f:id:cluex-developers:20170908153348p:plain

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を利用してさらにリッチにしていく予定です!

f:id:cluex-developers:20170908152754p:plain
続編のブログでは、Gemの導入などに関する豆知識などをお伝えできればと思います。

それではまた次回!

We’re hiring!!

弊社ではWeb / ネイティブアプリエンジニアを募集しております。
ご興味がありましたらお気軽にご連絡下さいませ!
エンジニアの方、ぜひ情報交換しましょう!

www.wantedly.com

www.wantedly.com