kikeda1104's blog

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

サービス層の導入

業務で、サービスクラスを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で調べるのはこのぐらいにして一時的な対応です。

参考

https://github.com/rails/rails/blob/0f80cb1b2b12f3c220391784d3a16473d7b87669/activerecord/lib/active_record/relation.rb#L3

to_json (Hash) - APIdock

関係ないけど後輩に教わった

vim正規表現\@=を教わった。vim情報はありがたい。

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

モデルは、Railsmodelと同様です。

class User < ApplicationRecord
end

Rails 4系で、mysql view 自体は利用できる。

参考

http://dev.classmethod.jp/server-side/db/mysql_practice_1/

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アプリのモジュールはどこに置くべきか問題 (公開版)