サービス層の導入
業務で、サービスクラスをmodels
に入れていたので、そのファイルの移動と自作でサービス層を追加しました。
service
クラス
Model
に書くにはコードの量が多い機能で、Fat model
になる恐れがあり、concern
に移す事も考えましたが、共通で利用することもないため
サービス層にロジックを移すことにしました。
ファイルの設置
app
下に、ファイルを作ります。
app/services/select_info_service.rb
class SelectInfoService def initialize(info) @title = info[:title] @body = info[:body] end def search ... end private attr_accessor :title、:body end
autoload_pathに追加
# config/application.rb config_autoload_path += "#{config.root}/app/services/**/*.rb"
Rails 5では、config/initialize/..
にファイルを作成すること推奨しています。
./bin/rails c
si = SelectInfoService.new si.search
module
化して、/lib
に置くか。共通処理であれば、controllers/concerns
または、
models/concerns
に置くか。迷うが、割とapp
の下にファイルを設置することが多く、それに見習った。
参考
Railsでサービスとフォームを導入してみる話 - assertInstanceOf('Engineer', $a_suenami)
ActiveRecord::Relation#orのworkaround
先日書いた記事の付け足しになりますね。or
を利用して検索ロジックを書いていましたが
merge
や、where
を利用することで構造に不一致になるケースがあり、HACK
,WORKAROUND
で対処しました。
原因はなんとなくイメージできているんですが、詳しい方いましたら、教えて頂きたいです。
- 完全に再現できるソースではなく、イメージです。
models
まず、モデルはこの親子関係になります。
# app/models/user.rb class user < ApplicationRecord has_many :shops end # app/models/shop.rb class shop < ApplicationRecord belongs_to :user end
controller
app/controllers/users.rb class Users < ApplicationController def search relations = user.joins(:shop).where(name: 'hoge') relations = if params[:conditions] == 'or' relations.or(relations.where(age: 19) else # relations.joins_valuesが更新される relations.merge(relations.where(age: 19)) end render json: relations.to_json({ include: :shops }) end end
relations.joins_values
これで、引数とレシーバーの構造が一致しているか検証しています。
joins
の引数(上記のサンプルコードでは、:shop)が入っていますので、これが一致するか見ています。
捕捉ですが、limit
, offset
, distinct
も見ています。
WORKAROUND
修正したのは下記。
app/controllers/users.rb class Users < ApplicationController def search relations = user.joins(:shop).where(name: 'hoge') relations = if params[:conditions] == 'or' relations.joins_values = relatoins.joins_values.uniq # HACK: relations.or(relations.where(age: 19) else relations.merge(relations.where(age: 19) end render json: relations.to_json({ include: :shops }) end end
joins_values
を上書きすることで対応した。これmerge
をしているからjoins_values
の値も追加されているのかと思い、where
に変えてみたが、
joins_values
の値は変わらず。githubで調べるのはこのぐらいにして一時的な対応です。
参考
関係ないけど後輩に教わった
Railsでmysqlのviewsを利用する。
弊社のプロジェクトで、view
を利用していたので、覚え書き。
views
CREATE VIEW user ( id, name, age ) AS SELECT id, name, age FROM A UNION SELECT id, name, age FROM B;
ここは、migrationに置き換えることもできる。
model
モデルは、Rails
のmodel
と同様です。
class User < ApplicationRecord end
Rails 4系で、mysql view
自体は利用できる。
参考
ActiveRecord::Relation#or
弊社の新規プロジェクトでは、Rails5が使われていてそのおかげで、or
が利用できた。
今日は、その辺りをまとめてみる。
ActiveRecord::Relation#or
Rails 4系では、sqlのor
をアプリで実装しようとするとfind_by_sql
、where
、joins
などに直接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)
これで、and
とor
の条件を切り換えられるロジックを書くことが容易になった。
参考
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_for
とform_tag
から、utf8のinput
タグは生成されない。
オプションで指定することもできるようだが、今回一部対応ではなく、プロジェクト全体から取り除いた。
参考
vim snippetの利用
vimからsnippetは以前から利用していますが、環境毎に設定していることがあり、再設定していたので こちらにまとめておく。
環境
.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.rb
でrubyのファイルを開いているなら、NeoSnippetEdit
では、ruby.snip
が開かれる。
ファイルがなければ、新規作成する。
参考
GitHub - Shougo/dein.vim: Dark powered Vim/Neovim plugin manager
GitHub - Shougo/neosnippet.vim: neo-snippet plugin contains neocomplcache snippets source
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オプションの挙動が違うので注意です。
参考
tmux : unknown option: status-utf8というエラーメッセージ
warningがでていたので、tmux.confを更新した。
status-utf8 has been removed after version 2.2.
ということで、setw -g status-utf8 on;
を削除しましょう。
参考
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について | 日々雑記