- 投稿日:2019-12-09T23:18:47+09:00
HamlとSassの短縮方法
はじめに
コードを書いていくうちに思いついた、Hamlコードを短くする方法の共有です。
正直当たり前な内容ではありますが、意外とこれが出来ることに気づきにくいのではないかと思い、投稿しました。実行
例えば次のようなHamlコードがあるとします。
.header ヘッダー .body .body__content1 .body__content1__red 一つ目のサンプル .body__content1__blue first sample .body__content2 .body__content2__red 二つ目のサンプル .body__content2__blue second sample .footter フッター
body
の部分にcontents1
とcontents2
があり、
それぞれ子要素にred
とblue
の二つのクラスがあります。
red
の方には日本語、blue
の方には英語でテキストを入力しています。このビューファイルに対し二つの文章を横並びにし、redクラスは赤文字、blueクラスは青文字にするためには次にようなCSSファイルを読み込めば良いです。
.body{ &__content1{ display: flex; .red{ font-color: red; } .blue{ font-color: blue; } } &__content2{ display: flex; .red{ font-color: red; } .blue{ font-color: blue; } } }この状態だと、
content1
もcontent2
も内容は一緒なので、とても無駄です。
無駄だけならまだしも、コードを書き換える際に間違いが生じることもあります。
このように、異なる全く同じCSSを当てる場合はクラス名を同じにすると間違いが起こりにくくなります。
先ほどのHamlコードは次のように書き換えられます。.header ヘッダー .body .body__content .body__content__red 一つ目のサンプル .body__content__blue first sample .body__content .body__content__red 二つ目のサンプル .body__content__blue second sample .footter フッターこのように書けば同じクラス名の別のオブジェクトとして扱われます
こうすればCSSを短縮して次のように書くことができます。.body{ &__content{ display: flex; .red{ font-color: red; } .blue{ font-color: blue; } } }これが2つのオブジェクト両方に適用されるので、コードを書き直す前と同じ結果になります。
終わりに
今回の内容は、htmlで書いている時は普通にやっていたことですが、
hamlでは階層を作りながら書いていくため全てのオブジェクトは別のクラスを持っていたなければいけないという思いこみがありましたが、その誤解が上手い具合に解けた気がします。
- 投稿日:2019-12-09T23:13:35+09:00
railsのlow level cache
low level cache
RailsGuideより
lowレベルキャッシュの最も効果的な実装方法は、Rails.cache.fetchメソッドを利用することです。このメソッドは、キャッシュの書き込みと読み出しの両方に対応しています。引数が1つだけの場合、キーを読み出し、キャッシュから値を取り出して返します。ブロックを引数として渡すと、キャッシュにヒットしなかった場合にブロックが実行されます。ブロックの戻り値は、指定のキャッシュキーの下にあるキャッシュに書き込まれます。キャッシュにヒットした場合は、ブロックを実行せずにキャッシュの値を返します。
def cache_users key = "cache_users" Rails.cache.fetch(key, expired_in: 60.minutes) do User.all.to_a end endオブジェクトをキャッシュしたい場合
class User < ApplicationRecord def cache_users(user_id) key = "#{cache_key}/cache_users" Rails.cache.fetch(key, expired_in: 60.minutes) do User.find(user_id) end endcache_keyメソッドを使っているので、キャッシュキーは233-20140225082222765838000/cache_usersのような形式になります。cache_keyで生成される文字列は、モデルのidとupdated_at属性を元にしています。
その為userが更新されるたびにキャッシュを無効にできるので、古いデータが返される心配がありません。注意
キャッシュする値は、ブロックの中の値です。
User.all
やUser.where()
などの返り値はActiveRecord_Relation
クラスのオブジェクトなので、
このオブジェクトをキャッシュしても、DB問い合わせは発生してしまいます。
DB問い合わせ結果をキャッシュしたい場合はto_a
する事で、DB問い合わせ行い、問合せ結果をArray
にした値をキャッシュさせます。参考
https://railsguides.jp/caching_with_rails.html
https://qiita.com/srockstyle/items/3f1dad0c88c9ef4c5288
https://qiita.com/yamashun/items/bf9a3d29de749cf18f2e
- 投稿日:2019-12-09T22:45:44+09:00
Rails リファクタリング まとめてみた
書いてあること
- リファクタリングとは
- リファクタリングは何のため?
ビューのリファクタリング
1.enum
2.enumの使い方コントローラーのリファクタリング
1.scopeを使って検索ロジックをモデルに移す
2.コントローラに書かれたロジックをモデルのメソッドで書き直す参考ページ
終わりに
リファクタリングとは
リファクタリングは処理の内容を変えずに冗長なコードを削除したり、コードを改善すること。
リファクタリングは何のため?
- 読み手が最短時間でコードを理解できるようになる(チーム開発など、人と作業をするとき負担を減らす)
- 修正がしやすくなる(未来の自分への優しさ)
ビューのリファクタリング
1.enum
int型、boolean型で定義されたカラムを、文字列で表現できる
schema.rbこんなテーブルができてます create_table "tasks", force: :cascade, options: "ENGINE=InnoDB DEFAULT CHARSET=utf8" do |t| t.string "title", null: false t.string "content" t.datetime "start_at", null: false t.datetime "finish_at", null: false t.integer "kind", null: false ←今回はこのカラムのロジックでビューの表示が変わるような記述がある t.boolean "finished", default: false, null: falseindex.html.haml- if task.kind == 0 私用 - if task.kind == 1 仕事 - if task.kind == 2 その他この記述で保守性が下がる理由
- kindの0, 1, 2がそれぞれ何を意味するのか、ビューを見ないと分からない
- kindの種類が増える度、新たにif文を足さなければいけない
2.enumの使い方
1.モデルにenumを定義
モデルclass Task < ApplicationRecord enum kind: [:individual, :work, :others] end2.hamlの書き換え
haml%tr %td - if task.kind == 0 私用 - if task.kind == 1 仕事 - if task.kind == 2 その他 %td = task.title ↓ /if文を削除し、task.kindと書き換える/ %tr %td = task.kind %td = task.title /以下省略/3.enumを使った書き換え
enumを定義すると、[モデル名].[カラム名の複数形] のような形で、そのカラムに設定したenumを全て表示することができるhaml.form-wrapper = form_for @task, html: {class: 'form-group'} do |f| = f.label :kind = f.select :kind, [0, 1, 2], {},class: 'form-control', placeholder: 0 /配列に数字がベタが記されている部分を変えます/ /以下略/"f.select"に引数として渡す.form-wrapper = form_for @task, html: {class: 'form-group'} do |f| = f.label :kind = f.select :kind,Task.kinds.keys, {}, class: 'form-control', placeholder: 0 /Task.kinds.keysのように記述することによって、enumで設定したkeyの一覧を得ることができる/
Task.kinds
のなかみ
=> {"individual"=>0,"work"=>1, "others"=>2}
Keyをつけると下記のようになる
=> ["individual", "work", "others"]
4.enum_helpのインストール
enum_help」というgemを利用すると、enumの日本語化が簡単に行えるgemfile# 末尾に追記 gem 'enum_help'
$ bundle install
する5.ja.ymlとビューを編集して日本語化
ja.ymlja: activerecord: ↓ #ja:の下に記述を追加する ja: enums: task: kind: individual: "私用" work: "仕事" others: "その他" activerecord:haml%td = task.kind ↓ #書換える %td = task.kind_i18nhaml= f.label :kind = f.select :kind,Task.kinds.keys, {}, class: 'form-control', placeholder: 0 ↓ #書換える = f.label :kind = f.select :kind, Task.kinds_i18n.invert, {}, class: 'form-control', placeholder: 0コントローラーのリファクタリング
1.scopeを使って検索ロジックをモデルに移す
index.html.hamldef index @task = Task.new @tasks = Task.where('start_at > ?', Time.zone.now).order(start_at: :asc) end@tasksの定義が冗長という問題は、モデルにscopeを定義することによって解決できます。
モデルにscopeを定義
models/task.rbclass Task < ApplicationRecord enum kind: [:individual, :work, :others] end ↓ class Task < ApplicationRecord enum kind: { individual: 0, work: 1, others: 2 } scope :incoming, -> { where('start_at > ?', Time.zone.now) } end定義したscopeを使って@tasksを再定義
controllerdef index @task = Task.new # モデルに定義したscopeはメソッドのように呼び出せる @tasks = Task.incoming.order(start_at: :asc) end2.コントローラに書かれたロジックをモデルのメソッドで書き直す
コントローラのupdateアクション内の「finishedカラムをtrueにする処理」を、Taskモデルのインスタンスメソッドとしてmodels/task.rbに定義し直す
models/task.rbclass Task < ApplicationRecord enum kind: { individual: 0, work: 1, others: 2 } scope :incoming, -> { where('start_at > ?', Time.zone.now) } # finishedカラムをtrueに更新するメソッドを定義 def update_finished_true self.finished = true if self.finished == false self.save end endcontrollerdef update @task = Task.find(params[:id]) if @task.finished == false @task.finished = true @task.save redirect_to tasks_path else render :index, alert: '既にタスク「#{task.title}」は完了しています ' end end ↓ # 定義しておいたメソッドを呼び出す def update @task = Task.find(params[:id]) @task.update_finished_true redirect_to tasks_path end元々は「finishedがtrueか、そうでないか」で更新を行うかどうかを決定していましたが、「現状finishedがtrueのtaskについて、updateアクションを呼び出す導線がビューにないこと」 「元々値がtrueのカラムにtrueを入れても問題がないこと」を理由に、if文を削除しています。
参考ページ
終わりに
リファクタリングって書き直すの理解しずらいよね。書いてはみたものの色々ぶっ飛んでてわかりずらいので知識が深まったタイミングで書き直したいな。
でも、綺麗にかければ未来の自分がわかりやすい〜ってなるのかな。いや、なってくれないと困る。笑
- 投稿日:2019-12-09T22:26:16+09:00
rails-tutorial第14章
ユーザーをフォローする
まずはRelationshipモデルを作っていこう
$ rails generate model Relationship follower_id:integer followed_id:integer
作成されたマイグレーションファイルにインデックスを書き足していく。
db/migrate/[timestamp]_create_relationships.rbclass CreateRelationships < ActiveRecord::Migration[5.0] def change create_table :relationships do |t| t.integer :follower_id t.integer :followed_id t.timestamps end add_index :relationships, :follower_id add_index :relationships, :followed_id add_index :relationships, [:follower_id, :followed_id], unique: true end end上二つは高速化のためのインデックスである。
最後は一意性を担保するためのインデックス。
これはfollower_idとfollowed_idの組み合わせは一つしかないよーって意味。これで2回同じユーザーをフォローするとかはできなくなる。
UserとRelationshipの関連付け
まずは以下を見てみよう
app/models/relationship.rbclass Relationship < ApplicationRecord belongs_to :follower, class_name: "User" belongs_to :followed, class_name: "User" endbelongs_to :followerとすると、Relationshipのfollower_idとFollowerクラスのidカラムを結びつけますよーという意味になる。
ただ、結びつけたいのはUserクラスのidカラムなので、オプションでclass_name: "User"とすることで、follower_idとUserクラスのidカラムが結びつく。
では、Userモデルのファイルにはどのように書けば良いのだろうか?
app/models/user.rbclass User < ApplicationRecord has_many :microposts, dependent: :destroy has_many :active_relationships, class_name: "Relationship", foreign_key: "follower_id", dependent: :destroy . . . endまず、has_many :active_relationshipsとすると、active_relationshipsクラスを探そうとするが、そんなクラスはないのでオプションでclass_name: "Relationship"と指定している。逆に言えば、active_relationshipsの部分はなんでも良いのである。
次は、foreign_key: "follower_id"の部分だ。
これを指定しないと、UserクラスのidカラムをRelationshipクラスのどのカラムと関連づけるのかがわからなくなってしまう。(デフォルトではuser_idカラムと関連付けようとする)なので、外部キーとして、Userクラスのidカラムと関連づけるカラムを指定してあげている。
これにより、@user.active_relationships
というような参照方法が可能になる。また、@relationship.follower
という参照方法も可能になるってこと。逆に言えば、belongs_toやhas_manyはメソッドを定義するメソッドであるとも言える。
(このケースだと、active_relationshipメソッド、followerメソッド)なので、@user.active_relationships.first.followed
とすると、@userが最初にフォローしたユーザーが返ってくるということになる。ここで全く関係ない余談
&.(ぼっち演算子)はレシーバーであるオブジェクトに対してあるメソッドを実行した時、そのオブジェクトがnilの場合、nilを返すことでエラーを出さなくしています。&.(ぼっち演算子)とはレシーバーであるオブジェクトがnilでなければそのまま結果を返し、nilの場合はnilを返すメソッドなのです。
もっと簡潔に
@user.active_relationships.first.followedだと長いのでどうにかできないか
app/models/user.rbclass User < ApplicationRecord has_many :microposts, dependent: :destroy has_many :active_relationships, class_name: "Relationship", foreign_key: "follower_id", dependent: :destroy has_many :following, through: :active_relationships, source: :followed . . . endこれは、@user.active_relationships.followedを
followingメソッドを定義することにより、
@user.followingというように簡潔にかけるようにする。
(active_relationshipsメソッドを経由してfollowedメソッドを実行するのをfollowingと名付けますよーってこと)relationshipモデルのバリデーションのテストをする
test/models/relationship_test.rbrequire 'test_helper' class RelationshipTest < ActiveSupport::TestCase def setup @relationship = Relationship.new(follower_id: users(:michael).id, followed_id: users(:archer).id) end test "should be valid" do assert @relationship.valid? end test "should require a follower_id" do @relationship.follower_id = nil assert_not @relationship.valid? end test "should require a followed_id" do @relationship.followed_id = nil assert_not @relationship.valid? end end次はバリデーションを設定する
app/models/relationship.rbclass Relationship < ApplicationRecord belongs_to :follower, class_name: "User" belongs_to :followed, class_name: "User" validates :follower_id, presence: true validates :followed_id, presence: true endさらに、生成されたRelationship用のfixtureでは、マイグレーション (リスト 14.1) で制約させた一意性を満たすことができません。ということで、ユーザーのときと同じで (リスト 6.31でfixtureの内容を削除したように)、今の時点では生成されたRelationship用のfixtureファイルも空にしておきましょう
ちなみにテストが全て落ちるというときは、fixtureを見た方が良い。
そもそも、fixtureのサンプルデータが間違っており、dbにそれを伝えたことで全てのテストが落ちるらしい。これでテストは通る。
その他のメソッド定義
次にfollowingなどの便利メソッドをテストする
test/models/user_test.rbrequire 'test_helper' class UserTest < ActiveSupport::TestCase . . . test "should follow and unfollow a user" do michael = users(:michael) archer = users(:archer) assert_not michael.following?(archer) michael.follow(archer) assert michael.following?(archer) michael.unfollow(archer) assert_not michael.following?(archer) end endこの状態では、まだ定義してないメソッドがあるので、それを定義していかないといけない。
app/models/user.rbclass User < ApplicationRecord . . . def feed . . . end # ユーザーをフォローする def follow(other_user) following << other_user end # ユーザーをフォロー解除する def unfollow(other_user) active_relationships.find_by(followed_id: other_user.id).destroy end # 現在のユーザーがフォローしてたらtrueを返す def following?(other_user) following.include?(other_user) end private . . . end少しわかりにくいのは、
def follow(other_user) following << other_user endこれは、そもそもfollow(other_user)メソッドがインスタンスメソッドなので、
self.following << other_user
の省略形だったことがわかる。
また、self.followingはフォローしているユーザーの配列を返すので、<<で問題ない。ただ、どうやってrelationshipインスタンスが生成されるか、dbに保存されるかわからないので、安ラボの動画で書いてあるコードに変更。
def follow(other_user) self.active_relationships.create(followed_id: other_user.id) endまた、
def following?(other_user) following.include?(other_user) endinclude?メソッドは配列の要素に引数が含まれているかを判断してくれるメソッド。
余談:ダックタイピング
改良前
class EmploymentHandler def work(employees) employees.each do |employee| case employee when Staff then employee.work when Manager then employee.work end end end end class Staff def work do_clean_up end def do_clean_up # ... end end class Manager def work do_check end def do_check # ... end endダックタイピングを使うと、
class EmploymentHandler def work(employees) employees.each do |employee| employee.work end end end class Staff def work do_clean_up end def do_clean_up # ... end end class Manager def work do_check end def do_check # ... end endwhen case文がいらなくなったし、workメソッドを持っているクラスであれば、work(employees)メソッドを使えるようになった。
workメソッドを持っているということが、「ガーと鳴けば」に当たるのでは?
フォロワーを考える
これはフォローの時と逆のことをすれば良い
app/models/user.rbclass User < ApplicationRecord has_many :microposts, dependent: :destroy has_many :active_relationships, class_name: "Relationship", foreign_key: "follower_id", dependent: :destroy has_many :passive_relationships, class_name: "Relationship", foreign_key: "followed_id", dependent: :destroy has_many :following, through: :active_relationships, source: :followed has_many :followers, through: :passive_relationships, source: :follower . . . endこれで自分のフォロワーが@user.followersで参照できるようになったので、
テストを書いていこう。test/models/user_test.rbrequire 'test_helper' class UserTest < ActiveSupport::TestCase . . . test "should follow and unfollow a user" do michael = users(:michael) archer = users(:archer) assert_not michael.following?(archer) michael.follow(archer) assert michael.following?(archer) assert archer.followers.include?(michael) michael.unfollow(archer) assert_not michael.following?(archer) end endUIを実装していこう
これでメソッドなどは定義できたので、UIを実装していく。
まずはフォローのサンプルデータを作るために
db/seeds.rb# リレーションシップ users = User.all user = users.first following = users[2..50] followers = users[3..40] following.each { |followed| user.follow(followed) } followers.each { |follower| follower.follow(user) }ちなみに、followingとfollowersはローカル変数?
$ rails db:migrate:reset
$ rails db:seedルーティング設定
次にフォローしてる一覧ページとフォローされてる一覧ページを作るためのルーティングを設定していく。
urlは
/users/1/following や /users/1/followersのような形にしたい。それを踏まえると、
config/routes.rbRails.application.routes.draw do root 'static_pages#home' get '/help', to: 'static_pages#help' get '/about', to: 'static_pages#about' get '/contact', to: 'static_pages#contact' get '/signup', to: 'users#new' get '/login', to: 'sessions#new' post '/login', to: 'sessions#create' delete '/logout', to: 'sessions#destroy' resources :users do member do get :following, :followers end end resources :account_activations, only: [:edit] resources :password_resets, only: [:new, :create, :edit, :update] resources :microposts, only: [:create, :destroy] endmemberメソッドを使うと、
/users/:id/...
この...に何を入れますか?というのを書き足すことができるじゃあ、followingアクションとfollowersアクションはどこに作るの?
Usersコントローラに書けば良い。statsパーシャルを作る。
次にフォロワーなどの統計情報を表示するstatsパーシャルを作る。
app/views/shared/_stats.html.erb<% @user ||= current_user %> <div class="stats"> <a href="<%= following_user_path(@user) %>"> <strong id="following" class="stat"> <%= @user.following.count %> </strong> following </a> <a href="<%= followers_user_path(@user) %>"> <strong id="followers" class="stat"> <%= @user.followers.count %> </strong> followers </a> </div> そしたら、できたパーシャルをhomeページ(ログイン時)と、プロフィールページに挿入しよう。 その前に、なぜ<% @user ||= current_user %>のようなコードになるか見ていこう。 これは、static_pageコントローラのhomeアクションとuserコントローラのshowアクションで定義されているインスタンス変数が異なるからである。 showアクションには@userが定義されているが、static_pageコントローラでは定義されていない。なので、@userがいなければcurrent_userメソッドを呼び出すようにしている。 また、current_userメソッドはapplicationコントローラにmoduleをincludeしているので使える。 では、実際に埋め込んでいこう ```app/views/static_pages/home.html.erb <% if logged_in? %> <div class="row"> <aside class="col-md-4"> <section class="user_info"> <%= render 'shared/user_info' %> </section> <section class="stats"> <%= render 'shared/stats' %> </section> <section class="micropost_form"> <%= render 'shared/micropost_form' %> </section> </aside> <div class="col-md-8"> <h3>Micropost Feed</h3> <%= render 'shared/feed' %> </div> </div> <% else %> . . . <% end %>次にcssを整えてあげる
app/assets/stylesheets/custom.scss. . . /* sidebar */ . . . .gravatar { float: left; margin-right: 10px; } .gravatar_edit { margin-top: 15px; } .stats { overflow: auto; margin-top: 0; padding: 0; a { float: left; padding: 0 10px; border-left: 1px solid $gray-lighter; color: gray; &:first-child { padding-left: 0; border: 0; } &:hover { text-decoration: none; color: blue; } } strong { display: block; } } .user_avatars { overflow: auto; margin-top: 10px; .gravatar { margin: 1px 1px; } a { padding: 0; } } .users.follow { padding: 0; } /* forms */ . . .フォローボタンの設置
これは、自分以外のユーザーのプロフィールページで、フォローしてなかったらフォローボタンが、フォローしたらフォロー解除ボタンが表示されるようにする。
これをdryに書くにはフォローボタンをそのままパーシャルにすると便利。
具体的には、フォローボタン、アンフォローボタン、その二つをif文で表示するfollow_formパーシャルの計3つを作る。
app/views/users/_follow_form.html.erb<% unless current_user?(@user) %> <div id="follow_form"> <% if current_user.following?(@user) %> <%= render 'unfollow' %> <% else %> <%= render 'follow' %> <% end %> </div> <% end %><% unless current_user?(@user) %>
はもし@userが自分と同じなら、フォローボタンを表示しないようにする。次はfollowパーシャルと、unfollowパーシャルを作成する。
app/views/users/_follow.html.erb<%= form_for(current_user.active_relationships.build) do |f| %> <div><%= hidden_field_tag :followed_id, @user.id %></div> <%= f.submit "Follow", class: "btn btn-primary" %> <% end %>app/views/users/_unfollow.html.erb<%= form_for(current_user.active_relationships.find_by(followed_id: @user.id), html: { method: :delete }) do |f| %> <%= f.submit "Unfollow", class: "btn" %> <% end %>unfollowの方は、そのままform_forの引数にインスタンスを渡すと、patchリクエストになってしまうので、html: {method: :delete}としている。
三つのパーシャルができたので、follow_formパーシャルをusersのshowアクションのページに設置しよう。
app/views/users/show.html.erb<% provide(:title, @user.name) %> <div class="row"> <aside class="col-md-4"> <section class="user_info"> <h1> <%= gravatar_for @user %> <%= @user.name %> </h1> </section> <section class="stats"> <%= render 'shared/stats' %> </section> </aside> <div class="col-md-8"> <%= render 'follow_form' if logged_in? %> <% if @user.microposts.any? %> <h3>Microposts (<%= @user.microposts.count %>)</h3> <ol class="microposts"> <%= render @microposts %> </ol> <%= will_paginate @microposts %> <% end %> </div> </div>そしたら、relationshipsコントローラのcreateアクションとdestroyアクションのルーティングを設定しておこう。
config/routes.rbRails.application.routes.draw do root 'static_pages#home' get 'help' => 'static_pages#help' get 'about' => 'static_pages#about' get 'contact' => 'static_pages#contact' get 'signup' => 'users#new' get 'login' => 'sessions#new' post 'login' => 'sessions#create' delete 'logout' => 'sessions#destroy' resources :users do member do get :following, :followers end end resources :account_activations, only: [:edit] resources :password_resets, only: [:new, :create, :edit, :update] resources :microposts, only: [:create, :destroy] resources :relationships, only: [:create, :destroy] endフォロワーやフォローしてる人一覧が出るページを作る。
そしたら、次はrelationshipsコントローラにdestroyアクションを定義していこう。
また、別々のアクションなのに、同じテンプレートを表示するように実装していく。/users/id/followingと/users/id/followersで同じビューを使うってこと。
では、早速作っていこう
TDDで作っていくので、
test/controllers/users_controller_test.rbrequire 'test_helper' class UsersControllerTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) @other_user = users(:archer) end . . . test "should redirect following when not logged in" do get following_user_path(@user) assert_redirected_to login_url end test "should redirect followers when not logged in" do get followers_user_path(@user) assert_redirected_to login_url end endapp/controllers/users_controller.rbclass UsersController < ApplicationController before_action :logged_in_user, only: [:index, :edit, :update, :destroy, :following, :followers] . . . def following @title = "Following" @user = User.find(params[:id]) @users = @user.following.paginate(page: params[:page]) render 'show_follow' end def followers @title = "Followers" @user = User.find(params[:id]) @users = @user.followers.paginate(page: params[:page]) render 'show_follow' end private . . . endまずは、共通の変数をもつ、followingアクションと、followersアクションを定義する。
次に、フォローしているユーザーとフォロワーの両方を表示するshow_followビューを定義する。
注意点としては、パーシャルではなく、usersリソースのビューであるという点だ。これで、異なるアクションから同じビューを呼び出し、内容をごっそり変えるというテクニックができる。
app/views/users/show_follow.html.erb<% provide(:title, @title) %> <div class="row"> <aside class="col-md-4"> <section class="user_info"> <%= gravatar_for @user %> <h1><%= @user.name %></h1> <span><%= link_to "view my profile", @user %></span> <span><b>Microposts:</b> <%= @user.microposts.count %></span> </section> <section class="stats"> <%= render 'shared/stats' %> <% if @users.any? %> <div class="user_avatars"> <% @users.each do |user| %> <%= link_to gravatar_for(user, size: 30), user %> <% end %> </div> <% end %> </section> </aside> <div class="col-md-8"> <h3><%= @title %></h3> <% if @users.any? %> <ul class="users follow"> <%= render @users %> </ul> <%= will_paginate %> <% end %> </div> </div>注目すべきは
<% if @users.any? %> <div class="user_avatars"> <% @users.each do |user| %> <%= link_to gravatar_for(user, size: 30), user %> <% end %> </div> <% end %>これは、もしフォローしているユーザー、自分をフォローしているユーザが1人以上いれば、顔画像の集合体を表示しますよーってこと。
で、その下の、
<% if @users.any? %> <ul class="users follow"> <%= render @users %> </ul> <%= will_paginate %> <% end %>これは、フォローしているユーザー、または自分をフォローしているユーザーがいればその集合体を表示するということ。
render @usersなので、eachメソッドを使って、_userパーシャルが展開される。
また、will_pagenateに関しては、@usersを指定しても良いが、usersのviewでuserリソースを扱うことがデフォルトで設定されているので、別に書かなくても良い。
次に、統合テストを作る。
$ rails generate integration_test following
まずはテスト用のサンプルデータを作っていく
test/fixtures/relationships.ymlone: follower: michael followed: lana two: follower: michael followed: malory three: follower: lana followed: michael four: follower: archer followed: michael作ったサンプルデータを使って統合テストを書いていく
test/integration/following_test.rbrequire 'test_helper' class FollowingTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) log_in_as(@user) end test "following page" do get following_user_path(@user) assert_not @user.following.empty? assert_match @user.following.count.to_s, response.body @user.following.each do |user| assert_select "a[href=?]", user_path(user) end end test "followers page" do get followers_user_path(@user) assert_not @user.followers.empty? assert_match @user.followers.count.to_s, response.body @user.followers.each do |user| assert_select "a[href=?]", user_path(user) end end endこれでテスト通る。
Relationshipsコントローラの実装。
フォローボタンを設置したはいいが、肝心のコントローラがまだなので、それを実装していく。
まずは、コントローラを作成する。
$ rails generate controller Relationships
次にrelationshipsコントローラの基本的なアクセス制御についてテストを書いていこう。
test/controllers/relationships_controller_test.rbrequire 'test_helper' class RelationshipsControllerTest < ActionDispatch::IntegrationTest test "create should require logged-in user" do assert_no_difference 'Relationship.count' do post relationships_path end assert_redirected_to login_url end test "destroy should require logged-in user" do assert_no_difference 'Relationship.count' do delete relationship_path(relationships(:one)) end assert_redirected_to login_url end endこのテストを通るようにするために、各アクションとバリデーションを設定してく
app/controllers/relationships_controller.rbclass RelationshipsController < ApplicationController before_action :logged_in_user def create end def destroy end end次に二つのアクションの中身を実装していく。
app/controllers/relationships_controller.rbclass RelationshipsController < ApplicationController before_action :logged_in_user def create user = User.find(params[:followed_id]) current_user.follow(user) redirect_to user end def destroy user = Relationship.find(params[:id]).followed current_user.unfollow(user) redirect_to user end endまずはcreateアクションから。
hidden_fieldタグで@user.idを格納したfollowed_idを送ったので、
params[:followed_id]と参照することができる。
ここにはプロフィールページの@userのidが入る。次にdestroyアクションを見ていこう。
params[:id]のidはrelationshipのidカラムが入る。
なので、params[:id]をもつrelationshipにfollowedメソッドを実行することにより、
フォローされているユーザーが戻り、userという変数に格納される。correct_userのバリデーションを設定しないのは?
それはcurrent_user.followにある。
もし、仮にいたずらでポストリクエストやデリートリクエストを送ったとする。
だとしても、フォローするのもフォローを解除するのもcurrent_userなので、誰かのアカウントを乗っ取ってフォローしたりフォローを解除しているわけではない。
他人と他人の関係性を勝手に作られるのなら困るけど、別に攻撃者のフォロワーやフォローしてる人が増え用が増えまいが関係ないよって話。一応これでアクションの実装は完了。
発展編Ajaxを使ったフォローボタンの実装
これは、フォローする前とした後で、htmlにそこまで差がないことから、もう一度1から描画しなくても、変わった部分だけJS使って変えられないか?という話。
app/views/users/_follow.html.erb<%= form_for(current_user.active_relationships.build, remote: true) do |f| %> <div><%= hidden_field_tag :followed_id, @user.id %></div> <%= f.submit "Follow", class: "btn btn-primary" %> <% end %>まず、Ajaxを有効にするには、form_forの引数?オプションでremote: trueを有効にする。
そして、フォロー解除フォームでも同じことをする
app/views/users/_unfollow.html.erb<%= form_for(current_user.active_relationships.find_by(followed_id: @user.id), html: { method: :delete }, remote: true) do |f| %> <%= f.submit "Unfollow", class: "btn" %> <% end %>フォームの更新が終わったので、今度はこれに対応するRelationshipsコントローラを改造して、Ajaxリクエストに応答できるようにしましょう。こういったリクエストの種類によって応答を場合分けするときは、respond_toメソッドというメソッドを使います。
app/controllers/relationships_controller.rbclass RelationshipsController < ApplicationController before_action :logged_in_user def create @user = User.find(params[:followed_id]) current_user.follow(@user) respond_to do |format| format.html { redirect_to @user } format.js end end def destroy @user = Relationship.find(params[:id]).followed current_user.unfollow(@user) respond_to do |format| format.html { redirect_to @user } format.js end end endこれ何をやっているかというと、current_user.followでDBへの処理は終わってる。
その時点で、jsを使ってデータを再読み込みしてるらしい。また、ユーザーの中にはjsが無効になっている人もいるので、無効だったらhtmlを使うように以下の処理をしておく
config/application.rbrequire File.expand_path('../boot', __FILE__) . . . module SampleApp class Application < Rails::Application . . . # 認証トークンをremoteフォームに埋め込む config.action_view.embed_authenticity_token_in_remote_forms = true end end次にjsと埋め込みRubyを使ってフォローの関係性を作成する。
app/views/relationships/create.js.erb$("#follow_form").html("<%= escape_javascript(render('users/unfollow')) %>"); $("#followers").html('<%= @user.followers.count %>');app/views/relationships/destroy.js.erb$("#follow_form").html("<%= escape_javascript(render('users/follow')) %>"); $("#followers").html('<%= @user.followers.count %>');ここで何をやってるかというと、id = follow_formの部分を見つけて、以下(html("<%= escape_javascript(render('users/unfollow')) %>");)のhtmlに切り替えますよーっていう意味らしい。
これでAjaxの実装は終了。ただ、そこまで重要ではない。
Ajaxのテストをする
xhrオプションをtrueにすることで対応できるらしい。
test/integration/following_test.rbrequire 'test_helper' class FollowingTest < ActionDispatch::IntegrationTest def setup @user = users(:michael) @other = users(:archer) log_in_as(@user) end . . . test "should follow a user the standard way" do assert_difference '@user.following.count', 1 do post relationships_path, params: { followed_id: @other.id } end end test "should follow a user with Ajax" do assert_difference '@user.following.count', 1 do post relationships_path, xhr: true, params: { followed_id: @other.id } end end test "should unfollow a user the standard way" do @user.follow(@other) relationship = @user.active_relationships.find_by(followed_id: @other.id) assert_difference '@user.following.count', -1 do delete relationship_path(relationship) end end test "should unfollow a user with Ajax" do @user.follow(@other) relationship = @user.active_relationships.find_by(followed_id: @other.id) assert_difference '@user.following.count', -1 do delete relationship_path(relationship), xhr: true end end endこのままでテストは通る。
これでフォローボタン、アンフォローボタンのAjax版が終了
ステータスフィードを完成させよう
具体的には、フォローしてるユーザーの投稿が自分のタイムラインに出るということ。
で、答えがわかったので、まずはテストを書いていこう
test/models/user_test.rbrequire 'test_helper' class UserTest < ActiveSupport::TestCase . . . test "feed should have the right posts" do michael = users(:michael) archer = users(:archer) lana = users(:lana) # フォローしているユーザーの投稿を確認 lana.microposts.each do |post_following| assert michael.feed.include?(post_following) end # 自分自身の投稿を確認 michael.microposts.each do |post_self| assert michael.feed.include?(post_self) end # フォローしていないユーザーの投稿を確認 archer.microposts.each do |post_unfollowed| assert_not michael.feed.include?(post_unfollowed) end end endこの状態ではfeedメソッドがプロトタイプなので落ちてしまう。
さて、これをどうやって実現するか?
current_user.microposts + current_user.following.map { |n| n.microposts }
これでも、自分と自分のフォローしている人の投稿を取得できるけど、発行されるSQL文が非常に多くなってしまう。
これをどうやったら簡潔に、そして1回のSQL文の発行で収めることができるか?
フィードを実装していく。
フィードはタイムラインのことだよ。
これはsqlに関係してて難しい。
app/models/user.rbclass User < ApplicationRecord . . . # パスワード再設定の期限が切れている場合はtrueを返す def password_reset_expired? reset_sent_at < 2.hours.ago end # ユーザーのステータスフィードを返す def feed Micropost.where("user_id IN (?) OR user_id = ?", following_ids, id) end # ユーザーをフォローする def follow(other_user) following << other_user end . . . endこれは、一つ目の?にfollowing_ids
following_idsはhas_manyメソッドを定義すると同時に使えるようになるメソッド。
フォローしているユーザーのidの集合体を引っ張ってくる。2つ目の?にはidが入る。self.idの略かな?
これでテストが通る。
サブセレクト
SQLの発行回数を減らして高速化しようねーって話。
app/models/user.rbclass User < ApplicationRecord . . . # ユーザーのステータスフィードを返す def feed Micropost.where("user_id IN (:following_ids) OR user_id = :user_id", following_ids: following_ids, user_id: id) end . . . end最終的な実装
app/models/user.rbclass User < ApplicationRecord . . . # ユーザーのステータスフィードを返す def feed following_ids = "SELECT followed_id FROM relationships WHERE follower_id = :user_id" Micropost.where("user_id IN (#{following_ids}) OR user_id = :user_id", user_id: id) end . . . endrenderのurl指定について
renderのurlは基本的にはapp/views以下の相対パスを入れる。
例えば、render 'shared/stats'
html.erbは省略してもok便利コマンド
$ rails test
$ git add -A
$ git commit -m "Add user following"
$ git checkout master
$ git merge following-users$ git push
$ git push heroku
$ heroku pg:reset DATABASE
$ heroku run rails db:migrate
$ heroku run rails db:seed
- 投稿日:2019-12-09T22:03:12+09:00
RSpec導入、記述ルール
目的
新しいポートフォリオを作成する際に、RSpecを利用したTDD(テスト駆動開発)
に挑戦するため、RSpec導入します。ついでにRSpecによるテストの簡単な記述ルールについて説明したいと思います。RSpecとは
RSpec とは Ruby プログラマー向けのBDD(Behaviour-Driven Development) ツールです。
BDD(Behaviour-Driven Development)は、
できることは普通のテストと同じで、加えて、
これから作成しようとするプログラムに期待される「振る舞い」や「制約条件」、
つまり「要求仕様」に近い形で、自然言語(英語)を併記しながらテストコードを記述することができるものです。メリットは、テストコードの可読性があがる上、テストコードが要求仕様となりうることと、
要求仕様からテストコードを起こす際も、スムーズにコードに移行しやすいことです。RSpecの導入
まずは RSpec の Gem パッケージをインストールします。
・・・ group :development, :test do gem 'rspec-rails', '~> 3.5' end ・・・ちなみに、RSpec は test フレームワークなのに、なぜインストールグループに development を追加するかというと、RSpec にはテストファイルを作成する generator があり、それを利用するために default の RAILS_ENV である development にインストールしておくと楽だからです。
インストールします。
$ bundle installGem パッケージがインストールされたら、次は Rails ソフトウェアに対して RSpec 用の初期ファイルをインストールする。
$ rails generate rspec:install Running via Spring preloader in process 9045 create .rspec create spec create spec/spec_helper.rb create spec/rails_helper.rbテストファイルの記述ルール
テスト記述ルールとしては、テストする内容を説明する文章を引数として、describe, context, it を使って記述します。
1 つのテスト内容は it で記述し、期待する動作(expect)を 1 つ含めます。require 'rails_helper' RSpec.describe Diff, type: :model do it "is valid with diff string" do diff = Diff.new(diff: "some diff") expect(diff).to be_valid end endit に続く文字列は動詞から始める。
ここまでで 1つのテスト項目の記述ですが、実行すると、なぜitの次が動詞から始まる説明文を記述していたのか理由が分かります。
Diff is valid with diff string Finished in 0.02296 seconds (files took 0.35495 seconds to load) 1 example, 0 failuresこのように、describe, it で記述した内容がテスト結果に表示されるため、実行結果を見れば何をテストしているのかが分かるようになっています。
テストが特定の条件を想定する場合は context を使ってその条件を記述します。
ここで describe, context は入れ替えても動作しますし、以下の例では describe, context は記述せず it だけ記述しても動作しますが、テストが想定する内容が分かりやすくなるので積極的に使いましょう。require 'rails_helper' RSpec.describe Micropost, type: :model do describe "search posts by term" do context "when no post is found" do it "returns an empty collection" do expect(Micropost.search("John Doe")).to be_empty end end end end感想
RSpecの導入は思っていたより簡単でした。
RSpecテストの作成についてまだ知識が浅いため今回は初歩のところだけ説明しました。
知識がついたらテストの作成方法について詳しいブログを投稿します!!
- 投稿日:2019-12-09T20:42:21+09:00
【Rails】Sassファイルから画像を指定する
Railsアプリを作成中、Sassファイルから背景画像でつまづいたのでメモ
前提
sass-railsを使っている。(デフォルトで入っていた)
画像はapp/assets/images/
に保存。指定方法
image-url('<ファイル名>')
で指定app/assets/stylesheets/style.scssh1 { background-image: image-url('logo.jpg') }その他
画像は
public/
に保存する方法もあるらしい。その場合の指定方法は知りません。
CSSで使うようなファイルはapp/assets/images/
に保存して、public/
にはユーザーがアップロードした画像とかを保存する、といった使い分けが一般的なようです。参考
Rails で背景に画像を表示したいのですが取り込み方を教えてください。
Railsで扱う画像はassetsとpublic、どちらに置くといいのでしょうか?
- 投稿日:2019-12-09T19:57:20+09:00
はじめてのRuby on Rails環境構築(Mac)(2019/12/09)
はじめに
こんにちは。
ここでは、これまで主に、JavascriptやTypescriptでフロントを開発していたエンジニアが、ローカルのMacにRuby on Railsの開発環境を作ってみましたのでその手順をまとめてみました。
エンジニアスペック(主な開発経験)
Java
Python
Javascript
Typescript
React
ReactNative
Next
Node
Vue
など。Homebrewのインストール
今回、購入直後の真新しいMacに環境構築を試みたので、まずはHomebrewのインストールから始めます。
https://brew.sh/index_ja$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"おっと、、エラーが出た。↓
$ /usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)" ==> This script will install: /usr/local/bin/brew /usr/local/share/doc/homebrew /usr/local/share/man/man1/brew.1 /usr/local/share/zsh/site-functions/_brew /usr/local/etc/bash_completion.d/brew /usr/local/Homebrew Press RETURN to continue or any other key to abort You have not agreed to the Xcode license. Before running the installer again please agree to the license by opening Xcode.app or running: sudo xcodebuild -licensexcodeのライセンス承認をしろとのこと。
ライセンス承認後、再度上記コマンドを実行します。成功!
Rubyのインストール
現時点でv2.6.5が使えそうなので、こちらを選択します。
$ brew install rbenv ruby-build $ rbenv install --list 2.6.3 2.6.4 2.6.5 2.7.0-dev 2.7.0-preview1 2.7.0-preview2 2.7.0-preview3 $ rbenv install 2.6.5pathを通します。
$ echo 'export PATH="~/.rbenv/shims:/usr/local/bin:$PATH"' >> ~/.bash_profile $ echo 'eval "$(rbenv init -)"' >> ~/.bash_profile $ source ~/.bash_profileglobal環境を汚さないように、特定のディレクトリ下でv2.6.5を使えるようにします。
$ cd ./特定のディレクトリ $ rbenv local 2.6.5bundlerのインストール
次に、bundlerをインストールします。
gem でインストールするのですが、rbenv local
したディレクトリで行います。$ cd rbenv local 2.6.5 を実行したディレクトリ $ gem install bundler $ bundler -v Bundler version 2.0.2それ以外のディレクトリで
bundler -v
すると、rbenv: bundler: command not found The `bundler' command exists in these Ruby versions: 2.6.5のエラーが出ます。
Railsのインストール
まず、workspace 用のディレクトリを作ります。
mkdir workspaceGemfile を生成します。下記コマンドを実行すると
Gemfile
が生成されます。$ bundle initGemfile の下記1行のコメントアウトを削除します。
# frozen_string_literal: true source "https://rubygems.org" git_source(:github) {|repo_name| "https://github.com/#{repo_name}" } gem "rails" ← ここ# railsのインストール $ bundle install --path=vendor/bundle # railsのバージョンチェック $ bundle exec rails -v Rails 6.0.1 # workspace内にプロジェクトを作成 $ bundle exec rails new . # 起動 $ bundle exec rails server => Booting Puma => Rails 6.0.1 application starting in development => Run `rails server --help` for more startup options Puma starting in single mode... * Version 4.3.1 (ruby 2.6.5-p114), codename: Mysterious Traveller * Min threads: 5, max threads: 5 * Environment: development * Listening on tcp://127.0.0.1:3000 * Listening on tcp://[::1]:3000 Use Ctrl-C to stopYay! You’re on Rails!
最後に、
http://localhost:3000/
へアクセスしてページが表示されたら完了です!
ruby のバージョンが2.6.5になってました!
作業時間としてはおおよそ1h程度で、非常に簡単に環境が構築できました!以上でRuby on Railsの開発環境ができましたので、いろいろ遊んでみようかと思います!
- 投稿日:2019-12-09T19:30:41+09:00
Rails6 数値入力時にtypeをnumberにしたのに数値として扱われず困った話
目的
- 勉強の実施時間を記録するwebアプリ作成の際に数値のinputで詰まり、コミュニティの方に助けていただいた話をまとめる
- そもそもセオリーを理解できていなかったので二度とこんなことない様にまとめる。
.to_i
を使って数値に変換できることはなんとなく知識としてあったが定着していなかったのでまとめる。目標
- すでにDBに格納されている数値に入力数値を足してDBに格納する。
- 不動小数点や符号などはとりあえず考えず前述の目標をクリアする。
結論
- 教えていただいた現役エンジニアさんのお言葉「入力値はtype指定してもStringになってしまうので受け取った側で型を指定して変換する」
問題のコード
下記に問題のコードの一部を抜粋する。
コントローラ
def update @post = Post.find_by(id: params[:id]) @post.study_time = @post.study_time + params[:study_time] @post.save redirect_to("/posts/#{@post.id}") endビュー
<%= form_tag("/posts/#{@post.id}/update") do %> <p>今日つみかさねた時間</p> <input type="number" name="study_time"> <input type="submit" value="今日のつみかさね登録"> <% end %>詰まったところまでの概要
前述の問題のコードにて「今日のつみかさね登録」ボタンを押したところ下記のエラーが出た。
no implicit conversion of integer into stringエラーの内容から足そうとしている数値の型があっていないことがわかった。
筆者はビューファイルの
input
でtype="number"
を指定して入力型を数値にしようと試みたが同じエラーが出た。解決しようと試みたがいろいろ試してくうちに混乱してしまった。
解決方法
- コントローラで受け取った値の型を数値に変換すことにより問題は解決した。
正常動作したコード
下記に教えていただいた内容を元に修正を行なったコードを記載する。
コントローラ
def update @post = Post.find_by(id: params[:id]) @post.study_time = @post.study_time + params[:study_time].to_i @post.save redirect_to("/posts/#{@post.id}") endビュー
<%= form_tag("/posts/#{@post.id}/update") do %> <p>今日つみかさねた時間</p> <!-- type="number"だと0~9までの入力しか受け付けられないためおって修正が必要 --> <input type="number" name="study_time"> <input type="submit" value="今日のつみかさね登録"> <% end %>反省
.to_i
で数値に変換できることは知ってはいたが使いどころが理解できてなかった。- そもそもセオリーを理解できていなかった。
- 今考えると諦めなければ自己解決できたかもしれない。
よかったこと
- エラー文をコピペで解決することをしなかった。
- エラー文から問題箇所を特定することができた。
- 投稿日:2019-12-09T19:25:25+09:00
【Rails】エラーメッセージを使い回す
全てのフォームで、エラーメッセージを使いまわせるようにします。
リファクタリング前
以下のような書き方だと、
@task
に対するエラーメッセージしか表示できないので、エラーメッセージ出力部分の記述をパーシャルに分けて使い回すことができません。view<h1>新規タスク</h1> <% if @task.errors.any? %> <div> <ul style="color: red"> <% @task.errors.full_messages.each do |message| %> <li><%= message %></li> <% end %> </ul> </div> <% end %> <%= form_for(@task) do |f| %> . .リファクタリング後
フォームの中に
<%= render 'shared/error_messages', object: f.object %>
と書き込みview<h1>新規タスク</h1> <%= form_for(@task) do |f| %> <%= render 'shared/error_messages', object: f.object %> ・ ・パーシャルで
object.errors
を使ってエラーメッセージを呼び出します。shared/error_messages.html.erb<% if object.errors.any? %> <div> <ul> <% object.errors.full_messages.each do |msg| %> <li><%= msg %></li> <% end %> </ul> </div> <% end %>注意
エラーメッセージが特定のActive Recordオブジェクトに関連付けられている場合のみ使える
→SessionはActive Recordオブジェクトに管理づけられていないので、ログインなどでは使えない
→flashで代用する必要がある
- 投稿日:2019-12-09T19:16:39+09:00
Terraform + Ansible + Rails
はじめに
Railsのアプリケーションを複数管理しているので今後の手間も考え、
何かテンプレ的なものが出来たらいいなと思って作成してみました。![]()
今後の運用において改善点もまだまだあると思ってますのでご意見ご要望はどしどし頂けたらと思います。![]()
今回のゴール
Terraformで構築したEC2インスタンスにAnsibleを使ってRailsアプリケーションをデプロイするまでをゴールとします。
![]()
事前準備
Docker, Terraform, aws-cli, Ansibleのインストールをお願いします。
brew install docker brew install terraform brew install awscli brew install ansibleちなみに私の環境は下記のようになっています。
macOS Mojave 10.14.6
Docker 18.09.2
Compose 1.23.2
Terraform 0.12.3
aws-cli 1.15.50
Ansible 2.6.0テンプレートリポジトリ
DockerでRails環境構築
以前に記事書いたのでこちらを参考に構築してください。
![]()
RubyをインストールせずにDockerでRails環境を構築するここまで完了した方は先ほどのテンプレートリポジトリから
terraform
とansible
フォルダをコピーし、個人のリポジトリを作成してpushしておいてください。TerraformでAWSにインフラ準備
EC2, RDS, LBの構成で進めます。
terraform.tfstate格納用のS3バケットとAWSクレデンシャルは各自で用意してください。
S3バケット名:tf-bucket
AWSクレデンシャルprofile名:tf-profile~/.aws/credentials[tf-profile] aws_access_key_id = XXXXXXXXXXXXXXXXX aws_secret_access_key = YYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYYterraform/security_group/main.tfprovider "aws" { region = "ap-northeast-1" version = "2.12.0" profile = "tf-profile" } terraform { required_version = ">= 0.12" backend "s3" { bucket = "tf-bucket" region = "ap-northeast-1" key = "sample/sg/terraform.tfstate" encrypt = true profile = "tf-profile" } } # EC2に設定するセキュリティグループ resource "aws_security_group" "ec2_security_group" { name = "${var.app_name}-sg" vpc_id = var.vpc_id ingress { from_port = "22" to_port = "22" protocol = "tcp" cidr_blocks = ["0.0.0.0/0"] } ingress { from_port = "80" to_port = "80" protocol = "tcp" security_groups = [aws_security_group.lb_security_group.id] } egress { from_port = "0" to_port = "0" protocol = "-1" cidr_blocks = ["0.0.0.0/0"] } tags = { Name = "${var.app_name}-sg" } } output "ec2_sg_id" { value = aws_security_group.ec2_security_group.id } //...これより後ろの設定はそのまま残しておいてください。terraform/security_group/variable.tfvariable "vpc_id" { default = "your-vpc-id" } variable "app_name" { default = "sample-app" }ファイルの修正が完了したら下記コマンドを順番に実行してみてください。
terraform/security_group/terraform init terraform plan terraform apply最後まで実行出来たらセキュリティグループが作成されているはずです。
output
メソッドで最終的に作成されたセキュリティグループIDを出力していることを覚えて追いてください。これを後ほどEC2作成時に利用します。次にEC2の作成です。
Keypairは事前に作成し、サブネットも各自のサブネットIDを指定してください。
サブネット:subnet-000111222、subnet-333444555
Keypair:tf-keypairterraform/ec2/main.tfprovider "aws" { region = "ap-northeast-1" version = "2.12.0" profile = "tf-profile" } terraform { required_version = ">= 0.12" backend "s3" { bucket = "tf-bucket" region = "ap-northeast-1" key = "sample/ec2/terraform.tfstate" encrypt = true profile = "tf-profile" } } data "terraform_remote_state" "security_group" { backend = "s3" config = { bucket = "tf-bucket" region = "ap-northeast-1" key = "sample/sg/terraform.tfstate" profile = "tf-profile" } } resource "aws_instance" "ec2" { count = var.instance_count ami = var.ami_id instance_type = var.instance_type key_name = var.key_pair subnet_id = lookup(var.subnets, count.index % 2) associate_public_ip_address = "true" vpc_security_group_ids = [data.terraform_remote_state.security_group.outputs.ec2_sg_id] tags = { Name = "${var.app_name}_${count.index + 1}" } } output "instance_ids" { value = { for instance in aws_instance.ec2 : instance.id => instance.private_ip } } output "instance_count" { value = var.instance_count }terraform/ec2/variable.tfvariable "ami_id" { default = "ami-0b898040803850657" } variable "instance_count" { default = 1 } variable "subnets" { default = { "0" = "subnet-000111222" "1" = "subnet-333444555" } } variable "instance_type" { default = "t3.micro" } variable "key_pair" { default = "tf-keypair" } variable "app_name" { default = "sample-app" }ファイルの修正が完了したら下記コマンドを順番に実行してみてください。
terraform/ec2/terraform init terraform plan terraform apply最後まで実行出来たらEC2が作成されているはずです。
作成したKeypairにてssh接続出来るか各自確認をお願いします。RDSとLBの手順は割愛しますが、似たような感じでテンプレとして用意していますのでご活用頂けたらと思います。
![]()
AnsibleでRuby環境構築
以前に記事を書いています。→ AnsibleでRuby環境構築
今回はこれに+αとして実際にGitサーバからソースをCloneし、CapistranoでRailsアプリをデプロイするテンプレまで作成しました。
まずはansibleユーザーがssh接続するための設定をします。
ansible/development--- [server] 先ほどTerraformで作成したEC2のIP [server:vars] ansible_user=ec2-user ansible_ssh_private_key_file=~/.ssh/tf-keypair.pem [server_secret:children] server [development:children] server次にアプリ名やデプロイ先のフォルダなどを指定をします。
ここで先ほどcommitした個人のリポジトリを指定してください。
あと、sshでClone出来るように各自で公開鍵と秘密鍵を作成してGitサーバ側に公開鍵を設定してください。
GitHubでssh接続する手順~公開鍵・秘密鍵の生成からansible/group_vars/server.yml--- ruby_version: 2.6.3 bundler_version: 2.0.2 git_url: git@github.com:hoge.git app_user: sample-app project_name: sample deploy_directory: /opt/{{ project_name }}先ほど作成した秘密鍵やその他暗号化が必要な情報は
ansible-vault
で暗号化してください。
vault_pass_sampleをvault_passへリネームして下記コマンドを実行してください。ansible-vault edit group_vars/server_secret.ymlここにrailsのmaster_keyや先ほど作成した秘密鍵や公開鍵などを設定してください。
ansible/group_vars/server_secret.ymlrails_master_key: xxx ssh_authorized_keys: 'ssh-rsa XXX' ssh_private_key: | -----BEGIN RSA PRIVATE KEY----- XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX ... -----END RSA PRIVATE KEY-----最後にgitブランチやcapistrano実行時のenvを
group_vars/{env}.yml
に設定して、
下記コマンドを実行するとEC2インスタンス環境にRuby環境の構築からGitリポジトリのCloneまで一通り行ってくれます。# 初回のみ実行 ansible-playbook init.yml -i development # 2回目以降デプロイ用 ansible-playbook deploy.yml -i development※nginx.confやlogrotate.confなどもテンプレとして配置してますので各プロジェクトの設定に応じてご利用ください。
Capistrano
参考までにCapistrano設定ファイル群を
capistrano
フォルダに置いてますので興味のある方は覗いてみてください。
- 投稿日:2019-12-09T18:53:37+09:00
link_toメソッドの具体的な仕組み
Scaffoldでプロジェクトを作ると下記のようなリンクがindexなどに生成されるとおもうが、link_toメソッドの中に何があるのかいまいち理解せずに進めてしまっていた。
<% @products.each do |product| %> ----省略ーーー <span><%= link_to 'Show', product %></span> <span><%= link_to 'Edit', edit_admin_product_path(product) %></span> <span><%= link_to 'Destroy', admin_product_path(product), method: :delete, data: { confirm: 'Are you sure?' } %></span>Showは表示される名前であることはわかると思います。
しかしproductってなんだ??
多分eachメソッドのproducなんだろうけどproduct単体でいるのがよくわからない。。。
実はこれ省略されているんです!!
参考<span><%= link_to 'Show', product_path(product) %></span>これが省略せずに書いたものです。productはパラメーターとして渡されているんですね。
下記の流れをちゃんと把握しているとよく理解できると思います!
- 画面からリンク押下してURLとHTTPメソッドがリクエストされる
- それが ターミナルにログとして出る
- コントローラーで想定したアクションが呼び出される
- どのコントローラー、アクションが選択されたかログとして出る
- アクションの途中で値が見たくなれば pやputs でできる
- レスポンスして画面が表示される
- 投稿日:2019-12-09T18:52:18+09:00
Devise を使って Active Directory から情報を取得する方法
はじめに
この記事も Qrunch で書いた記事ですが、技術的なフィードバックがもらえると嬉しいので、Qiita に再掲します。
なお、Qiita 版の前回の記事はこちらです。ここから本文
前回はActive Directory(AD)に登録されたユーザーで、ログインできるところまで実装しました。
認証だけならこれでいいのですが、実際は AD に登録されている氏名やメールアドレスをとってきたりすると思います。
今回はそうした関連情報の取得をやってみたいと思います。なお、今回のリポジトリはこちらです
前提
今回は以下のユーザーを AD 上に作成して、AD に登録されている情報を取得したいと思います。
項目 設定値 属性名 ユーザー名 test_taro sAMAccountName 姓 テスト sn 名 太郎 givenName 電話番号 070-1234-5678 telephoneNumber 役職 係長 title メールアドレス taro-test@example.com 社員番号 00123 employeeID マイグレーションファイルの作成とマイグレート
AD のユーザーでログインできるようになった前回の続きから、という体で書いていきたいと思います。
まずは、それぞれの項目を User テーブルに格納できるように、カラムを追加します。$ bundle exec rails g migration AddColumnToUser last_name:string first_name:string phone_number:string title:string mail:string employee_number:string Running via Spring preloader in process 42345 invoke active_record create db/migrate/2019MMDDhhmmss_add_column_to_user.rbこんな感じのマイグレーションファイルが作成されます。
class AddColumnToUser < ActiveRecord::Migration[5.2] def change add_column :users, :last_name, :string add_column :users, :first_name, :string add_column :users, :phone_number, :string add_column :users, :title, :string add_column :users, :mail, :string add_column :users, :employee_number, :string end endマイグレートします。
$ bundle exec rails db:migrate == 20190926141900 AddColumnToUser: migrating ================================== -- add_column(:users, :last_name, :string) -> 0.0020s -- add_column(:users, :first_name, :string) -> 0.0006s -- add_column(:users, :phone_number, :string) -> 0.0005s -- add_column(:users, :title, :string) -> 0.0004s -- add_column(:users, :mail, :string) -> 0.0003s -- add_column(:users, :emaployee_number, :string) -> 0.0004s == 20190926141900 AddColumnToUser: migrated (0.0049s) =========================各種情報を取得する
各種情報の取得は
User
モデルにコールバックとして実装します。class User < ApplicationRecord before_save :get_last_name, :get_first_name, :get_phone_number, :get_title, :get_mail, :get_emplpyee_number devise :ldap_authenticatable, :rememberable, :trackable private def get_last_name last_name = Devise::LDAP::Adapter.get_ldap_param(username, 'sn') self.last_name = last_name.first.force_encoding('UTF-8') unless last_name.nil? end def get_first_name first_name = Devise::LDAP::Adapter.get_ldap_param(username, 'givenName') self.first_name = first_name.first.force_encoding('UTF-8') unless first_name.nil? end def get_phone_number phone_number = Devise::LDAP::Adapter.get_ldap_param(username, 'telephoneNumber') self.phone_number = phone_number.first.to_s unless phone_number.nil? end def get_title title = Devise::LDAP::Adapter.get_ldap_param(username, 'title') self.title = title.first.force_encoding('UTF-8') unless title.nil? end def get_mail mail = Devise::LDAP::Adapter.get_ldap_param(username, 'mail') self.mail = mail.first.to_s unless mail.nil? end def get_emplpyee_number employee_number = Devise::LDAP::Adapter.get_ldap_param(username, 'employeeID') self.employee_number = employee_number.first.to_s unless employee_number.nil? end end日本語の値を取得する処理についてはこちらの記事を参考にさせていただきました。
ActiveDirectoryの日本語ユーザ名(DisplayName)をRailsのDeviseで扱う
ありがとうございました。一旦この状態で、
bundle exec rails server
をして、Rails アプリを起動します。
起動後にtest_taro
でログインして、正常にログインしたら、Rails のコンソールからレコードを確認します。irb(main):001:0> User.first User Load (1.2ms) SELECT "users".* FROM "users" ORDER BY "users"."id" ASC LIMIT ? [["LIMIT", 1]] => #<User id: 1, username: "test_taro", created_at: "2019-09-26 14:52:03", updated_at: "2019-09-26 15:02:45", last_name: "テスト", first_name: "太郎", phone_number: "070-1234-5678", title: "係長", mail: "taro-test@example.com", employee_number: "00123">きちんと AD に登録した情報が取れていました。
ビューの編集
あとは、ビューで表示してあげるだけですね。
ログイン後にリダイレクトされるhome
画面で表示したいと思います。<h1>StaticPages#home</h1> <p>Find me in app/views/static_pages/home.html.erb</p> <p>Welcome <%= current_user.username %> !</p> <li>ユーザー名: <%= current_user.username %></li> <li>姓: <%= current_user.last_name %></li> <li>名: <%= current_user.first_name %></li> <li>電話番号: <%= current_user.phone_number %></li> <li>役職: <%= current_user.title %></li> <li>メールアドレス: <%= current_user.mail %></li> <li>社員番号: <%= current_user.employee_number %></li> <%= link_to "ログアウト", destroy_user_session_path %>まとめ
AD には様々な情報が登録されていますが、devise_ldap_authenticatable を使って比較的かんたんに情報を取得できると思います。
まだ試せてはいませんが、devise_ldap_authenticatable では、Rails アプリ側でパスワードを変更し、変更したパスワードを AD 側に書き戻すこともできるようなので、こちらにもトライしてみたいと思います。
- 投稿日:2019-12-09T18:38:18+09:00
Devise を使って Active Directory 認証を実装する
はじめに
この記事は Qrunch に投稿した記事です。
普段の業務でも AD 認証を使うことが多いのですが、きちんと Devise の使い方等がわかっていない部分があり、技術的なツッコミをいただけるとありがたいなーと思って、Qiita に再掲しました。ここから本文
社内の認証基盤で、Active Directory(AD) を使っている企業は多くありますが、業務システムの認証基盤として、 社内の AD を使用したいというニーズもあると思います。
今回は、Rails で業務システムを作ることを前提に、Devise を使用して AD 認証を実装してみます。Ruby と Rails のバージョンは↓のとおりです。
$ ruby -v ruby 2.6.1p33 (2019-01-30 revision 66950) [x86_64-linux] $ bundle exec rails -v Rails 5.2.3Gemfile の編集
AD 認証を実装するために、Devise と、Devise LDAP Authenticatable(Devise LDAP) を使用します。
ちなみに、Devise の詳しい解説については、よくわかってないのでこの記事では取り扱いませんのであしからず。gem 'devise' gem 'devise_ldap_authenticatable'編集したら、
bundle install
でインストールします。Devise と Devise LDAP のセットアップ
はじめに、Devise をセットアップし
User
モデルを作成します。
その後で、Devise LDAP のセットアップを行います。bundle exec rails g devise:install ## Devise のインストール bundle exec rails g devise User ## User モデルの作成 bundle exec rails g devise_ldap_authenticatable:install ## Devise LDAP のインストールUser モデルの編集
生成された
User
モデルを編集します。
トラブルシュートなどでの利用を想定して、ログインしたPCのIPアドレスを記録したり、ログイン回数などを記録できる:trackable
を追記しました。app/models/user.rbclass User < ApplicationRecord devise :ldap_authenticatable, :rememberable, :trackable endマイグレーションファイルの修正
修正した
User
モデルに合わせて、マイグレーションファイルを修正します。
また、デフォルトではメールアドレスとパスワードで認証を行うのですが、メールアドレスを持っていなかったり、AD の mail 属性を使っていない場合を考慮して、ログインユーザー名sAMAccountName
でログインできるようにします。db/migrate/XXXXXXXXXX_devise_create_users.rb# frozen_string_literal: true class DeviseCreateUsers < ActiveRecord::Migration[5.2] def change create_table :users do |t| ## LDAP authenticatable t.string :username, null: false, default: "", unique: true ## Database authenticatable # t.string :email, null: false, default: "" # t.string :encrypted_password, null: false, default: "" ## Recoverable # t.string :reset_password_token # t.datetime :reset_password_sent_at ## Rememberable t.datetime :remember_created_at ## Trackable t.integer :sign_in_count, default: 0, null: false t.datetime :current_sign_in_at t.datetime :last_sign_in_at t.string :current_sign_in_ip t.string :last_sign_in_ip ## Confirmable # t.string :confirmation_token # t.datetime :confirmed_at # t.datetime :confirmation_sent_at # t.string :unconfirmed_email # Only if using reconfirmable ## Lockable # t.integer :failed_attempts, default: 0, null: false # Only if lock strategy is :failed_attempts # t.string :unlock_token # Only if unlock strategy is :email or :both # t.datetime :locked_at t.timestamps null: false end add_index :users, :username, unique: true # add_index :users, :email, unique: true # add_index :users, :reset_password_token, unique: true # add_index :users, :confirmation_token, unique: true # add_index :users, :unlock_token, unique: true end endマイグレーションファイルを修正したら、
bundle exec rails db:migrate
でマイグレートします。Devise の初期設定
Devise の初期設定を行います。変更した箇所だけを抜粋します。
今回は、アプリ側でユーザー登録しなくても、AD にユーザーがいればログイン可能なようにしたいと思います。config/initializers/devise.rbconfig.ldap_logger = true # LDAP クエリを Rails のログに記録する。 config.ldap_create_user = true # すべての有効な AD ユーザーがログイン可能になり、自動的にユーザーのレコードが登録される。 config.ldap_update_password = false # パスワード変更を AD 側に書き戻さないようにする。 config.ldap_use_admin_to_bind = true # LDAP認証時のバインドで Administrator を利用する。 config.authentication_keys = [:username] # 認証で利用するキー。今回は、User モデルの username を利用する。 config.case_insensitive_keys = [:username] # キーの大文字小文字を区別しない config.strip_whitespace_keys = [:username] # キーに含まれる空白を削除するLDAP の設定
AD の接続情報などを設定します。
config/ldap.ymldevelopment: host: <ドメインコントローラ名> port: 389 attribute: sAMAccountName base: CN=Users,DC=example,DC=local admin_user: CN=Administrator,CN=Users,DC=example,DC=local admin_password: <admin_user のパスワード> ssl: falseView の作成
Devise の View を作成します。今回は、ユーザー登録やメールでの登録確認等は行わないので、ログイン画面だけを作成します。
config/initializers/devise.rb
でログイン時に自動的にアカウントが作成されるようにしたので、AD にアカウントさえあれば、ユーザー登録は特にいらないかもしれません。
ここはアプリの仕様に左右される部分だと思います。bundle exec rails generate devise:views users -v sessionsView の修正
作成したログイン画面を修正します。
デフォルトではメールアドレスの入力欄になっているので、これを Windows アカウント名の入力欄に変更します。app/views/users/sessions/new.html.erb<h2>Log in</h2> <%= form_for(resource, as: resource_name, url: session_path(resource_name)) do |f| %> <div class="field"> <%= f.label :username, "Windows アカウント名" %><br /> <%= f.text_field :username, autofocus: true, autocomplete: "username" %> </div> <div class="field"> <%= f.label :password %><br /> <%= f.password_field :password, autocomplete: "current-password" %> </div> <% if devise_mapping.rememberable? %> <div class="field"> <%= f.check_box :remember_me %> <%= f.label :remember_me %> </div> <% end %> <div class="actions"> <%= f.submit "Log in" %> </div> <% end %> <%= render "users/shared/links" %>Controller の生成
次にコントローラを作成します。
ログイン周りのハンドリングができれば良いので、Sessions のコントローラのみを生成します。bundle exec rails generate devise:controllers users -c=sessionsController の修正
生成したコントローラはすべてコメントアウトされているので、適宜コメントを外します。
app/controllers/users/sessions_controller.rb# frozen_string_literal: true class Users::SessionsController < Devise::SessionsController before_action :configure_sign_in_params, only: [:create] # GET /resource/sign_in def new super end # POST /resource/sign_in def create super end # DELETE /resource/sign_out def destroy super end protected # If you have extra params to permit, append them to the sanitizer. def configure_sign_in_params devise_parameter_sanitizer.permit(:sign_in, keys: [:username]) end endログイン後のページを作成
今の状態だと、ログイン画面はありますがログイン後に表示するページがありません。
静的なページhome
を作って、ログイン後はhome
に遷移するようにします。bundle exec rails g controller StaticPages home
デフォルトで生成されるページだとあまり面白くないので、
username
を表示させたいと思います。
また、どこからもログアウトできないと困るので、ログアウトのリンクを追記します。app/views/static_pages/home.html.erb<h1>StaticPages#home</h1> <p>Find me in app/views/static_pages/home.html.erb</p> <p>Welcome <%= current_user.username %> !</p> <%= link_to "ログアウト", destroy_user_session_path %>routes の編集
コントローラを作成した際にいくつか自動で追加されているルートもありますが、今回のアプリではログイン画面を URL の ルートにしたいと思います。
こんな感じで routes.rb を編集します。config/routes.rbRails.application.routes.draw do get 'home', to: 'static_pages#home' devise_for :users, controllers: { sessions: 'users/sessions' } devise_scope :user do root to: 'users/sessions#new' get 'sign_in', to: 'users/sessions#new' get '/users/sign_out', to: 'users/sessions#destroy' end endデフォルトの動作では、ログインしたあとに URL のルートに遷移するため、ログイン画面が無限ループします。
それを回避するために、application_controller.rb
に、after_sign_in_path_for
メソッドを追記してhome
に遷移させます。app/controllers/application_controller.rbclass ApplicationController < ActionController::Base rescue_from DeviseLdapAuthenticatable::LdapException do |exception| render :text => exception, :status => 500 end def after_sign_in_path_for(resource) home_path end endログインしてみる
ここまでできたら
bundle exec rails s
でサーバーを起動して、実際にログインしてみましょう。
localhost:3000
にアクセスしたら、こんなログイン画面が表示されるはずです。
AD に登録されているユーザー名とパスワードを入力してログインすると、
アプリ側で特にユーザー登録をしていませんが、ADで認証されたユーザーが Rails のデータベースに登録されているので、無事にログインできました。
最後に
ところで、ひとつバグがあります。
今の状態だとログインしていなくてもhome
にアクセスできてしまいます。
そこで、home
は認証必須にしたいと思います。
before_action
フィルタで、認証済みのユーザーのみがhome
にアクセスできるようにします。app/controllers/static_pages_controller.rbclass StaticPagesController < ApplicationController before_action :authenticate_user! def home end endこれで、ダイレクトに/homeにアクセスしようとすると、ログイン画面にリダイレクトされるようになりました。
今回作成したアプリのリポジトリはGithubに登録しています。
ad_auth
- 投稿日:2019-12-09T17:02:15+09:00
railsでのデータの保存、更新、削除の方法 Create,Update,Delete
はじめに
今回はrailsでのデータの新規作成、更新、削除の方法をまとめていきます。
データの新規保存
newメソッド+saveメソッドかcreateメソッドでレコードを新規作成します。
newメソッド+saveメソッドの例
#モデル名.new(カラム名: データ)+モデル名.saveのセットで保存されます。 #saveメソッド忘れに注意ください。これがないと保存されません。 #以下が例です。 post = Post.new(name: "記事1", title: "hogehoge") post.savecreateメソッドの例
#モデル名.create(カラム名: データ)で保存されます。 #createメソッドはnewとsaveメソッドを一度に実行できます。 #以下が例です。 Post.create(name: "記事1", title: "hogehoge")データの更新
saveメソッドかupdateメソッドでレコードの更新を行います。
自身はupdateメソッドをよく使います。updateメソッドの例
#モデル名.update(カラム名: データ)で保存されます。 #update前にfindでupdateをするデータを取得します。 #以下が例です。 post= Post.find(1) post.update(name: "記事2", title: "higehige")saveメソッドの例
#モデルの各項目に値を入れていき、最後にsvaeメソッドで更新をかけます。 #以下が例です。 post= Post.find(1) post.name = "記事2" post.title = "higehige" post.saveデータの削除
destroyメソッドかdeleteメソッドを使いデータを削除します。
destroyメソッドの例(モデルクラスでdependent: :destroyを指定して関連付けたデータも自動的に削除されます)
#モデル名.find(削除したいid).destroy(カラム名: データ)でデータを削除します。 #以下が例です Post.find(1).destroy #ちなみにレコード全消去したい場合は以下になります。 Post.destroy_alldeleteメソッドの例(関連するデータは削除されません)
#モデル名.find(削除したいid).delete(カラム名: データ)でデータを削除します。 #以下が例です Post.find(1).delete #ちなみにレコード全消去したい場合は以下になります。 Post.delete_allおわりに
基本的なrailsの新規作成、更新、削除について紹介いたしました。
データ新規登録時のsaveメソッドを忘れてはまらないように注意ください!!
- 投稿日:2019-12-09T16:22:26+09:00
Rails scaffoldで3回のコピペで快適に投稿モッグを作成
Rails scaffoldで3回のコピペで快適に投稿モッグを作成
rails new
からbundle install
までを一発コマンドで実行!$ rails _5.2.3_ new sample-app -d postgresql && cd sample-app && bundle install --path vendor/bundle/config/application.rbmodule SampleApp class Application < Rails::Application # ここから下を追加 config.generators do |g| g.javascripts false g.helper false g.test_framework false end end end
scaffold
からrails s
までを一発コマンドで実行!$ rails g scaffold post title:string details:string && rails db:create && rails db:migrate && rails s終了です。
- 投稿日:2019-12-09T14:13:40+09:00
Railsコンソールで日本語入力ができない現象【Docker】
はじめに
Railsの参考書を読みながら勉強を進めているときに、
DockerのRailsコンテナ内で、Railsコンソールを起動し、日本語文章を入力しようとしたら、
入力はできるのに、エンターキーを押すとターミナルに表示されないという現象が起きたので、その解決方法についてです。
解決方法
DockerFileに、
ENV LANG C.UTF-8
を記入して
docker-compose up --build
で再起動これでコンソールに日本語を入力できるようになります。
参考にしたページ:Docker / rails console で日本語入力できない問題
https://gist.github.com/tasiyo7333/2163a09129ed36639645145a0146d8d3
- 投稿日:2019-12-09T12:21:16+09:00
2億件の本番データを無事に変換した話
この記事はクラウドワークス Advent Calendar 2019 の11日目の記事です。
昨日は@lemtosh469による「アジャイル開発のスケーリングの方法」でした。
はじめに
みなさんこんにちは、クラウドワークスでサーバーサイドエンジニアをしている@shimopataです。普段はRailsを中心に機能開発したり改修作業とかに勤しんでいます。
先日、大量のデータ変換を伴うテーブルの差し替え作業をしました。データ量も多く、大変な作業でしたが計画、検証からやらせてもらい貴重な経験だったので、公開できる範囲で共有させて頂こうと思います。やりたいこと
あるテーブルに保存されているデータの形式を変換し保存し直す。
対象は以下のテーブル
テーブル名 レコード数 テーブルA 65,000件 テーブルB 4,300,000件 テーブルC 200,000,000件 2億件もレコードあるのかー
やったこと
調査と方針決め
まずはじめに、対象のテーブルがどのように使用されているのかについて調べました。一度登録されたデータがユーザの操作等によって更新されるかどうかで、対応方針が大きく異なると思い、調査し始めました。
結果、一度登録されたデータはユーザーによって、変更・削除されないことが分かり、以下の計画で変換することにしました。【計画】
1. 変換後の値を保存するテーブルを作成
2. データを変換するスクリプトを実行し、1で作成したテーブルに保存
3. テーブル名をリネームして差し替える
4. 不要なモデル、テーブルを削除メリットとしては次の通り、安心安全にデータの変換が行えるのでこの方式を採用しました。
- 書き込みを行うテーブルはユーザが直接触るテーブルではないため、書き込み時にテーブルをロックしてもユーザ影響はなく、日中帯に作業ができる。
- 変換前のデータがテーブルに残るので何かあっても、そこからリカバリーができる。
スクリプトの作成について
データ件数が多く、処理時間が長くなってしまうことから、少しでも短くするために以下の対応を行いました。
find_in_batches
を使用して、変換速度の底上げ繰り返し処理をする場合、データ数が少なければ、
each
などを事足りるのですが、今回は、データ数が多いのでfind_in_batchesを使用しました。find_eachとの違いは
- find_eachはデフォルトで1000個単位(引数batch_size:で指定可能)でデータを取得し、一件ずつをeachで処理していく
- find_in_batchesはデフォルトで1000個単位(引数batch_size:で指定可能)でデータを取得し,1配列毎に処理していく
どのくらい違いが出るかを手元にあるテストデータで試してみます。
find_eachBenchmark.realtime do User.find_each(batch_size: 1000) { |user| p user } end -------以下、実行結果----------- <User id: 1, created_at: "2019-12-15 00:00:00", updated_at: "2019-12-15 00:00:00"> <User id: 2, created_at: "2019-12-15 00:00:00", updated_at: "2019-12-15 00:00:00"> ... <User id: 2000, created_at: "2019-12-15 00:00:00", updated_at: "2019-12-15 00:00:00"> => 1.006721000012476find_in_batchesBenchmark.realtime do User.find_in_batches(batch_size: 1000) { |user| p user } end -------以下、実行結果----------- [<User id: 1, created_at: "2019-12-15 00:00:00", updated_at: "2019-12-15 00:00:00">, ・・・ <User id: 1000, created_at: "2019-12-15 00:00:00", updated_at: "2019-12-15 00:00:00">] [<User id: 1001, created_at: "2019-12-15 00:00:00", updated_at: "2019-12-15 00:00:00">, ・・・ <User id: 2000, created_at: "2019-12-15 00:00:00", updated_at: "2019-12-15 00:00:00">] => 0.41866779996780682000件程度のデータで約二倍の速度の違いが出ていますね。対象となるデータはこの1000000倍なので、
find_in_batches
を使うしかないですね。また、バッチサイズのサイジングもこのスクリプトのキーポイントでした。この数値を大きくする事で一度に処理できるデータの数を増やすことができ、その分、速度が早くなるのですが、当然、メモリを多く使用してしまいます。安全策をとるのであれば、夜に少しづつ変換を進めていくこともできたのですが、作業締め切りもあるため日中帯にスクリプトを流す必要がありました。
速度を出すために、バッチサイズを大きくしたい、でも、サービスに負荷をかけれない。。。
そのため、検証環境に大量のダミーデータを用意し、検証を行いました。バッチサイズの大きさを変更し実行しては、AWSコンソールメトリクスを確認を繰り返し、最適値を探しました。速度面、リソースの安全性からバッチサイズは5000としました。
activerecord-importを使用して書き込み速度の底上げ
activerecord-importというgemを使用しました。これは、一括でINSERT処理やUPDATE処理を行えるようにするgemで、スクリプト実行時間の大幅な短縮をすることができました。
使い方としては次のような感じです。sample.rbbooks = [] 10.times do |i| books << Book.new(name: "book #{i}") end Book.import booksmodelに
import
というメソッドが追加されるので、引数に取り込みたいデータを入れてあげるだけで、簡単にINSERT処理が行えます。また、オプションもたくさんあるため(特定のカラムだけ取り込む、取り込み時にバリデーションを張るなど)、興味があったら、gemのREADMEをみてみてください。こちらも、どのくらい違いが出るかを手元にあるテストデータで試してみます。
sample_bulk_import.rbusers = [] Benchmark.realtime do 1000.times do |i| users << TestUser.new(id: i,name: "test") end TestUser.import users end ------------以下実行結果----------------- 0.1599934000405483sample_import.rbBenchmark.realtime do 1000.times do |i| TestUser.create!(name: "test") end end ------------以下実行結果----------------- 5.3943878000136465圧倒的に早そうですね。
変換スクリプトの実施
事前検証も終え、負荷なども問題ないことを確認した上で、本番サーバーに対して変換作業を行いました。いくら使用していないテーブルとはいえ、本番環境に対する変更のため、二週の間、対象となるデータを約1000万件単位に分割し勤務時間帯にのみスクリプトを実行していたのですが、とにかくデータ量が多く、時間がかかりました。リソースの監視はAWSのマネジメントコンソールの情報を中心に確認していました。
いつ、どのような不具合が発生するのか分からないという状況で長時間作業していくのはなかなか体力と精神を削られる作業で大変でした。
テーブルの差し替え
変換作業が完了後、テーブル名を変換するmigrationを作成し、テーブルの差し替えを行いました。
xxxx_rename_table.rbdef up rename_table :target_table, :backup_target_table rename_table :converted_table, :target_table end作業を初めて見ると、なかなかmigrationが終わらず、原因を探るために、migration実行中に発行しているクエリを確認しました。
次のクエリを実行して見てみると、インデックスの再作成で時間かかっている事が分かりました。
SELECT * FROM information_schema.PROCESSLIST WHERE Time >= 10 AND command = 'Query' ORDER BY Time DESC;
クエリ自体は問題なく発行されているので、ただ待つしかなく、結果として、作業時間を超えてしまったのですが、大量のデータ取り扱ってく上での学びになりました。終わりに
テーブルの差し替え作業のタイミングでチームを移ってしまい、先輩エンジニアに作業を引き継ぎました。この作業のタイミングで、tableBのデータについて、特定の条件下で登録したデータがユーザによって削除であることが分かりました。1が、なんとかこれも対応してもらい、ほんと、ありがとうございました。
データ変換をやり始めてからでないと分からないリスクとして、
- バリデーションなどの制約から外れているデータ
- 過去に何らかの理由で手動でデータ修正をして、想定していない形で入っているデータ
などを想定していたのですが、実際にやってみると意外に該当するものは少なく、よかったです。
振り返ってみるとやっていること自体は単純だったりするのですが、やっていた当時は「本当に大丈夫なんだろうか、問題ないのだろうか」とリスクがないことを証明する悪魔の証明をずっとしながら、作業工程を組み立てていたので、非常に不安になりながら作業を進めていました。
そんな状況の中でも、相談に乗ってくれたり、作業に協力してくれたエンジニアに支えられてなんとかゴールまで行けたのかなと思います。ゴールに辿り着くまでに色々失敗をしたりしたのですが、学びとして得られるものも大きく、今後に生かしていきたいと思います。
最初の調査の時に気づけなかったのは反省です。。。 ↩
- 投稿日:2019-12-09T11:28:38+09:00
VMWareの共有フォルダ上でRailsプロジェクトを作成しようとしてハマった
問題
新しいrailsプロジェクトを作ろうとして、
rails new
したら下記のようなエラーが出た。Gem::Ext::BuildError: ERROR: Failed to build gem native extension. current directory: /mnt/hgfs/Dev/rails_project/vendor/bundle/ruby/2.6.0/gems/bootsnap-1.4.5/ext/bootsnap /home/n/.rbenv/versions/2.6.5/bin/ruby -I /home/n/.rbenv/versions/2.6.5/lib/ruby/site_ruby/2.6.0 -r ./siteconf20191209-2795-4ulozw.rb extconf.rb creating Makefile current directory: /mnt/hgfs/Dev/rails_project/vendor/bundle/ruby/2.6.0/gems/bootsnap-1.4.5/ext/bootsnap make "DESTDIR=" clean current directory: /mnt/hgfs/Dev/rails_project/vendor/bundle/ruby/2.6.0/gems/bootsnap-1.4.5/ext/bootsnap make "DESTDIR=" compiling bootsnap.c bootsnap.c: In function ‘get_ruby_platform’: bootsnap.c:240:3: warning: ISO C90 forbids mixed declarations and code [-Wdeclaration-after-statement] struct utsname utsname; ^~~~~~ bootsnap.c: In function ‘bs_cache_path’: bootsnap.c:266:39: warning: format ‘%llx’ expects argument of type ‘long long unsigned int’, but argument 5 has type ‘uint64_t {aka long unsigned int}’ [-Wformat=] sprintf(*cache_path, "%s/%02x/%014llx", cachedir, first_byte, remainder); ~~~~~~^ %014lx bootsnap.c: In function ‘bs_rb_fetch’: bootsnap.c:307:3: warning: ISO C90 forbids mixed declarations and code [-Wdeclaration-after-statement] char * cachedir = RSTRING_PTR(cachedir_v); ^~~~ bootsnap.c:653:13: warning: ‘output_data’ may be used uninitialized in this function [-Wmaybe-uninitialized] else if (!NIL_P(output_data)) goto succeed; /* fast-path, goal */ ^ bootsnap.c:624:9: note: ‘output_data’ was declared here VALUE output_data; /* return data, e.g. ruby hash or loaded iseq */ ^~~~~~~~~~~ bootsnap.c: At top level: cc1: warning: unrecognized command line option ‘-Wno-self-assign’ cc1: warning: unrecognized command line option ‘-Wno-parentheses-equality’ cc1: warning: unrecognized command line option ‘-Wno-constant-logical-operand’ cc1: warning: unrecognized command line option ‘-Wno-cast-function-type’ linking shared-object bootsnap/bootsnap.so current directory: /mnt/hgfs/Dev/rails_project/vendor/bundle/ruby/2.6.0/gems/bootsnap-1.4.5/ext/bootsnap make "DESTDIR=" install /usr/bin/install -c -m 0755 bootsnap.so ./.gem.20191209-2795-1foym7j/bootsnap No such file or directory @ rb_file_s_rename - (./.gem.20191209-2795-1foym7j/bootsnap, /mnt/hgfs/Dev/rails_project/vendor/bundle/ruby/2.6.0/extensions/x86_64-linux/2.6.0/bootsnap-1.4.5/bootsnap) Gem files will remain installed in /mnt/hgfs/Dev/rails_project/vendor/bundle/ruby/2.6.0/gems/bootsnap-1.4.5 for inspection. Results logged to /mnt/hgfs/Dev/rails_project/vendor/bundle/ruby/2.6.0/extensions/x86_64-linux/2.6.0/bootsnap-1.4.5/gem_make.out An error occurred while installing bootsnap (1.4.5), and Bundler cannot continue. Make sure that `gem install bootsnap -v '1.4.5' --source 'https://rubygems.org/'` succeeds before bundling.どうやらoutputファイルの書き込みに失敗しているらしい。
環境
- Windows 10 (ホストOS)
- Ubuntu 18 LTS (ゲストOS)
- VMWare Workstation 15
- Rails 6
原因
VMWareの共有フォルダ機能を使用して、ホストOSのフォルダをゲストOSに共有して、この上で開発をしようとしていたのだがこれが良くなかった。
共有フォルダのパーミッションを確認すると、所有権が全てrootになっており、変更ができない。
このために書き込みに失敗したらしい。/mnt/hgfs/Dev/rail_project $ ls -la 合計 37 drwxrwxrwx 1 root root 4096 12月 9 11:13 . drwxrwxrwx 1 root root 4096 12月 9 11:10 .. drwxrwxrwx 1 root root 0 12月 9 11:10 .bundle drwxrwxrwx 1 root root 4096 12月 9 11:13 .git -rwxrwxrwx 1 root root 681 12月 9 11:13 .gitignore -rwxrwxrwx 1 root root 6 12月 9 11:13 .ruby-version -rwxrwxrwx 1 root root 1962 12月 9 11:13 Gemfile -rwxrwxrwx 1 root root 3337 12月 9 11:12 Gemfile.lock -rwxrwxrwx 1 root root 374 12月 9 11:13 README.md -rwxrwxrwx 1 root root 227 12月 9 11:13 Rakefile drwxrwxrwx 1 root root 4096 12月 9 11:13 app drwxrwxrwx 1 root root 0 12月 9 11:13 bin drwxrwxrwx 1 root root 4096 12月 9 11:13 config -rwxrwxrwx 1 root root 130 12月 9 11:13 config.ru drwxrwxrwx 1 root root 0 12月 9 11:13 db drwxrwxrwx 1 root root 0 12月 9 11:13 lib drwxrwxrwx 1 root root 0 12月 9 11:13 log -rwxrwxrwx 1 root root 222 12月 9 11:13 package.json drwxrwxrwx 1 root root 4096 12月 9 11:13 public drwxrwxrwx 1 root root 0 12月 9 11:13 storage drwxrwxrwx 1 root root 4096 12月 9 11:13 test drwxrwxrwx 1 root root 0 12月 9 11:13 tmp drwxrwxrwx 1 root root 0 12月 9 11:13 vendor解決
共有フォルダ機能を使うのを止めて、ゲストOS上のディレクトリをsambaでホストOSと共有することにしました。
なおゲストOSとホストOSでsambaを使うためには、VMWareのネットワークアダプタの設定をブリッジ接続にする必要があります。
- 投稿日:2019-12-09T11:16:35+09:00
rails s で(http://localhost:3000)が立ち上がらない解決方法
新規アプリ作成に入り、 $ rails new のあと
.....$ rails s にて localhost:3000 が立ち上がらない場合に解決した方法。
ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
結論:初心者のため、 「rails s 立ち上がらない 」等の検索を先に行ってみたが、
結局の解決方法は(実行するべきコマンドすらもが)書いてある,という事で、今後の勉強になりました。ーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーーー
............Webpacker configuration file not found /Users/..../config/webpacker.yml. Please run rails webpacker:install Error: No such file or directory @ rb_sysopen - .../config/webpacker.yml (RuntimeError)エラー文の中に、webpackerファイルが無い、インストールするように出た為、
$ rails webpacker:install
Yarn not installed. Please download and install Yarn from https://yarnpkg.com/lang/en/docs/install/すると、今度は https://yarnpkg.com/lang/en/docs/install/
からダウンロードするように出る。
$ brew install yarn で出来るとサイト内にある為、コマンド実行。You should change the ownership of these directories to your user.sudo chown -R $(whoami) /usr/local/share/man/man5 /usr/local/share/man/man7すると今度は、権限を変えるように出た為、書いてある通りにコマンド実行。
($ ls -lで 現在の確認を一応行い)$ sudo chown -R $(whoami) /usr/local/share/man/man5 /usr/local/share/man/man7
(パスワード:入力成功)
$ rails s 成功
無事にいつもの画面が出た。
- 投稿日:2019-12-09T09:28:45+09:00
【Rails】modelやhelperに無駄なmethodを増やさないpresenter
Presenter
という層を導入してみて、良かったなあってこと、書こうと思います?
正直 n番煎じで、どこにでもある記事なのですが、Presenter
を使う時のルールから考えれば、感じたメリットがやや違うなあと思ったので自分なりに書きます?
(実は、導入したのは弊社の別のエンジニアなのですが、もういないので勝手にいただきます?)こんなRailsの開発を回避したい!
そもそも、どういう開発で
Presenter
が使われるのか?
僕なりに、問題提起していこうかと思います。Viewのロジックをどんどんモデルに追加してしまう
厳密には以下のようなことはありえないですが、
それview
以外じゃ使わないでしょう的なことを察してほしいです。?class User < ApplicationRecord def self.name_array(users) users.map { |e| e.name } end end以下の通りですが、viewのロジックばっかりのモデルは少し嫌ですね。
モデルとは、データベースや外部サービスへのアクセスをはじめ、ビジネスロジック全般を担当するコンポーネントのことです。
出典 : モデルの基本controllerのアクションmethod の外でインスタンス変数を作ってしまう
別に問題はないけど「え、こんなインスタンス作ったっけ?」ってなります。
(気づかずに同じインスタンスを作ってしまうとかも嫌ですね。)class UsersController < ApplicationController before_action :set_greate_users def index @target_users = current_user.target_users end private def set_greate_users @great_users = current_user.great_users end end以下の記事でも、謎の場所でインスタンスを定義するな!と言ってる過激派もいます。
Controller Best Practices: Don’t Hide Instance Variables神helperにしてしまう
そもそもviewのロジックはhelperに追加しようというのが基本的だと思うのですが、危険性を書きます。
- helperは、どこでも読み込まれるので、メソッド名が衝突する可能性がある
- helperの修正を行うと、意図しない箇所も変更されてしまう
- 色々なhelperから呼び出されると、ロジックを追うのだけで大変になる
module UserHelper def great_users current_user.great_users end endほぼ以下の記事の受け売りなのは秘密です。
Decorator と Presenter を使い分けて、 Rails を ViewModel ですっきりさせようなぜそんな開発になったの?
僕も開発していて、ハッとしたので、書き残します。
rubocopのAbcSizeが怒るから
ありがちですね。
そもそもアクションmethodを起点に何かが起こるので、アクションmethod内で色々したくなります。
でも、ロジックが多く書けない以上、インスタンス変数から呼び出せるようにモデルにロジックを追加するしかない気持ちもわかります。Metrics/AbcSize: Assignment Branch Condition size for show is too high. [16.31/15]
AbsSize
を追加されたプルリク
- New cop Metrics/AbcSize
AbcSize
のデフォルトを考え直そうという提案がされたプルリク
- Relax metrics cops threshold
↓ちゃんと明文化することが大事って書いてありました。I think that at this point is way more important to document some of the defaults and why they were picked than relaxing them. Not to mention that default values like 18 and 14 seem extremely random.
rails_best_practicesに言われたから
rails_best_practices
がアクションmethod内部で変数に対して4回以上methodを呼び出すと、以下のようなエラーが出力されます。
2~3個のインスタンスを作成していたら、確かに出がちなエラーだと思います。/Users/.../app/controllers/users_controller.rb:5 - move model logic into model (current_user use_count > 4)Presenterを導入しよう
ここからは n番煎じ、presenterの導入方法なので、どの記事を読んでも同じです。
他の記事読んでも大差ないです。Presenterを使う時のマイルール
何でも
Presenter
を使えってことではありません。
僕なりの使い方を書きます。
- インスタンス変数が4個以上になるなら
Presenter
*4という数字に意図はありません、ノリです。- どこでも使う
view
のロジック(URLの置換)などはhelper
を使うDecorator
という考え方は入れない(混乱するので)Presenterのコード
弊社では
Presenter
にいくつもの引数を渡せるように、BasePresenter
を作っています。app/presenters/base_presenter.rbrequire 'delegate' class BasePresenter < SimpleDelegator attr_reader :resource, :args def initialize(resource, **args) @resource = resource @args = args super(resource) end endクラス名は、どのコントローラーのどのアクションmethodなのかわかりやすいようにしています
app/presenters/users/index_presenter.rbmodule Users class IndexPresenter < BasePresenter # arg[:taregt_id]で、第2引数のhashの中身を呼び出せます # ↓ロジックを書く def target_name User.find(arg[:target_id]).name end alias_method :current_user, :resource # ↑resourceでも呼び出せますが、current_userで呼び出せるようにしています end endcontrollerで呼び出すだけです
class UsersController < ApplicationController def index @presenter = Users::IndexPresenter.new(current_user, target_id: params[:id]) end endあとは
@presenter.target_name
で呼び出せます!DecoratorとPresenterの違いって?
僕もよくわかりません?
あえて載せませんが、rails presenter
で検索してヒットする記事は全てDecorator/Presenter
の解釈が違いました。
弊社では Decorator/Presenter の記事に書いてあった Exhibit vs Presenter が一番近いなと思いました。Decorator パターンは定義上、1個のオブジェクトしか内包しないので、「Presenter」のように2個のオブジェクトに委譲を行うような設計は厳密には Decorator パターンではありません。
弊社では
Presenter
にcurrent_user
という基本的なオブジェクトと、その他のhashを渡しています。
内部で複数のインスタンス変数が定義されたりするので、ちょっと特殊な使い方です。所感
そういえば、
Presenter
を導入した記事を読むと、昔の人はrails
でhtml
を返却していたみたいですね。
弊社はrails
はjson
を返却するAPIで、Vue
でフロントを制御しています。
昔の人はどんな開発をしていたんだろうか、、、あまり想像できていない??
- 投稿日:2019-12-09T08:55:21+09:00
docker + #rails 環境の bundle install で `No such file or directory (needed by /bundle/gems/mysql2-x.x.x/lib/mysql2/mysql2.so`
bootsnapが悪さをしているだろうか?(bootsnapってなんだっけ?
/bundle/gems/bootsnap- y.y.y/lib/bootsnap/load_path_cache/core_ext/kernel_require.rb:21:in `require': Error loading shared library libmysqlclient.so.18: No such file or directory (needed by /bundle/gems/mysql2-x.x.x/lib/mysql2/mysql2.so) - /bundle/gems/mysql2-x.x.x/lib/mysql2/mysql2.so (LoadError)対策
bundle ディレクトリを全て削除してからinstallし直し
rm -rf .bundle bundle installOriginal by Github issue
- 投稿日:2019-12-09T02:19:53+09:00
Rails6.0.2の変更点まとめ
Ateam Finergy Inc. Advent Calendar 2019の10日目です。
11月5日にRails6.0.1がリリースされましたが、11月27日には早くも6.0.2rc1がリリースされています。
Rails6.0.1から6.0.2への変更点をまとめました。1. Active Support
1-1. initialize時に訳文(
i18n
)をeager load
するRails6.0.1までは、
i18n
の読み込みによってアプリケーションの初期レスポンスが遅くなっていました。そこでRails6.0.2からは、アプリケーションのinitialize時にi18n
をeager load
することで、レスポンス速度の低下を避けるようになりました。具体的には、以下の変更が追加されています。
activesupport/lib/active_support/i18n_railtie.rbconfig.eager_load_namespaces << I18n
config.eager_load_namespaces
は元々Railsに存在しているオプションで、ここに名前空間を設定するとconfig.eager_load
オプションをtrue
にしている場合に読み込まれます。1-2.
ActiveSupport::Notifications
でスレッドごとのCPUタイムクロックを使用するように
ActiveSupport::Notifications::Instrument
クラスで、CPU時間の取得にProcess::CLOCK_THREAD_CPUTIME_ID
を使用するようになりました。
Rails6.0.1まではProcess::CLOCK_PROCESS_CPUTIME_ID
を使用していましたが、これでは一般的によく使われているマルチスレッドのWebサーバにおいて、現在のリクエストだけでなく他のリクエストにも時間を費やしてしまうリスクがあります。そこで、Process::CLOCK_THREAD_CPUTIME_ID
に変更されました。
またこの変更に伴い、Process::CLOCK_THREAD_CPUTIME_ID
が定義されているかをチェックするようになっています。
- 参照
2. Active Job
2-1.
Sidekiq
がActive Job
クラスのsidekiq_options
にアクセスできるように
Sidekiq
がActive Job
クラスにアクセスすることにより、そのActive Job
タイプに設定されているsidekiq_options
にアクセスすることができるようになりました。
Rails6.0.1まではActive Job
のクラス名をto_s
していたため文字列型になってしまい、sidekiq_options
にはアクセスできませんでした。Rails6.0.2からはto_s
を無くしたことで、アクセスできるようになりました。3. Action Pack
3-1. Railsエンジンにマウント可能なルーティングヘルパーを、システムテストで使えるように
Rails6.0.1のシステムテストにおいて、Railsエンジンにマウント可能なルーティングヘルパー(ex:
engine_name.root_url
)を呼び出すと、未定義のエラーが発生します。
Rails6.0.2では、このエラーを解消しています。
- 参照
4. Railties
4-1.
Scaffold
ジェネレーターの衝突チェックを修正
Scaffold
で新たに生成したクラスと既に存在するクラス名が衝突した場合のチェックの処理が、修正されています。
- 参照
4-2. 各環境ごとに
autoload_paths
とautoload_once_paths
とeager_load_paths
が設定できるように各環境の設定ファイル(
config/environments/*.rb
)で、autoload_paths
とautoload_once_paths
とeager_load_paths
の値が設定できるようになりました。5. 最後に
今回はマイナーバージョンアップなので、細かなバグ修正やパフォーマンス向上がほとんでしたね。
今後もRails6の品質とパフォーマンスが、より向上していくことを期待しています。
- 投稿日:2019-12-09T01:19:11+09:00
Railsエンジニアの視点でHanamiを解説してみた
はじめに
新しい職場に来てもう少しで2ヶ月が経過します。現在の業務でHanamiを書き始め、何となく慣れてきたので、アドベンドカレンダーの機会にHanamiについて書いてみたいと思います。しかし、Hanamiはまだまだマイナーなフレームワークであるため、Railsエンジニアの視点で分かりやすいように解説していきます。
Hanamiとは
2017年4月にバージョン 1.0.0 がリリースされたばかりの比較的新しいRubyのフレームワークです。Railsとの違いで代表的なものはこんなところです。
長期的なメンテナンスに向いたフレームワーク
Rails はMVCやActiveRecordに仕様の大部分が依存したフレームワークになっています。 一方でHanami は DDD (ドメイン駆動設計) をベースにしつつ、ある程度柔軟性を残した状態で開発出来るフレームワークとなっています。 もう少し具体的に言うと、Rails はサービスを素早くローンチすることに向いている一方で Hanami は長期的な保守を念頭においた開発に向いているといった感じです。
ただし、Railsと同様にMVC全体がサポートされたフルスタックなフレームワークなので、必ずしも初期ローンチを犠牲にしている訳ではありません。最近では、マイクロサービスを念頭に置いた開発を行うようになってきたこともあり、初期ローンチとサービス成長の両方を見据えた選択肢としては良さそうなフレームワークです(ここはあくまでも2ヶ月間書いてきた人間の初感ですが。。)。
マジックが少ない
マジックと言うと少し分かりにくいですが、Rubyはメタプログラミングを上手く活用することで、プログラムを書くプログラムを書くことが可能です(初学者を混乱させてしまう説明かも。。)。もう少し具体的に言うと、実行時に与えられた引数に合わせて処理を柔軟に変化させるクラスやメソッドが書けるといった感じです(さらに分らなかったらごめんなさい。。)。メタプログラミングで書かれたクラスやメソッドは内部処理を知らなくても柔軟に使用出来てしまうことから、このような言われ方がされます。
特に、Railsは、ActiveRecordやActiveSupport系などの強力なGemによりマジックが生み出され、開発者が詳細を意識しなくても書けてしまうフレームワークになっています。このあたりがRailsは書けるけど、Rubyは書けないみたいなエンジニアが生まれてしまう原因だったり。。しかし、ビジネス的な視点で見ると、早くローンチ出来た方がビジネスでのトライ&エラーが出来るため、Railsが広まった大きな理由でもあり、問題というよりは利点だと思われます。
一方、Hanamiの場合は、一部で特定のGemに依存している部分は見られるものの、マジックが少なく、 ほぼPure Ruby に近いフレームワークとなっています。ちなみに、ActiveSupportが使えないため、一番最初に困るのが、
present?
とblank?
メソッドが使えなくなる点です。オブジェクトの種類を意識しないと無意識にNoMethodError
を引き起こすコードを書きます。あとは、長くなりそうなので、技術的な特徴などは公式ホームページをご覧下さい。
公式ホームページはこちら
hanamiコマンド
Hanamiでは、Railsと同様に
hanami
コマンドが用意されています。細かいところで違いはあるものの、ほとんどrails
がhanami
になっただけです。プロジェクトの作成
bundle exec hanami new . --database=mysqlHanami console
bundle exec hanami consoleディレクトリ構造
上記のコマンドを実行した後のディレクトリ構造はこんな感じです。
今回、環境にdocker-composeを利用しています。dbディレクトリとDocker関係のファイルは気にしないで下さい。
apps配下にRailsでも馴染みがある
controllers
、templates
、views
が存在します。そして、Railsと大きく違う点は、lib配下にDDDらしさが伺える
entities
とrepositories
が存在する点です。簡単に説明すると、この2つがModelの役割を果たします。. |-- Dockerfile |-- Gemfile |-- Gemfile.lock |-- README.md |-- Rakefile |-- apps | `-- web | |-- application.rb | |-- assets | | |-- favicon.ico | | |-- images | | |-- javascripts | | `-- stylesheets | |-- config | | `-- routes.rb | |-- controllers | |-- templates | | `-- application.html.erb | `-- views | `-- application_layout.rb |-- config | |-- boot.rb | |-- environment.rb | `-- initializers |-- config.ru |-- db | |-- migrations | |-- mysql | | `-- volumes | `-- schema.sql |-- docker-compose.yml |-- lib | |-- app | | |-- entities | | |-- mailers | | | `-- templates | | `-- repositories | `-- app.rb |-- public `-- spec |-- app | |-- entities | |-- mailers | `-- repositories |-- features_helper.rb |-- spec_helper.rb |-- support | `-- capybara.rb `-- web |-- controllers |-- features `-- views `-- application_layout_spec.rbGenerater
これは、users#showというアクションを作成するコマンドです。もう、なんとなく分かると思いますが、RailsでいうところのControllerのActionです。
Hanamiの大きな特徴として、ControllerはAction毎にファイルを作ります。詳しくは、Controllerの章で解説します。
# bundle exec hanami generate action web users#show create apps/web/controllers/users/show.rb create apps/web/views/users/show.rb create apps/web/templates/users/show.html.erb create spec/web/controllers/users/show_spec.rb create spec/web/views/users/show_spec.rb insert apps/web/config/routes.rbRouting
Railsと全く同じに書けます。詳細に調べて行くと、セッションの有無に応じてルーティングを行うような高度なルーティングも出来ますが、Railsと同じ感覚でこのあたりさえ覚えておけば、ほとんど使えてしまいそうです。
apps/web/config/route.rbget '/', to: "users#show" post '/new', to: "users"または
apps/web/config/route.rbresources :users, only: [:show, :create]Model
まずは、hanamiコマンドでModelを作成します。以下のコマンドを実行すると、
entities
、repositories
が作成されます。bundle exec hanami generate model user create lib/src/entities/user.rb create lib/src/repositories/user_repository.rb create db/migrations/20191208133857_create_users.rb create spec/src/entities/user_spec.rb create spec/src/repositories/user_repository_spec.rbMigration
そして、マイグレーションファイルもRailsとそれほど変わりません。
db/migrations/20191207110038_create_users.rbHanami::Model.migration do change do create_table :users do primary_key :id column :name, String, null: false column :email, String, null: false column :created_at, DateTime, null: false column :updated_at, DateTime, null: false end end endマイグレーションを実行するコマンドはこんな感じです。
bundle exec hanami db prepareEntity
特殊な要件がなければ、これだけで使用出来ます。RailsのModelと同じように、
Hanami::Entity
を継承しているだけですが、面白い違いがあります。class User < Hanami::Entity endHanamiのconsoleで試して見ると、違いが分かります。Userクラスをインスタンス化する時に値は入れられるものの、後から代入しようとすると値が入りません。
つまり、Entityはイミュータブルな設計になっており、
initialize
の段階でしか値を入力することが出来ません。つまり、Entityはデータをオブジェクトとして扱うためのただの入れ物です。
user = User.new(email: 'test@gmail.com') user.email #=> "test@gmail.com" #=> user.email = 'hoge@gmail.com' #=> NoMethodError: undefined method `email='Repository
それでは、どうやって値を代入するかですが、その役割はRepositoryが担います。つまり、DBへの更新はRepositoryの役割であり、SQLを発行するのもここが担います。
create
、update
、delete
、all
、find
などの基本メソッドは、Hanami::Repository
を継承するだけで予めサポートされます。他に必要なメソッドは以下のように自作する必要があります。また、ここに複雑なロジックを噛ませることも可能です。しかし、ここにトリッキーなメソッドを定義するのは自殺行為なのでやめた方がいいです。ここでバグを仕込むとDBに保存されるデータが汚染され、恐ろしいことになるので、ロジックは出来るだけ別階層に書くべきです。遊びで試したところ、思い切ったクエリも実行出来るので下手なことをすると地獄を見そうで怖いです。
lib/src/repositories/user_repository.rbclass UserRepository < Hanami::Repository def find_by_email(email) users.where(email: email).first end endちなみに使うときはこんな感じです。
user_repository = UserRepository.new @user = user_repositor.find_by_email("test@gmail.com")View
ViewはRailsとほとんど同じです。デフォルトのTemplateに、eRuby(.erb)が使用されています。
大きな違いはViewクラスが用意されている点です。apps/web/views/users/show.rbmodule Web module Views module Users class Show include Web::View def title 'あるユーザーの情報' end end end end endあとはerbから呼び出すだけで使用できます。Rails経験者は勘が働くと思いますが、
<h2>
タグで呼び出されているコードはControllerで説明します。apps/web/templates/users/show.html.erb<h1><%= title %></h1> <h2><%= user.name %></h2> <h2><%= user.email %></h2>どうやら、Viewクラスが存在することで、パーシャル化を行う際に非常に重宝するとの解説が見られますが、業務であまりいじらない箇所なので詳細な知見までは把握出来ていません。
Controller
Generaterの章で少し触れますが、HanamiのContorollerはAction毎に別ファイルに記載します。RailsだとControllerクラス配下のメソッドで定義されていたものが、Actionクラス毎に定義されるのだと理解すると分かりやすいと思います。
apps/web/controllers/users/show.rbmodule Web::Controllers::Users class Show include Web::Action params do required(:id).filled(:str?) end expose :user def call(params) if params.valid? redirect_to '/' else self.status = 422 return end @user = UserRepository.new.find(params(:id)) end end endフォームから送信された値がcallメソッドに引数として渡され、処理が行われます。このとき、
params.valid?
で以下のように定義されたValidationが実行されます。今回は文字列かどうかのチェックだけが行われていますが、オリジナルのメソッドを定義すれば、有効なEmailアドレスの文字列が不正ではないかなどのチェックもここで可能です。
params do required(:id).filled(:str?) endそして、Viewの章で少し触れていますが、インスタンスメソッドを以下のように定義することで、View側に渡すことが出来ます。そして、Railsと同様にテンプレート内で使用が可能です。
ちなみに、Railsと違い、ここで定義しないとインスタンス変数は外に出ませんので注意が必要です。
expose :userInteractor
最後に、一番馴染みがない名前が登場します。この用語の解説を始めると、ドメイン駆動設計の話やクリーンアーキテクチャの話に広がってしまうため、今回は省略します(私も勉強中のため、説明出来るほど理解が進んでいない。。)。
そこで、ここでは、RepositoryとControllerの間に入る層だと思えば、少し理解しやすいと思います。
また、Repositoryの章で複雑なロジックは、Repositoryクラスに書かないように注意を促しましたが、HanamiのビジネスロジックはこのInteractorに書くことになります。
例えば、Controllerの章で例にしたActionのロジックを移設するとこんな感じです。
lib/bookshelf/interactors/user/show.rbrequire 'hanami/interactor' module UserInteractor class Show include Hanami::Interactor expose :user def call(id) @user = UserRepository.new.find(id) end end endそして、Controllerはこのようになります。
apps/web/controllers/users/show.rbmodule Web::Controllers::Users class Show include Web::Action include Hanami::Interactor params do required(:id).filled(:str?) end expose :user def call(params) if params.valid? redirect_to '/' else self.status = 422 return end @user = UserInteractor::Show.new.call(params[:id]) end end end業務上の経験でしかありませんが、基本的にビジネスロジックはInteractorに詰め込んでいくため、ちょっとした仕様変更だとここを弄るだけで済んでしまうことがあり、個人的にはHanamiの良さだと思っています。
最後に
長い解説になってしまいすみません。。もし、少しでも興味を持っていただけるのなら、Hanamiに触れてみてください。
また、今回は、Advent Calendar に間に合わせるために、サンプルを用意出来ませんでしたが、検証用のdocker-composeとサンプルリポジトリをあとで追加したいと思います。
以上です。
参考文献
HanamiはRubyの救世主(メシア)となるか、愚かな星と散るのか
Hanami Getting Started guide
- 投稿日:2019-12-09T01:12:11+09:00
dbの指定を忘れてrails newしてしまった。しにたい。
症状
アプリを作成している時、いつもの如くSequel Pro確認したらdbがない
原因
いつもはrails newする時に
rails new -d mysqlとしていたのだが
rails newというやり方でディレクトリを作成してしまった。
処置
Gemfilegem 'mysql' #gem 'sqlite3'上記のようにgemfileにmysqlを追記。
sqlite3は念のためコメントアウトして置いてbundle installそしてdatabase.ymlを下記のように編集
database.ymldefault: &default adapter: sqlite3 pool: 5 timeout: 5000 development: adapter: mysql2 encoding: utf8 database: [データベース名] pool: 5 username: root password: host: localhostその後
rake db:createでデータベースを作成。これでなんとかデータベースはmysqlになった。
まとめ
色々中途半端な対策かもしれません。今回はまだデータベースにデータを入れてない状態だったのでこれで済んだ。あとアプリを仕上げて本番環境とかになったら後遺症見たいのが出てくるのかもしれないがそれはその時対策します。
備忘録としてですが、
ご指摘あればぜひよろしくお願いします。