kikeda1104's blog

備忘録・技術に関することを書いています。(webエンジニア)

ActiveRecord::Relation#or

弊社の新規プロジェクトでは、Rails5が使われていてそのおかげで、orが利用できた。 今日は、その辺りをまとめてみる。

ActiveRecord::Relation#or

Rails 4系では、sqlorをアプリで実装しようとするとfind_by_sqlwherejoinsなどに直接sqlを書くか。Arelを利用して実装を進めていた。 Rails 5系からorが利用できるようになっているので、直接sqlを記述することなく、ActiveRecordの操作で完結できる。

# A || B

 Model.where(name: 'hoge').or(Model.(name: 'fuga'))

# A && B || C
 Model.where(name: 'hoge').where(age: 10).or(Model.where(name: 'fuga')) 

# A ‖ B && C
 Model.where(name: 'hoge').or(Model.where(age: 10)).where(name: 'fuga'))

という結果が返ってくる。

must be structurally compatible

構造互換性が必要なので、orに指定するActiveRecord::Relationとレシーバの構造は統一する。

互換性がなければ、下記のArgumentErrorが発生する。

ArgumentError: Relation passed to #or must be structurally compatible. Incompatible values: [:limit]

サンプル

Model.joins(:hoges)
     .merge(Model.where(name: nil)
     .or(Hoge.where(fuga: nil)
=> 
SELECT "model".* FROM "model" INNER JOIN "hoges" ON "hoges"."model_id" = "models"."id" WHERE ("models"."name" IS NULL OR "hoges"."fuga" IS NULL)

これで、andorの条件を切り換えられるロジックを書くことが容易になった。

参考

ActiveRecord::Relation

Rails 5 adds OR support in Active Record | BigBinary Blog

Rails 5: ActiveRecord OR query - Stack Overflow

form_tag、form_forからutf8というパラメータをはずす

検索の機能で、HTTP methodsをpostからgetに切り替えた際に、utf8というパラメータが含まれていたので、これを取り除いた。

form_for, form_tagから自動で生成されるinputタグだが、不要だったので、自動生成されないように変更した。

モンキーパッチ

module ActionView
  module Helpers
    module FormTagHelper
      def utf8_enforcer_tag
        ''.html_safe
      end
    end
  end
end

上記で、form_forform_tagから、utf8のinputタグは生成されない。 オプションで指定することもできるようだが、今回一部対応ではなく、プロジェクト全体から取り除いた。

参考

Rails の `utf8=✓` の歴史と消し方と snowman ☃ - Qiita

vim snippetの利用

vimからsnippetは以前から利用していますが、環境毎に設定していることがあり、再設定していたので こちらにまとめておく。

環境

Mac OS

vim

.vimrc

.vimrcに追加

...
if &compatible set nocompatible               " Be iMproved endif

execute 'set runtimepath^=~/.vim/repos/github.com/Shougo/dein.vim'
" Required:
 call dein#begin('~/cache/dein')

 call dein#add('Shougo/dein.vim')
 call dein#add('Shougo/neocomplete.vim')

" ここから
 call dein#add('Shougo/neosnippet')
 call dein#add('Shougo/neosnippet-snippets')
" ここまで
call dein#end()

filetype plugin indent on

" ここから
" Plugin key-mappings.  " <C-k>でsnippetの展開
imap <C-k>     <Plug>(neosnippet_expand_or_jump)
smap <C-k>     <Plug>(neosnippet_expand_or_jump)
xmap <C-k>     <Plug>(neosnippet_expand_target)
let g:neosnippet#snippets_directory = '~/.vim/snippets/'

if has('conceal')
  set conceallevel=2 concealcursor=niv
endif
" ここまで
...

dein#install()

vimを起動して、:call dein#install()

もしくは、vimrcに下記を記述する。

" pluginsがインストール済みか確認
if dein#check_install()
  call dein#install()
endif

使い方

Ctrl + kで利用する。

snippetの追加

独自のsnippetを利用する場合には、snippets_directoryで指定したフォルダにsnipファイルを作成して、追加していくか。 vimを起動してNeoSnippetEditコマンドを叩く。

vim hoge.rbrubyのファイルを開いているなら、NeoSnippetEditでは、ruby.snipが開かれる。 ファイルがなければ、新規作成する。

参考

GitHub - Shougo/dein.vim: Dark powered Vim/Neovim plugin manager

GitHub - Shougo/neosnippet.vim: neo-snippet plugin contains neocomplcache snippets source

Vimプラグイン初心者がスニペット機能を導入して、独自スニペットを追加できるまでの流れ - Qiita

Rubyのモジュール

コードを整理する際に、classではなく、moduleで整理したいと思う場面がありました。

Railsで書いていることもあり、ActiveSupport::Concernにまとめる形にします、 moduleクラスに機能を混ぜ合わせることで複数のクラスで機能を共有する(Mix-in)という機能を提供しています。

さらにActiveSupport::Concernは、関心事を分離するために独立した機能を定義したモジュールが配置されますので、これを利用することにします。

ActiveSupport::Concern

module DataAble
  extend ActiveSupport::Concern
  
  included do
    scope :with_deleted -> { where.not(deleted_at: nil) }
    scope :without_deleted -> { where(deleted_at: nil) }
  end

  def set_data
    @data = Data.pluck(:id, :name)
  end
end
class Hoge
  include SetDataAble
end

これで, できるようになります。

@hoge = Hoge.new
@hoge.set_date

Hoge.with_deleted

参考

ruby on rails - なぜRailsのモジュールでは"able"をサフィックスとして付けるのでしょうか? - スタック・オーバーフロー

複数ファイル内の置換

複数ファイルを一度に置換したくて使っているコマンドです。 railsのroutes.rbを修正して、ルーティングヘルパが変わったので一斉に置換しました。

find . -type f -print0 | xargs -0 sed -i -e “s/info_salutation_path/salutation_path/

macOSの場合は、iオプションの挙動が違うので注意です。

参考

Mac環境でsedを使うときの注意 - tjinjin's blog

Rails のルーティング | Rails ガイド

constraintsの利用について

後輩にvimの折り畳み開閉のショートカットキーを聞かれてて、忘れてたのに落ち込んだり、

参加したセミナーでテストコードに関する有益な話を聞けてほっこりしてます。

コミュ力って重要。

Railsのroutes.rbで、ルーティングを無条件で利用すると、public下の置いているファイルとルーティングが当たってしまうので、

これを回避するためにURIを変えるか。制限を入れるかを考えていて、制限を入れることにしました。

constraintsを使って定義する。

環境

constraints

定義したルーティングに制限を追加することできます。

# config/routes.rb
Rails.application.routes.draw do
   get 'users/:user_id' => 'users#show', constraints: /\d+/
end

これは、ブロックも利用できて、複数のルーティングに制限を加える際に利用できる。

Rails.application.routes.draw do
   constraints /\d+/ do
     get 'welcome/:id' => 'welcome#show'
     ...
   end
end

今回は、DBの値を利用した制限を追加する形で実装を進めていてconstraintsのファイル保存場所について

appを検討したが、ひとまずlibに保存しています。

まず、lib/autoloadを起動時に自動で読み込むようにします。

# config/application.rb
...
config.autoload_paths += %W(#{Rals.root}/lib)
...
# lib/autoload/constraints/zone_constraint.rb
class ZoneConstraint
  def matches?(request)
    paths = request.path_info&.split('/')&.reject(&:blank?)
    paths.size.times do |i|
      case i
      when 0
        fail if ExampleA.find_by(key: areas[i]).blank?
      when 1
        fail if ExampleB.find_by(key: areas[i]).blank?
      when 2
        fail if ExampleC.find_by(key: areas[i]).blank?
      else
        fail
      end
    end
    true
  rescue
    false
  end
end

最後にroutes.rbに制限を加えます。

# config/routes.rb

constaints ZoneConstraint do
  get ':example_a/:example_b/:example_c' => 'example#index
  ...
end

データベースに保存されていない場合、ルーティングを参照しないようにします。

参考

Rails3 事始め: [Rails3] 現在のURLを取得(request オブジェクト)

rails routing constraintsについて | 日々雑記

constraintsの意味 - 英和辞典 Weblio辞書

Rails のルーティング | Rails ガイド

Railsアプリのモジュールはどこに置くべきか問題 (公開版)

friendly idを利用したURI設計

プロジェクトでSEO対策と数値を含めずにURIを設計するため、friendly_idを採用してURIの再設計をしました。

railsではURIにmodelの主キーを含める形の設計が主になりますが、数値では分かりづらくなります。(resourceでは作成されませんが)

それほど複雑な仕様でもないため、gemを使わず、参照しているmodelのnameカラムがありましたので英語名を追加することで実現しました。

サンプルコードは、実際に実装したコードとは異なります。

環境

Rails appの作成

# rails new friendly_app
# cd friendly_app
# ./bin/rails g scaffold users name_en:string
# ./bin/rails db:migrate

実装

app/models/user.rbに、idではなくname_enを利用するように書き換える。

# app/models/user.rb
モデルに'#to_param'を追記する。

class User < ApplicationRecord
 validates :canonical_name,
        uniqueness: { case_sensitive: false },
        format: { with: /\A[A-Za-z][\w-]\z/ },
        length: { minimum: 3, maximum: 25 }

  def to_param
    name_en ? name_en : id.to_s
  end

  def self.find(input)
    find_by(name_en: input) || super
  end
end

rails s

app serverを起動して。

# ./bin/rails s

動作確認

  • users/newでユーザを作成する。
  • users/:name_enに移動する。

f:id:kikeda1104:20160829233032p:plain

まとめ

route.rbがresourcesのままになっており params[:id]でname_enの値を受け渡すことになりわかりずらくなるので、idを受け渡さない画面ではroute.rbを変更しました。

参考URL

Friendly URLs in Rails · GitHub

Railsで、URLにIDでなく名前を入力して、アクセスする方法 - Qiita

ActiveRecord::Integration

Railsの正規表現でよく使われる \A \z って何?? - Qiita

Rspecでseeds.rbをリセット時に読み込む。

プロジェクトにrspecとfactory girlのgemを追加して seeds.rbを読み込んでから、毎回テストを起動したかったのでこのあたりをまとめておく。

環境

rspecとfactory girlの導入

まずは、Gemfileにrspec_railsfactory_girl_railsを追加してbundle installします

# Gemfileにgemを追加
group :development, :test do
  gem 'rspec_rails'
  gem 'factory_girl_rails'
end

group :test do
  gem 'database_clear'
end
bundle install

rspec install

$ rails g rspec:install

環境別のseedを作成する

これはRails.env.hoge?で切り分ければ、seeds.rbにまとめることもできる。

env毎のファイルを作成する。

mkdir db/seeds
touch db/seeds/test.rb
touch db/seeds/development.rb
touch db/seeds/production.rb

db/seeds/test.rbdb/seeds/development.rbに記述を追加

values = { email: 'hoge@example.com'... } # attriubutesを設定する。
Admin.create(values)

seeds.rbに下記のコードを追加。

load(Rails.root.join( 'db', 'seeds', "#{Rails.env.downcase}.rb"))

テスト時にdbをリセットして、seedsを読み込む。

#clean_withの引数に、exceptでクリーンにしないmodel名を記述することもできます。

config.before(:suite) do
  load Rails.root.join('db', 'seeds.rb')
  DatabaseCleaner.clean_with :truncation
  # DatabaseCleaner.clean_with :truncation, { except: %w(categories brands) }
end

参考URL

blog.inouetakuya.info

環境ごとに投入する初期データを変える - Qiita

環境にあわせてrake db:seedを実行する - how to code something

gem unicornの起動・定義・再起動

こんにちは。kikedaです。 毎回忘れるので、備忘録代わりに書いておきます

unicorn

コマンド

  • 起動

    railsのrootディレクトリで実行する

bundle exec unicorn_rails -c config/unicorn.rb -E development -D
  • 停止

    railsのrootディレクトリで実行する

kill -QUIT `cat /tmp/unicorn.pid`