Rails の require_dependency とは何か?

Rails Guide を見てみますが、いまいちピンとこないのでソースを見てみました。 http://railsguides.jp/autoloading_and_reloading_constants.html#require-dependency

先にまとめ

require_dependency とは、production環境では require
development環境では Kernel.load する。
Kernel.load は呼ばれるたびにロードされるので、開発時には効率が良い。
通常は、autoload でことが足りそうな気がしますが、 何かautload_pathsに含まれないものをrequireする際には使うと良いでしょう。

ソース見てみるよー

require_dependency

activesupport-5.0.0.1/lib/active_support/dependencies.rb

      def require_dependency(file_name, message = "No such file to load -- %s")
        file_name = file_name.to_path if file_name.respond_to?(:to_path)
        unless file_name.is_a?(String)
          raise ArgumentError, "the file name must either be a String or implement #to_path -- you passed #{file_name.inspect}"
        end

        Dependencies.depend_on(file_name, message)
      end

depend_on を呼んでます。

depend_on

    def depend_on(file_name, message = "No such file to load -- %s.rb")
      path = search_for_file(file_name)
      require_or_load(path || file_name)
    rescue LoadError => load_error
      if file_name = load_error.message[/ -- (.*?)(\.rb)?$/, 1]
        load_error.message.replace(message % file_name)
        load_error.copy_blame!(load_error)
      end
      raise
    end

require_or_load を呼んでいます。
 
別件ですが、なんかdefaultのmessageが呼び出し元と若干違いますね。
少し違和感があります(後でもう少し見てみるかも)。

require_or_load 前半

少し長いので分割。

    def require_or_load(file_name, const_path = nil)
      file_name = $` if file_name =~ /\.rb\z/
      expanded = File.expand_path(file_name)
      return if loaded.include?(expanded)

      Dependencies.load_interlock do
        # Maybe it got loaded while we were waiting for our lock:
        return if loaded.include?(expanded)

        # Record that we've seen this file *before* loading it to avoid an
        # infinite loop with mutual dependencies.
        loaded << expanded
        loading << expanded

ActiveSupport::Concurrency::ShareLock を使って安全にロックを掛けつつ loaded に読み込み済みのリスト(Set)を保持しています。

こんな感じ。

#<Set: {"/Users/rochefort/work/rails5_example/app/controllers/articles_controller",
 "/Users/rochefort/work/rails5_example/app/controllers/application_controller",
 "/Users/rochefort/work/rails5_example/app/helpers/application_helper",
 "/Users/rochefort/work/rails5_example/app/helpers/tags_helper",
 "/Users/rochefort/work/rails5_example/app/helpers/user_sessions_helper",
 "/Users/rochefort/work/rails5_example/app/helpers/users_helper",
 "/Users/rochefort/work/rails5_example/app/helpers/articles_helper",
 "/Users/rochefort/work/rails5_example/app/models/article",
 "/Users/rochefort/work/rails5_example/app/models/application_record",
 "/Users/rochefort/work/rails5_example/app/uploaders/image_uploader"}>

require_or_load 後半

begin
  if load?
    # Enable warnings if this file has not been loaded before and
    # warnings_on_first_load is set.
    load_args = ["#{file_name}.rb"]
    load_args << const_path unless const_path.nil?

    if !warnings_on_first_load or history.include?(expanded)
      result = load_file(*load_args)
    else
      enable_warnings { result = load_file(*load_args) }
    end
  else
    result = require file_name
  end

load? がtrueの場合は、load_file を呼んでいますが、falseの場合は require しているだけのようです。

load_file

まずは load_file を見てみると Kernel.load しています。

def load_file(path, const_paths = loadable_constants_for_path(path))
  const_paths = [const_paths].compact unless const_paths.is_a? Array
  parent_paths = const_paths.collect { |const_path| const_path[/.*(?=::)/] || ::Object }

  result = nil
  newly_defined_paths = new_constants_in(*parent_paths) do
    result = Kernel.load path
  end

  autoloaded_constants.concat newly_defined_paths unless load_once_path?(path)
  autoloaded_constants.uniq!
  result
end

load?

続いてload?

def load?
  mechanism == :load
end

mechanism という値を参照しているだけです。

mechanism

こいつはどこから来るのか?

mattr_accessor :mechanism
self.mechanism = ENV['NO_RELOAD'] ? :require : :load

一見、環境変数で出しわけているだけのようですが
おそらくconfigのファイルで出しわけてそうなので、config/production.rb を見てみます。
 
しかし、直接 mechanism を設定している箇所はありません。 ですが、以下が怪しそうです。

config.cache_classes = true

railties

railties-5.0.0.1/lib/rails/application/bootstrap.rb

module Rails
  class Application
    module Bootstrap


      # Sets the dependency loading mechanism.
      initializer :initialize_dependency_mechanism, group: :all do
        ActiveSupport::Dependencies.mechanism = config.cache_classes ? :require : :load
      end

ありました!
cach_classes の値を見て require / load を設定していました。

RailsのSQLiteで正規表現を使う

Postgres や MySQL では、書き方は違えど正規表現を使うことができます。 しかし、RailsのDevelopment(SQLite)で実行すると、エラーとなります。

SQLite3::SQLException: no such function: REGEXP

SQLiteでのREGEXP

こちらを見てみると SQLite3におけるREGEXP演算子 - めらんこーど地階

The REGEXP operator is a special syntax for the regexp() user function. 
No regexp() user function is defined by default and so use of the REGEXP operator 
will normally result in an error message. If an application-defined SQL function 
named "regexp" is added at run-time, then the "X REGEXP Y" operator will be 
implemented as a call to "regexp(Y,X)".

SQLite Query Language: expression を見てみると
確かに、application-defined SQL function で regexp を追加しろと記載があります。

Rails ではどうすれば良いか?

ググるとこちらのgemがありました。
Rails5でとりあえず動作するようにgemspecを修正したPRを出したら速攻で取り込んでもらえたので
今はGemfileに突っ込んだら使える状態です。
AaronLasseigne/sqlite3_ar_regexp

(以下のケースでは REGEXP 使う必要はないのですが、)
とりあえず動作するかどうかを見るために手元で動かしてみました。

names = ["ジェイミー・ヴァーディ", "中村憲剛"]
Person.where("name REGEXP :names", names: names.join("|"))

以下のSQLが発行されます。 REGEXP が使用されているのが分かります。

SELECT "people".* FROM "people" WHERE (name REGEXP 'ジェイミー・ヴァーディ|中村憲剛')

どうやってるの?

sqlite3_ar_regexp/extension.rb at master · AaronLasseigne/sqlite3_ar_regexp
を見ると以下を利用しています。

connection.create_function('regexp', 2) do |func, pattern, expression|

てっきり SQLite側に仕込む必要があるかと思ったのですが、各種driver側でユーザ定義関数を作成できるメソッドがそれぞれ用意されているようでした。
sqlite3 だと以下。 Method: SQLite3::Database#create_function — Documentation for luislavena/sqlite3-ruby (master)

まとめ

とりあえず使ってみたい人向け。速度などは未検証。

Acts-as-taggable-on を拡張する

タグの管理にmbleigh/acts-as-taggable-on を使っています。 非常に使いやすくて重宝しているのですが、一部のタグのみ他のテーブルと関連づけしたかったので 拡張してみました。

やりたいこと

schema.rb
acts-as-taggable-on を使うと以下のようなテーブルが作成されます。

  create_table "tags", force: :cascade do |t|
    t.string  "name"
    t.integer "taggings_count", default: 0
    t.index ["name"], name: "index_tags_on_name", unique: true
  end

データとしては以下のような感じです。
id=1,2 はカテゴリーですが、id=3 は人物です。 別途、人物を管理するpeopleテーブルを保有しているとすると、 このテーブルとのreferenceを付与するだけで、「通常のタグ一覧」と「人物一覧」が簡単に取り出せます。

id name taggings_count
1 アート 10
2 歴史 20
3 ジェイミー・ヴァーディ 3

referenceを追加すると以下のようなイメージです。

id name taggings_count person_id
1 アート 10 null
2 歴史 20 null
3 ジェイミー・ヴァーディ 3 100

やり方

$ rails g migration AddPersonRefToTags person:references

# migration file
class AddPersonRefToTags < ActiveRecord::Migration[5.0]
  def change
    add_reference :tags, :person, foreign_key: true
  end
end

config/initializers/acts_as_taggable_on.rb

ActsAsTaggableOn::Tag.class_eval do
  belongs_to :person
end

model
point は class_name で ActsAsTaggableOn::Tag を指定すること。

class Person < ApplicationRecord
  has_one :tag, class_name: "ActsAsTaggableOn::Tag"
end

データ抽出
記事にタグ付けされたタグの一覧と、人物タグの一覧を取り出します。(Articleにタグ付けをできるようにするのは、acts_as_taggable_on の基本機能なのでここでは割愛)
 
pointは、acts_as_taggable_on利用時にtags_on というメソッドが提供されるのですが、この引数に condisions が渡せるので以下のようにすれば良いです。
acts_as_taggable_on 親切設計ですね!

    @tags = Article.tags_on(:tags, conditions: { person_id: nil })
    @people = Article.tags_on(:tags, conditions: "person_id is not null")

あとは、has_one / belongs_to を設定しているので、各モデルのオブジェクトから辿ることができます。