20201129のRailsに関する記事は30件です。

ポートフォリオ作成過程 Railsチュートリアル2章

Rails scaffoldジェネレータとMVCモデル

そこで、railsチュートリアルの第2章が終わったのでその復讐の意味も込めて今回は記述致します。

・scaffoldジェネレータ

scaffoldジェネレータを使用すると自動でWEBアプリが作られます。
しかし、この章では概要を知り理解する事が目的なのであくまでも、
簡素で作りたいなら、scaffoldジェネレータを使用する事だと私は感じました。

・MVCモデル

Model=モデル(チュートリアルではデータベースのテーブル)

View=ユーザー(アプリを利用する人)が操作・結果をみる画面

Controller=ViewとModelのやりとりを処理(CRUD等)して返す。

上記の3つのことをさしている様です。
それぞれの役割をハッキリさせて、
プログラミングをすることでエラーを探して修正したり、
それぞれが独立して扱えるようにする事で改修も容易になると感じました。

※気になった用語
CRUD(クラッド)
システムでデータベース(DB)操作の基本で使用されていた。簡単に調べるとデータ操作の必要最低限の機能を指している様です。
・Create(作成(追加))
・Read(読取(参照))
・Update(更新)
・Delete(削除)

今回の学習を通して感じたこと

とりあえず、チュートリアルを通して学んだことをポートフォリオに追加していく方が私自身の転職活動の近道になると思いました。

ただし、理解をする事も重要でいざ説明を求められた時に対処できないでは認められないのがこの業界です。

これを噛み砕いて説明できるとお客様とのやりとりもスムーズになるいい訓練と思って作っていくことで学習もよくなると思いました。

とりあえず、CRUDとログイン機能のセキュリティ機能追加を作成後、

テストをするのですがCD/CI(すみません詳しくはテスト工程に入ってから調べるので言葉のみ使います)を利用することも挑戦する予定です。

ただ、「いつまでにポートフォリオ完成して理解などの復習するの?」はまだ思いついてない状況ですので、よく考えて実行したいと反省しております。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ポートフォリオ作成記録 Railsチュートリアル2章

Rails scaffoldジェネレータとMVCモデル

そこで、railsチュートリアルの第2章が終わったのでその復讐の意味も込めて今回は記述致します。

・scaffoldジェネレータ

scaffoldジェネレータを使用すると自動でWEBアプリが作られます。
しかし、この章では概要を知り理解する事が目的なのであくまでも、
簡素で作りたいなら、scaffoldジェネレータを使用する事だと私は感じました。

・MVCモデル

Model=モデル(チュートリアルではデータベースのテーブル)

View=ユーザー(アプリを利用する人)が操作・結果をみる画面

Controller=ViewとModelのやりとりを処理(CRUD等)して返す。

上記の3つのことをさしている様です。
それぞれの役割をハッキリさせて、
プログラミングをすることでエラーを探して修正したり、
それぞれが独立して扱えるようにする事で改修も容易になると感じました。

※気になった用語
CRUD(クラッド)
システムでデータベース(DB)操作の基本で使用されていた。簡単に調べるとデータ操作の必要最低限の機能を指している様です。
・Create(作成(追加))
・Read(読取(参照))
・Update(更新)
・Delete(削除)

今回の学習を通して感じたこと

とりあえず、チュートリアルを通して学んだことをポートフォリオに追加していく方が私自身の転職活動の近道になると思いました。

ただし、理解をする事も重要でいざ説明を求められた時に対処できないでは認められないのがこの業界です。

これを噛み砕いて説明できるとお客様とのやりとりもスムーズになるいい訓練と思って作っていくことで学習もよくなると思いました。

とりあえず、CRUDとログイン機能のセキュリティ機能追加を作成後、

テストをするのですがCD/CI(すみません詳しくはテスト工程に入ってから調べるので言葉のみ使います)を利用することも挑戦する予定です。

ただ、「いつまでにポートフォリオ完成して理解などの復習するの?」はまだ思いついてない状況ですので、よく考えて実行したいと反省しております。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RailsでWixやShopifyのような独自ドメインの紐付けができるマルチテナントサービスを作る

背景

最近流行りのノーコードサービスでは自分のウェブサイトやサービスを作って、独自ドメインを登録できたりしますよね? あれどうやってやるんだろうな?Railsでもできるのかな?と思ったので挑戦してみました。

※この記事ではアプリケーションレベルでの、ドメイン、サブドメインの取り扱い、データの分離について紹介します。本番環境では場合によってプロキシーサーバーを立てたり、追加でのDNS設定が必要です。

独自ドメインの紐付けとは?

通常のウェブサービスだとユーザーのページは「https://サービスのドメイン.com/user/ユーザーのID」のような感じです。しかしWix,Shopify、BASEのようなノーコードサービスでは自分のドメイン、またはサブドメインを登録してサイトを展開することができます。独自ドメインの紐付けとは「https://サービスのドメイン.com/user/ユーザーのID」を「https://ユーザーが取得したドメイン.com」からアクセスできるようにすることです。

マルチテナントサービスとは?

プラットフォーム上にユーザーが独自のデータ領域を持つサービスです。例えば、SmartHRのようなSaaSサービスはクライアントが法人で法人は様々な社員データをサービスに登録することになります。この時、会社Aの個人情報が会社Bの個人情報と同じデータプールで管理されていたらリスクですよね? なのでこのようなサービスではデータ領域をユーザー(テナント)ごとに分けて管理することが多いです(マルチテナンシー)。

作るもの

オンラインで簡単にオンラインスクールを開設できるサービスを作ろう!(仮 anyclass)
講師(instructor)はスクール(school)を作成できる
schoolテーブルはdomain,subdomainカラムを持っていて自分で設定できる
68747470733a2f2f71696974612d696d6167652d73746f72652e73332e61702d6e6f727468656173742d312e616d617a6f6e6177732e636f6d2f302f3232343937342f64376562376435662d343561302d343562612d363335352d6132313663366331623061332e706e67.png

この記事で説明しないこと

deviseを使ったユーザー認証等、独自ドメイン紐付けに関係のない部分

この記事での目標

プラットフォーム上で作成したスクールに独自ドメインでアクセスできるようになること

実装手順

  • スクールのページをサブドメインごとに切り分ける
  • スクールのページに独自ドメインを紐付ける
  • テナントごとにデータを分離する(途中)

STEP1:スクールページをサブドメインごとに切り分ける

ここではユーザーがスクール開設時に登録したサブドメインをもとに、スクールのトップページを表示するということをやっていきます。

1. ルーティングの制御

まずルーティングを設定していきます。
www以外のサブドメインへリクエストがきたらこの範囲のパスを読み込みます!というのを実行する為の設定です。railsのconstrain機能を使って実装します。

参考記事: rails routing constraintsについて
https://y-yagi.tumblr.com/post/92386974040/rails-routing-constraints%E3%81%AB%E3%81%A4%E3%81%84%E3%81%A6

lib以下にconstarain用のクラスを定義します。
constrain用のクラスで定義したmatches?がtrueの場合、任意のスコープ内にあるパスが採用される。という感じです。

lib/school_domain.rb
class SchoolDomain
  class << self
    def matches?(request)
      matches_subdomain?(request)
    end

    private
      def matches_subdomain?(request)
        # wwwの場合は追加するルーティングにマッチングしないようにしておく
        request.subdomain.present? && request.subdomain != 'www'
      end
  end
end

route.rb
Rails.application.routes.draw do
  #constratinファイルの読み込み
  require Rails.root.join('lib', 'school_domain.rb')

  #constratinでの制御 matches?で該当したパスはここのルーティングが適応される
  constraints SchoolDomain do 
    scope module: :school do
      get '/', to: 'schools#show'
      resources :lessons, only: [:show, :index]
    end
  end

  ...省略

2. サブドメインの抽出とスクールの検索

requestに入ってくるドメインの情報を元にサブドメインを特定します。このとき、wwwがサブドメインの場合は除外しておきたいので、そのロジックも入れます。

このメソッドはbefore_actionで呼び出すのですが、先ほど定義したルーティングないで実行されるコントローラーで呼びだすことが多いと思います。例えば今回の場合は、schools controllerのshowで呼び出します。 

application_controller.rb
    def set_school_by_domain_name
      if request.subdomain.present? && request.subdomain != 'www'
        @school = School.find_by(subdomain: request.subdomain)
      end  

      # 該当するスクールがない場合、URLをリセットしてトップページにリダイレクト
      redirect_to root_url(domain: ENV["MY_DOMAIN_NAME"], subdomain: "") unless @school.present?
    end

ローカルで動作確認
ローカルでの動作確認は2種類方法があります。一つは、etc/hostファイルを変更して任意のURLがきたらlocalhost:3000を向くようにする方法、2つ目は、lvh.me:3000のようなループバックドメインの利用する方法。今回はループバックドメインを使います。

ループバックドメインとは、

メインドメインでアクセスしてもサブドメインでアクセスしても 127.0.0.1 に返してくれるドメイン(サービス)

参照元記事:Railsでユーザごとにサブドメインが切られたユーザページを提供する手順
https://qiita.com/himatani/items/729568277588fcf37720

...実際にやってみる

まず講師登録&ログイン
スクリーンショット 2020-11-29 22.55.19.png

スクール作成
スクリーンショット 2020-11-29 22.56.57.png

スクリーンショット 2020-11-29 22.57.38.png

スクールページへアクセス(ローカル環境: http://helloworld.lvh.me:3000/)
うまく表示できました!
スクリーンショット 2020-11-29 22.58.28.png

試しに適当なURL(helloworld.lvh.me:3000とか)にアクセスすると、講師のダッシュボードトップに戻ってくるので、そっちもうまく行ってそうです。

本番環境での注意点

通常のサービスドメインのSSLに加えて、サブドメインのワイルドカードを持ったSSLを取得していないと全てのサブドメインをSSL通信させることはできません。

特にHerokuでデプロイしている場合は注意が必要です。ACMでサブドメインのSSLを発行できないので、自分で取得または作成してアップロードすることになります。僕はLet'sEncryptoを使ってオレオレ証明書を作成しました。その際に参考にした記事です。

参考記事1: MacOS に CertBot を入れて Let's Encrypt 証明書を作ってみる
https://neos21.hatenablog.com/entry/2019/03/11/080000

参考記事2: Let's Encrypt ワイルドカード証明書の取得手順メモ
https://qiita.com/fukmaru/items/dd75f2a3601472590ead

参考記事3: Heroku SSL + Let's Encrypt で、ワイルドカードなSSL証明書を設定する
https://qiita.com/fukmaru/items/dd75f2a3601472590ead

STEP2:独自ドメインとの紐付け

サブドメインの紐付けがうまくいきました。次は独自ドメインの紐付けです。
まずconstrainの部分を変更して、別ドメインがきた時も同じルーティングを採用するようにします。

lib/school_domain.rb
class SchoolDomain
  class << self
    def matches?(request)
      matches_custom_domain?(request) || matches_subdomain?(request)
    end

    private
      def matches_custom_domain?(request)
        request.domain.present? && request.domain != ENV["MY_DOMAIN_NAME"]
      end

      def matches_subdomain?(request)
        # wwwの場合は追加するルーティングにマッチングしないようにしておく
        request.subdomain.present? && request.subdomain != 'www'
      end
  end
end

次にapplication_controllerでドメインをrequestから抽出して、schoolを検索する処理をします

application_controller.rb
    def set_school_by_domain_name
      if request.domain.present? && request.domain != ENV["MY_DOMAIN_NAME"]
        @school = School.find_by(domain: request.domain)
      end

      if request.subdomain.present? && request.subdomain != 'www'
        @school = School.find_by(subdomain: request.subdomain)
      end  

      # 該当するスクールがない場合、URLをリセットしてトップページにリダイレクト
      redirect_to root_url(domain: ENV["MY_DOMAIN_NAME"], subdomain: "") unless @school.present?
    end

この状態でローカル環境でテストしていきます!

僕の環境ではlvh.meをENV["MY_DOMAIN_NAME"]に設定しているので、別のループバックURL(localtest.me)を独自ドメインとして登録してテストします。
今の時点でlocaltest.me:3000にアクセスしても、正常にroo_urlに戻されました。

スクリーンショット 2020-11-29 23.10.42.png

この状態で、localtest.me:3000にアクセスします
スクリーンショット 2020-11-29 23.15.11.png

うまくいきました!

本番環境で独自ドメインを紐付ける

本番環境の実装方法は、環境によって異なるので今回の記事では割愛します?‍♂️
後日追記するかもしれません。
軽く調べた感じだと「多段CNAME」や「プロキシサーバー」がキーワードになっている印象

ユーザー側で必要な処理

  • 独自ドメインの取得
  • DNSの設定(cnameまたはAレコード) BASEやSHOPIFYのドメイン登録ページがわかりやすい(https://apps.thebase.in/detail/6)

STEP3: テナントごとにデータを分離する

個人情報を取り扱うようなSaaSサービスは、テナントごとにデータを分離することが重要です。データの分離がされていないと何かの拍子に会社Aの情報を会社Bのユーザーに向けて表示してしまう等のリスクが発生します。

apartment gem

「Rails マルチテナンシー」で調べるとapartment gemについての記事がよく出てきます。apartment gemはマルチテナント実装には最適なgemですが、スケールしていく上で落とし穴もあるようです。SmartHRさんが、aparmtnet gemを最初使っていて、途中でCitusData(postgresqlの拡張)に乗り換えるというイベントがありました。詳しくは参考記事をよんでください。

参考記事: SmartHR が定期メンテナンスを始めた理由とやめる理由
https://tech.smarthr.jp/entry/2018/04/06/100000

activerecord-multi-tenant

マルチテナンシーを同一DB上で実現させるためのgemです。テナントごとにテーブルが別れているわけではないので、データの横断は不可能ではないですが、アプリケーションレベルでデータの領域分けを強制させられるのでマルチテナンシーとして機能しそうです(全部は理解できていない汗)

参考記事: ActiverecordMultiTenant でマルチテナンシー by yks0406さん (マルチテナンシーについて、Railsでのアプローチについて一番詳しくまとまっている記事です)
https://note.com/yks0406/n/n09c181400561

参考記事に習って、今回のサンプルアプリもactiverecord-multi-tenantで実装を進めていきます。

スクール(テナント)ごとに生徒の情報を管理する

?‍♂️2020年12月中に追記予定?‍♂️

最後に

SaaSやノーコードアプリを開発するエンジニアのかた増えてきていると思います。
参照記事多目、まとめ記事と言っても過言ではないですが、お役に立てれば幸いです。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【RSpec-rails】ユーザーモデル単体テストコード

はじめにrspec-railsfactory_bot_rails、'fakerを導入する

Gemfile
group :development, :test do
  gem 'rspec-rails'
  gem 'factory_bot_rails'
  gem 'faker'
end

次にターミナルで$ bundle installrails g rspec installを行う。作成された.rspecファイルに--format documentation(表示の出力がきれいになるようにフォーマットを指示する)を記述

ターミナル
 $ bundle install
 $ rails g rspec install
.rspec
 --format documentation

仕様に沿ったユーザーモデルのデータを設定する

重複したemailは登録できないことにしているため、uniqueを付けて一意となるようにする

spec/factories/users.rb
FactoryBot.define do
  factory :user do
    email { Faker::Internet.unique.free_email }
    password { '123456' }
    password_confirmation { password }
  end
end

factory_botを使う

インスタンス変数userに、先程設定したuserのデータを入れる

spec/models/user_spec.rb
RSpec.describe User, type: :model do
  describe '#create' do
    before do
      @user = FactoryBot.build(:user)
    end
  end
end

createアクションをテストする。テスト項目を書き出す

spec/models/user_spec.rb
RSpec.describe User, type: :model do
  describe '#create' do
    before do
      @user = FactoryBot.build(:user)
    end

    it 'email, password, password_confirmationが存在すれば登録できること' do
    end

    it 'emailが空では登録できないこと' do
    end

    it '重複したemailが存在する場合登録できないこと' do
    end

    it 'emailには@が含まれてなければ登録できないこと' do
    end

    it 'passwordが空では登録できないこと' do
    end

    it 'passwordが6文字以上であれば登録できること' do
    end

    it 'passwordが5文字以下では登録できないこと' do
    end

    it 'password_confirmationが必須であること' do
    end
  end
end

洗い出した項目を検証するためのコードを書く

RSpec.describe User, type: :model do
  describe '#create' do
    before do
      @user = FactoryBot.build(:user)
    end

    it 'emailが空では登録できないこと' do
      @user.email = ''
      @user.valid?
      expect(@user.errors.full_messages).to include("Email can't be blank")
    end

    it '重複したemailが存在する場合は登録できないこと' do
      @user.save
      another_user = FactoryBot.build(:user)
      another_user.email = @user.email
      another_user.valid?
      expect(another_user.errors.full_messages).to include('Email has already been taken')
    end

    it 'emailに@が含まれていなければ登録できないこと' do
      @user.email = 'sample.com'
      @user.valid?
      expect(@user.errors.full_messages).to include('Email is invalid')
    end

    it 'passwordが空では登録できないこと' do
      @user.password = ''
      @user.valid?
      expect(@user.errors.full_messages).to include("Password can't be blank")
    end

    it 'passwordが6文字以上であれば登録できること' do
      @user.password = "123456"
      expect(@user).to be_valid
    end

    it 'passwordが5文字以下では登録できないこと' do
      @user.password = '12345'
      @user.password_confirmation = '12345'
      @user.valid?
      expect(@user.errors.full_messages).to include('Password is too short (minimum is 6 characters)')
    end

    it 'password_confirmationが空では登録できないこと' do
      @user.password_confirmation = ''
      @user.valid?
      expect(@user.errors.full_messages).to include("Password confirmation doesn't match Password")
    end
  end
end

テストコードを実行する

spec/models/user_spec.rb
# spec/models/user_spec.rbの実行
$ bundle exec rspec spec/models/user_spec.rb
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

メタプログラミングRuby(第I部・金曜日まで)を読んでいく

第5章 木曜日:クラス定義

  • JavaやC#におけるクラスの定義は、あなたが「これがオブジェクトに期待する動作です」と言うと、コンパイラが「了解。やってみます」と答える。クラスのオブジェクトを生成して、メソッドを呼び出すまでは何も起こらない。
  • Rubyにおけるクラスの定義は違っており、classキーワードは、オブジェクトの動作を規定しているだけではなく、実際にコードを実行している。
  1. クラスマクロ(クラスを修正する方法)
  2. アラウンドエイリアス(メソッドでコードをラップする方法)

クラス定義

  • クラス定義にはメソッドの定義だけでなく、あらゆるコードを置くことができる。
class MyClass
  puts 'Hello'
end

# => Hello
  • メソッドやブロックと同じように、クラス定義も最後の命令文の値を返す。
result = class MyClass
  self
end

result # => MyClass
  • クラス(やモジュール)定義の中では、クラスがカレンとオブジェクトselfになる。
    • クラスやモジュールは単なるオブジェクトなため、クラスもselfになれる。

カレントクラス

  • Rubyのプログラムは、常にカレントオブジェクトselfを持っている。
  • それと同様に、常にカレントクラス(あるいはカレントモジュール)も持っている。

    • メソッドを定義すると、それはカレントクラスのインスタンスメソッドになる。
  • カレントオブジェクトはselfで参照を獲得できるが、カレントクラスを参照するキーワードはない。

    • カレントクラスを追跡するには、コードを見る。
  • プログラムのトップレベルでは、カレントクラスはmainのクラスのObjectになる

    • トップレベルにメソッドを定義すると、Objectのインスタンスメソッドになるのはこのため。
  • classキーワードでクラス(あるいはmoduleキーワードでモジュール)をオープンすると、そのクラスがカレントクラスになる。

  • メソッドのなかでは、カレントオブジェクトのクラスがカレントクラスになる。

class C
  def m1
    def m2; end
  end
end

class D < C; end

obj = D.new
obj.m1

C.instance_methods(false) # => [:m1, :m2]

class_eval

  • Module#class_eval は、そこにあるクラスのコンテキストでブロックを評価する。
def add_method_to(a_class)
  a_class.class_eval do
    def m; 'Hello!'; end
  end
end

add_method_to String
"abc".m # => "Hello!"
  • Module#class_eval は、BasicObject#instance_eval とはまったく別物。

    • instance_eval はselfを変更するだけだが、 class_evalself とカレントクラスを変更する。
    • class_eval はselfを変更することによって、classキーワードと同じようにクラスを再オープンできる。
    • Module#class_eval はclassよりも柔軟。
      • classが定数を必要とするのに対して、class_eval はクラスを参照している変数なら何でも使える。
    • classは現在の束縛を捨てて、新しいスコープをオープンするのに対し、class_eval はフラットスコープを持っている。
      • つまり「スコープゲート」で学んだように、class_eval ブロックのスコープの外側にある変数も参照できる。
    • instance_evalinstance_exec という双子のメソッドがあるように、 module_evalclass_eval にも、外部のメソッドをブロックに渡せる module_execclass_exec というメソッドがある。
  • クラス以外のオブジェクトをオープンしたいなら、instance_eval を使う。

  • クラス定義をオープンして、defを使ってメソッドを定義したいなら、class_eval を使う。

    • 「このオブジェクトをオープンしたいが、クラスのことは気にしない」なら instance_eval がいいし、「ここでオープンクラスを使いたい」なら class_eval の方がいい。

カレントクラスまとめ

  • Rubyのインタプリタは、常にカレントクラス(あるいはカレントモジュール)の参照を追跡している。defで定義された全てのメソッドは、カレントクラスのインスタンスメソッドになる。
  • クラス定義の中では、カレントオブジェクトselfとカレントクラス(定義しているクラス)は同じである。
  • クラスへの参照を持っていれば、クラスは class_eval (あるいは module_eval )でオープンできる。

クラスインスタンス変数

  • カレントクラスの理論が役立つ。

  • Rubyのインタプリタは、全てのインスタンス変数はカレントオブジェクトselfに属しているが、これはクラスでも同じ。

class MyClass
  @my_var = 1
end
  • クラスのインスタンス変数とクラスのオブジェクトのインスタンス変数は別物
    • 2つの @my_var という名前の変数があるが、それぞれ異なるスコープで定義されており、別々のオブジェクトに属している。
      • クラスインスタンス変数がアクセスできるのはクラスだけであり、クラスのインスタンスやサブクラスからはアクセスできない。
class MyClass
  @my_var = 1
  def self.read; @my_var; end
  def write; @my_var = 2; end
  def read; @my_var; end
end

obj = MyClass.new
obj.read  # => nil
obj.write
obj.read  # => 2
MyClass.read  # => 1

クラス変数

class C
  @@v = 1
end
  • クラス変数はサブクラスや通常のインスタンスメソッドからもアクセスできる。
  • クラス変数はクラスではなく、クラス階層に属している。
class D < C
  def my_method; @@v; end
end

D.new.my_method  # => 1

クラスのトリック

  • 以下のコードをclassキーワードを使わずに書くことができる。
class MyClass < Array
  def my_method
    'Hello!'
  end
end
  • クラスはClassクラスのインスタンスなので、Class.newを呼び出して作ることができる。
    • Class.newは引数(新しいクラスのスーパークラス)と、新しいクラスのコンテキストで評価するブロックを受け取る。
c = Class.new(Array) do
  def my_method
    'Hello!'
  end
end
  • クラス名は単なる定数であるので、上記の無名関数を割り当てることができる。
    • また、Rubyはクラスの名前をつけようとしていることを理解して、定数がClassオブジェクトを参照し、Classオブジェクトも定数を参照するようになる。
c.name # => "MyClass"

特異メソッド

  • 単一のオブジェクトに特化したメソッドのこと。
    • Rubyでは特定のオブジェクトにメソッドを追加できる。
    • 特異メソッドは下記の構文もしくは Object#define_singleton_methods で定義できる。
str = "just a regular string"  # 普通の文字列

def str.title?
  self.upcase == self
end

str.title?  # => false
str.methods.grep(/title/)  # => [:title?]
str.singleton_methods  # => [:title?]

ダックタイピング

  • 静的言語
    • 「あるオブジェクトがTの型を持つのは、それがTクラスに属している。あるいはTインターフェイスを実装している。」
  • 動的言語
    • 「オブジェクトの型はそのクラスとは厳密に結びついていない。型はオブジェクトが反応するメソッドの集合に過ぎない。」
      • 流動的な型のことをダックタイピングと呼ぶ。

クラスメソッド

  • クラスは単なるオブジェクト
  • クラス名は単なる定数

=> クラスのメソッド呼び出しは、オブジェクトのメソッドの呼び出しと同じ。

  • クラスメソッドはクラスの特異メソッド
def obj.a_singleton_method; end
def MyClass.another_class_method; end

def object.method
  # メソッドの中身
end
  • 上記の object の部分には、オブジェクトの参照、クラス名の定数、selfのいずれかが使える。

クラスマクロ

  • Rubyのオブジェクトにはアトリビュートがない。
    • アトリビュートのようなものが欲しいときは、読み取り用と書き込み用の2つのミミックメソッドを定義する。
class MyClass
  def my_attribute=(value)
     @my_attribute = value
  end

  def my_attribute
    @my_attirbute
  end
end

obj = MyClass.new
obj.my_attribute = 'x'
obj.my_attribute  # => x
  • このようなメソッド(アクセサ)を書いていると、すぐに退屈になる。

    • だが、Module#attr_* 族のメソッドを使えば、一気にアクセサを生成できる。
    • Module#attr_reader は読み取り用を生成し、Module#attr_writer は書き込み用を生成し、Module#attr_accessor は読み書き両用を生成する。
  • attr_* 族のメソッドはModuleクラスで定義されているので、selfがモジュールであってもクラスであっても使える。attr_accessorのようなメソッドをクラスマクロと呼ぶ。

  • クラスマクロを使って古い名前を廃止する方法

class Book
  def title # ...

  def subtitle # ...

  def lend_to(user)
    puts "Lending to #{user}"
    # ...
  end

  def self.deprecate(old_method, new_method)
    define_method(old_method) do |*args, &block|
      warn "Warning: #{old_method}() is deprecated. Use #{new_method}()."
      send(new_method, *args, &block)
    end
  end

  deprecate :GetTitle, :title
  deprecate :LEND_TO_USER, :lend_to
  deprecate :title2, :subtitle
end

特異クラス

  • 特異メソッドは、オブジェクトモデルのどこに住むことができない。

    • Rubyのオブジェクトは、裏に特別なクラスを持っており、それはオブジェクトの特異クラスと呼ばれる。
  • Object#class などのメソッドは、特異クラスを丁寧に隠してしまう。

  • しかし、classキーワードを使った特別な構文を使うと、特異クラスのスコープに入ることができる。

class << an_object
  # あなたのコードをここに
end
  • 特異クラスの参照を取得したければ、スコープの外にselfを返せればいい。
obj = Object.new

singleton_class = class << obj
  self
end

singleton_class.class  # => Class
  • または、特異クラスを参照するselfを戻さなくても、 Object#singleton_class という便利なメソッドが使える。
"abc".singleton_class  # => #<Class:#<String:0x3331df0>>
  • 先ほどの例から、特異クラスはクラスであるが、特別なクラスであることがわかる。
    • まず、 Object#singleton_classclass << という変わった構文を使わなければ見ることができない。
    • 次に、インスタンスを一つしか持てない
      • だから特異クラスはシングルトンクラスとも呼ばれる。
    • そして、継承ができない。
    • 最後に、特異クラスはオブジェクトの特異メソッドが住んでいる場所
def obj.my_singleton_method; end
singleton_class.instance_methods.grep(/my_/)  # => [:my_singleton_method]

特異クラスのメソッド探索

  • 特異メソッドも通常のメソッド探索に当てはまる。
    • オブジェクトが特異メソッドを持っていれば、Rubyは通常のクラスではなく、特異クラスのメソッドから探索を始める。
      • これで特異メソッドを呼び出せる。
    • 特異クラスにメソッドがなければ、継承チェーンを上ヘ進み、特異クラスのスーパークラスにたどり着く。
      • これはオブジェクトの通常のクラスになる。

特異クラスと継承

  • obj -> (singleton)class -> #obj
  • #obj(singleton-method(特異メソッド)) -> superclass -> D
  • D -> (singleton)class -> #D
    • -> superclass -> C
  • #D -> superclass -> #C
  • C -> (singleton)class -> #C
    • -> superclass -> Object
  • #C -> superclass -> #Object
  • Object -> (singleton)class -> #Object
    • -> superclass -> BasicObject
  • #Object -> (singleton)class -> #BasicObject
  • BasicObject -> (singleton)class -> #BasicObject
  • #BasicObject -> superclass -> Class

大統一理論

  • 「Rubyのオブジェクトモデルには、クラス、特異クラス、モジュールがある。インスタンスメソッド、クラスメソッド、特異メソッドがある。」
  1. オブジェクトは1種類しかない。それが通常のオブジェクトかモジュールになる。
  2. モジュールは1種類しかない。それが通常のモジュール、クラス、特異クラスのいずれかになる。
  3. メソッドは1種類しかない。メソッドはモジュール(大半はクラス)に住んでいる。
  4. 全てのオブジェクトは(クラスも含めて)「本物のクラス」を持っている。それが通常のクラスか特異クラスである。
  5. 全てのクラスは(BasicObjectを除いて)ひとつの祖先(スーパークラスかモジュール)を持っている。つまり、あらゆるクラスがBasicObjectに向かって1本の継承チェーンを持っている。
  6. オブジェクトの特異クラスのスーパークラスは、オブジェクトのクラスである。クラスの特異クラスのスーパークラスはクラスのスーパークラスの特異クラスである(3回唱えてみよう)。
  7. メソッドを呼び出すときは、Rubyはレシーバの本物のクラスに向かって「右へ」進み、継承チェーンを「上へ」進む。Rubyのメソッド探索について知るべきことは以上だ。
  • Rubyプログラマなら「この複雑な階層で最初に呼ばれるメソッドはどれだろう?」「あのオブジェクトからこのメソッドを呼び出せるだろうか?」といったオブジェクトモデルに関する難しい質問に出会うことがあるだろう。
    • このような質問に出会った時には、先ほどの7つのルールを見返して、オブジェクトモデルの図をさっと描いてみれば、すぐに答えが見つかるはずだ。

クラスメソッドの構文

  • クラスメソッドはクラスの特異クラスにあるメソッド。
# 1. クラス名が重複した定義
def MyClass.a_class_method; end

# 2. クラス定義の中のselfがクラス自身になることをうまく活用している。
class MyClass
  def self.another_class_method; end
end

# 3. 特異クラスをオープンして、そこにメソッドを定義している。(特異クラスを明示的に意識しているため、Ruby界隈で一目置かれるコード)
class MyClass
  class << self
    def yet_another_class_method; end
  end
end

特異クラスとinstance_eval

  • instance_evalself を変更し、 class_evalself とカレントクラスの両方を変更していた。
  • instance_eval もカレントクラスを変更する。
    • カレントクラスをレシーバの特異クラスに変更する。
s1, s2 = "abc", "def"

s1.instance_eval do
  def swoosh!; reverse; end
end

s1.swoosh! # => "cba"
s2.respond_to?(:swoosh!) # => false
  • とはいえ、カレントクラスを変更するために instance_eval を使うことはほとんどない。
    • instance_eval の意味は「 self を変更したい 」のまま。

クラスのアトリビュート

# 動くには動くが、全てのクラスにアトリビュートが追加されてしまう。
class MyClass; end

class Class
  attr_accessor :b
end

MyClass.b = 42
MyClas.b  # => 42

# MyClassにだけアトリビュートを追加するには、特異クラスにアトリビュートを追加する。
class MyClass
  class << self
    attr_accessor :c
  end
end

MyClass.c = 'It works!'
MyClass.c  # => "It works!"

# 特異クラスにメソッドを定義するとクラスメソッドになるので、以下のように書くのと同じ。
def MyClass.c=(value)
  @c = value
end

def MyClass.c
  @c
end

モジュールの不具合

# クラスがモジュールをインクルードすると、モジュールのインスタンスメソッドが手に入るが、
# クラスメソッドはモジュールの特異クラスの中にいる。
module myModule
  def self.my_method; 'hello'; end
end

class MyClass
  include MyModule
end

MyClass.my_method  # NoMethodError!

# my_methodをMyModuleの普通のインスタンスメソッドとして定義し、
# それから、MyClassの特異クラスで、モジュールをインクルードする。
module MyModule
  def my_method; 'hello'; end
end

class MyClass
  class << self
    include MyModule
  end
end

MyClass.my_method  # => "hello"
  • クラスがモジュールをインクルードすると、モジュールのインスタンスメソッドが手に入るが、クラスメソッドはモジュールの特異クラスの中にいる
  • my_methodMyModule の普通のインスタンスメソッドとして定義し、それから、 MyClass の特異クラスで、モジュールをインクルードする。
  • my_methodMyClass の特異クラスのインスタンスメソッドである。つまり、 my_methodMyClass のクラスメソッドである。
    • この技法は クラス拡張 と呼ばれる。

クラスメソッドとinclude

  • クラス拡張を使ってクラスの特異クラスにメソッドを定義すれば、それはクラスメソッドになる。
    • クラスメソッドは特異メソッドの特殊なケースにすぎないので、このトリックはどんなオブジェクトにも適用できる。
  • 普通のオブジェクトに適用した場合、これはオブジェクト拡張と呼ばれる。
module MyModule
  def my_method; 'hello'; end
end

obj = Object.new

class << obj
  include MyModule
end

obj.my_method  # => "hello"
obj.singleton_methods  # => [:my_method]

Object#extend

  • クラス拡張とオブジェクト拡張はよく使われる機能なので、そのためだけにメソッドが提供されている。
  • Object#extend は、レシーバの特異クラスにモジュールをインクルードするためのショートカットである
module MyModule
  def my_method; 'hello!'; end
end

obj = Object.new
obj.extend MyModule
obj.my_method  # => 'hello'

class MyClass
  extend MyModule
end

MyClass.my_method  # => 'hello'

メソッドラッパー

  • メソッドの中にメソッドをラップする方法は3つある。

アラウンドエイリアス

  • Module#alias_method を使えば、Rubyのメソッドにエイリアス(別名)をつけることができる。
    • Rubyには alias キーワードもあり、トップレベルでメソッドにエイリアスをつけるときに便利。
# alias_method
class MyClass
  def my_method; 'my_method()';  end
  alias_method :m, :my_method
end

obj = MyClass.new
obj.my_method # => "my_method()"
obj.m  # => "my_method()"

# alias
class MyClass
  alias_method :m2, :m
end

obj.m2  # => "my_method()"
  • String#sizeString#length のエイリアス。
  • Interger クラスには少なくとも5つの異なる名前を持つメソッドがある。

メソッドの再定義

  • 新しいメソッドを定義して、元のメソッドの名前をつけること。
    • これなら元のメソッドをエイリアスで呼び出すことができる。
class String
  alias_method :real_length, :length

  def length
    real_length > 5 ? 'long' : 'short'
  end
end

"War and Peace".length  # => "long"
"War and Peace".real_length  # => 13
  • Thorというコマンドラインユーティリティを構築するRuby gemの例。
    • Thorにはrake2thorというRakeビルドファイルをThorスクリプトに変換するプログラムが含まれており、その処理の中でrake2thorは、Rakefileをloadして、Rakefileがrquireする全てのファイルを保管する必要がある。
input = ARGV[0] || 'Rakefile'
$requires = []

module Kernel
  def require_with_record(file)
    # グローバルの配列に、Rakefileからrquireされるファイルの名前を保管している。
    # Kernel#callerメソッドで呼び出し側のスタックを取得し、スタックの2番目がrake2thorであれば、
    # スタックの1番目はrequireを呼び出すRakefileになる。
    $requires << file if caller[1] =~ /rake2thor:/
    require_without_record file
  end
  # メソッド名をrequire_with_recordにしているが、
  # 実際にはKernel#requireを変更している。
  alias_method :require_without_record, :require
  alias_method :require, :require_with_record
end
  • アラウンドエイリアスとは、新しいrequireが、古いrequireの「周囲(アラウンド)をラップ」しているトリックのこと。
  • アラウンドエイリアスは3つの手順で書ける。

    1. メソッドにエイリアスをつける。
    2. メソッドを再定義する。
    3. 新しいメソッドから古いメソッドを呼び出す。
  • アラウンドエイリアスの欠点

    1. 新しいメソッド名でクラスを汚染してしまうこと -> メソッドをエイリアスした後にprivateにすれば解決できる。
    2. メソッドの変更を考えていない既存のコードを破壊しかねないこと -> Ruby2.0からは、既存のメソッドの周囲に機能を追加する方法が1つではなく2つ導入された。

Refinementsラッパー

  • Refinementsはクラスのコードにパッチを貼り付けるようなものであるが、アラウンドエイリアスの代わりに使うことができる。

    • リファインしたメソッドからsuperを呼び出すと、元のリファインしていないメソッドが呼び出せる。
  • Stringクラスをリファインして、lengthメソッドの周囲に機能を追加した例

module StringRefinements
  refine String do
    def length
      super > 5 ? 'long' : 'short'
    end
  end
end

using StringRefinements
"War and Peace".length  # => "long"
  • Refinementsラッパーが適用されるのは、ファイルの最後まで(Ruby2.1からはモジュール定義の終わりまで)。
    • あらゆるところに適用されるアラウンドエイリアスよりも、こちらの方が一般的に安全であると言える。

Prependラッパー

  • Module#prependinclude と似ているが、継承チェーンでインクルーダーの上ではなく下にモジュールが挿入されるところが違う。
    • つまり、プリペンどしたモジュールがインクルーダーのメソッドをオーバーライドできる。
      • そして、元のメソッドはsuperで呼び出せる。
module ExplicitString
  def length
    super > 5 ? 'long' : 'short'
  end
end

String.class_eval do
  prepend ExplicitString
end

"War and Peace".length  # => "long"
  • Refinementsラッパーのようにローカルなものではないが、Refinementsラッパーやアラウンドエイリアスよりも明示的で綺麗な方法だとされている。
module AmazonWrapper
  def reviews_of(book)
    start = Time.now
    result = super
    time_taken = Time.now - start
    puts "reviews_of() took more than #{time_taken} seconds" if time_taken > 2
    result
  rescue
    puts "reviews_of() failed"
    []
  end
end

Amazon.class_eval do
  prepend AmazonWrapper
end

クイズ

  • 数値の + 演算子は Fixnum#+ のシンタックスシュガー

    • 1 + 1 と書けば、パーサーが内部で 1.+(1) に変換している。
  • Fixnum#+ を再定義して、結果に常にプラス1する例。

class Fixnum
  alias_method :old_plus, :+

  def +(value)
    self.old_plus(value).old_plus(1)
  end
end

まとめ

  • クラス定義がself(あなたが呼び出したメソッドのデフォルトのレシーバ)とカレントクラス(あなたが定義したメソッドのデフォルトのいばしょ)に与える影響について調べた。
  • 特異メソッドや特異クラスと仲良くなり、オブジェクトモデルとメソッド探索に関する新たな知見を得た。
  • 新魔術をいくつか覚えた。クラスインスタンス変数、クラスマクロ、Prependラッパーなど。

  • 「クラス」は、「クラスやモジュール」の短縮形であり、クラスに関することは全てモジュールにも当てはまる。

    • 例えば、「カレントクラス」はモジュールにも当てはまり、「クラスインスタンス変数」は「モジュールインスタンス変数」にもなる。

第5章 金曜日:コードを記述するコード

Kernel#eval

  • Kernel#eval は、渡されたコード文字列を実行して、その結果を戻す。
array = [10, 20]
element = 30
eval("array << element")  # => [10, 20, 30]

REST Clientの例

  • REST Clientはコード文字列を作成及び評価することで、ループの中で一気に全てを定義している。
POSSIBLE_VERBS = ['get', 'put', 'post', 'delete']

POSSIBLE_VERBS.each do |m|
  eval <<-end_eval
    def #{m}(path, *args, &b)
      r[path].#{m}(*args, &b)
    end
  end_eval
end
  • ヒアドキュメント
    • 上記のコードで、evalの後に続いているのは通常のRubyの文字列。
    • クオートの代わりに、<<-と任意の終端子(ここではend_eval)で文字列が開始する。そして、その終端子のみが含まれる行で文字列が終了する。
    • このコードが、こうした文字列の代わりになるものを使い、4つのコード文字列を生成してevalした結果、それぞれがget、put、post、deleteになる。

Bindingオブジェクト

  • スコープをオブジェクトにまとめたもの。

    • Bindingを作ってローカルスコープを取得すれば、そのスコープを持ちまわすことができる。
    • evalと一緒に組み合わせて使えば、後からそのスコープでコードを実行することができる。
  • Bindingオブジェクトは、Kernel#bindingメソッドで生成できる。

  • Bindingオブジェクトにはスコープは含まれているが、コードは含まれていないため、ブロックよりも「純粋」なクロージャと考えることができる。

    • 取得したスコープでコードを評価するには、evalの引数にBindingを渡せばいい。
class MyClass
  def my_method
    @x = 1
    binding
  end
end

b = MyClass.new.my_method

eval "@x", b  # => 1

irbの例

  • irbは、標準入力やファイルをパースして、それぞれの行をevalに渡すシンプルなプログラム。

    • こうしたプログラムはコードプロセッサと呼ばれる。
  • irbのソースコードの深いところにあるworspace.rbというファイルでevalを呼び出している。

# statementsはRubyのコード行。
# @bindingはirbがコードを異なるコンテキストで実行するときにこの引数を使う(instance_evalとよく似ている)。
# fileとlineは例外が発生したときにスタックとレースを調整するために使う。
eval(statements, @binding, file, line)

「コード文字列」対「ブロック」

  • Kernel#evalは、eval族の特殊ケースである。

    • class_evalやinstance_evalのようにブロックを受け取るのではなく、コード文字列を評価する
  • instance_evalやclass_evalはコード文字列またはブロックのいずれかを受け取ることができる。

  • 文字列にあるコードは、ブロックにあるコードと大きな違いはない。

    • コード文字列はブロックと同じように、ローカル変数にアクセスすることもできる。
array = ['a', 'b', 'c']
x = 'd'
array.instance_eval "self[1] = x"

array # => ["a", "d", "c"]
  • ブロックとコード文字列はよく似ているので、多くの場合はどちらも使うことができる。
    • しかし、可能であればコード文字列を避けるべき。

evalの問題点

  • コード文字列は、シンタックスハイライトや自動保管といったエディタの機能が使えないことが多い。
  • 誰にでも使えるものだが、コード文字列は読むのも修正するのも難しいことが多い。
  • Rubyは評価するまでコード文字列の構文エラーを報告しない。
    • そのため、実行時に予期せずに失敗するような脆弱性のあるプログラムになる可能性もある。

コードインジェクション

  • Arrayのメソッドを確認する手っ取り早い方法は、evalを使ったユーティリティを書いて、サンプルの配列にメソッドを呼び出して、その結果を確かめること。
    • これを配列探索と呼ぶ。
def explore_array(method)
  code = "['a', 'b', 'c'].#{method}"
  puts "Evaluating: #{code}"
  eval code
end

loop { p explore_array(gets.chomp) }

find_index("b")
# => Evaluating: ['a', 'b', 'c'].find_index('b')
# 1

map! { |e| e.next }
# => Evaluating: ['a', 'b', 'c'].map! {|e| e.next }
# ['b', 'c', 'd']
  • 上記のコードは、悪意あるユーザーが、あなたのコンピュータの脅威になる任意のコードを実行できてしまう。
    • ハードディスクを消去されるかもしれないし、アドレス帳の全ての宛先に情熱的なラブレターを送られてしまうかもしれない。
  • このような脆弱性を吐く行為のことをコードインジェクション攻撃と呼ぶ。
object_id; Dir.glob("*")
# => ['a', 'b', 'c'].object_id; Dir.glob("*")
# => [プライベートな情報がズラズラと表示される]

コードインジェクションから身を守る

  • 悪質なコードを書く方法は無数に存在する。
  • 自分で書いた文字列にだけevalを使うように制限すればいいかもしれないが、複雑なケースになると、文字列がどこから来たかを追跡するのは驚くほど難しい。
  • evalは完全に追放すべきだと唱えるプログラマもいる。
  • evalを追放するのであれば、状況に応じて代替となる技法を探さなければいけない。
    • 動的メソッドと動的ディスパッチによって置き換えることができる。
POSSIBLE_VERBS = ['get', 'put', 'post', 'delete']

# POSSIBLE_VERBS.each do |m|
#   eval <<-end_eval
#     def #{m}(path, *args, &b)
#       r[path].#{m}(*args, &b)
#     end
#   end_eval
# end

POSSIBLE_VERBS.each do |m|
  define_method m do |path, *args, &b|
    r[path].send(m, *args, &b)
  end
end
# def explore_array(method)
#   code = "['a', 'b', 'c'].#{method}"
#   puts "Evaluating: #{code}"
#   eval code
# end

def explore_array(method, *arguments)
  ['a', 'b', 'c'].send(method, *arguments)
end

オブジェクトの汚染とセーフレベル

  • Rubyは、潜在的に安全ではないオブジェクト(特に外部から来たオブジェクト)に自動的に汚染の印をつけてくれる。
    • 汚染オブジェクトには、ウェブフォーム、ファイル、コマンドライン、さらにはシステム変数から、プログラムが呼び込んだ文字列が含まれる。
  • 汚染された文字列を操作して新しい文字列を作ると、その新しい文字列も汚染される。
# オブジェクトが汚染されているかどうかをtainted?メソッドを呼び出して確認している。

# ユーザー入力を読み込む
user_input = "User input: #{gets()}"
puts user_input.tainted?

# <= x = 1
# => true
  • 全ての文字列が汚染されているかどうかを自分で確認しなければいけないのであれば、安全ではない文字列を自分で追跡するのと大差ない。

    • 一方、Rubyはセーフレベルという考えも一緒に提供している。
  • セーフレベルは、オブジェクトの汚染を上手く補完してくれるもの。

    • セーフレベルを設定する(グローバル変数$SAFEに値を設定する)と、潜在的に危険な操作をある程度は制限できる。
  • セーフレベルはデフォルトの0から3の4つから選択できる。

    • 例えばセーフレベル2では、ファイル操作はほとんど認められていない。
    • 0より大きいセーフレベルでは、Rubyは汚染した文字列を評価できない。
$SAFE = 1
user_input = "User input: #{gets()}"
eval user_input

# <= x = 1
# => SecurityError: Insecure operation - eval
  • 安全性を自分で調整するには、コード文字列を評価する前に明示的に汚染を除去してから( Object#untaint を呼び出す)、あとはセーフレベルに頼って、ディスクアクセスのような危険な操作を抑止すればいい。
  • セーフレベルを慎重に扱えば、eval用に制御した環境を作ることができる。
    • そのような環境をサンドボックスと呼ぶ。

ERBの例

  • 標準ライブラリのERBは、Rubyのデフォルトのテンプレートシステムである。
  • このライブラリはコードプロセッサであり、Rubyをどのようなファイルにも埋め込むことができる。

  • <%= ... %> タグの部分にRubyのコードが含まれている。このテンプレートをERBに渡すと、コードが評価される。

<p><strong>Wake up!</strong>It's a nice sunny <%= Time.new.strftime("%4") %>.</p>
require 'erb'
erb = ERB.new(File.read('template.rhtml'))
erb.run

# => <p><strong>Wake up!</strong>It's a nice sunny Friday.</p>
  • テンプレートから抜き出したRubyのコードを受け取り、それをevalに渡すメソッドがERBのソースにある。
    • new_toplevel は、TOPLEVEL_BINDING のコピーを戻すメソッド。
    • インスタンス変数 @src は、ERBのタグの中身。
    • インスタンス変数 @safe_level は、ユーザーが必要とするセーフレベル。
      • セーフレベルが設定されていなければ、タグの中身がそのまま評価される。
      • セーフレベルが設定されていれば、ERBはサンドボックスを作る。その中で、グローバルのセーフレベルをユーザーの指定と一致させ、Procをクリーンルームにして、別のスコープでコードを実行している($SAFEの新し値が有効なのはProcの中だけ。他のグローバル変数とは違い、callの後に以前の値にリセットされる)。
class ERB
  def result(b=new_toplevel)
    if @safe_level
      proc {
        $SAFE = @safe_level
        eval(@src, b, (@filename || '(erb)'), 0)
      }.call
    else
      eval(@src, b, (@filename || '(erb)'), 0)
    end
  end
end

add_checked_attributeの例1

  • カーネルメソッドであり、コード文字列で書かれ、アトリビュートがnilまたはfalseの時に、実行時例外を発生させる add_checked_attribute メソッドの実装。
def add_checked_attribute(klass, attribute)
  eval "
    class #{klass}
      def #{attribute}={value}
        raise 'Invalid attribute' unless value
         @#{attribute} = value
      end

      def #{attribute}()
        @#{attribute}
      end
    end
  "
end

add_checked_attributeの例2

  • evalを使って定義し、このメソッドが将来的に公開された場合、コードインジェクション攻撃の標的になる。
  • また、コード文字列を使わないメソッドで書き直したほうが、人間の読み手にとって読みやすく、もっと洗練されたものができる。
def add_checked_attribute(klass, attribute)
  klass.class_eval do
    define_method "#{attribute}=" do |value|
      raise 'Invalid attribute' unless value
      instance_variable_set("@#{attribute}", value)
    end

    define_method attribute do
      instance_variable_get "@#{attribute}"
    end
  end
end

add_checked_attributeの例3

  • ブロックを使ってアトリビュートの妥当性を確認することで、柔軟に妥当性を確認できるようにしたい。
def add_checked_attribute(klass, attribute, &validation)
  klass.class_eval do
    define_method "#{attribute}=" do |value|
      raise 'Invalid attribute' unless validation.call(value)
      instance_variable_set("@#{attribute}", value)
    end

    define_method attribute do
      instance_variable_get "@#{attribute}"
    end
  end
end

add_checked_attributeの例4

  • カーネルメソッドをすべてのクラスで使える クラスマクロ に変更する。
    • add_checked_attribute メソッドを attr_checked メソッドに変更する。
    • attr_checked をあらゆるクラス定義で使うには、ClassまたはModuleのインスタンスメソッドにすればいい。
    • メソッドが実行される時にはクラスがselfの役割を担うので、class_evalを呼び出す必要すらない。
class Class
  def attr_checked(attribute, &validation)
    define_method "#{attribute}=" do |value|
      raise 'Invalid attribute' unless validation.call(value)
      instance_variable_set("@#{attribute}", value)
    end

    define_method_attribute do
      instance_variable_get "@#{attribute}"
    end
  end
end

フックメソッド

  • GUIでマウスクリックのイベントをキャッチするように、クラスの継承、モジュールのクラスへのミックスイン、メソッド定義などのイベントをキャッチする。
  • クラスが継承された時や新しいメソッドを獲得した時に、何らかのコードを実行sルウ。

inheritedメソッド

class String
  def self.inherited(subclass)
    puts "#{self}#{} に継承された"
  end
end

class MyString < String; end

# => String は MyString に継承された
  • inherited はClassのインスタンスメソッド。
    • クラスが継承された時にRubyが呼び出してくれる。
    • Class#inheritedはデフォルトでは何もしないので、上記の例のように自分のコードでオーバーライドして使う。
    • Class#inheritedのようなメソッドは、特定のイベントにフックを掛けることから、 フックメソッド と呼ばれる。

その他のフック

  • Module#included や Module#prepend
module M1
  def self.included(othermod)
    puts "M1 は #{othermod} にインクルードされた"
  end
end

module M2
  def self.prepended(othermod)
    puts "M2 は #{othermod} にプリペンドされた"
  end
end

class C
  include M1
  prepend M2
end

# => M1 は C にインクルードされた
# => M2 は C にプリペンドされた
  • Module#extend

    • Module#extendをオーバーライドすれば、モジュールがオブジェクトを拡張した時にコードを実行できる。
  • Module#method_added、method_removed、method_undefined

module M
  def self.method_added(method)
    puts "新しいメソッド:M##{method}"
  end

  def my_method; end
end

# => 新しいメソッド:M#my_method
  • これらのフックは、オブジェクトのクラスに住むインスタンスメソッドにしか使えない。

    • オブジェクトの得意クラスに住む特異メソッドでは動作しない。
  • 特異メソッドのイベントをキャッチするには、

    • Kernel#singleton_method_added、singleton_method_removed、singleton_method_undefinedを使う。
  • Module#includedは、おそらく最も広く使われているフック。

標準メソッドにプラグイン

  • 「フックメソッド」では、モジュールがインクルードされた時にコードを実行するために、Module#includedをオーバーライドすることを学んだ。
  • だが、そのイベントそのものをプラグインする(= 反対側から操作する)こともできる。
    • includeメソッドでモジュールをインクルードするのだから、Module#includedの代わりにModule#includeをオーバーライドすればいい。
module M; end

class C
  def self.include(*modules)
    puts "Called: C.include(#{modules})"
    super
  end

  include M
end

# => Called: C.include(M)
  • Module#includedのオーバーライドとModule#includeのオーバーライドの重要な違い
    • Module#includedはフックメソッドとして使われるだけなので、デフォルトの実装は空。
    • Module#includeは、モジュールに実際にインクルードしなければならない。
      • そのため、自分で絵作るフックコードからベースとなるModule#includeの実装をsuperを使って呼び出す必要がある。
      • superを忘れると、イベントはキャッチできるが、モジュールのインクルードができなくなってしまう。

VCRの例

  • VCRは、HTTP呼び出しを記録および再生するgem。
    • VCRのRequestクラスは、Normalizers::Bodyモジュールをインクルードしている。
module VCR
  class Request # ...

  include Normalizers::Body
  # ...
  • Bodyモジュールは、HTTPメッセージボディーを扱う body_from などのメソッドを定義している。

    • モジュールをインクルードすると、これらのメソッドがRequestクラスのメソッドになる。
    • つまり、RequestがNormalizers::Bodyをインクルードすることにより、クラスメソッドを手に入れた。
  • 一方クラスがモジュールをインクルードすると、通常はクラスメソッドではなく、インスタンスメソッドが手に入る。

    • Normalizers::Bodyなどのミックスインは、以下のようにインクルーダーのクラスメソッドを定義している。
module VCR
  module Normalizers
    module Body
      def self.included(klass)
        klass.extend ClassMethods
      end

      module ClassMethods
        def body_from(hash_or_string)
          # ...
  • BodyにはClassMethodsと言う名前の内部クラスがあり、そこにbody_fromなどの通常のインスタンスメソッドが定義されている。
  • Bodyにはincludedというフックメソッドがある。

  • RubyがBodyをインクルードすると、一連のイベントがトリガーされる。

    • Rubyが、Bodyのincludedフックを呼び出す。
    • フックがRequestに戻り、ClassMethodsモジュールをエクステンドする。
    • extendメソッドが、ClassMethodsのメソッドをRequestの特異クラスにインクルードする。
  • その結果、body_fromなどのインスタンスメソッドがRequestの特異クラスにミックスインされ、実質、Requestのクラスメソッドになる。

add_checked_attributeの例5

  • add_checked_attributeの例4では、attr_checkedと言う名前のクラスマクロを定義していた。

    • このクラスマクロは、Classのインスタンスメソッドで、すべてのクラスで利用可能。
  • attr_checkedへのアクセスを制限する。

    • CheckedAttributesモジュールをインクルードしたクラスだけがアクセスできるようにしたい
module CheckedAttributes
  def self.included(base)
    base.extend ClassMethods
  end

  module ClassMethods
    def attr_checked(attribute, &validation)
     define_method "#{attribute}=" do |value|
       raise 'Invalid attribute' unless validation.call(value)
       instance_variable_set("@#{attribute}", value)
     end

     define_method_attribute do
       instance_variable_get "@#{attribute}"
     end
    end
  end
end
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails 一意性 DB メモ

Railsでは、データを保存段階でバリデーションをかけてもだめ

なぜか?
DBレベルでも一意性を保証しなければならない

どういうことか…?
保存をする(フィルタリング)段階で「一意性」を設定しても
リクエストを連続で送ってしまうと同じユーザーが作成されてしまうらしい
→だから、DBレベルでも一意性を保証しないといけない

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

ヘルパーメソッドの概要

ヘルパーメソッドの中でも[form_withメソッド]の中身がまだ完璧に覚えていないので言語化しながら記事を投稿したいと思います。

フォームを実装するために、HTMLのformタグの代わりにform_withメソッドを用いる。rubyとして取り扱われるので<%= %>で囲む。
form_withメソッドを使う利点はUrlパスやRubyの埋め込みができるためです。

では早速form_withメソッドの中身を記述し概要を確認します。

<%= form_with url:~~~path, method::post, local:true do |form|%>
<% end %>

~~~はターミナルでrails routesで確認したパスを指定します。

では一つずつ解説します。

url:~~~path リクエストを送信したいパスを指定  (例)newアクションからcreateアクションに遷移する際のパスを指定。

method::post(オプション) 送信するHTTPメソッドを指定する。POST意味[送信する、作成する]これもrails routesで確認したのを指定する

local:true HTMLとしてフォームを送信するために記述する。

もし違うよってことがありましたら指摘の方よろしくお願いします!!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Amazon LinuxインスタンスにRails6をインストールするまでの手順

この記事でやろうとしていること

タイトルの通りですが、Amazon LinuxインスタンスにRails6.0.3をインストールするまでの流れです。
なぜRails6.0.3か?というのは、おなじみ「Railsチュートリアル」の第6版で指定されているバージョンだから、というだけです。

RailsチュートリアルではAWSの統合開発環境 (IDE)「Cloud9」の利用を前提としています。Cloud9はRailsをインストールする上での前提パッケージが既に入っているので、

$ gem install rails -v 6.0.3

と叩くだけですんなりRailsが入ってくれますが、一歩cloud9という温室から外に出るとそうはいきません。

というわけで、何番煎じかはわかりませんが、少なくとも筆者はいろいろググって何とか解決したので、需要はあるはず。さっそく解説に入っていきます。

まず、Amazon Linuxインスタンスの詳細は↓の通り。普通にEC2インスタンスをデプロイする際の一番最初に出てくるやつです。
WS000000.JPG

Gitインストール

まずはGitをインストールする必要があります。
後述の手順で、Gitレポジトリをクローンしていろいろとインストールするためです。

ここは特に迷いなく、

# yum install git

でOKです。

rbenvインストール

rbenvとは、複数のRubyのバージョンを管理し、プロジェクトごとにRubyのバージョンを指定して使うことを可能としてくれるツールです。
また、(本記事においてはここが重要なのですが)Rubyのインストールもサポートしてくれます。

基本的にはGitHubのrbenvレポジトリのREADMEに書いている通りに従う流れですが、

Gitレポジトリをクローン
# git clone https://github.com/rbenv/rbenv.git ~/.rbenv
ディレクトリ移動後、ソースからインストール
# cd ~/.rbenv && src/configure && make -C src
warning: gcc not found; using CC=cc
aborted: compiler not found: cc

と、gccがインストールされていないとコケてしまいますので、

# yum install gcc

でインストールしてから、再度ソースインストールしましょう。

# cd ~/.rbenv && src/configure && make -C src

rbenvインストール完了後も引き続きREADMEに従って、

# echo 'export PATH="$HOME/.rbenv/bin:$PATH"' >> ~/.bash_profile

とパスを通してから、rbenv init コマンドで初期設定します。

# ~/.rbenv/bin/rbenv init
# Load rbenv automatically by appending
# the following to ~/.bash_profile:

eval "$(rbenv init -)"

と出るので、従いましょう。

# eval "$(rbenv init -)"

パスがしっかり通っていることも確認して、

# source ~/.bash_profile
# echo $PATH
/root/.rbenv/shims:/root/.rbenv/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/root/bin

renv-doctorスクリプトを使用して、rbenvが正しく設定されていることを確認します。

# curl -fsSL https://github.com/rbenv/rbenv-installer/raw/master/bin/rbenv-doctor | bash
Checking for `rbenv' in PATH: /root/.rbenv/bin/rbenv
Checking for rbenv shims in PATH: OK
Checking `rbenv install' support: not found
Unless you plan to add Ruby versions manually, you should install ruby-build.
Please refer to https://github.com/rbenv/ruby-build#installation

Counting installed Ruby versions: none
  There aren't any Ruby versions installed under `/root/.rbenv/versions'.
  You can install Ruby versions like so: rbenv install 2.2.4
Checking RubyGems settings: OK
Auditing installed plugins: OK

出力結果の3行目に「Checking `rbenv install' support: not found」と出力されています。
これが使えないと最新版のRubyをrbenvでインストールできないので、ここからはruby-buildをインストールします。

ruby-buildインストール

ここからは、GitHubのruby-buildレポジトリのREADMEに書いている通りに従っていきます。
rbenvのディレクトリに「plugins」ディレクトリを作って、レポジトリをクローンしてプルするだけです。

# mkdir -p "$(rbenv root)"/plugins
# git clone https://github.com/rbenv/ruby-build.git "$(rbenv root)"/plugins/ruby-build
# git -C "$(rbenv root)"/plugins/ruby-build pull

Rubyインストール

さあ、これで晴れてrbenvを使ってRubyインストールが出来るようになりました。
まずはインストール可能なRubyのバージョンを確認しましょう。

# rbenv install --list
2.5.8
2.6.6
2.7.2
jruby-9.2.13.0
maglev-1.0.0
mruby-2.1.2
rbx-5.0
truffleruby-20.3.0
truffleruby+graalvm-20.3.0

Only latest stable releases for each Ruby implementation are shown.
Use 'rbenv install --list-all' to show all local versions.

ここでは最新版の「2.7.2」をインストールすることとします。

# rbenv install 2.7.2
Downloading ruby-2.7.2.tar.bz2...
-> https://cache.ruby-lang.org/pub/ruby/2.7/ruby-2.7.2.tar.bz2
Installing ruby-2.7.2...

BUILD FAILED (Amazon Linux 2 using ruby-build 20201118)

Inspect or clean up the working tree at /tmp/ruby-build.20201128132117.3587.qONaMm
Results logged to /tmp/ruby-build.20201128132117.3587.log

Last 10 log lines:
        from ./tool/rbinstall.rb:846:in `block (2 levels) in install_default_gem'
        from ./tool/rbinstall.rb:279:in `open_for_install'
        from ./tool/rbinstall.rb:845:in `block in install_default_gem'
        from ./tool/rbinstall.rb:835:in `each'
        from ./tool/rbinstall.rb:835:in `install_default_gem'
        from ./tool/rbinstall.rb:799:in `block in <main>'
        from ./tool/rbinstall.rb:950:in `block in <main>'
        from ./tool/rbinstall.rb:947:in `each'
        from ./tool/rbinstall.rb:947:in `<main>'
make: *** [do-install-all] Error 1

おや、makeでコケていますね。

結論としては「openssl-devel」パッケージが必要なところ、インストールされていなかったことが原因です。

私は正直、ここで小1時間あれやこれや調べて、当てずっぽうで問題解決に至ったのですが、じつは↓にruby-buildに必要な前提パッケージが説明されていました。
公式に勝る情報はないですね。
https://github.com/rbenv/ruby-build/wiki#suggested-build-environment

openssl-develはサクっとyumでインストールしましょう。

# yum install -y openssl-devel

その後、

# rbenv install 2.7.2

で無事にrubyの最新版をインストールすることに成功しました。

rubyのパスが2つある件(未解決)

ここまでの手順でrubyをインストール完了しましたが、パスが通っていません。
ruby本体の在り処を探してみますと・・・

# ll ~/.rbenv/shims/ruby
-rwxr-xr-x 1 root root 385 Nov 28 13:53 /root/.rbenv/shims/ruby

# ll ~/.rbenv/versions/2.7.2/bin/ruby
-rwxr-xr-x 1 root root 184688 Nov 28 13:52 /root/.rbenv/versions/2.7.2/bin/ruby

あれ。2箇所にある。別にシンボリックリンクってわけでもないし。ファイルサイズがかなり違う…
以下のように、どちらもrubyコマンドとして実行できます。

# ~/.rbenv/shims/ruby -v
ruby 2.7.2p137 (2020-10-01 revision 5445e04352) [x86_64-linux]

# ~/.rbenv/versions/2.7.2/bin/ruby -v
ruby 2.7.2p137 (2020-10-01 revision 5445e04352) [x86_64-linux]

ちょっとこの点、ちゃんと調べられていないのですが、rbenvで確認すると後者のパスで認識してるっぽいので、筆者の場合はそちらでパスを通し、Rubyインストールを終了としました。

# rbenv versions
* 2.7.2 (set by /root/.rbenv/version)

# echo 'export PATH="$HOME/.rbenv/versions/2.7.2/bin:$PATH"' >> ~/.bash_profile
# source ~/.bash_profile

Railsインストール

お膳立ては整いましたので、あとはRailsチュートリアルの通り、Rails6.0.3をインストールするだけです。

# gem install rails -v 6.0.3
# rails -v
Rails 6.0.3

お疲れ様でした。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】DBと連携してプルダウン(ドロップダウン)メニューをつくる(勉強中)

SIer企業で働く社会人4年目OLです。
仕事をやめる予定はないのですが、手に職をつけていつか副職ができるといいなと思いWEBアプリ開発の勉強し始めました。(2ヵ月くらい)

練習でWEBアプリ開発をしていますが、初めてつまったのがプルダウンメニューの作成なので勉強がてらまとめてみます。同じように悩んでる人の役に立てたらいいなと思います。

勉強始めたばかりなのでここ違うよ!っていうところがあればご指摘いただけると嬉しいです。

やりたいこと

DB(Gameモデル)に格納しているデータをプルダウン(ドロップダウン)で選択して別のDB(Stageモデル)に保存したい。

プルダウン.png

モデル設計

Gameモデル

カラム
id integer
game_name text

Stageモデル

カラム
id integer
game_id ※ integer
stage_name text

※Stageのgame_idがGameのidを外部参照しているイメージ

form_tagで実装

これまでユーザからの入力は全部form_tagで実現していたので、まずはそれでむりやり実装したみた。

新規登録用ページ(stage_a)のView

<!-- 新規登録アクション(stage_n)にデータ渡す -->
<%= form_tag("/stage_n") do %>
  <div class="form">
    <div class="form-body">
      <p>game_name</p>
      <select name="game_name">
        <%@games.each do |game|%>
          <option value=<%="#{game.game_name}"%>><%=game.game_name%></option>
        <%end%>
      </select>
      <p>stage_name</p>
      <input type="text" name="stage_name">
      <input type="submit" value="保存">
    </div>
  </div>
<% end %>

新規登録用のController

  def stage_a
    @games = Game.all
  end

  def stage_n
    @game = Game.find_by(game_name: params[:game_name])
    @stage = Stage.new(game_id: @game.id,stage_name: params[:stage_name])
    @stage.save
  end

新規登録用ページ(stage_a)のブラウザでの見え方

form_tag.png

期待値通りにできた。のでこれでいいのかとも思ったが、ネットで検索してみるとそもそもモデルに紐づいたユーザ入力はform_tagではなくてform_forを使うのが一般的らしい。

form_forで実装

RailsのインプットはProgateでしかしていないのでform_forは初見だが、見よう見まねで実装してみた。

新規登録用ページ(stage_a)のView

<!-- 新規登録アクション(stage_n)にデータ渡す -->
<%= form_for("@stage", url: {controller: 'home', action: 'stage_n' }) do |f|%>
  <div class="form">
    <div class="form-body">
      <p>game_name</p>
      <%= f.collection_select(:game_id,@games,:id,:game_name)%>
      <p>stage_name</p>
      <%= f.text_field :stage_name%>
      <%= f.submit "保存"%>
    </div>
  </div>
<% end %>

新規登録用のController

  def stage_a
    @games = Game.all
    @stage = Stage.new
  end

  def stage_n
    Stage.create(stage_params)
    redirect_to("/stage_view")
  end

    private
    def stage_params
      params.require(:@stage).permit(:stage_name, :game_id)
    end

新規登録用ページ(stage_a)のブラウザでの見え方

form_for.png

なんとか期待値通りになった。無理にループを自分で実装しなくてもよくてすっきりと記述できた。

form_for("保存するモデルのインスタンス", url: {送信先のアクション}) do |f|

本来なら送信先のアクションについても記載しなくても自動でルーティングしてくれるらしいが、うまくルーティングできなかったため記載した。
プルダウン形式は以下のf.collection.selectで実現できた。

f.collection_select(保存カラム,参照インスタンス,保存する参照先カラム,表示する参照先カラム)

ついでに、通常のテキストボックス(1行)は以下のf.text_field

f.text_field 保存カラム

form_forの使い方については【Rails】form_forの使い方をマスターしよう!のページがとても分かりやすかった。というよりRailsのヘルパーメソッドに基本的にこのサイトにまとめられているので今後参考にしていきたい。

form_withで実装

from_forについて調べていたら、Rails5.1以降はform_withを使用することが推奨されているとのこと。これについても見よう見まねで実装してみた。

新規登録用ページ(stage_a)のView

<!-- 新規登録アクション(stage_n)にデータ渡す -->
<%= form_with(model: @stage, url: {controller: 'home', action: 'stage_n'} ,local: true) do |form|%>
  <div class="form">
    <div class="form-body">
      <p>game_name</p>
      <%= form.collection_select(:game_id,@games,:id,:game_name)%>
      <p>stage_name</p>
      <%= form.text_field :stage_name%>
      <%= form.submit "保存"%>
    </div>
  </div>
<% end %>

新規登録用のController

  def stage_a
    @games = Game.all
    @stage = Stage.new
  end


  def stage_n
    @stage=Stage.new(game_id: params[:stage][:game_id],stage_name: params[:stage][:stage_name] )
    @stage.save
    redirect_to("/stage_view")
  end

新規登録用ページ(stage_a)のブラウザでの見え方

form_with.png

これも他と変わらずに期待値通り。正直form_forとそんなに変わらないじゃんと思った。参考にしたページの記述に似せたためControllerに多少違いはあるが、同じ書き方をしても問題ないと思っている。(試してはいない)

form_tagとform_forとform_withについて

それぞれの使い分けについてもざっくり調べてみた。まず、form_tagform_forについては以下のサイトが参考になった。

Railsのform_for/form_tagの分け方の意図としては、

form_for: 任意のmodelに基づいたformを作るときに使う
form_tag: modelに基づかないformを作るときに使う

ということです。
つまり、あるuserモデルに基づいたuserを作成するときはform_forを使い、
そうではなく、検索窓のような何のモデルにも基づかないformを作りたいときはform_tagを使うのが原則です。

【Rails】form_for/form_tagの違い・使い分けをまとめた

モデルに紐づいたフォームか否かが使い分けの基準と理解した。

そして、form_withform_tagform_forの機能をまとめたヘルパーメソッドである。form_withの使用が推奨されていることから、Rails5.1以降を使用する場合はform_withを使用すべきと考えてよいと思う。

プルダウン同士の連携

プルダウンメニューは作れたが、次は別の問題が発生した。
1つ目のプルダウンを選択したら2つ目のプルダウンで選択できる内容が変更できるような入力フォームを作ろうと考えたが、Viewを更新しないとアクションにデータを渡せない。

これを解決するためにはAjaxを理解する必要があるようだが今は???状態なので調べて理解・実装したら、別の記事でまとめようと思う。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

swiperを導入したけど画像のレスポンシブがうまくいかないよ

はじめに

Railsで自作アプリを開発中の私。(bootstrapを使用)
swiperを使ってサイトの見た目を良くしたいと思い、さっそく導入することに。

その結果、問題なく動きました。

レスポンシブ対応もしました。

ただ、画像がレスポンシブ対応してくれませんでした。

swiperを使うときswiper-containerにheightを設定しますが、画面サイズが小さくなってもheightが固定なので変な余白が生じてしまいます。
またいい感じに調節しても画面幅を小さくすると、画像が見切れるといった別の問題が発生します。

私は当初、メディアクエリを用いてブレイクポイントごとにheightを変更するゴリ押し方法でこれらの問題を解決していました。(これが普通かもしれません。)

でもせっかくbootstrapを使用しているのに追加でメディアクエリを使うのはなんか嫌だなと思ったり。

そんなとき簡単に解決する方法を見つけました。
共有させていただきます。

こんな人に向けて

1.Rails+bootstrapの開発中でswiperを使用したが、レスポンシブがうまくいかない人。
2.メディアクエリを使いたくない人。
3.難しいコードは分からない初心者の人。

解決方法

top.html.erb
<div class="container">
    <div class="swiper-container">
        <div class="swiper-wrapper">
            <div class="swiper-slide"><%= image_tag 'test1.jpg', class:"img-fluid" %></div> 
            <div class="swiper-slide">Slide 2</div>
            <div class="swiper-slide">Slide 3</div>
            <div class="swiper-slide">Slide 4</div>
        </div>
        <div class="swiper-button-prev swiper-button-white"></div>
        <div class="swiper-button-next swiper-button-white"></div>
    </div>
</div>

重要なのは上から4行目の部分。

 <div class="swiper-slide"><%= image_tag 'test1.jpg', class:"img-fluid" %></div> 

"img-fluid" を画像のクラスに指定すると解決してくれます。

before

application.scss
//swiper内の画像調整のための高さ調節
/* すべてのサイズで適用される設定 */
.swiper-container {
    width: 100%;
    height: 600px;
}
/* lg(991px)以下の際に適用される設定 */
@media screen and (max-width: 991px) {
  .swiper-container {
    height: 400px;
  }
}
/* md(767px)以下の際に適用される設定 */
@media screen and (max-width: 767px) {
  .swiper-container{
    height: 300px;
  }
}
/* sm(543px)以下の際に適用される設定 */
@media screen and (max-width: 543px) {
  .swiper-container{
    height: 270px;
  }
}

after

top.html.erb
<div class="swiper-slide"><%= image_tag 'test1.jpg', class:"img-fluid" %></div>
application.scss
.swiper-container {
    width: 100%;
}

これでコードがすっきりしました。

ぜひ試してみてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

30代未経験からRails, AWS, Docker, CircleCIでポートフォリオを作成するまで

プログラミング未経験の31歳男がRuby on Rails, jQuery, AWS, Docker, CircleCIといった技術を使ってWebアプリを作りました。

この記事では、最初にアプリの紹介をした後に、

  • どれくらいの時間かけて作ったのか
  • なぜこれらの技術を使うことにしたのか
  • 特に大変だったところ
  • どのように学習したのか

といったことについて、お伝えできればと思います。

どんなアプリを作ったのか

減量アプリです。(筋トレしてる人が健康的に体脂肪を減らすためのアプリ)

「これなら自分でも続けられる」をコンセプトに「PFC MASTER」というアプリを開発しました。

アプリのURLはこちらです。
https://pfcmaster.work/
(レスポンシブ対応しておりスマホからも見られますが、グラフが崩れます)

Githubはこちらです。
https://github.com/naota7118/pfc-master

なんでこのアプリを作ったのか

僕自身が「減量がなかなか続かなくて、まだ腹筋を割れたことがない」という悩みを抱えていました。
自分自身の悩みを解決するようなアプリを作りたいと思い、減量アプリを作ることにしました。

「どうしたら減量を途中で挫折せずに続けられるようになるかな?」

今まで減量に挫折した原因を考えたところ、下記の2つが思い当たりました。

〈これまで減量が続かなかった原因〉

  • 1日どれくらい食べれば体脂肪落とせるのかわからなくて、適当に食べてしまっていた。
  • 減量が進んでいるのかどうか、進捗がわからなくてモチベーションが保てなかった。

このような原因に対してどうしたら減量を続けやすくなるか考えた結果、下記の2つを思いつきました。

  • 「1日何キロカロリーまで食べていいのか」を自分の体重から自動で計算してくれて、食べるごとに確認できたらカロリーコントロールしやすくなるのではないか。
  • 体重やカロリーの推移をグラフで見られるようにすれば、成果が出てるのがわかってモチベーションが保てるのではないか。

このように原因やそれを解決する策を考えて、グラフ機能をメインとした減量アプリを作ることにしました。

アプリの写真と説明

トップページ

使い方を細かく説明しなくても、パッと見て一目で使い方わかる外観を意識して作りました。
スクリーンショット 2020-11-23 午後8.39.00.png

グラフ

体重とカロリーの推移がグラフという形で見られるようにしました。
自分の頑張りが目に見えることで「よし、いい感じだ。もっと頑張ろう」と思えるのではないかと考えたのです。
82912645752047340005a4a76361350b.gif

カレンダーと画像一覧

食べたものや筋トレをカレンダーや写真で記録して確認できるようにしました。
写真でカラダの変化を可視化することでそれもモチベーションにつながると考えました。
また、他の減量に成功した方がどんなものを食べて減量できたのか知って真似できるため、という目的もあります。
9fd6b65dde2703f27bec05eac512d889.gif

カロリーの自動計算

「1日○キロカロリー食べれば体脂肪を落とせる」という減量の目安となる"1日の摂取カロリー目をユーザーが体重と体脂肪率を入力したら自動で計算されるようにしました。

※ちなみに、摂取カロリーの計算式はバズーカ岡田先生の著書『除脂肪メソッド』を参考にしました。
e30c9e2a5ca5a1523686697876e9b603.gif

自動計算機能によって、「今日あと何キロカロリーまで食べて大丈夫か」がわかるようにしました。
これで減量の成功率を高められると考えました。
スクリーンショット 2020-11-29 午後5.55.14.png

使用技術

  • フロントエンド
    HTML(Haml), CSS(Sass), jQuery, boostrap4

  • サーバーサイド
    Ruby 2.5.1, Rails 5.2.4.3

  • インフラ
    CircleCI, Nginx, MySQL, Docker/Docker-compose, AWS(VPC, EC2, RDS, IAM, Route53, S3)

サーバーサイドはRuby on Rails、フロントエンドはSassとjQueryで実装しました。
開発環境にはDocker-composeを使用しました。

CI/CDパイプラインに関しては、CircleCIによりmasterブランチにmergeしたら自動でRSpecのテストとRubocopのリファクタリングが実行されるように設定しました。

ER図

PFCMASTERのER図.png

インフラ構成図

AWSインフラ構成図.png

アプリを作るのにかかった期間

トータル6ヶ月です。
当初の予定では8月にはAWSにデプロイした時点で転職活動を始める予定でしたが、「DockerやCirlcleCIを導入するところまでやり切りたい」と思い、結局6ヶ月かかりました。

半年間どのように進めてきたかは下記の通りです。

期間 やったこと
2020年4月 テーマを決めた。データベース設計。
2020年5〜7月 (スクールのチーム開発と並行する形で)
RailsのCRUD機能、いいね機能(非同期通信)、コメント機能(非同期通信)、フォロー機能。
Chart.jsを使ったグラフ機能、jQueryを使った自動計算機能。
2020年8月 Unicorn, Nginxを使ってAWSにデプロイ。
Capistranoを使って自動デプロイ。
2020年9月 Haml, Sassで各ページのマークアップ。
Boostrapを使ってレスポンシブ対応させる。
Dockerを使って開発環境を構築。
2020年10月 CircleCIでRSpecの自動テスト、Rubocopの自動リファクタリングを通す。
Gitのエラー解決で誤ってGitリポジトリのファイルを消してしまい、その修復のためAWSへのデプロイをやり直す。
2020年11月 TwitterAPIを使って投稿するとTwitterに自動投稿されるように設定(ローカル環境のみ)
転職活動開始。現在PHPで2つ目のアプリを開発中。

なぜ6ヶ月もかかってしまったのか

誰にも相談せず1人でエラーを解決することにこだわり過ぎて、エラー解決に時間をかけ過ぎたことがいちばんの原因です。

エンジニアになるためには、「エラーに直面した時に"すぐ質問せず自分で問題解決する力"が求められる」との考えから、できるだけ自分でエラーを解決することにこだわっていました。

今振り返ると、リミットを設けてある程度自分で考えたら、リミットがきた時点で質問すべきだったと思います。
実際の仕事では1人のエラー解決をずっと待ってもらえないからです。

現在、「2020年中に完成させる」というリミットを決めて2つめのアプリを開発しています。

なぜRails, jQuery, Docker, CircleCIを使うことにしたのか?

RailsとjQueryを選んだ理由

最速で開発する方法として、これらの技術を選びました。なるべく早くアプリを完成させ、転職活動を開始し、1日も早くエンジニアとして働きたいと思ったためです。
(結果的に半年もかかったので説得力ありませんが...)

なぜRubyとjQueryを使えば最速で開発できると思ったかというと、スクールで簡単なCRUD機能を持ったアプリを開発した経験があり、他の言語に比べて理解していた部分が大きく開発する「こうやって作っていくんだ」というイメージがしやすかったためです。

AWS, Docker, CircleCIを使うことにした理由

これらの技術を使用した理由は、wantedlyで求人情報を見たところ、多くの企業でこれらの技術を使用していたためです。
「多くの企業で使われている技術はどんな技術なのだろう」と興味を持ったのと、実務で使うことを見越して「早いうちから自分で使って慣れておきたい」と考えたためです。

特に大変だったところ

特に挙げるとすると、下記の6点です。
詰まったエラーの解決法をQiita記事にアウトプットしていました。

どのように学習したのか

技術 学習方法
HTML/CSS/Ruby/Rails/jQuery スクールのカリキュラム
Boostrap4 Boostrap日本語リファレンス
AWS AWS:ゼロから実践するAmazon Web Services。手を動かしながらインフラの基礎を習得
(デプロイ編①)世界一丁寧なAWS解説。EC2を利用して、RailsアプリをAWSにあげるまで
Docker 米国AI開発者がゼロから教えるDocker講座
CircleCI 米国AI開発者がゼロから教えるDocker講座
Chart.js Chart.js日本語ドキュメント

今後の課題

  • 2日連続投稿した時に「すごい!!」と表示されるようにする。
  • プロフィール画像を登録できるようにする。(現状は女性ユーザーも外国人男性がプロフィール画像になってしまう。)
  • 運動した日と運動しなかった日をカレンダーでわかるようにする。

企業の方へ

ここまでお読みいただきありがとうございました。
現在、webエンジニアになるため転職活動を行っております。
少しでも興味を持って頂けましたら、Twitterの@naota7118までDM頂ければ幸いです。
何卒よろしくお願いいたします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

30代未経験からRails, AWS, Docker, CircleCIを使って減量アプリを作りました

プログラミング未経験の31歳男がRuby on Rails, jQuery, AWS, Docker, CircleCIといった技術を使ってWebアプリを作りました。

この記事では、最初にアプリの紹介をした後に、

  • どれくらいの時間かけて作ったのか
  • なぜこれらの技術を使うことにしたのか
  • 特に大変だったところ
  • どのように学習したのか

といったことについて、お伝えできればと思います。

どんなアプリを作ったのか

減量アプリです。(筋トレしてる人が健康的に体脂肪を減らすためのアプリ)

「これなら自分でも続けられる」をコンセプトに「PFC MASTER」というアプリを開発しました。

アプリのURLはこちらです。
https://pfcmaster.work/
(レスポンシブ対応しておりスマホからも見られますが、グラフが崩れます)

Githubはこちらです。
https://github.com/naota7118/pfc-master

なんでこのアプリを作ったのか

僕自身が「減量がなかなか続かなくて、まだ腹筋を割れたことがない」という悩みを抱えていました。
自分自身の悩みを解決するようなアプリを作りたいと思い、減量アプリを作ることにしました。

「どうしたら減量を途中で挫折せずに続けられるようになるかな?」

今まで減量に挫折した原因を考えたところ、下記の2つが思い当たりました。

〈これまで減量が続かなかった原因〉

  • 1日どれくらい食べれば体脂肪落とせるのかわからなくて、適当に食べてしまっていた。
  • 減量が進んでいるのかどうか、進捗がわからなくてモチベーションが保てなかった。

このような原因に対してどうしたら減量を続けやすくなるか考えた結果、下記の2つを思いつきました。

  • 「1日何キロカロリーまで食べていいのか」を自分の体重から自動で計算してくれて、食べるごとに確認できたらカロリーコントロールしやすくなるのではないか。
  • 体重やカロリーの推移をグラフで見られるようにすれば、成果が出てるのがわかってモチベーションが保てるのではないか。

このように原因やそれを解決する策を考えて、グラフ機能をメインとした減量アプリを作ることにしました。

アプリの写真と説明

トップページ

使い方を細かく説明しなくても、パッと見て一目で使い方わかる外観を意識して作りました。
スクリーンショット 2020-11-23 午後8.39.00.png

グラフ

体重とカロリーの推移がグラフという形で見られるようにしました。
自分の頑張りが目に見えることで「よし、いい感じだ。もっと頑張ろう」と思えるのではないかと考えたのです。
82912645752047340005a4a76361350b.gif

カレンダーと画像一覧

食べたものや筋トレをカレンダーや写真で記録して確認できるようにしました。
写真でカラダの変化を可視化することでそれもモチベーションにつながると考えました。
また、他の減量に成功した方がどんなものを食べて減量できたのか知って真似できるため、という目的もあります。
9fd6b65dde2703f27bec05eac512d889.gif

カロリーの自動計算

「1日○キロカロリー食べれば体脂肪を落とせる」という減量の目安となる"1日の摂取カロリー目をユーザーが体重と体脂肪率を入力したら自動で計算されるようにしました。

※ちなみに、摂取カロリーの計算式はバズーカ岡田先生の著書『除脂肪メソッド』を参考にしました。
e30c9e2a5ca5a1523686697876e9b603.gif

自動計算機能によって、「今日あと何キロカロリーまで食べて大丈夫か」がわかるようにしました。
これで減量の成功率を高められると考えました。
スクリーンショット 2020-11-29 午後5.55.14.png

使用技術

  • フロントエンド
    HTML(Haml), CSS(Sass), jQuery, boostrap4

  • サーバーサイド
    Ruby 2.5.1, Rails 5.2.4.3

  • インフラ
    CircleCI, Nginx, MySQL, Docker/Docker-compose, AWS(VPC, EC2, RDS, IAM, Route53, S3)

サーバーサイドはRuby on Rails、フロントエンドはSassとjQueryで実装しました。
開発環境にはDocker-composeを使用しました。

CI/CDパイプラインに関しては、CircleCIによりmasterブランチにmergeしたら自動でRSpecのテストとRubocopのリファクタリングが実行されるように設定しました。

ER図

PFCMASTERのER図.png

インフラ構成図

AWSインフラ構成図.png

アプリを作るのにかかった期間

トータル6ヶ月です。
当初の予定では8月にはAWSにデプロイした時点で転職活動を始める予定でしたが、「DockerやCirlcleCIを導入するところまでやり切りたい」と思い、結局6ヶ月かかりました。

半年間どのように進めてきたかは下記の通りです。

期間 やったこと
2020年4月 テーマを決めた。データベース設計。
2020年5〜7月 (スクールのチーム開発と並行する形で)
RailsのCRUD機能、いいね機能(非同期通信)、コメント機能(非同期通信)、フォロー機能。
Chart.jsを使ったグラフ機能、jQueryを使った自動計算機能。
2020年8月 Unicorn, Nginxを使ってAWSにデプロイ。
Capistranoを使って自動デプロイ。
2020年9月 Haml, Sassで各ページのマークアップ。
Boostrapを使ってレスポンシブ対応させる。
Dockerを使って開発環境を構築。
2020年10月 CircleCIでRSpecの自動テスト、Rubocopの自動リファクタリングを通す。
Gitのエラー解決で誤ってGitリポジトリのファイルを消してしまい、その修復のためAWSへのデプロイをやり直す。
2020年11月 TwitterAPIを使って投稿するとTwitterに自動投稿されるように設定(ローカル環境のみ)
転職活動開始。現在PHPで2つ目のアプリを開発中。

なぜ6ヶ月もかかってしまったのか

誰にも相談せず1人でエラーを解決することにこだわり過ぎて、エラー解決に時間をかけ過ぎたことがいちばんの原因です。

エンジニアになるためには、「エラーに直面した時に"すぐ質問せず自分で問題解決する力"が求められる」との考えから、できるだけ自分でエラーを解決することにこだわっていました。

今振り返ると、リミットを設けてある程度自分で考えたら、リミットがきた時点で質問すべきだったと思います。
実際の仕事では1人のエラー解決をずっと待ってもらえないからです。

現在、「2020年中に完成させる」というリミットを決めて2つめのアプリを開発しています。

なぜRails, jQuery, Docker, CircleCIを使うことにしたのか?

RailsとjQueryを選んだ理由

最速で開発する方法として、これらの技術を選びました。なるべく早くアプリを完成させ、転職活動を開始し、1日も早くエンジニアとして働きたいと思ったためです。
(結果的に半年もかかったので説得力ありませんが...)

なぜRubyとjQueryを使えば最速で開発できると思ったかというと、スクールで簡単なCRUD機能を持ったアプリを開発した経験があり、他の言語に比べて理解していた部分が大きく開発する「こうやって作っていくんだ」というイメージがしやすかったためです。

AWS, Docker, CircleCIを使うことにした理由

これらの技術を使用した理由は、wantedlyで求人情報を見たところ、多くの企業でこれらの技術を使用していたためです。
「多くの企業で使われている技術はどんな技術なのだろう」と興味を持ったのと、実務で使うことを見越して「早いうちから自分で使って慣れておきたい」と考えたためです。

特に大変だったところ

特に挙げるとすると、下記の6点です。
詰まったエラーの解決法をQiita記事にアウトプットしていました。

どのように学習したのか

技術 学習方法
HTML/CSS/Ruby/Rails/jQuery スクールのカリキュラム
Boostrap4 Boostrap日本語リファレンス
AWS AWS:ゼロから実践するAmazon Web Services。手を動かしながらインフラの基礎を習得
(デプロイ編①)世界一丁寧なAWS解説。EC2を利用して、RailsアプリをAWSにあげるまで
Docker 米国AI開発者がゼロから教えるDocker講座
CircleCI 米国AI開発者がゼロから教えるDocker講座
Chart.js Chart.js日本語ドキュメント

今後の課題

  • 2日連続投稿した時に「すごい!!」と表示されるようにする。
  • プロフィール画像を登録できるようにする。(現状は女性ユーザーも外国人男性がプロフィール画像になってしまう。)
  • 運動した日と運動しなかった日をカレンダーでわかるようにする。

ここまでお読みいただきありがとうございました。
現在、webエンジニアになるため転職活動を行っております。
もし企業の採用担当者の方で、この記事を読んで少しでも興味を持って頂くなんてことがもしありましたら、Twitterの@naota7118までDM頂ければ幸いです。
何卒よろしくお願いいたします。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【RSpec】ユーザーの新規登録について結合テストを行う時に意識すること

 最近、RSpecを使ってオリジナルアプリのテストを実装しているのですが、新規登録で詰まったところがあるので、メモしておきたいと思います(といっても非常に単純なことなのですが?)。

詰まったこと

 まず、ユーザーモデルのバリデーションは以下の通りです。

app/models/user.rb
devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :validatable

# ニックネームのバリデーション
validates :nickname, presence: true, length: { maximum: 30 }

# メールのバリデーション
EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i.freeze
validates :email, format: { with: EMAIL_REGEX }

# パスワードのバリデーション
PASSWORD_REGEX = /\A(?=.*?[a-z])(?=.*?\d)[a-z\d]+\z/i.freeze
validates_format_of :password, with: PASSWORD_REGEX, message: 'は英字と数字の両方を含めて6文字以上のものを設定してください', on: :create

 今回詰まった結合テストは以下の通りです。

spec/system/users_spec.rb
require 'rails_helper'

RSpec.describe "Users", type: :system do

  let!(:user) { FactoryBot.create(:user) }
  scenario "新規登録を行う" do
    # トップページを開く
    visit root_path
    find('div[class="menu-wrapper"]').click
    # メニューボタンをクリックすると、新規登録ボタンがある。
    expect(page).to have_content('新規登録')
    find('a[class="login"]').click
    # 新規登録画面へ移動する。
    visit new_user_registration_path
    # ニックネームを入力
    fill_in 'nickname', with: user.nickname
    # メールアドレスを入力
    fill_in 'メールアドレス', with: user.email
    # パスワードを入力
    fill_in 'パスワード', with: user.password
    # 確認用パスワードを入力
    fill_in 'パスワード(再入力)', with: user.password_confirmation
    # 会員登録ボタンをクリックすると、ユーザーモデルのカウントが1上がる
    expect{
      find('input[name="commit"]').click
    }.to change{ User.count }.by(1)
  end
end

 FactoryBotに設定したテストデータはこちらです。

spec/factories/users.rb
FactoryBot.define do
  factory :user, aliases: [:follow] do
    nickname { Faker::Lorem.characters(number: 30) }
    email { Faker::Internet.free_email }
    password = 'a12345'
    password { password }
    password_confirmation { password }
  end
end

この状態で「bundle exec rspec」を実行すると……

ターミナル
1) Users 新規登録を行う
     Failure/Error:
       expect{
         find('input[name="commit"]').click
       }.to change{ User.count }.by(1)

       expected `User.count` to have changed by 1, but was changed by 0

 [Screenshot]: /Users/○○/……/failures_r_spec_example_groups_users_新規登録を行う_○○.png
(中略)

 ユーザーの新規登録を正常に行うことができませんでした。screenshotを確認したところ、どうやらemailに設定している
「他のユーザーが既に登録しているメールアドレスを登録することはできない(重複できない)」
というバリデーションに引っかかった模様。

 そこで、rails_helpers.rbの記述を見直したり、FactoryBotのemailカラムに使っているFakerというGemについて調べたりしていたのですが、次の方法で解決しました。

spec/system/users_spec.rb
require 'rails_helper'

RSpec.describe "Users", type: :system do

 # createではなく、buildを使用
  let!(:user) { FactoryBot.build(:user) }

  scenario "新規登録を行う" do
    (中略)
  end
end

 上記のように、createメソッドではなくbuildメソッドを使用することでテストが成功しました。

 createメソッドを使うと、FactoryBotで生成したnickname、email、password、password_confirmationがデータベースに保存されます。つまり、既に登録されているemailを再び、
「fill_in 'メールアドレス', with: user.email」
で入力して登録しようとしていたわけですね。
 そのため、「メールアドレスは重複して保存することはできません!」というバリデーションに引っかかったのでした。

 というわけで、非常に単純なことでしたが?

 設定しているバリデーションを確認しながら、「createメソッドとbuildメソッドのどちらを使うべきか」を判断しなければならないことを学びました。最近はcreateメソッドばかりを使っていたので、視野を広く持たなければいけないなと感じました。

 以上です。もし何か誤りなどありましたらご指摘いただけますと幸いです。
 ここまで読んでいただきありがとうございました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rspecのシステムテストでクッキーに関するテストしてみた

はじめに

皆さん始めまして!
駆け出しエンジニアのプージニアことpoo3です!
今回初投稿となるので多めに見ていただけると幸いです。

対象となる読者様

・Rspecのシステムテストでクッキーに関してテストを行いたい方

参考記事

@rokさんの記事
https://qiita.com/rok/items/09f5cab47338d0fa9188

結論:gem 'show_me_the_cookies'を導入しよう

まずはGemfileに 'show_me_the_cookies'を追加

Gemfile.rb
gem 'show_me_the_cookies'

次にみんな大好き 'bundle install' ??

bundle install

rails_helperの中でインストールした'show_me_the_cookies'を使えるようにincludeする
(システムテストの中で使う事を想定しております)

rails_helper.rb
config.include ShowMeTheCookies, :type => :system

上記完了したらシステムテストの中でクッキー弄り放題です?

メソッドなどは下記ご参考ください

https://github.com/nruth/show_me_the_cookies

実際に書いてみたテストコード

Rspec初心者なのでツッコミどころ満載かもしれませんが笑
今回は分かりやすいようにあえてリファクタリングはしてない状態で載せます!

user_login_spec.rb
  feature "remember_me機能" do
    before do
      #ユーザを作成する
      @test_user = FactoryBot.create(:user)
    end

    scenario "チェクボックスON" do
      #ログインする
      visit login_path
      expect(page).to have_content 'Log in'
      fill_in 'session_email', with: @test_user.email
      fill_in 'session_password', with: @test_user.password
      check 'session_remember_me'
      expect(page).to have_checked_field('session_remember_me')
      click_button 'ログイン'
      #永続クッキー作成されているか確認
      show_me_the_cookies
      expect(get_me_the_cookie('remember_token')).to_not eq nil
      #クッキーがきちんと仕事をするか確認
      expire_cookies
      visit root_path
      expect(page).to have_content 'ログアウト'
    end

    scenario "チェックボックスOFF" do
      #ログインする
      visit login_path
      expect(page).to have_content 'Log in'
      fill_in 'session_email', with: @test_user.email
      fill_in 'session_password', with: @test_user.password
      expect(page).to have_unchecked_field('session_remember_me')
      click_button 'ログイン'
      #永続クッキー作成されていない事を確認
      show_me_the_cookies
      expect(get_me_the_cookie('remember_token')).to eq nil
    end
  end

上記コードで使用している'show_me_the_cookies'のメソッド紹介

show_me_the_cookies
現在のクッキー情報を出力する
クッキーの中身の情報を確認したい時に便利!
expire_cookies
期限切れのクッキーとセッションを全て破棄する!!
実質一度ブラウザを閉じた状態にすることができる✌️
get_me_the_cookie(cookie_name)
特定のクッキー情報をハッシュで取得

終わりに

少し短いですが以上になります!
これからも週1回程度何かしらの記事を書けたらと思います?
ここまで読んでくれた方ありがとうございました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【Rails】carrierwave(キャリアウエーブ)で動画の投稿(保存) 

はじめに

キャリアウエーブ(carrierwave)を導入しました。
画像ではなく、動画の投稿方法を紹介します。
 

目次

  1. carrierwave導入
  2. 動画の保存準備
  3. 動画の投稿

開発環境

ruby 2.6.5
rails 6.0.0

実装

それでは実装していきます〜

1. carrierwave導入

まずはGemをインストールします。

ターミナル
gem "carrierwave"
ターミナル
bundle install

gemを読み込むために、rails s して必ずサーバーを再起動させてください。

1.2 アップローダークラスの生成

動画専用のファイルを作成します。

ターミナル
rails g uploader video_top

構文はこちらです。

ターミナル
rails g uploader アップローダー名

今回はvideo_tpoという名前で作ります。

そうすると以下の様なファイルが自動生成されます。

スクリーンショット 2020-11-22 5.49.30.png

いろいろ記述してありますが、extension_whitelistの部分をコメントインしてMOV wmv mp4を追記します。

app/uploaders/video_uploader.rb
#省略

  # Add a white list of extensions which are allowed to be uploaded.
  # For images you might use something like this:
  def extension_whitelist
    %w(jpg jpeg gif png MOV wmv mp4) #←ここを追記
  end

#省略

アップロードするファイルの拡張子やサイズ、保存するパスを指定する事が出来ますが、今回は動画なので MOV wmv mp4この3っつを追記します。

ここまでがキャリアウエーブの導入です。

2. 動画の保存準備

続いて動画の保存をします。
MVCの作成をしていきます。

まずはモデル作成します。

ターミナル
rails g model top

今回はtopという名前で作成します。

まず自動生成されてるマイグレーションファイルに追記します。

db/migrate/2020XXXXXXXXX_create_tops.rb
class CreateTops < ActiveRecord::Migration[6.0]
  def change
    create_table :tops do |t|
      t.string :video_top #←ここ

      t.timestamps
    end
  end
end

今回はvideo_topという名前でカラムを追加します。

ターミナル
rails db:migrate

次に、できたモデルに、アップローダークラスとカラムの紐づけをしていきます。

app/models/top.rb
class Top < ApplicationRecord
  mount_uploader :video_top, VideoUploader
end

構文はこちらです。
mount_uploader [:カラム名], [アップローダークラス]

カラム名はvideo_topでアップローダークラスは最初に作成したアップローダークラスのクラス名VideoUploaderです。

コントローラーの作成・編集をします。

ターミナル
rails g controller top index new create

保存と再生に最低限必要なアクションはindex new createです。

自動で記述さてるnew createに追記し、paramsを許可してデータを取得できるようにします。

app/controllers/tops_controller.rb
class TopsController < ApplicationController
  def index
  end

  def new
    @top = Top.new #←ここを追記
  end

  def create
    @top = Top.new(tops_params) #←ここを追記
    @top.save! #←保存だけします 「!」をつける事でエラーを表示してくれます。
  end


  private

#ここからがデータを許可する記述
  def tops_params
    params.require(:top).permit(:video_top)
  end
end

これで保存する裏側は完成です。

あとはビューを作って投稿ができるようにします。

3. 動画の投稿

自動生成されてるviewのnewに追記します。

app/views/tops/new.html.erb
<h1>Tops#new</h1>
<p>Find me in app/views/tops/new.html.erb</p>

#ここから追記
<%= form_with model: @top, local: true do |f| %>

<div class="field">
  <%= f.label :video %>
  <%= f.file_field :video_top, :accept => 'video/*' %>
</div>

<div class="#">
  <%= f.submit "作成する" ,class:"#" %>
  <%=link_to 'もどる', root_path, class:"back-btn" %>
</div>
<% end %>

ルーティングを設定して保存してみます。

config/routes.rb
Rails.application.routes.draw do
  root to: 'tops#index'
  resources :tops
end

http://localhost:3000/tops/new

これで保存機能の完成です!

次に、保存ができたらファイルを確認してみます。

3.1 動画ファイルの確認とgitとの連携

video_topカラムにはファイル名が保存され、動画自体はpublic/top/video_top/「id名」に保存されてます。

スクリーンショット 2020-11-22 7.02.26.png

Githubと連携させている方であれば、commit・pushの際データ量が多すぎるので、Git管理下から除外しときます。

.gitignore
#省略
/public/packs
/public/packs-test
/node_modules
/yarn-error.log
yarn-debug.log*
.yarn-integrity

/public/uploads #←これを追記

保存が確認されました。

次回、ビューで表示させます。ページに遷移したら(リロード)自動で再生してくれる方法と、ボタンを押して手動再生する方法があるので、二種類紹介します。
よかったら参考にしてみてください。
【Rails】carrierwave(キャリアウエーブ)の再生方法 「自動再生」「手動再生」
後日更新予定

まとめ

キャリアウエーブ(carrierwave)を使った動画の「投稿」方法でした。
下記の参考にせていただいた記事の中に、動画ではなく「画像」を保存してるので記事もあるので、そちらも興味あれば参考にしてみてください。

最後に

私はプログラミング初学者ですが、自分と同じ様にエンジニアを目指す方々の助けになればと思い、記事を投稿しております。
それではまた次回お会いしましょう〜

参考

https://pikawaka.com/rails/carrierwave

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Nginx+Unicorn(WEB/APサーバーの分離構成)で静的ファイルが読み込めない対処について

アプリをAPサーバー側にデプロイし、Nginxを使ったリバースプロキシ設定を行ったところ、以下画像のようにcssがうまく読み込めない問題が発生したので、その解決までの工程を忘れないようにメモしておきます。

qiita1.PNG

今回の環境

リバースプロキシ.png

  • WEBサーバーにはNginx
  • APサーバーにはUnicorn
  • ruby -v 2.6.6
  • rails -v 5.2.4.4
  • PC Windows10

Google Chromeで検証

Chromeでエラーの出ているページを開いたら、F12か右クリックの検証を選択。
すると以下のようなエラーが確認された。
qiita2.PNG
リソースのロードに失敗したと表示されている。
この時の404は表示させているWEBサーバー側、つまりNginxから404が出ている。
念のため、ログでエラーが出ていないかチェックしたいので、APサーバー側(Unicorn)を調べてみる。

APサーバー(Unicorn)のログ確認

rails配下のlogを確認する。

/var/www/rails/hoge
$ cd log
$tail -f unicorn.log
# -fはファイルの記述を監視。

INFO -- : worker=1 ready
INFO -- : worker=0 ready
INFO -- : reaped #<Process::Status: pid 3997 exit 0> worker=0
INFO -- : reaped #<Process::Status: pid 3998 exit 0> worker=1
INFO -- : master complete
INFO -- : Refreshing Gem list
INFO -- : listening on addr=0.0.0.0:3000 fd=9
INFO -- : master process ready
INFO -- : worker=1 ready
INFO -- : worker=0 ready

一度ブラウザをリロード。
特にそれっぽいエラーはないのでctrl+cで離脱。

WEBサーバー(Nginx)のエラーログ確認

Nginx配下(/etc/nginx)にあるnginx.confをvimで開いてみる。

/etc/nginx
$ sudo vim nginx.conf

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

#error_logの場所が記述されているのでこれをコピー。

$ sudo tail -f /var/log/nginx/error.log

sudo tail -fを叩いてブラウザを一度リロードしてみたが、特にエラーは出なかった。

nginx.confのaccses_logを確認する

先ほどのnginx.confをvimで開いて、accses_logを確認してみる。

/etc/nginx
$ sudo vim nginx.conf

user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log;
pid /run/nginx.pid;

access_log  /var/log/nginx/access.log  main;
#access_logの場所が記述されているのでこれをコピー。

$ sudo tail -f /var/log/nginx/access.log

叩いて、ブラウザをリロード。

すると、

"GET /assets/application-03e8ae3db0f190f7147e6299a7a6cb74a90a3f8de03988a2de0595377b02fa60.css HTTP/1.1" 404 1722

assetsの404エラーが確認できた。
railsアプリのpublic配下にassetsがあるので行ってみる。

/var/www/rails/hoge/public
$ cd assets
$ ls

application-03e8ae3db0f190f7147e6299a7a6cb74a90a3f8de03988a2de0595377b02fa60.css

ブラウザのエラーも"03e8"から始まるcssなので一致。

プリコンパイルする際に何かあるっぽい

アプリをデプロイする際、productionでpreconpileしている。

bundle exec rake assets:precompile RAILS_ENV=production

production環境をチェックしたいのでconfig配下のenvironmentsに移動してみる。

/var/www/rails/hoge/config
$ cd environments
$ vim prodaction.rb
#prodaction.rbをvimで開く。

config.public_file_server.enabled = ENV['RAILS_SERVE_STATIC_FILES'].present?
#これが静的ファイルを読み込んでいない原因らしいので環境変数を定義してあげる。

$ export RAILS_SERVE_STATIC_FILES=true
$ env | grep RAILS
$ RAILS_SERVE_STATIC_FILES=true
# 反映されていたらOK

$ unset RAILS_SERVE_STATIC_FILES
# 以前の環境に戻す場合。
  • 補足

present?メソッドはnil, “”, “ “(半角スペースのみ), 空の配列, 空のハッシュが含まれている場合falseつまり、偽とするので元々設定されていない状態だとfalseを返す。
なのでRAILS_SERVE_STATIC_FILESに=falseの状態でもpresent?メソッドは「何か入ってるよ!」つまり真となるのでexport RAILS_SERVE_STATIC_FILES=falseでも結果としてはapサーバ側から静的ファイル
を読み込めるようになるらしい。

UnicornとNginxを再起動させる

Unicornをkillして起動。
Nginxを停止して起動。
これで静的ファイルの読み込みが行われていれば成功。
ブラウザに戻ってWEBサーバーのIPアドレスを叩くと……

qiita3.PNG

やった(´▽`)!

参考

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Cloud9(Amazon Linux+Rails5.2+MySQL5.7)

1 現状のMySQLバージョンを確認

$ mysql --version

2 旧バージョン(5.5)パッケージ関連を削除

$ sudo yum -y remove mysql-config mysql55-server mysql55-libs mysql55

3 新バージョン(5.7)パッケージ関連をインストール

sudo yum -y install mysql57-server mysql57

4 日本語文字化け対策を実施

$ sed -e "/utf8/d" -e "/client/d" -e "/^\[mysqld_safe\]$/i character-set-server=utf8\n\n[client]\ndefault-character-set=utf8" /etc/my.cnf |sudo tee /etc/my.cnf

5 MySQL確認&起動

$ mysql --version
$ sudo service mysqld start

6 MySQL Account を作る

$ sudo mysql -u root   #rootユーザーでログイン
$ mysql> create user 'ユーザー名' identified by 'パスワード';  #ユーザー作成
$ mysql> grant all on *.* to 'ユーザー名';  #ALL権限付与

#作成したユーザーの確認は下記コマンド
$ mysql> select User,Host from mysql.user;

7 順番にライブラリをインストール&mysql2(gem)のインストール

$ sudo yum install mysql57-devel
$ sudo yum -y install ruby-devel
$ sudo yum groupinstall "Development Tools"

$ gem install mysql2

8 Railsをインストール

$ gem install rails -v 5.2.1

9 プロジェクトを作成

$ rails new projectname --database=mysql

10 config/database.ymlの編集

default: &default
  adapter: mysql2
  encoding: unicode
  pool: 5
  # 以下、3行追加
  username: <username> # 設定したMySQL Accountと同一のもの
  password: <password> # 設定したMySQL Accountと同一のもの
  host: localhost

development:
  <<: *default
  database: appname_development # appnameのところは、rails new時のappnameになっているはずです。
test:
  <<: *default
  database: appname_test # appnameのところは、rails new時のappnameになっているはずです。

11 rails db:createでデータベースを作成

$ cd appname
$ rails db:create

#rails5の場合は不要
$ source <(curl -sL https://cdn.learnenough.com/yarn_install)
$ yarn install --check-files
$ rails webpacker:install

#config/enviroments/development.rb
config.hosts.clear
  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

RailsのTransactionの使い方について

はじめに

はじめまして。
railsを使用した開発を行っている実務経験1ヶ月のひよっこです。
業務の中でトランザクションの処理の実装を担当させていただく機会がありましたので復習のために投稿させていただきます。
下記投稿の内容は
http://markdaggett.com/blog/2011/12/01/transactions-in-rails/
という内容を一部和訳したものとなっています。

対象

railsを触り始めたばかりの初学者向けの内容となっております。もちろん経験者の方大歓迎です。不足している部分や間違っている部分ありましたらご指摘いただけますと大変嬉しいです。

Transactionの目的

複数のSQL文の変更に対して、全てのアクションが成功した際にDBの変更を発生させるという条件を守らせるために使用するのがTransationの目的です。Transactionにより、データの統一性を保つことができます。
下記のコードは本を100円で購入→本のデータを保存→balance(残高)の保存という3つの処理の例を表しています。この中でどれか一つが失敗する、例えば本を100円で買ったはずがエラーになってしまった。→残高の処理は行われたので所持金は減っているのに本の情報は保存されなかった、などデータの整合性に不具合が生じます。その際にSQL処理を全部ロールバックされるのが、Transactionの特徴です。

この中のどれか一つでも例外が発生するちrollbackしてくれる!
ActiveRecord::Base.transaction do
  book.buy(100) # 本を100円で買うメソッド
  book.save!    # 上記の本の情報が登録される処理
  balance.save! # 自分の残高の保存
end

Transactionはクラスメソッドでもインスタンスでも使える!

Railsでは、トランザクションはすべてのActiveRecordモデルでクラスメソッドとインスタンスメソッドとして利用できます。つまり、以下のアプローチはどちらも同じように有効です。

クラス変数
Client.transaction do
  @client.users.create!
  @user.clients(true).first.destroy!
  Product.first.destroy!
end

インスタンス変数
@client.transaction do
  @client.users.create!
  @user.clients(true).first.destroy!
  Product.first.destroy!
end

上記のコードの中でトランザクションで参照されるモデルクラスがいくつか異なることに気づいたでしょうか?
1つのトランザクション・ブロック内でモデル・タイプが混在していても全く問題ありません。
これは、トランザクションがモデルインスタンスではなくデータベース接続にバインドされているからです。原則として、トランザクションが必要になるのは、複数のレコードへの変更が単一のユニットとして成功しなければならない場合だけです。(ユニットとは、、、本を買う→本の情報保存→自分の残高を減らすという一連の処理のまとまりの事)さらに、Railsはすでに#saveと#destroyメソッドをトランザクションでラップしているので、1つのレコードを更新するときにトランザクションが必要になることはありません。

Transactionのrollbackが発生する条件

rollbackが発生するには、「例外」が必要となります。

上記コードではsaveに!をつけていますが、これがついているメソッドは、失敗したら例外を吐くメソッドです、transactionを使うときは、saveではなくsave!、destroyではなくdestroy!を使うとtransactionがちゃんと拾ってくれます。これをつけないと不整合なデータが保存validationの網をかいくぐって保存されてしまう可能性もあります。

ActiveRecord::Base.transaction do
  david.update_attribute(:amount, david.amount -100)
  mary.update_attribute(:amount, 100)
end

例えば上記のコードではupdate_attributeを使用していますがrailsでは#update_attributeは更新に失敗したときに例外を投げないように設計されています。これはfalseを返すので、使用するメソッドが失敗したときに例外を投げるようにする必要があります。先ほどの例の書き方は次のようになります。

ActiveRecord::Base.transaction do
  david.update_attributes!(:amount => -100)
  mary.update_attributes!(:amount => 100)
end

次の例は#find_byメソッドを使ってトランザクション内でレコードを見つけています。find_byはレコードが返されなかった場合にnilを返すように設計されていますが
通常の #findメソッドは ActiveRecord::RecordNotFound 例外を吐きますよね。

ActiveRecord::Base.transaction do
  david = User.find_by_name("david")
  if(david.id != john.id)
    john.update_attributes!(:amount => -100)
    mary.update_attributes!(:amount => 100)
  end
end

上記のコードは問題があります。どこかわかりますか?

正解はfind_byメソッドの結果がnilであった場合変数davidにnilが代入され、条件分岐をスルーしてしまうところです。
これでは意図しない結果になってしまうでしょう。下記が適切です。

ActiveRecord::Base.transaction do
  david = User.find_by_name("david")
  raise ActiveRecord::RecordNotFound if david.nil?
  if(david.id != john.id)
    john.update_attributes!(:amount => -100)
    mary.update_attributes!(:amount => 100)
  end
end

find_byに対しての例外が発生すると、ロールバックが完了した後、トランザクションの外で例外が発生します。この例外をキャッチし処理をするコードを準備する必要がありますね。

例外を発生させずにロールバックさせる方法

例外を使わずに、Transactionをロールバックさせたい場合は、ActiveRecord::Rollbackを使いましょう。
ActiveRecord::Rollback を使用することで、例外を発生させずにトランザクションを実行することも可能です。これにより、コード内の他の場所でレスキューする必要がなく、トランザクションを無効にしてデータベースレコードをリセットすることができます。

トランザクションを使う上で避けるべきよくあるパターン

単一のレコードのみを更新する
無駄に入れ子にしている
ロールバックが発生しないコードを含むトランザクション
コントローラーでのトランザクションの使用

以上となります。随時新たに知ったこと等あれば追記していこうと思っています。
ここまで見て頂き本当にありがとうございました!

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

docker-composeを使った開発環境構築実践! Rails + Nuxt + MySQL

はじめに

docker-composeを使ったローカル開発環境構築について解説します!

ここでは、フロントエンドにNuxt.js, バックエンドにRails, DBにMySQLを使った構成にします。

YouTube動画

動画で確認したい方はこちらもどうぞ!
【YouTube動画】 docker-compose による開発環境構築入門
docker-compose による開発環境構築入門

GitHub

GitHubにコードを載せているので、実際に動かしてみたい方はこちらもどうぞ!
https://github.com/yassun-youtube/docker-compose-sample

git clone git@github.com:yassun-youtube/docker-compose-sample.git


実際のコード
docker-compose.yml
version: "3.8"
services:
  rails:
    build: ./rails-sample
    ports:
      - 3000:3000
    volumes:
      - ./rails-sample:/app
      - /app/node_modules
    stdin_open: true
    tty: true
    command: sh -c "yarn && bundle exec rails server -b 0.0.0.0 -p 3000"
    depends_on:
      - mysql
  nuxt:
    build: ./nuxt-sample
    ports:
      - 8000:8000
    volumes:
      - ./nuxt-sample:/app
      - /app/node_modules
    command: sh -c "yarn && yarn dev"
  mysql:
    image: mysql:8.0.22
    volumes:
      - db-data:/var/lib/mysql
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
      MYSQL_ROOT_HOST: '%'
      MYSQL_DATABASE: test-database
volumes:
  db-data:
# ./nuxt-sample/Dockerfile
FROM node:14.15.1-alpine3.12

WORKDIR /app

COPY package.json yarn.lock ./

RUN yarn install

CMD ["yarn", "dev"]
# ./rails-sample/Dockerfile
FROM ruby:2.7.1-alpine

RUN set -x && apk add --no-cache \
                      # for mysql
                      mysql-dev \
                      # for nokogiri
                      build-base \
                      # for tzinfo-data
                      tzdata \
                      yarn
WORKDIR /app

COPY Gemfile Gemfile.lock package.json yarn.lock ./

RUN bundle install
RUN yarn install

CMD ["bundle", "exec", "rails", "s", "-b", "0.0.0.0"]
database.yml
default: &default
  adapter: mysql2
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000

development:
  <<: *default
  username: root
  password:
  database: rails-sample-dev
  host: mysql

test:
  <<: *default
  username: root
  password:
  database: rails-sample-test
  host: mysql
nuxt.config.js
...
  proxy: {
    '/api': 'http://rails:3000'
  },
...


docker-composeとは

docker-composeとは複数コンテナやネットワーク、ボリュームを一括管理できるツールです。
Dockerをインストールすれば、ついてくるので、別途インストールする必要はありません。

よく使うコマンド

docker-composeでよく使うコマンドを紹介します。

docker-compose up

docker-compose.ymlに記載されたコンテナを起動するために使います。
-d オプションを付けると、デタッチモードで起動します。

docker-compose down

起動したコンテナを停止して削除するときに使います。
削除しない場合は、CTRL + Cを押すか docker-compose stopで止めることができます。

コンテナをデタッチモードで動かしているときは、docker-compose psと併用して使います。

docker-compose build

docker-compose.ymlに記載されたサービス (nuxtとかrails, mysql) をビルドするときに使います。
ymlファイル内のimage:またはbuildに書かれているパスにあるDockerfileのイメージを参照して、ビルドを開始します。

docker-compose run <サービス名> <コマンド>

docker-compose.ymlに記載されたサービスを指定してコマンドを実行するときに使います。

docker-compose exec <サービス名> <コマンド>

既に起動しているコンテナを指定して、コマンドを実行するときに使います。

docker-compose.ymlの概要

docker-compose.ymlで指定できる、version services networks volumesについて紹介します。

version

docker-composeの書き方のバージョンを指定するときに使います。
この記事を書いている段階 (2020/11/29) では、3.8です。

services

コンテナを定義するために使います。
ここで、railsとかnuxt, mysqlのDockerfileを指定したり、イメージを直接指定して使います。

networks

各サービスがどのネットワークと接続するかを定義できます。

volumes

ホストOSとゲストOSでデータを同期するときに使います。
今回はMySQLのデータを同期するために使います。

実際の設定ファイル

実際の設定ファイルは以下のようになります。

docker-compose.yml
version: "3.8"
services:
  rails:
    build: ./rails-sample
    ports:
      - 3000:3000
    volumes:
      - ./rails-sample:/app
      - /app/node_modules
    stdin_open: true
    tty: true
    command: sh -c "yarn && bundle exec rails server -b 0.0.0.0 -p 3000"
    depends_on:
      - mysql
  nuxt:
    build: ./nuxt-sample
    ports:
      - 8000:8000
    volumes:
      - ./nuxt-sample:/app
      - /app/node_modules
    command: sh -c "yarn && yarn dev"
  mysql:
    image: mysql:8.0.22
    volumes:
      - db-data:/var/lib/mysql
    environment:
      MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
      MYSQL_ROOT_HOST: '%'
      MYSQL_DATABASE: test-database
volumes:
  db-data:

大枠の設定

上記設定ファイルの大枠をみていきます。
設定ファイルはバージョン3.8を利用し、サービスにはrails, nuxt, mysqlを指定しています。
DBのデータを同期するために、volumes: db-data:を指定しています。

docker-compose.yml
version: "3.8"
services:
  rails: ...
  nuxt: ...
  mysql: ...
volumes:
  db-data:

services: mysqlの設定

imageでMySQL 8.0.22のDockerイメージを取得し、volumesでデータを同期しています。
environmentで環境変数を設定でき、ここではrailsからROOTユーザーとして、アクセスできるようにしています。
また、作成するDB名はtest-databaseとしました。

docker-compose.yml
mysql:
  image: mysql:8.0.22
  volumes:
    - db-data:/var/lib/mysql
  environment:
    MYSQL_ALLOW_EMPTY_PASSWORD: 'yes'
    MYSQL_ROOT_HOST: '%'
    MYSQL_DATABASE: test-database

services: nuxtの設定

nuxtは別途Dockerfileを作成し、そこからイメージを取るようにしました。
Dockerfileの場所をbuildで指定しています。

portsでは、ホストOSの8000ポートとゲストOSの8000ポートを繋げる設定をしています。

volumesの1行目./nuxt-sample:/appは、ホストOSのnuxt-sampleをゲストOSのappと同期するための設定です。
2行目の/app/node_modulesはホストOSのnode_moduleとゲストOSのnode_moduleを同期しないための設定です。
node_modulesはOS依存のパッケージが入る可能性があるので、このように除外しています。

commandでは、コンテナ起動時にシェルで実行するコマンドを設定しています。
node_modulesを作るために、yarnを実行し、yarn devでnuxtを起動しています。

docker-compose.yml
nuxt:
  build: ./nuxt-sample
  ports:
    - 8000:8000
  volumes:
    - ./nuxt-sample:/app
    - /app/node_modules
  command: sh -c "yarn && yarn dev"

次にDockerfileをみていきます。
FROMで使用するDockerイメージを指定します。
開発環境なので、軽量なalpineにしています。

WORKDIRで以後、コマンドを実行するディレクトリを指定します。

COPY, RUN, CMDでパッケージをゲストOSにコピーし、yarn, yarn devを実行しています。
docker-compose.yml側で同じ設定をしているので、なくても大丈夫です。

# ./nuxt-sample/Dockerfile
FROM node:14.15.1-alpine3.12

WORKDIR /app

COPY package.json yarn.lock ./

RUN yarn install

CMD ["yarn", "dev"]

services: railsの設定

build, ports, volumesは先ほどと同じような意味です。

dockerでbinding.pryなどのデバッグができるように、stdin_open: truetty: trueを指定しています。

depends_onでは、mysqlを指定し、mysqlのコンテナが起動するまではrailsのコンテナが起動しないようにしています。

docker-compose.yml
rails:
  build: ./rails-sample
  ports:
    - 3000:3000
  volumes:
    - ./rails-sample:/app
    - /app/node_modules
  stdin_open: true
  tty: true
  command: sh -c "yarn && bundle exec rails server -b 0.0.0.0 -p 3000"
  depends_on:
    - mysql

Dockerfileの方もnuxtと同じように依存パッケージをインストールして、railsサーバーを起動しています。

# ./rails-sample/Dockerfile
FROM ruby:2.7.1-alpine

RUN set -x && apk add --no-cache \
                      # for mysql
                      mysql-dev \
                      # for nokogiri
                      build-base \
                      # for tzinfo-data
                      tzdata \
                      yarn
WORKDIR /app

COPY Gemfile Gemfile.lock package.json yarn.lock ./

RUN bundle install
RUN yarn install

CMD ["bundle", "exec", "rails", "s", "-b", "0.0.0.0"]

database.ymlの設定

RailsサーバーとMySQLを接続するために、docker-compose.ymlのサービスで指定している名前をdatabase.ymlで設定する必要があります。

services: mysqlと設定していたので、database.ymlのhostにもmysqlと設定しておきます。

database.yml
default: &default
  adapter: mysql2
  pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
  timeout: 5000

development:
  <<: *default
  username: root
  password:
  database: rails-sample-dev
  host: mysql

test:
  <<: *default
  username: root
  password:
  database: rails-sample-test
  host: mysql

nuxt.config.jsの設定

NuxtからRailsにプロキシしたい場合は、nuxt.config.jsのproxyにサービス名を記述するだけでプロキシできます。

nuxt.config.js
...
  proxy: {
    '/api': 'http://rails:3000'
  },
...

おわりに

実際にコードを見ながら、動かしながら学んだ方が理解が深まると思うので、ぜひGitHubのリポジトリもご確認ください!
https://github.com/yassun-youtube/docker-compose-sample

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

find_or_initialize_byとfind_or_create_byは何が違う?

はじめに

find_or_initialize_byfind_or_create_byという便利なメソッドがありますが、今まであまり意識せず使っていました。
ふと違いが気になったので調べてみました!

書き方

モデル.find_or_initialize_by(条件)
モデル.find_or_create_by(条件)

両メソッドの違い

find_or_initialize_by
条件を指定して初めの1件を取得し,1件もなければ作成(new)

find_or_create_by
条件を指定して初めの1件を取得し,1件もなければ作成(create)

つまり新規作成して保存するか、しないかです。

ソースコード

既に存在する場合は既存のレコードを取得

User.find_or_initialize_by(name: "花山薫", age: 20)
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."name" = ? AND "users"."age" = ? LIMIT ?  [["name", "花山薫"], ["age", 20], ["LIMIT", 1]]
=> #<User id: 1, name: "花山薫", age: 20, created_at: "2020-11-29 06:45:10", updated_at: "2020-11-29 06:45:10">

User.find_or_create_by(name: "花山薫", age: 20)
  User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."name" = ? AND "users"."age" = ? LIMIT ?  [["name", "花山薫"], ["age", 20], ["LIMIT", 1]]
=> #<User id: 1, name: "花山薫", age: 20, created_at: "2020-11-29 06:45:10", updated_at: "2020-11-29 06:45:10">

存在しない場合は作成(new or create)

User.find_or_initialize_by(name: "範馬勇次郎", age: 44)
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."name" = ? AND "users"."age" = ? LIMIT ?  [["name", "範馬勇次郎"], ["age", 44], ["LIMIT", 1]]
=> #<User id: nil, name: "範馬勇次郎", age: 44, created_at: nil, updated_at: nil>

User.find_or_create_by(name: "範馬勇次郎", age: 44)
  User Load (0.1ms)  SELECT "users".* FROM "users" WHERE "users"."name" = ? AND "users"."age" = ? LIMIT ?  [["name", "範馬勇次郎"], ["age", 44], ["LIMIT", 1]]
   (0.0ms)  begin transaction
  User Create (0.9ms)  INSERT INTO "users" ("name", "age", "created_at", "updated_at") VALUES (?, ?, ?, ?)  [["name", "範馬勇次郎"], ["age", 44], ["created_at", "2020-11-29 07:50:49.127558"], ["updated_at", "2020-11-29 07:50:49.127558"]]
   (0.6ms)  commit transaction
=> #<User id: 4, name: "範馬勇次郎", age: 44, created_at: "2020-11-29 07:50:49", updated_at: "2020-11-29 07:50:49">

最後に

何か間違っている点などありましたらご指摘いただけると幸いですm(__)m

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Macbookデータ移行したらエラーが出た!?

macbook airからmacbook proにデータ移行してコマンドを打つと、、、

主に実行したコマンドは、

ruby -v

rails -v

などです。

以下のエラーが出ました。

dyld: Library not loaded: /usr/local/opt/gmp/lib/libgmp.10.dylib
  Referenced from: /Users/jun/.rbenv/versions/2.6.5/bin/ruby
  Reason: image not found

なぜなんなんだと、いろいろ試しているうちに解決策が見つかりました。

homebrewのバージョンがおかしかったのか、

brew update && brew upgrade

上記のコマンドでHomebrewを最新の状態にします。

そうすると、エラーが解決し使えるようになりました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails Tutorial 拡張機能のメッセージ機能を作ってみた(その4):送信したDMに加え受信したDMを表示

Rails Tutorialの第14章にある、メッセージ機能を作る件の続きです。

前回までメッセージを作成する機能ができました。
今回は受信したDMが表示される機能画面を作ります。

送信したDMに加え受信したDMを表示

DMの画面に送信したDMだけでなく、受信したDMも表示される機能を追加します。
「14.3ステータスフィード」を読みます。
14.3と同様に、テストから書きます。
1.自分が受信者のDMがchatに含まれていること
2.自分が送信者のDMがchatに含まれていること
3.自分が受信者でないDMは含まれないこと
ここで、3は1の裏返しです。テストとしては両方やる必要があります。

Userモデルにchatメソッドがあるので、テストはUserモデルに書くことになります。

test/models/user_test.rb
  test "chat should have the right dms" do
    michael = users(:michael)
    archer  = users(:archer)
    lana    = users(:lana)

    # 自分が送信者のDMを確認
    michael.sent_dms.each do |dm|
      assert michael.chat.include?(dm)
    end
    # 受信者のDMを確認
    test_dm1 = michael.sent_dms.create!(content: "send message test1", 
                                            receiver_id: archer.id)
    assert    michael.chat.include?(test_dm1)
    assert     archer.chat.include?(test_dm1)
    assert_not   lana.chat.include?(test_dm1)

テストはREDです。chatメソッドを変更します。

app/models/user.rb
  def chat
    #Dm.where("sender_id = ?", id)
    Dm.where("sender_id = :user_id OR receiver_id = :user_id", user_id: id)
  end

テストを再度実行します。

ubuntu:~/environment/sample_app (create-dm) $ rails test test/models/user_test.rb 

GREENになりました。

すべてのテストを実行

全てのテストを実行してみます。エラーになりました。

 FAIL["test_dm_display", DmsTest, 2.135752142000001]
 test_dm_display#DmsTest (2.14s)
        Expected /Deep\ v\ mustache\ godard\ austin\./ to match "<!DOCTYPE html>\n<html>\n  <head>\n    <!-- <title> | Ruby on R
...
        test/integration/dms_test.rb:19:in `block (2 levels) in <class:DmsTest>'

テストを見直します。sent_dmsをchatで置き換える必要があるところがいくつか見つかりました。
変更前:assert_match @user.sent_dms.count.to_s, response.body
変更後:assert_match @user.chat.count.to_s, response.body

変更前:@user.sent_dms.paginate(page: 1).each do |dm|
変更後:@user.chat.paginate(page: 1).each do |dm|

全てのテストがGREENになりました。

rails serverで表示してみます。
dm9.png

誰からのDMか分からない

受信したDMは、名前にreceiverが表示されている、つまり自分が表示されているので、誰が送信したDMなのかが分からない、と気が付きました。

送信したDMと受信したDMを混ぜて表示すると設計したときには、気が付かなかったです。

対策として、表示する名前は
・送信したDMは、受信者
・受信したDMは、送信者
にするように変更します。

それと同様に受信したDMは、レイアウトを右寄せにします。
TwitterのDM画面を調べて気が付きました。

いくつかソースを眺めて、app/views/dms/_dm.html.erbを変更すればよいと分かりました。

app/views/dms/_dm.html.erb
<li id="dm-<%= dm.id %>">
  <%= link_to gravatar_for(dm.receiver, size: 50), dm.receiver %>
  <span class="user"><%= link_to dm.receiver.name, dm.receiver%></span>
  <span class="content"><%= dm.content %></span>
  <span class="timestamp">
    Sent <%= time_ago_in_words(dm.created_at) %> ago.
  </span>
</li>

送信者を表示するテスト

機能を作る前にテストから考えます。

test/integration/dms_test.rb
# 受信者の画面では送信者が表示
    delete logout_path
    log_in_as(@receiver)
    get dms_path
    assert_select "span.user", @user.name

REDになるはずがGREENです。fixtureを調べます。既にテストデータが入っていたので、別のユーザーにすることにします。
受信者をarcherからmaloryに変えます。

test/fixtures/users.yml
malory:
  name: Malory Archer
test/integration/dms_test.rb
  def setup
    @user = users(:michael)
    @receiver = users(:malory)
  end

テストは想定通りの、REDになりました。

        <Michael Example> expected but was
        <Malory Archer>..
        Expected 0 to be >= 1.

テストができるようになったので、viewを変更していきます。
チュートリアルでuserを確認していたif文を参考にします。

app/views/users/_user.html.erb
  <% if current_user.admin? && !current_user?(user) %>
    | <%= link_to "delete", user, method: :delete,
                                  data: { confirm: "You sure?" } %>
  <% end %>

を参考にして、変更します。

app/views/dms/_dm.html.erb
  <% if dm.sender = user   %>
    <%= link_to gravatar_for(dm.receiver, size: 50), dm.receiver %>
    <span class="user"><%= link_to dm.receiver.name, dm.receiver %></span>
  <% else %>
    <%= link_to gravatar_for(dm.sender, size: 50), dm.sender %>
    <span class="user"><%= link_to dm.sender.name, dm.sender %></span>
  <% end %>

テストしたところ、エラーになりました。

test_send_dm_with_invalid_receiver#DmsTest (0.84s)
ActionView::Template::Error:         ActionView::Template::Error: undefined local variable or method `user' for #<#<Class:0x000055c46520e520>:0x000055c465212e18>
        Did you mean?  @user
            app/views/dms/_dm.html.erb:3:in `_app_views_dms__dm_html_erb__3083936748904538070_47150999444720'
            app/views/dms/index.html.erb:7:in `_app_views_dms_index_html_erb___2703126462765815417_47150999303880'
            test/integration/dms_test.rb:26:in `block in <class:DmsTest>'

@userを使えばいいのか、tutorialを読み直します。
コントローラーで定義した変数、例えば@user@chat_itemsは、ビューで使えると理解しています。

ビューの中では、変数名dmをなぜ@なしで使えるのかがあやふやだったので読み直します。

リスト 10.51の説明を参考に考えると、DmクラスとRailsが判断し、_dm.html.erbという名前のパーシャルを探しに行きます。パーシャルの中ではそのクラスの変数名、この場合はクラス名に対応したdmという名前の変数に値がセットされています。
10章のまとめにも

render @usersを実行すると、自動的に_user.html.erbパーシャルを参照し、各ユーザーをコレクションとして表示する

と書いてありました。

リスト 10.57と

  <% if current_user.admin? && !current_user?(user) %>
    | <%= link_to "delete", user, method: :delete,
                                  data: { confirm: "You sure?" } %>
  <% end %>

リスト 13.51:

    <% if current_user?(micropost.user) %>
      <%= link_to "delete", micropost, method: :delete,
                                       data: { confirm: "You sure?" } %>
    <% end %>

を参考に修正します。

app/views/dms/_dm.html.erb
  <% if current_user?(dm.sender) %>
    <%= link_to gravatar_for(dm.receiver, size: 50), dm.receiver %>
    <span class="user"><%= link_to dm.receiver.name, dm.receiver %></span>
  <% else %>
    <%= link_to gravatar_for(dm.sender, size: 50), dm.sender %>
    <span class="user"><%= link_to dm.sender.name, dm.sender %></span>
  <% end %>

テストがGreenになりました。

rails serverで表示してみます。受信したDMは送信者が表示されました。
dm10.png

受信と送信でレイアウトを変更

完成した画面を見て気が付いたのですが、それぞれのDMが送信したものか受信したものかの見分けが付きません。

Twitterでは画面のレイアウトを送信と受信で左寄せと右寄せに分けています。
同様に変更します。

リスト 13.26、さらにリスト 5.8: を読みます。

app/assets/stylesheets/custom.scss
 text-align: center;

    .user {
      margin-top: 5em;
      padding-top:0;
    }

    .user {
      margin-top: 5em;
      padding-top:0;
      text-align: right;
    }

ネットで「CSS 右寄せ」で検索してみます。
https://qiita.com/Rock22/items/e4e89f15c29e1415977d
試しに変更して、表示してみます。

app/assets/stylesheets/custom.scss
  .microposts {
    list-style: none;
    padding: 0;
    text-align: right;

     .gravatar {
      /*float: left; */
      float: right;

右寄せが期待どおりにできました。
dm11.png

現在はDMのCSSはmicropostsクラスを使っているので、新たにdmのクラスを2つ作ります。

app/assets/stylesheets/custom.scss
  /* dms sent and received*/
  .dms_sent {
    list-style: none;
    padding: 0;
    text-align: right;

    .gravatar {
      float: right;

    }  
  }
    /* dms sent and received*/
  .dms_received {
    list-style: none;
    padding: 0;

    .gravatar {
      float: left;

    }  
  }

OLタグからはclassの指定を削除します。

app/views/dms/index.html.erb
    <ol>
      <%= render @chat_items %>  
      <%#= render @dms %>  
    </ol>

受信と送信でクラスを分けます。

app/views/dms/_dm.html.erb
<% if current_user?(dm.sender) %>
  <li id="dm-<%= dm.id %>" class="dms_sent">
    <span class="gravatar"><%= link_to gravatar_for(dm.receiver, size: 50), dm.receiver %></span>
    <span class="user"><%= link_to dm.receiver.name, dm.receiver %></span>
    <span class="content"><%= dm.content %></span>
    <span class="timestamp">
      Sent <%= time_ago_in_words(dm.created_at) %> ago.
    </span>
  </li>
<% else %>
  <li id="dm-<%= dm.id %>" class="dms_received">
    <%= link_to gravatar_for(dm.sender, size: 50), dm.sender %>
    <span class="user"><%= link_to dm.sender.name, dm.sender %></span>
    <span class="content"><%= dm.content %></span>
    <span class="timestamp">
      Sent <%= time_ago_in_words(dm.created_at) %> ago.
    </span>
  </li>
<% end %>

試しに表示してみます。送信の右寄せ、受信の左寄せができました。
dm12.png

さらに送信と受信が分かるように、文言を加えました。from:とto: ReceivedとSentの対にしました。

app/views/dms/_dm.html.erb
<% if current_user?(dm.sender) %>

    <span class="user">to:<%= link_to dm.receiver.name, dm.receiver %></span>

      Sent <%= time_ago_in_words(dm.created_at) %> ago.

<% else %>

    <span class="user">from:<%= link_to dm.sender.name, dm.sender %></span>

      Received <%= time_ago_in_words(dm.created_at) %> ago.

<% end %>

dm13.png

所要時間

11/23から11/28までの5.5時間です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

超簡単!erbで動的にシンプルなカレンダを作る方法

アドベントカレンダの記念すべき1日目なのでカレンダネタです。

この記事を読んだらできること

  • 任意の範囲でカレンダが作れる
  • 特定の日だけデザインを変えたり、文章を変えたりできる

いきなりですがコード。

<% # この辺りはViewModelあたりに入れるのがお行儀がいい %>
<% # カレンダにしたい月を含む日付であればなんでもいい %>
<% target_month = Date.new("2020-12-01") %>
<% # カレンダのはじまりの日(例では月初) %>
<% start_day = target_month.beginning_of_month %>
<% # カレンダの終わりの日(例では月末) %>
<% last_day  = target_month.end_of_month %>

<% # カレンダ本体 %>
<table>
  <thead>
    <tr>
      <% %w[日 月 火 水 木 金 土].each do | day | %>
        <%= content_tag :th, day %>
      <% end %>
    </tr>
  </thead>

  <tbody>
    <tr>
      <% # 月初部分の曜日調整 %>
      <% start_day.wday.times do %>
        <%= content_tag :td, '' %>
      <% end %>

      <% (start_day..last_day).each do | date | %>
        <%= content_tag :td, date.day %>
        <% # wday == 6 は土曜日なので、tableの次の行(tr)へと移行する %>
        <%== '</tr><tr>' if date.wday == 6 %>
      <% end %>
    </tr>    
  </tbody>
</table>

今回は月初から月末までのオーソドックスなものですが start_day last_day を変えれば任意の範囲でのカレンダが作れます。
また、 <%= content_tag :td, day.day %> を下記のようにすればクリスマスだけ表示を変えたりCSSのclassを付与したりもできます。

<% if date.month == 12 && date.day == 25 %>
  <%= content_tag :td, 'Xmas', class: 'xmas' %>
<% else %>
  <%= content_tag :td, date.day %>
<% end %>

シンプルな分、カスタマイズが自由なカレンダだとおもいます。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsでparticles.jsを使ってみた

はじめに

javascript ライブラリのparticle.jsを使ってみました。
途中、思うようにいかずつまづいていたので、うまくいったやり方を書いておきます。

幾何学模様や、宇宙みたいな粒子をマウスの動きに合わせてアニメーションを設定できる、楽しいやつです。

image.png
snowバージョン
image.png

環境

mac OS バージョン10.15.7
Rails 5.1.6
particles.js 2.0.0

step.1 デモジェネレーターで作りたい見た目を作る

①デモサイトでアニメーションを作る
デモジェネレーターのリンク

https://vincentgarreau.com/particles.js/

で好きなアニメーションを設定する。
カラーはもちろん、アニメーションのスピードや粒子の方向なども設定できる。

②設定したファイルをダウンロード

Download current config (json)

 のボタンでダウンロードできます。

③public/assets 内にダウンロードしたJSONファイルを配置
私はこのファイルを app/assetsに配置してずっとうまくいかないな〜ってなってました。
JSONはimgファイルとかと同じ扱いらしいです。

step.2 パッケージのインストール

Packages install

gem install particles-js-rails
bundle install

step.3 コードを書く

bodyタグの閉じタグ直前に以下を記述

application.html.erb
<script src="https://cdn.jsdelivr.net/particles.js/2.0.0/particles.min.js"></script>
<script>
particlesJS.load('particles-js', 'assets/particles.json', function() {
      console.log('callback - particles.js config loaded');
    });
</script>

assets/particles.jsonの箇所は配置したファイルの名前に合わせる。
ダウンロード直後はparticles-00.jsonのようになっているはず

アニメーションを配置したい箇所に以下を記述

top.html.erb
<div id="particles-js"></div> 

CSSファイルに以下を記述

top.scss
#particles-js {
    width:100%;
    height: 100%;
    margin:0 auto;
    background-color: rgb(230, 182, 216)
}

これで行けるはず。

もしprecompileのエラーが出てきたら、下記を試してみてください

config/initializers/assets.rb
Rails.application.config.assets.precompile += %w( particles.js )

参考(本家)

https://github.com/VincentGarreau/particles.js/
スターを付けて使いましょう。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【RSpec】Ruby on Rails チュートリアル「第4章」のテストを RSpec で書いてみた

はじめに

こちらは前回の続きとなります。

【RSpec】Ruby on Rails チュートリアル「第3章」のテストを RSpec で書いてみた
https://qiita.com/t-nagaoka/items/27b51ef1610c4c336a80

対象者

  • Ruby on Rails チュートリアルのテストを Rspec で実施予定、または実施してみたい人
  • Ruby on Rails チュートリアル「第4章」のテストを Rspec で書きたいけど、書き方が分からない人

テストコード

実際にテストコードを書き換えた結果が下記になります。

Minitest

static_pages_controller_test.rb
class StaticPagesControllerTest < ActionDispatch::IntegrationTest
  test "should get home" do
    get static_pages_home_url
    assert_response :success
    assert_select "title", "Ruby on Rails Tutorial Sample App"
  end

  test "should get help" do
    get static_pages_help_url
    assert_response :success
    assert_select "title", "Help | Ruby on Rails Tutorial Sample App"
  end

  test "should get about" do
    get static_pages_about_url
    assert_response :success
    assert_select "title", "About | Ruby on Rails Tutorial Sample App"
  end
end

RSpec

static_pages_spec.rb
require "rails_helper"

RSpec.describe "StaticPages", type: :request do
  before do
    @base_title = "Ruby on Rails Tutorial Sample App"
  end

  describe "Home page" do
    it "should get home" do
      visit static_pages_home_url
      expect(page.status_code).to eq(200)
      expect(page).to have_title "#{@base_title}"
      expect(page).not_to have_title "Home |" # タイトルに「Home |」が含まれていないことを確認
    end
  end

  describe "Help page" do
    it "should get help" do
      visit static_pages_help_url
      expect(page.status_code).to eq(200)
      expect(page).to have_title "Help | #{@base_title}"
    end
  end

  describe "About page" do
    it "should get about" do
      visit static_pages_about_url
      expect(page.status_code).to eq(200)
      expect(page).to have_title "About | #{@base_title}"
    end
  end
end

ポイント

「 expect(page).to have_title "xxx" 」 だけでは "xxx" がタイトルに含まれていればテストをパスするので、例えタイトルに含まれていて欲しくない "yyy" がタイトルに含まれていてもテストをパスしてしまいます。そのため、タイトルに "xxx" が含まれていて "yyy" がが含まれていないことを確認するためには、上記のエクスペクテーションの他に 「 expect(page).not_to have_title "yyy" 」 を追加で記述し、別途タイトルに "yyy" が含まれていないことを確認する必要があります。

次回

「第5章」のテストコードを RSpec に書き換える予定です。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Railsの検索機能を実装する際の変更 -Railsメモ

これから学習してきたことメモする為と、
自分がどういったところにつまづいていたかをメモ

まず、今回は勤怠管理アプリのindexページに対して
ユーザーの名前を"あいまい検索”で探しだし
さらに、通常のh1タイトルが"ユーザー一覧"となっている部分を
あいまい検索を実行した際に"検索結果"というタイトルに変わっているようにする
という内容です。

参考にさせていただいたページはこちらで、

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

[Fargate 入門] ECS(Fargate)でrakeタスクを実行するまで

Fargateで単純なrakeタスクを実行するまでの手順。
タスク定義やコンテナの設定などがややこしかったため備忘録として。

準備

実行するrakeタスクとコンテナイメージを用意します。

rakeタスクの作成

今回はrakeタスク実行が確認できれば良いので単純なタスクを用意。

namespace :ecs_task do
  task :hoge do
    puts "task started"
  end
end

コンテナイメージの用意

以下のようなDockerfileを作成し、rakeタスクが実行できる環境を用意します。

Dockerfile
FROM ruby:2.5.3

# 環境変数
ENV LANG C.UTF-8
ENV APP_ROOT /app

# ソースをコンテナに転送
ADD ./ $APP_ROOT

# コンテナ内の作業ディレクトリの設定
WORKDIR $APP_ROOT

# gemのインストール
RUN gem update bundler
RUN bundle install

コンテナイメージをECRへプッシュする

Fargateでタスクを実行するためにDockerfileからイメージを作成してECRにプッシュします。
(Docker Hubでも良いですが今回はECRを利用)

マネジメントコンソールからリポジトリを作成しましょう。
下記のようにリポジトリが作成されたらOKです。
repository.png
プッシュコマンドの表示を押すと、ECRへイメージをプッシュする手順が表示されるので、手順にしたがってプッシュします。
プッシュ後、下記のようにイメージが存在すればOKです。
ecr image.png

準備は完了したので、ECSでクラスターとタスク定義を作成していきましょう。

ECS

マネジメントコンソール上でクラスターとタスク定義を作成していきます。

クラスターの作成

ECS > クラスターからクラスターを作成します。
今回はFargateを利用するので、ネットワーキングのみ(AWS Fargateを使用)を選択。
cluster template.png

クラスターの名前を入れて作成。
今回は既存のVPCを利用するのでVPCは作成しませんでした。
cluster setting.png
これでクラスター作成は完了しました。

タスク定義の作成

次にクラスターで実行させるタスクを作成していきます。
起動タイプの互換性の選択で、こちらもFargateを選択します。

タスクとコンテナの設定

次にタスクとコンテナの設定をしていきます。
task1.png

タスクロール

タスクを実行するコンテナに付与するIAMロールです。タスクでS3やRDSなどAWSサービスにアクセスする必要がある場合は、必要な権限を持ったロールを設定します。
今回は不要なのでスキップ。

タスクの実行IAMロール

タスクを実行(開始)するために必要なロールです。
説明にもありますが、コンテナイメージをECRからプルしたり、コンテナのログをCloudWatch Logsに吐き出すための権限を持ったロールが必要になります。

run_task_role.png
初回は新しいロールの作成で自動的に必要なロールを作成してくれるのでこのままでOK。

タスクサイズ

タスクの実行に使用されるメモリとCPUを設定します。(Fargateでは必須)
ひとまず最小限のメモリとCPUを設定しました。
task_size.png

コンテナの設定

タスク実行に必要なコンテナの設定をします。

基本設定

container_standard.png

スタンダード(基本設定)
コンテナ名 コンテナの名前
イメージ コンテナのURI(ECRからコピーできます)
プライベートレジストリの認証 プライベートのリポジトリからイメージをプルする場合に必要(ECRを利用する場合は不要)
メモリ制限(MiB) コンテナに対するメモリの制限ですが、Fargateではタスクに対してサイズを設定しているので、コンテナで制限をかける意味はよくわかっていません。
ポートマッピング コンテナのポートとホストインスタンスのポートを紐づける場合に設定します。
今回は単純なrake実行のみなので使用しません。

環境

コンテナ環境で実行したいコマンドと作業ディレクトリを以下のように指定。
container_env.png

その他

ヘルスチェックやネットワークの設定などがありますが今回は使用しないので入力する必要はありません。

以上でタスク定義の設定は終了です。

タスクの実行

さっそくタスクを実行していきましょう。

タスクにチェックを入れてタスクの実行をクリック。
start_task.png

タスク実行時の設定を入力していきます。
起動タイプはFARGATE、タスクを実行するVPCやサブネットなどを入力し、タスクの実行をクリック。
run_task.png

タスクのプロビジョニングが開始されます。
task_provisioning.png

しばらく待っているとRUNNINGに。
task_running.png

ログの確認

ちゃんとタスクが実行されているかログを確認してみましょう。
タスクの実行ログはデフォルトではCloudWatch Logsに吐き出されます。

ロググループに/ecs/タスク名が作成されていますのでクリック。
タスクが実行されたことがわかりました。
log.png

所感

初めてECSを触ったということもありますが、色々設定することが多く初めは手間取りました。
タスク定義(コンテナ周り)など、まだまだ今回使っていない設定項目は多数あるので、今後調べていきます。
また今回は1コンテナ、単純なrakeタスクの実行しかしていないので、複数コンテナの連携(フロント、バックエンド、DB)などをECSで行ってみようと思います。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

deviseの導入の仕方

手順

  1. Gemfileに最下部にgem 'devise'と記述する
  2. bundle installする
  3. rails g devise:installする
  4. モデルを作成するためにrails g devise userを実行する
  5. ビューファイルを設置するためにrails g devise:viewsを実行する

2~5はターミナルで操作します。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

「ログアウトしますか?」の確認メッセージを表示させたい

こんな人に向けて

1.Railsでアプリケーションを作成している人。

2.ログアウトや投稿の際に出てくる
「ログアウトしますか?」ーーー「OK」・「キャンセル」
のようなメッセージを実装したい人。

方法

JSの知識がなくてもRailsだと簡単にできます。

top.html.erb
<%=link_to "logout", destroy_user_session_path, class:"li nav-link text-dark",method: :delete, data:{confirm:"  [確認]  ログアウトしますか?"}%>

大事なのは最後の部分。

data:{confirm: " [確認] ログアウトしますか?"}

dsBuffer1.bmp.png

data:{confirm: "戻っていいのかい?"}

dsBuffer.bmp.png

表示させたい文章を””内に入れればOKです。

link_toだけでなくform_withでも使用できるので試してみてください。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Rails 504 time outエラー対応

概要

rails, puma, nginxで構築した環境で504 time outエラーが発生した時に対応したことについてまとめます。
状況としてはpumaとnginxが起動状態で、DBにアクセスする前に504エラーが表示されていたので処理に時間がかかっていることが原因ではないという想定で原因調査をしました。

環境

rails:5.3.4
puma:3.12.4
nginx:1.19.1

原因

デプロイ時にconfigで定義しているcredentials.yml.encファイルの中身が書き換わってしまうことが原因でした。

解決方法

書き換わったファイルを別の名前にリネームして、以下のコマンドを実行

export SECRET_KEY_BASE=`bundle exec rake secret`

このコマンドを実行することで新しくcredentials.yml.encが作成され、time outエラーは解消されました。

まとめ

504 time outエラー時に対応したことについてまとめました。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む