12/08/2018, 12:32

Phân tích lỗi xảy ra khi trùng class name trong thư mục `lib` của Rails

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 ...

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à:

  1. Nhận yêu cầu về SuperTool
  2. Do chưa được load, nên Rails sẽ thực hiện tìm kiếm super_tool.rb
  3. lib được ưu tiên tìm
  4. lib/my_tools/super_tool.rb được tìm thấy
  5. 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ủa SuperTool
  6. 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.

0