Rails не загружает классы при десериализации объектов YAML/Marshal
- Rails: 3.0.3
- Рубин: 1.9.2
Попытка десериализации очень простого объекта с помощью YAML.load
или же Marshal.load
создает поврежденный объект, потому что принадлежащий ему класс не требуется в процессе десериализации.
Пример:
# app/models/my_model.rb
class MyModel
attr_accessor :id
end
# test/unit/serializing_test.rb
require 'test_helper'
class SerializingTest < Test::Unit::TestCase
def test_yaml_serialize_structure
my_model = MyModel.new
my_model.id = 'my model'
File.open( "#{Rails.root}/tmp/object.yml" , 'w' ) do |f|
YAML::dump(my_model, f)
end
end
def test_yaml_deserialize_structure
object = YAML.load_file "#{Rails.root}/tmp/object.yml"
assert( object.instance_of? MyModel )
assert_equal( 'my model', object.id )
end
end
С помощью этого кода мы можем запустить этот сеанс консоли оболочки без каких-либо ошибок:
$ ruby -Itest test/unit/serializing_test.rb -n test_yaml_serialize_structure
$ ruby -Itest test/unit/serializing_test.rb -n test_yaml_deserialize_structure
Но если я запускаю вызовы десериализации из консоли Rails, объект не десериализуется должным образом, потому что класс никогда не требуется:
$ rails c
ruby-1.9.2-p0 > object = YAML.load_file "#{Rails.root}/tmp/object.yml"
=> #<Syck::Object:0x0000010322ea30 @class="MyModel", @ivars={"id"=>"my model"}>
Я знаю, что единственная проблема состоит в том, что класс не требуется, потому что, если я требую это вручную, все работает:
ruby-1.9.2-p0 > require "#{Rails.root}/app/models/my_model"
=> ["MyModel"]
ruby-1.9.2-p0 > object = YAML.load_file "#{Rails.root}/tmp/object.yml"
=> #<MyModel:0x0000010320c8e0 @id="my model">
Я представил только примеры YAML, но с маршалом все почти так же.
Также скажем, что хотя я воспроизводил проблему в консоли Rails, изначально эта проблема сводила меня с ума при обычном запросе к моему приложению.
Итак, вопрос в том, как я могу десериализовать объекты в Rails, не требуя вручную всех моих классов?
Спасибо
е.
6 ответов
Что ж, после прочтения @tadman и нескольких ответов, которые я получил в списке рассылки испанского языка ror [1], я собрал несколько полезных советов, когда вам нужно разобраться с десериализацией Ruby и загрузкой классов в Rails:
Супер быстрое решение
использование config.cache_classes = true
в вашем development.rb
но вы потеряете класс автообновления.
Лучшее решение
Требовать все классы, которые будут десериализованы, но не с require
но с require_dependency
[2] поэтому в среде разработки класс автообновления будет работать.
Элегантное решение
Обезьяна-патч YAML и самоцвет Маршала, чтобы сказать им, чтобы позвонить require_dependency
когда они находят неопределенный класс для десериализации.
И @Xavi прислал мне предложение обезьяны-патча Marshal
(он говорит, что написал его в эфире, и он не тестировался, поэтому используйте его на свой страх и риск) [3]
Я описал эту "проблему" на GitHub: https://github.com/rails/rails/issues/1585
Автоматически требовать классы для загрузки YAML способом, который предлагает @fguillen, изящно, я написал этот короткий monkey-patch.
Он просто пытается требовать_зависимости любого класса, который класс Psych ToRuby разрешает в классы.
У меня работает в сериализованной Active Record, которая хранит экземпляр пользовательского класса, YMMV.
module Psych::Visitors
ToRuby.class_eval do
alias :resolve_class_without_autoload :resolve_class
def resolve_class klassname
begin
require_dependency klassname.underscore
rescue NameError, LoadError
end
resolve_class_without_autoload klassname
end
end
end
Мне пришлось немного адаптировать ответ @ben-patterson, чтобы он работал (используя Rails 5.0.2):
module Psych::Visitors
ToRuby.class_eval do
def resolve_class(klassname)
begin
class_loader.load klassname
rescue ArgumentError
require_dependency klassname.underscore
klassname.constantize
end
end
end
end
Насколько я знаю, и YAML, и Marshal не используют автозагрузчик Rails. Вы должны идти вперед и предварительно загружать любые классы, которые могут потребоваться для десериализации.
Это немного, если суетиться, особенно в среде разработки, где почти ничего не загружается, прежде чем это необходимо.
В версии rails 7 патч обезьяны должен перехватывать ошибку Psych::DisallowedClass вместо ArgumentError или NameError, LoadError, как это было предложено @ben-patterson и @panzi.
module Psych::Visitors
ToRuby.class_eval do
def resolve_class(klassname)
begin
class_loader.load klassname
rescue Psych::DisallowedClass => e
require_dependency klassname.underscore
klassname.constantize
end
end
end
end