Phân tích lỗi xảy ra khi trùng class name trong thư mục `lib` của Rails
Bài đăng này đã không được cập nhật trong 9 năm
Trong một dự án, mình từng gặp trường hợp khi đặt tên class là Error:Api
thì bị báo lỗi, nhưng khi đổi tên thành Error:Response
thì lại không còn lỗi nữa. Lúc đó không biết nguyên nhân tại sao, chỉ note lại để tìm hiểu khi có thời gian. Sau này thì khách hàng đã tìm ra lý do và viết bài hướng dẫn trên quiita. Mình xin phép được dịch lại bài viết đó (honho)
Thông thường thì mọi người đều đặt các libraries ở trong thư mục lib, tuy nhiên nếu cứ chỉ định autoload_paths
không suy nghĩ gì thì rất có thể sẽ gặp lỗi không mong muốn.
Cụ thể là hiện tượng phát sinh lỗi khi trùng class name mặc dù đã chia directory và truyền namespace khác nhau.
Nếu đã từng đọc bài viết này thì tôi nghĩ gặp lỗi này sẽ biết cách giải quyết ngay (hoho).
Mở đầu
Tôi thường thấy nhất ba cách sau đây để thêm lib
trong Rails
(đều là khai báo trong file config/application.rb
nhé) :
config.autoload_paths += %W(#{config.root}/lib)
config.autoload_paths += Dir["#{config.root}/lib/**/"]
config.autoload_paths += %W(#{config.root}/lib)
config.autoload_paths += Dir["#{config.root}/lib/**/"]
Nếu có một file trong directory bất kỳ thuộc lib
và một file nằm ngay trong app/xxx
bị trùng tên, thì với cách khai báo thứ 2 và 3 sẽ bị phát sinh lỗi trùng filename và chết lăn quay (haha).
Ví dụ
Với cách 2 hoặc cách 3 như trên,
$ tree app/models/
app/models/
├── super_tool.rb # SuperTool
└── ultra_tool.rb # UltraTool
$ tree lib/
lib/
├── assets
├── my_tools
│ ├── super_tool.rb # MyTools::SuperTool
│ └── ultra_tool.rb # MyTools::UltraTool
└── tasks
sẽ phát sinh lỗi
[1] pry(main)> SuperTool
LoadError: Unable to autoload constant SuperTool, expected /my_app/lib/my_tools/super_tool.rb to define it
Rõ ràng đã chia directory, sử dụng namespace khác nhau vậy nhưng vẫn cứ lỗi..
Phân tích
Nội dung lỗi
Theo nội dung lỗi thì SuperTool
đang được mong muốn định nghĩa ở lib/my_tools/super_tool.rb
nhưng thực tế trong file đó lại đang định nghĩa theo cấu trúc thư mục (MyTools::SuperTool
) nên phát sinh lỗi.
Mà ngay từ đầu tôi gọi SuperTool
là muốn lấy từ app/models/super_tool.rb
đấy chứ (khoc).
Vậy có lẽ trình tự xử lý là:
- Nhận yêu cầu về
SuperTool
- Do chưa được load, nên Rails sẽ thực hiện tìm kiếm
super_tool.rb
lib
được ưu tiên tìmlib/my_tools/super_tool.rb
được tìm thấy- Vì một lý do gì đó mà Rails lại ko để ý đến directory
my_tools
mà lại đi tìm định nghĩa củaSuperTool
- Thực tế định nghĩa là
MyTools::SuperTool
nên báo lỗi
Tôi nghĩ mọi thứ đã xảy ra như vậy đấy.
Chúng ta thử xem nguyên nhân là gì nhé.
Autoload Paths (thứ tự ưu tiên)
Đầu tiên, xem thử trình tự hiện tại ra sao nào.
$ bin/rails r 'puts ActiveSupport::Dependencies.autoload_paths'
/my_app/lib
/my_app/lib/
/my_app/lib/assets/
/my_app/lib/tasks/
/my_app/lib/my_tools/
/my_app/app/assets
/my_app/app/controllers
/my_app/app/helpers
/my_app/app/mailers
/my_app/app/models
/my_app/app/controllers/concerns
/my_app/app/models/concerns
Đúng như suy nghĩ phía trên, lib
có độ ưu tiên cao nhất.
Sau khi thử các kiểu thì tôi thấy vấn đề không nằm ở mức độ ưu tiên khi tìm kiếm, vậy tại sao my_tools
trong my_tools/super_tool.rb
lại bị bỏ qua chứ ???
Autoload Paths (directory)
Nhìn lại một lần nữa vào các paths được liệt kê ở trên, đột nhiên tôi thấy gì đó hơi lạ.
/my_app/app/models
/my_app/app/models/concerns
Ủa, hình như không thấy Models::Hogeable
bên trong thư mục concerns
được liệt kê ra ?!
Liếc lại lên trên nào:
/my_app/lib
/my_app/lib/my_tools/
Cũng giống như ở trường hợp Concerns
thì các files trong my_tools
không cần phải khai báo kiểu như MyTools::Hoge
cũng được. Vậy là Rails sẽ mong muốn trong my_tools/super_tool.rb
có khai báo về SuperTool
.
Hoá ra đây chính là nguyên nhân gốc rễ của lỗi này !! (honho)
Rails lấy điểm xuất phát là các directories được liệt kê trong Autoload Paths, từ đó sử dụng các quy tắc liên quan đến cấu trúc thư mục, file name, class name..
Nguyên nhân
Là do dòng sau trong file config/application.rb
config.autoload_paths += Dir["#{config.root}/lib/**/"]
Chính việc nhét (hồi quy) tất cả các path trong lib
vào Autoload Paths đã gây ra hiện tượng lỗi trên.
Giải quyết
Nếu chúng ta chỉ khai báo như sau trong config/application.rb
thì sao
config.autoload_paths += %W(#{config.root}/lib)
Kết quả đây (honho)
$ bin/rails r 'puts ActiveSupport::Dependencies.autoload_paths'
/my_app/lib
/my_app/app/assets
/my_app/app/controllers
/my_app/app/helpers
/my_app/app/mailers
/my_app/app/models
/my_app/app/controllers/concerns
/my_app/app/models/concerns
(yeah3).. gọi thử SuperTool
xem nào
[1] pry(main)> SuperTool
class SuperTool < ActiveRecord::Base {
:id => :integer,
:description => :string,
:content_id => :integer,
:created_at => :datetime,
:updated_at => :datetime,
}
DONE !
Kết luận
Hãy sử dụng 1 trong 2 dòng sau trong config/application.rb
:
config.autoload_paths << Rails.root.join("lib")
config.autoload_paths += %W(#{config.root}/lib)
Thế là đủ.
Ví dụ khi cấu trúc thư mục bên trong lib
như sau:
$ tree lib/
lib/
├── assets
├── my_tools
│ ├── super_tool.rb
│ └── ultra_tool.rb
└── tasks
thì class name lần lượt là MyTools::SuperTool
và MyTools::UltraTool
.
Không quan tâm tới quy tắc về cấu trúc thư mục và class name thì sao?
Lúc đó hãy sử dụng 1 dòng sau thôi nhé:
config.autoload_paths += Dir["#{config.root}/lib/**/"]
(trong file config/application.rb
)
Lúc nãy thì class name trong super_tool.rb
chỉ là SuperTool
, để sử dụng cũng chỉ cần gọi SuperTool
thôi.
All rights reserved