- 投稿日:2020-11-29T23:49:35+09:00
ポートフォリオ作成過程 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(すみません詳しくはテスト工程に入ってから調べるので言葉のみ使います)を利用することも挑戦する予定です。
ただ、「いつまでにポートフォリオ完成して理解などの復習するの?」はまだ思いついてない状況ですので、よく考えて実行したいと反省しております。
- 投稿日:2020-11-29T23:49:35+09:00
ポートフォリオ作成記録 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(すみません詳しくはテスト工程に入ってから調べるので言葉のみ使います)を利用することも挑戦する予定です。
ただ、「いつまでにポートフォリオ完成して理解などの復習するの?」はまだ思いついてない状況ですので、よく考えて実行したいと反省しております。
- 投稿日:2020-11-29T23:42:52+09:00
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カラムを持っていて自分で設定できる
この記事で説明しないこと
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%A6lib以下にconstarain用のクラスを定義します。
constrain用のクラスで定義したmatches?がtrueの場合、任意のスコープ内にあるパスが採用される。という感じです。lib/school_domain.rbclass SchoolDomain class << self def matches?(request) matches_subdomain?(request) end private def matches_subdomain?(request) # wwwの場合は追加するルーティングにマッチングしないようにしておく request.subdomain.present? && request.subdomain != 'www' end end endroute.rbRails.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.rbdef 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...実際にやってみる
スクールページへアクセス(ローカル環境: http://helloworld.lvh.me:3000/)
うまく表示できました!
試しに適当な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/dd75f2a3601472590eadSTEP2:独自ドメインとの紐付け
サブドメインの紐付けがうまくいきました。次は独自ドメインの紐付けです。
まずconstrainの部分を変更して、別ドメインがきた時も同じルーティングを採用するようにします。lib/school_domain.rbclass 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.rbdef 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に戻されました。この状態で、localtest.me:3000にアクセスします
うまくいきました!
本番環境で独自ドメインを紐付ける
本番環境の実装方法は、環境によって異なるので今回の記事では割愛します?♂️
後日追記するかもしれません。
軽く調べた感じだと「多段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/100000activerecord-multi-tenant
マルチテナンシーを同一DB上で実現させるためのgemです。テナントごとにテーブルが別れているわけではないので、データの横断は不可能ではないですが、アプリケーションレベルでデータの領域分けを強制させられるのでマルチテナンシーとして機能しそうです(全部は理解できていない汗)
参考記事: ActiverecordMultiTenant でマルチテナンシー by yks0406さん (マルチテナンシーについて、Railsでのアプローチについて一番詳しくまとまっている記事です)
https://note.com/yks0406/n/n09c181400561参考記事に習って、今回のサンプルアプリもactiverecord-multi-tenantで実装を進めていきます。
スクール(テナント)ごとに生徒の情報を管理する
?♂️2020年12月中に追記予定?♂️
最後に
SaaSやノーコードアプリを開発するエンジニアのかた増えてきていると思います。
参照記事多目、まとめ記事と言っても過言ではないですが、お役に立てれば幸いです。
- 投稿日:2020-11-29T23:24:11+09:00
【RSpec-rails】ユーザーモデル単体テストコード
はじめに
rspec-rails
、factory_bot_rails
、'fakerを導入するGemfilegroup :development, :test do gem 'rspec-rails' gem 'factory_bot_rails' gem 'faker' end次にターミナルで
$ bundle install
、rails g rspec install
を行う。作成された.rspecファイルに--format documentation
(表示の出力がきれいになるようにフォーマットを指示する)を記述ターミナル$ bundle install $ rails g rspec install.rspec--format documentation仕様に沿ったユーザーモデルのデータを設定する
重複したemailは登録できないことにしているため、uniqueを付けて一意となるようにする
spec/factories/users.rbFactoryBot.define do factory :user do email { Faker::Internet.unique.free_email } password { '123456' } password_confirmation { password } end endfactory_botを使う
インスタンス変数userに、先程設定したuserのデータを入れる
spec/models/user_spec.rbRSpec.describe User, type: :model do describe '#create' do before do @user = FactoryBot.build(:user) end end endcreateアクションをテストする。テスト項目を書き出す
spec/models/user_spec.rbRSpec.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
- 投稿日:2020-11-29T23:22:39+09:00
メタプログラミングRuby(第I部・金曜日まで)を読んでいく
第5章 木曜日:クラス定義
- JavaやC#におけるクラスの定義は、あなたが「これがオブジェクトに期待する動作です」と言うと、コンパイラが「了解。やってみます」と答える。クラスのオブジェクトを生成して、メソッドを呼び出すまでは何も起こらない。
- Rubyにおけるクラスの定義は違っており、classキーワードは、オブジェクトの動作を規定しているだけではなく、実際にコードを実行している。
- クラスマクロ(クラスを修正する方法)
- アラウンドエイリアス(メソッドでコードをラップする方法)
クラス定義
- クラス定義にはメソッドの定義だけでなく、あらゆるコードを置くことができる。
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_eval
はself
とカレントクラスを変更する。class_eval
はselfを変更することによって、classキーワードと同じようにクラスを再オープンできる。Module#class_eval
はclassよりも柔軟。
- classが定数を必要とするのに対して、
class_eval
はクラスを参照している変数なら何でも使える。- classは現在の束縛を捨てて、新しいスコープをオープンするのに対し、
class_eval
はフラットスコープを持っている。
- つまり「スコープゲート」で学んだように、
class_eval
ブロックのスコープの外側にある変数も参照できる。instance_eval
にinstance_exec
という双子のメソッドがあるように、module_eval
やclass_eval
にも、外部のメソッドをブロックに渡せるmodule_exec
やclass_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_class
かclass <<
という変わった構文を使わなければ見ることができない。- 次に、インスタンスを一つしか持てない。
- だから特異クラスはシングルトンクラスとも呼ばれる。
- そして、継承ができない。
- 最後に、特異クラスはオブジェクトの特異メソッドが住んでいる場所。
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種類しかない。それが通常のモジュール、クラス、特異クラスのいずれかになる。
- メソッドは1種類しかない。メソッドはモジュール(大半はクラス)に住んでいる。
- 全てのオブジェクトは(クラスも含めて)「本物のクラス」を持っている。それが通常のクラスか特異クラスである。
- 全てのクラスは(BasicObjectを除いて)ひとつの祖先(スーパークラスかモジュール)を持っている。つまり、あらゆるクラスがBasicObjectに向かって1本の継承チェーンを持っている。
- オブジェクトの特異クラスのスーパークラスは、オブジェクトのクラスである。クラスの特異クラスのスーパークラスはクラスのスーパークラスの特異クラスである(3回唱えてみよう)。
- メソッドを呼び出すときは、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_eval
はself
を変更し、class_eval
はself
とカレントクラスの両方を変更していた。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_method
をMyModule
の普通のインスタンスメソッドとして定義し、それから、MyClass
の特異クラスで、モジュールをインクルードする。my_method
はMyClass
の特異クラスのインスタンスメソッドである。つまり、my_method
はMyClass
のクラスメソッドである。
- この技法は クラス拡張 と呼ばれる。
クラスメソッドと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#size
はString#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つの手順で書ける。
- メソッドにエイリアスをつける。
- メソッドを再定義する。
- 新しいメソッドから古いメソッドを呼び出す。
アラウンドエイリアスの欠点
- 新しいメソッド名でクラスを汚染してしまうこと -> メソッドをエイリアスした後にprivateにすれば解決できる。
- メソッドの変更を考えていない既存のコードを破壊しかねないこと -> 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#prepend
はinclude
と似ているが、継承チェーンでインクルーダーの上ではなく下にモジュールが挿入されるところが違う。
- つまり、プリペンどしたモジュールがインクルーダーのメソッドをオーバーライドできる。
- そして、元のメソッドは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 # => 1irbの例
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 endadd_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 " endadd_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 endadd_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 endadd_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
- 投稿日:2020-11-29T23:12:59+09:00
Rails 一意性 DB メモ
Railsでは、
データを保存段階でバリデーション
をかけてもだめなぜか?
DBレベルでも一意性を保証しなければならないどういうことか…?
保存をする(フィルタリング)段階で「一意性」を設定しても
リクエストを連続
で送ってしまうと同じユーザーが作成されてしまうらしい
→だから、DBレベルでも一意性を保証しないといけない
- 投稿日:2020-11-29T23:12:14+09:00
ヘルパーメソッドの概要
ヘルパーメソッドの中でも[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としてフォームを送信するために記述する。
もし違うよってことがありましたら指摘の方よろしくお願いします!!
- 投稿日:2020-11-29T22:30:57+09:00
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インスタンスをデプロイする際の一番最初に出てくるやつです。
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 srcrbenvインストール完了後も引き続き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/binrenv-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 pullRubyインストール
さあ、これで晴れて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-environmentopenssl-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_profileRailsインストール
お膳立ては整いましたので、あとはRailsチュートリアルの通り、Rails6.0.3をインストールするだけです。
# gem install rails -v 6.0.3 # rails -v Rails 6.0.3お疲れ様でした。
- 投稿日:2020-11-29T22:30:12+09:00
【Rails】DBと連携してプルダウン(ドロップダウン)メニューをつくる(勉強中)
SIer企業で働く社会人4年目OLです。
仕事をやめる予定はないのですが、手に職をつけていつか副職ができるといいなと思いWEBアプリ開発の勉強し始めました。(2ヵ月くらい)練習でWEBアプリ開発をしていますが、初めてつまったのがプルダウンメニューの作成なので勉強がてらまとめてみます。同じように悩んでる人の役に立てたらいいなと思います。
勉強始めたばかりなのでここ違うよ!っていうところがあればご指摘いただけると嬉しいです。
やりたいこと
DB(Gameモデル)に格納しているデータをプルダウン(ドロップダウン)で選択して別のDB(Stageモデル)に保存したい。
モデル設計
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
ではなくて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("保存するモデルのインスタンス", 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_for
とそんなに変わらないじゃんと思った。参考にしたページの記述に似せたためControllerに多少違いはあるが、同じ書き方をしても問題ないと思っている。(試してはいない)form_tagとform_forとform_withについて
それぞれの使い分けについてもざっくり調べてみた。まず、
form_tag
とform_for
については以下のサイトが参考になった。Railsのform_for/form_tagの分け方の意図としては、
form_for: 任意のmodelに基づいたformを作るときに使う
form_tag: modelに基づかないformを作るときに使うということです。
つまり、あるuserモデルに基づいたuserを作成するときはform_forを使い、
そうではなく、検索窓のような何のモデルにも基づかないformを作りたいときはform_tagを使うのが原則です。モデルに紐づいたフォームか否かが使い分けの基準と理解した。
そして、
form_with
はform_tag
とform_for
の機能をまとめたヘルパーメソッドである。form_with
の使用が推奨されていることから、Rails5.1以降を使用する場合はform_with
を使用すべきと考えてよいと思う。プルダウン同士の連携
プルダウンメニューは作れたが、次は別の問題が発生した。
1つ目のプルダウンを選択したら2つ目のプルダウンで選択できる内容が変更できるような入力フォームを作ろうと考えたが、Viewを更新しないとアクションにデータを渡せない。これを解決するためにはAjaxを理解する必要があるようだが今は???状態なので調べて理解・実装したら、別の記事でまとめようと思う。
- 投稿日:2020-11-29T22:01:35+09:00
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%; }これでコードがすっきりしました。
ぜひ試してみてください。
- 投稿日:2020-11-29T19:42:34+09:00
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日何キロカロリーまで食べていいのか」を自分の体重から自動で計算してくれて、食べるごとに確認できたらカロリーコントロールしやすくなるのではないか。
- 体重やカロリーの推移をグラフで見られるようにすれば、成果が出てるのがわかってモチベーションが保てるのではないか。
このように原因やそれを解決する策を考えて、グラフ機能をメインとした減量アプリを作ることにしました。
アプリの写真と説明
トップページ
使い方を細かく説明しなくても、パッと見て一目で使い方わかる外観を意識して作りました。
グラフ
体重とカロリーの推移がグラフという形で見られるようにしました。
自分の頑張りが目に見えることで「よし、いい感じだ。もっと頑張ろう」と思えるのではないかと考えたのです。
カレンダーと画像一覧
食べたものや筋トレをカレンダーや写真で記録して確認できるようにしました。
写真でカラダの変化を可視化することでそれもモチベーションにつながると考えました。
また、他の減量に成功した方がどんなものを食べて減量できたのか知って真似できるため、という目的もあります。
カロリーの自動計算
「1日○キロカロリー食べれば体脂肪を落とせる」という減量の目安となる"1日の摂取カロリー目をユーザーが体重と体脂肪率を入力したら自動で計算されるようにしました。
※ちなみに、摂取カロリーの計算式はバズーカ岡田先生の著書『除脂肪メソッド』を参考にしました。
自動計算機能によって、「今日あと何キロカロリーまで食べて大丈夫か」がわかるようにしました。
これで減量の成功率を高められると考えました。
使用技術
フロントエンド
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図
インフラ構成図
アプリを作るのにかかった期間
トータル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記事にアウトプットしていました。
- Chart.jsを用いたグラフ機能
体重とカロリーをどうやってグラフで表示させるかで苦労しました。ただ大変さを感じると同時に、自分でロジックを考えるのが楽しく、プログラミングを始めて以来いちばん面白さを瞬間でもありました。
- AWSへのデプロイ
Qiita記事:デプロイしたのにアプリがブラウザに表示されない原因はunicorn.rbの設定にあった
Qiita記事:Can't connect to local MySQL server through socket '/var/lib/mysql/mysql.sock'の解決法
Qiita記事:libmysqlclient.so.18: cannot open shared object file: No such file or directoryの解決法
- Capistranoでの自動デプロイ
Qiita記事:bundle exec cap production deployで生じた6つのエラーの解決法(bundle exec cap production deployで生じた6つのエラーの解決法)
- CirlcleCIのconfig.ymlの設定
Qiita記事:CicleCIでRspecとRubocop通すまでにつまずいたところとその解決法
- Gitの中身が大きすぎる問題の解決
誤ってGitオブジェクトの中身を削除してしまい、その修復に苦労しました。
Qiita記事:[エラー解決プロセス説明].gitディレクトリ階下の容量が大きすぎるので小さくしたいどのように学習したのか
技術 学習方法 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頂ければ幸いです。
何卒よろしくお願いいたします。
- 投稿日:2020-11-29T19:42:34+09:00
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日何キロカロリーまで食べていいのか」を自分の体重から自動で計算してくれて、食べるごとに確認できたらカロリーコントロールしやすくなるのではないか。
- 体重やカロリーの推移をグラフで見られるようにすれば、成果が出てるのがわかってモチベーションが保てるのではないか。
このように原因やそれを解決する策を考えて、グラフ機能をメインとした減量アプリを作ることにしました。
アプリの写真と説明
トップページ
使い方を細かく説明しなくても、パッと見て一目で使い方わかる外観を意識して作りました。
グラフ
体重とカロリーの推移がグラフという形で見られるようにしました。
自分の頑張りが目に見えることで「よし、いい感じだ。もっと頑張ろう」と思えるのではないかと考えたのです。
カレンダーと画像一覧
食べたものや筋トレをカレンダーや写真で記録して確認できるようにしました。
写真でカラダの変化を可視化することでそれもモチベーションにつながると考えました。
また、他の減量に成功した方がどんなものを食べて減量できたのか知って真似できるため、という目的もあります。
カロリーの自動計算
「1日○キロカロリー食べれば体脂肪を落とせる」という減量の目安となる"1日の摂取カロリー目をユーザーが体重と体脂肪率を入力したら自動で計算されるようにしました。
※ちなみに、摂取カロリーの計算式はバズーカ岡田先生の著書『除脂肪メソッド』を参考にしました。
自動計算機能によって、「今日あと何キロカロリーまで食べて大丈夫か」がわかるようにしました。
これで減量の成功率を高められると考えました。
使用技術
フロントエンド
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図
インフラ構成図
アプリを作るのにかかった期間
トータル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記事にアウトプットしていました。
- Chart.jsを用いたグラフ機能
体重とカロリーをどうやってグラフで表示させるかで苦労しました。ただ大変さを感じると同時に、自分でロジックを考えるのが楽しく、プログラミングを始めて以来いちばん面白さを瞬間でもありました。
- AWSへのデプロイ
Qiita記事:デプロイしたのにアプリがブラウザに表示されない原因はunicorn.rbの設定にあった
Qiita記事:Can't connect to local MySQL server through socket '/var/lib/mysql/mysql.sock'の解決法
Qiita記事:libmysqlclient.so.18: cannot open shared object file: No such file or directoryの解決法
- Capistranoでの自動デプロイ
Qiita記事:bundle exec cap production deployで生じた6つのエラーの解決法(bundle exec cap production deployで生じた6つのエラーの解決法)
- CirlcleCIのconfig.ymlの設定
Qiita記事:CicleCIでRspecとRubocop通すまでにつまずいたところとその解決法
- Gitの中身が大きすぎる問題の解決
誤ってGitオブジェクトの中身を削除してしまい、その修復に苦労しました。
Qiita記事:[エラー解決プロセス説明].gitディレクトリ階下の容量が大きすぎるので小さくしたいどのように学習したのか
技術 学習方法 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頂ければ幸いです。
何卒よろしくお願いいたします。
- 投稿日:2020-11-29T19:00:44+09:00
【RSpec】ユーザーの新規登録について結合テストを行う時に意識すること
最近、RSpecを使ってオリジナルアプリのテストを実装しているのですが、新規登録で詰まったところがあるので、メモしておきたいと思います(といっても非常に単純なことなのですが?)。
詰まったこと
まず、ユーザーモデルのバリデーションは以下の通りです。
app/models/user.rbdevise :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.rbrequire '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 endFactoryBotに設定したテストデータはこちらです。
spec/factories/users.rbFactoryBot.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.rbrequire '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メソッドばかりを使っていたので、視野を広く持たなければいけないなと感じました。
以上です。もし何か誤りなどありましたらご指摘いただけますと幸いです。
ここまで読んでいただきありがとうございました。
- 投稿日:2020-11-29T18:45:01+09:00
Rspecのシステムテストでクッキーに関するテストしてみた
はじめに
皆さん始めまして!
駆け出しエンジニアのプージニアことpoo3です!
今回初投稿となるので多めに見ていただけると幸いです。対象となる読者様
・Rspecのシステムテストでクッキーに関してテストを行いたい方
参考記事
@rokさんの記事
https://qiita.com/rok/items/09f5cab47338d0fa9188結論:gem 'show_me_the_cookies'を導入しよう
まずはGemfileに 'show_me_the_cookies'を追加
Gemfile.rbgem 'show_me_the_cookies'次にみんな大好き 'bundle install' ??
bundle installrails_helperの中でインストールした'show_me_the_cookies'を使えるようにincludeする
(システムテストの中で使う事を想定しております)rails_helper.rbconfig.include ShowMeTheCookies, :type => :system上記完了したらシステムテストの中でクッキー弄り放題です?
メソッドなどは下記ご参考ください
https://github.com/nruth/show_me_the_cookies
実際に書いてみたテストコード
Rspec初心者なのでツッコミどころ満載かもしれませんが笑
今回は分かりやすいようにあえてリファクタリングはしてない状態で載せます!user_login_spec.rbfeature "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回程度何かしらの記事を書けたらと思います?
ここまで読んでくれた方ありがとうございました!
- 投稿日:2020-11-29T17:52:28+09:00
【Rails】carrierwave(キャリアウエーブ)で動画の投稿(保存)
はじめに
キャリアウエーブ(carrierwave)を導入しました。
画像ではなく、動画の投稿方法を紹介します。
目次
- carrierwave導入
- 動画の保存準備
- 動画の投稿
開発環境
ruby 2.6.5
rails 6.0.0実装
それでは実装していきます〜
1. carrierwave導入
まずはGemをインストールします。
ターミナルgem "carrierwave"ターミナルbundle installgemを読み込むために、
rails s
して必ずサーバーを再起動させてください。1.2 アップローダークラスの生成
動画専用のファイルを作成します。
ターミナルrails g uploader video_top構文はこちらです。
ターミナルrails g uploader アップローダー名今回はvideo_tpoという名前で作ります。
そうすると以下の様なファイルが自動生成されます。
いろいろ記述してありますが、
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.rbclass 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.rbclass 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.rbclass 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.rbRails.application.routes.draw do root to: 'tops#index' resources :tops endhttp://localhost:3000/tops/new
これで保存機能の完成です!
次に、保存ができたらファイルを確認してみます。
3.1 動画ファイルの確認とgitとの連携
video_top
カラムにはファイル名が保存され、動画自体はpublic/top/video_top/「id名」に保存されてます。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)を使った動画の「投稿」方法でした。
下記の参考にせていただいた記事の中に、動画ではなく「画像」を保存してるので記事もあるので、そちらも興味あれば参考にしてみてください。最後に
私はプログラミング初学者ですが、自分と同じ様にエンジニアを目指す方々の助けになればと思い、記事を投稿しております。
それではまた次回お会いしましょう〜参考
- 投稿日:2020-11-29T17:47:06+09:00
Nginx+Unicorn(WEB/APサーバーの分離構成)で静的ファイルが読み込めない対処について
アプリをAPサーバー側にデプロイし、Nginxを使ったリバースプロキシ設定を行ったところ、以下画像のようにcssがうまく読み込めない問題が発生したので、その解決までの工程を忘れないようにメモしておきます。
今回の環境
- WEBサーバーにはNginx
- APサーバーにはUnicorn
- ruby -v 2.6.6
- rails -v 5.2.4.4
- PC Windows10
Google Chromeで検証
Chromeでエラーの出ているページを開いたら、F12か右クリックの検証を選択。
すると以下のようなエラーが確認された。
リソースのロードに失敗したと表示されている。
この時の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.logsudo 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 1722assetsの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=productionproduction環境をチェックしたいので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アドレスを叩くと……やった(´▽`)!
参考
- 投稿日:2020-11-29T17:30:34+09:00
Cloud9(Amazon Linux+Rails5.2+MySQL5.7)
1 現状のMySQLバージョンを確認
$ mysql --version2 旧バージョン(5.5)パッケージ関連を削除
$ sudo yum -y remove mysql-config mysql55-server mysql55-libs mysql553 新バージョン(5.7)パッケージ関連をインストール
sudo yum -y install mysql57-server mysql574 日本語文字化け対策を実施
$ 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.cnf5 MySQL確認&起動
$ mysql --version $ sudo service mysqld start6 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 mysql28 Railsをインストール
$ gem install rails -v 5.2.19 プロジェクトを作成
$ rails new projectname --database=mysql10 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
- 投稿日:2020-11-29T17:19:26+09:00
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! # 自分の残高の保存 endTransactionはクラスメソッドでもインスタンスでも使える!
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 endfind_byに対しての例外が発生すると、ロールバックが完了した後、トランザクションの外で例外が発生します。この例外をキャッチし処理をするコードを準備する必要がありますね。
例外を発生させずにロールバックさせる方法
例外を使わずに、Transactionをロールバックさせたい場合は、ActiveRecord::Rollbackを使いましょう。
ActiveRecord::Rollback を使用することで、例外を発生させずにトランザクションを実行することも可能です。これにより、コード内の他の場所でレスキューする必要がなく、トランザクションを無効にしてデータベースレコードをリセットすることができます。トランザクションを使う上で避けるべきよくあるパターン
単一のレコードのみを更新する
無駄に入れ子にしている
ロールバックが発生しないコードを含むトランザクション
コントローラーでのトランザクションの使用以上となります。随時新たに知ったこと等あれば追記していこうと思っています。
ここまで見て頂き本当にありがとうございました!
- 投稿日:2020-11-29T17:12:48+09:00
docker-composeを使った開発環境構築実践! Rails + Nuxt + MySQL
はじめに
docker-composeを使ったローカル開発環境構築について解説します!
ここでは、フロントエンドにNuxt.js, バックエンドにRails, DBにMySQLを使った構成にします。
YouTube動画
動画で確認したい方はこちらもどうぞ!
【YouTube動画】 docker-compose による開発環境構築入門
GitHub
GitHubにコードを載せているので、実際に動かしてみたい方はこちらもどうぞ!
https://github.com/yassun-youtube/docker-compose-samplegit clone git@github.com:yassun-youtube/docker-compose-sample.git
実際のコード
docker-compose.ymlversion: "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.ymldefault: &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: mysqlnuxt.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.ymlversion: "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.ymlversion: "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.ymlmysql: image: mysql:8.0.22 volumes: - db-data:/var/lib/mysql environment: MYSQL_ALLOW_EMPTY_PASSWORD: 'yes' MYSQL_ROOT_HOST: '%' MYSQL_DATABASE: test-databaseservices: 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.ymlnuxt: 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: true
とtty: true
を指定しています。depends_onでは、mysqlを指定し、mysqlのコンテナが起動するまではrailsのコンテナが起動しないようにしています。
docker-compose.ymlrails: 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: - mysqlDockerfileの方も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.ymldefault: &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: mysqlnuxt.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
- 投稿日:2020-11-29T16:55:17+09:00
find_or_initialize_byとfind_or_create_byは何が違う?
はじめに
find_or_initialize_by
とfind_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
- 投稿日:2020-11-29T14:20:25+09:00
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を最新の状態にします。
そうすると、エラーが解決し使えるようになりました。
- 投稿日:2020-11-29T14:01:52+09:00
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.rbtest "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.rbdef 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.rbGREENになりました。
すべてのテストを実行
全てのテストを実行してみます。エラーになりました。
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になりました。
誰からの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.nameREDになるはずがGREENです。fixtureを調べます。既にテストデータが入っていたので、別のユーザーにすることにします。
受信者をarcherからmaloryに変えます。test/fixtures/users.ymlmalory: name: Malory Archertest/integration/dms_test.rbdef 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は送信者が表示されました。
受信と送信でレイアウトを変更
完成した画面を見て気が付いたのですが、それぞれのDMが送信したものか受信したものかの見分けが付きません。
Twitterでは画面のレイアウトを送信と受信で左寄せと右寄せに分けています。
同様に変更します。リスト 13.26、さらにリスト 5.8: を読みます。
app/assets/stylesheets/custom.scsstext-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;現在は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 %>試しに表示してみます。送信の右寄せ、受信の左寄せができました。
さらに送信と受信が分かるように、文言を加えました。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 %>所要時間
11/23から11/28までの5.5時間です。
- 投稿日:2020-11-29T13:46:01+09:00
超簡単!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 %>シンプルな分、カスタマイズが自由なカレンダだとおもいます。
- 投稿日:2020-11-29T13:37:55+09:00
Railsでparticles.jsを使ってみた
はじめに
javascript ライブラリのparticle.jsを使ってみました。
途中、思うようにいかずつまづいていたので、うまくいったやり方を書いておきます。幾何学模様や、宇宙みたいな粒子をマウスの動きに合わせてアニメーションを設定できる、楽しいやつです。
環境
mac OS バージョン10.15.7
Rails 5.1.6
particles.js 2.0.0step.1 デモジェネレーターで作りたい見た目を作る
①デモサイトでアニメーションを作る
デモジェネレーターのリンクで好きなアニメーションを設定する。
カラーはもちろん、アニメーションのスピードや粒子の方向なども設定できる。②設定したファイルをダウンロード
Download current config (json)
のボタンでダウンロードできます。
③public/assets 内にダウンロードしたJSONファイルを配置
私はこのファイルを app/assetsに配置してずっとうまくいかないな〜ってなってました。
JSONはimgファイルとかと同じ扱いらしいです。step.2 パッケージのインストール
Packages install
gem install particles-js-railsbundle installstep.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.rbRails.application.config.assets.precompile += %w( particles.js )参考(本家)
https://github.com/VincentGarreau/particles.js/
スターを付けて使いましょう。
- 投稿日:2020-11-29T12:23:12+09:00
【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.rbclass 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 endRSpec
static_pages_spec.rbrequire "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 に書き換える予定です。
- 投稿日:2020-11-29T12:09:55+09:00
Railsの検索機能を実装する際の変更 -Railsメモ
これから学習してきたことメモする為と、
自分がどういったところにつまづいていたかをメモまず、今回は勤怠管理アプリのindexページに対して
ユーザーの名前を"あいまい検索”で探しだし
さらに、通常のh1タイトルが"ユーザー一覧"となっている部分を
あいまい検索を実行した際に"検索結果"というタイトルに変わっているようにする
という内容です。参考にさせていただいたページはこちらで、
- 投稿日:2020-11-29T11:55:25+09:00
[Fargate 入門] ECS(Fargate)でrakeタスクを実行するまで
Fargateで単純なrakeタスクを実行するまでの手順。
タスク定義やコンテナの設定などがややこしかったため備忘録として。準備
実行するrakeタスクとコンテナイメージを用意します。
rakeタスクの作成
今回はrakeタスク実行が確認できれば良いので単純なタスクを用意。
namespace :ecs_task do task :hoge do puts "task started" end endコンテナイメージの用意
以下のようなDockerfileを作成し、rakeタスクが実行できる環境を用意します。
DockerfileFROM 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です。
プッシュコマンドの表示を押すと、ECRへイメージをプッシュする手順が表示されるので、手順にしたがってプッシュします。
プッシュ後、下記のようにイメージが存在すればOKです。
準備は完了したので、ECSでクラスターとタスク定義を作成していきましょう。
ECS
マネジメントコンソール上でクラスターとタスク定義を作成していきます。
クラスターの作成
ECS > クラスターからクラスターを作成します。
今回はFargateを利用するので、ネットワーキングのみ(AWS Fargateを使用)を選択。
クラスターの名前を入れて作成。
今回は既存のVPCを利用するのでVPCは作成しませんでした。
これでクラスター作成は完了しました。タスク定義の作成
次にクラスターで実行させるタスクを作成していきます。
起動タイプの互換性の選択で、こちらもFargateを選択します。タスクとコンテナの設定
タスクロール
タスクを実行するコンテナに付与するIAMロールです。タスクでS3やRDSなどAWSサービスにアクセスする必要がある場合は、必要な権限を持ったロールを設定します。
今回は不要なのでスキップ。タスクの実行IAMロール
タスクを実行(開始)するために必要なロールです。
説明にもありますが、コンテナイメージをECRからプルしたり、コンテナのログをCloudWatch Logsに吐き出すための権限を持ったロールが必要になります。
初回は新しいロールの作成
で自動的に必要なロールを作成してくれるのでこのままでOK。タスクサイズ
タスクの実行に使用されるメモリとCPUを設定します。(Fargateでは必須)
ひとまず最小限のメモリとCPUを設定しました。
コンテナの設定
タスク実行に必要なコンテナの設定をします。
基本設定
スタンダード(基本設定) コンテナ名 コンテナの名前 イメージ コンテナのURI(ECRからコピーできます) プライベートレジストリの認証 プライベートのリポジトリからイメージをプルする場合に必要(ECRを利用する場合は不要) メモリ制限(MiB) コンテナに対するメモリの制限ですが、Fargateではタスクに対してサイズを設定しているので、コンテナで制限をかける意味はよくわかっていません。 ポートマッピング コンテナのポートとホストインスタンスのポートを紐づける場合に設定します。
今回は単純なrake実行のみなので使用しません。環境
コンテナ環境で実行したいコマンドと作業ディレクトリを以下のように指定。
その他
ヘルスチェックやネットワークの設定などがありますが今回は使用しないので入力する必要はありません。
以上でタスク定義の設定は終了です。
タスクの実行
さっそくタスクを実行していきましょう。
タスク実行時の設定を入力していきます。
起動タイプはFARGATE、タスクを実行するVPCやサブネットなどを入力し、タスクの実行をクリック。
ログの確認
ちゃんとタスクが実行されているかログを確認してみましょう。
タスクの実行ログはデフォルトではCloudWatch Logsに吐き出されます。ロググループに
/ecs/タスク名
が作成されていますのでクリック。
タスクが実行されたことがわかりました。
所感
初めてECSを触ったということもありますが、色々設定することが多く初めは手間取りました。
タスク定義(コンテナ周り)など、まだまだ今回使っていない設定項目は多数あるので、今後調べていきます。
また今回は1コンテナ、単純なrakeタスクの実行しかしていないので、複数コンテナの連携(フロント、バックエンド、DB)などをECSで行ってみようと思います。
- 投稿日:2020-11-29T11:44:59+09:00
deviseの導入の仕方
- 投稿日:2020-11-29T11:41:23+09:00
「ログアウトしますか?」の確認メッセージを表示させたい
こんな人に向けて
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: " [確認] ログアウトしますか?"}data:{confirm: "戻っていいのかい?"}表示させたい文章を””内に入れればOKです。
link_toだけでなくform_withでも使用できるので試してみてください。
- 投稿日:2020-11-29T10:57:09+09:00
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エラー時に対応したことについてまとめました。