Во-первых, определение has_many
изменилось между Rails 4 и Rails 5. Начнем с Rails 4.
Rails 4
Вы правы, что здесь происходит что-то подозрительное.Rails обманывает.Я собираюсь догадаться о обратной совместимости со старыми версиями Ruby, которые не имели такой богатый синтаксис аргументов, как сейчас.
name
, scope
и options
- это позиционные аргументы.Мы можем определить has_many
, чтобы увидеть, что происходит.
def has_many(name, scope = nil, options = {}, &extension)
puts "name: #{name}"
puts "scope: #{scope}"
puts "options: #{options}"
puts "extension: #{extension}"
end
Если мы запустим has_many :subscribers, through: :subscriptions, source: :user
...
name: subscribers
scope: {:through=>:subscriptions, :source=>:user}
options: {}
extension:
Ну, это не так. Давайте посмотрим на его источник ...
def has_many(name, scope = nil, options = {}, &extension)
reflection = Builder::HasMany.build(self, name, scope, options, &extension)
Reflection.add_reflection self, name, reflection
end
Аргументы передаются в Builder::HasMany.build
, который вызывает create_builder model, name, scope, options, &block
.Это вызывает new(model, name, scope, options, &block)
, чтобы создать новый экземпляр. В его инициализаторе мы находим это ...
def initialize(model, name, scope, options)
# TODO: Move this to create_builder as soon we drop support to activerecord-deprecated_finders.
if scope.is_a?(Hash)
options = scope
scope = nil
end
...
Так что это просто обман.Если scope
является хешем, он переключается на options
.Это довольно неприятный хак, видя, как неправильные аргументы проходят через несколько уровней вызовов методов, прежде чем они исправлены.
Rails 5
Rails 5 изменил сигнатуру has_many
.
def has_many(name, scope = nil, **options, &extension)
puts "name: #{name}"
puts "scope: #{scope}"
puts "options: #{options}"
puts "extension: #{extension}"
end
has_many :subscribers, through: :subscriptions, source: :user
И теперь все работает как положено.
name: subscribers
scope:
options: {:through=>:subscriptions, :source=>:user}
extension:
**
преобразует аргументы ключевых слов в хеш , и это заставляет его работать.
Когда вы звоните has_many :subscribers, through: :subscriptions, source: :user
, вот что происходит.
- Руби нужен первый позиционный
name
, поэтому он :subscribers
. - Ruby требуется второй позиционный
scope
, но он исчерпан позиционным, остальные аргументы являются ключевыми словами, поэтому он использует значение по умолчанию nil
. - Ruby sluПоднимает ключевые слова и помещает их в хэш
options
. extension
является аргументом блока, и это хорошо, если они не включены.
Вы можете прочитать больше окак происходит обработка аргументов в Ruby в документе Calling Methods .