新手问题 请教一个关于 Rails 中常量加载的问题?

Dounx · 2019年08月26日 · 最后由 Dounx 回复于 2019年08月27日 · 2611 次阅读

运行环境:Ruby 2.2.4,Rails 3.2.22,development 环境下,autoload_paths 默认。

最近在项目中遇到了这样一个问题:

# app/models/shop/event.rb
module Shop
  class Event < ActiveRecord
    def self.test
      'hello'
    end
  end
end

# app/controllers/api/books_controller.rb
class Api::BooksController < ApplicationController
  def self.test
    Shop::Event.test
  end
end

# rails console
pry> Api::BooksController.test
---> NameError: uninitialized constant Api::Shop::Event

倘若将 app/controllers/api/books_controller.rb 中的 Shop::Event.test 加上顶级限定符则不报错:

# app/controllers/api/books_controller.rb
class Api::BooksController < ApplicationController
  def self.test
    ::Shop::Event.test
  end
end

# rails console
pry> Api::BooksController.test
---> 'hello'

或者在调用 Api::BooksController.test 之前使用 load 'shop/event.rb' 也可以正常运行。

production 环境,在 rails console 下运行不会报错(不清楚是否是不是哪里加载了),想请教下是什么原因导致的呢?

相关主题:

  1. 自动加载和重新加载常量
  2. Rails 中的类加载机制
  3. Ruby 中的常量查找

Rails 的 autoload 加载机制导致的,默认下,Rails 只会去找某个类下的常量。也就是说

class A puts B end

当 B autoload 时,只会找 A::B 而不是像 Ruby 一样先找 A::B 再找 ::B

但生产环境不一样,Rails 会 preload 所有类,所以上述代码不会再 autoload, 就不会报错了。

Rails 6 新引用了 zeitwerk 加载机制,楼主可以去了解一下:https://guides.rubyonrails.org/autoloading_and_reloading_constants.html

lyfi2003 回复
class A
  puts B 
end

查找顺序不是应该是 A::B -> ::B 吗?我看 Rails 指南 是这么说的,所以应该不是 autoload 的问题吧,毕竟生产环境和开发环境的区别也就是 autoload 的时候调用的方法不同把。

查找顺序: Api::BooksController::Shop::Event -> Api::Shop::Event -> ::Shop::Event 组合 autoload_paths 数组里的路径,最终加载到 models/shop/event.rb。

不知道这样理解对不对?

如果你有个 Api::Shop 这个常量就会出现这样的问题,Rails 限定常量加载有点问题。

pinewong 回复

确实是有一个,但是 Api::Shop 底下没有定义 Event,这个时候 Rails 不会继续再向上找吗?而且生产环境就不会出现这个问题,是因为什么?

开发环境:

开发环境是惰性加载,Shop 常量一开始不存在,所以会触发 const_missing,使用 Rails 的自动加载机制。加载的顺序是:Api::BooksController::Shop -> Api::Shop(找到了) -> Shop,然后继续自动加载常量 Event,顺序:Api::Shop::Event -> API::Event -> Event,最终没找到,抛出异常

生产环境:

因为生产环境预先把所有 autoload 目录下的文件都加载过了,这里没有触发 const_missing,使用的是 Ruby 自身的常量查找。查找顺序是:Api::BooksController::Shop -> Shop(找到了)(这里没有 Api::Shop 是因为你定义 controller 的方式没有开启新的命名空间)。然后加载 Event 常量,得到 Shop::Event。

要彻底解决这类问题的话,我认为最好不要在自动加载类中使用限定常量(Rails 对于限定常量的加载处理不太好理解)。

首先把 controller 的限定常量改成相对常量定义:

module Api
  class BooksController < ApplicationController # 使用相对常量代替限定常量
    ...
  end
end

那么两种加载方式就一致了,生产环境也会报错了。解决这第二个问题的话,把另外一个限定常量改成绝对常量就好了。

# app/controllers/api/books_controller.rb
module Api
  class BooksController < ApplicationController # 使用相对常量代替限定常量
    def self.test
      ::Shop::Event.test # 使用绝对常量代替限定常量
    end
  end
end
pinewong 回复

感谢!

Dounx 关闭了讨论。 08月27日 14:08
Dounx 重新开启了讨论。 08月05日 14:16
Dounx 关闭了讨论。 08月05日 14:16
需要 登录 后方可回复, 如果你还没有账号请 注册新账号