Проблема с переопределением сеттера в ActiveRecord
Это не вопрос, а скорее отчет о том, как я решил проблему с write_attribute
, когда атрибут является объектом, в Rails 'Active Record
. Я надеюсь, что это может быть полезно для других, сталкивающихся с одной и той же проблемой.
Позвольте мне объяснить пример. Предположим, у вас есть два класса: Book
и Author
:
class Book < ActiveRecord::Base
belongs_to :author
end
class Author < ActiveRecord::Base
has_many :books
end
Очень просто. Но по какой-либо причине вам нужно переопределить метод Author
= на Book
. Поскольку я новичок в Rails, я следовал предложению Сэма Руби на Agile Web Development с Rails: используйте частный метод attribute_writer
. Итак, моя первая попытка:
class Book < ActiveRecord::Base
belongs_to :author
def author=(author)
author = Author.find_or_initialize_by_name(author) if author.is_a? String
self.write_attribute(:author, author)
end
end
К сожалению, это не работает. Что я получаю от консоли:
>> book = Book.new(:name => "Alice Adventures in Wonderland", :pub_year => 1865)
=> #<Book id: nil, name: "Alice Adventures in Wonderland", pub_year: 1865, author_id: nil, created_at: nil, updated_at: nil>
>> book.author = "Lewis Carroll"
=> "Lewis Carroll"
>> book
=> #<Book id: nil, name: "Alice Adventures in Wonderland", pub_year: 1865, author_id: nil, created_at: nil, updated_at: nil>
>> book.author
=> nil
Кажется, что Rails не признает, что это объект и ничего не делает: после атрибуции автор все еще ноль! Конечно, я мог бы попробовать write_attribute(:author_id, author.id)
, но это не помогает, когда автор еще не сохранен (он по-прежнему не имеет идентификатора!), И мне нужно, чтобы объекты сохранялись вместе (автор должен быть сохранен, только если книга действительна).
После многого поиска решения (и попробуйте много других вещей напрасно), я нашел это сообщение: http://groups.google.com/group/rubyonrails-talk/browse_thread/thread/4fe057494c6e23e8, поэтому наконец, у меня мог быть рабочий код:
class Book < ActiveRecord::Base
belongs_to :author
def author_with_lookup=(author)
author = Author.find_or_initialize_by_name(author) if author.is_a? String
self.author_without_lookup = author
end
alias_method_chain :author=, :lookup
end
На этот раз консоль была приятна для меня:
>> book = Book.new(:name => "Alice Adventures in Wonderland", :pub_year => 1865)
=> #<Book id: nil, name: "Alice Adventures in Wonderland", pub_year: 1865, author_id: nil, created_at: nil, updated_at: nil>
>> book.author = "Lewis Carroll"=> "Lewis Carroll"
>> book
=> #<Book id: nil, name: "Alice Adventures in Wonderland", pub_year: 1865, author_id: nil, created_at: nil, updated_at: nil>
>> book.author
=> #<Author id: nil, name: "Lewis Carroll", created_at: nil, updated_at: nil>
Трюк здесь alias_method_chain
, который создает перехватчик (в данном случае author_with_lookup
) и альтернативное имя для старого установщика (author_without_lookup
). Я признаюсь, что потребовалось некоторое время, чтобы понять эту договоренность, и я был бы рад, если кто-то захочет объяснить это подробно, но меня удивило отсутствие информации об этой проблеме. Мне нужно много google, чтобы найти только одно сообщение, которое по названию казалось первоначально не связанным с проблемой. Я новичок в Rails, так что вы думаете, ребята: это плохая практика?
Ответы
Ответ 1
Я рекомендую создать виртуальный атрибут вместо переопределения метода author=
.
class Book < ActiveRecord::Base
belongs_to :author
def author_name=(author_name)
self.author = Author.find_or_initialize_by_name(author_name)
end
def author_name
author.name if author
end
end
Затем вы можете делать классные вещи, например, применять его к полю формы.
<%= f.text_field :author_name %>
Будет ли это работать для вашей ситуации?
Ответ 2
Когда вы переопределяете аксессор, вы должны установить фактический атрибут БД для write_attribute
и self[:the_attribute]=
, а не имя атрибута, сгенерированного ассоциацией, который вы переопределяете. Это работает для меня.
require 'rubygems'
require 'active_record'
ActiveRecord::Base.establish_connection(:adapter => "sqlite3", :dbfile => ":memory:")
ActiveRecord::Schema.define do
create_table(:books) {|t| t.string :title }
create_table(:authors) {|t| t.string :name }
end
class Book < ActiveRecord::Base
belongs_to :author
def author=(author_name)
found_author = Author.find_by_name(author_name)
if found_author
self[:author_id] = found_author.id
else
build_author(:name => author_name)
end
end
end
class Author < ActiveRecord::Base
end
Author.create!(:name => "John Doe")
Author.create!(:name => "Tolkien")
b1 = Book.new(:author => "John Doe")
p b1.author
# => #<Author id: 1, name: "John Doe">
b2 = Book.new(:author => "Noone")
p b2.author
# => #<Author id: nil, name: "Noone">
b2.save
p b2.author
# => #<Author id: 3, name: "Noone">
Я настоятельно рекомендую сделать то, что предлагает Райан Бэйтс; создайте новый атрибут author_name
и оставьте методы, сгенерированные ассоциацией, такими, какие они есть. Меньше путаницы, меньше путаницы.
Ответ 3
Я решил эту проблему, используя alias_method
class Book < ActiveRecord::Base
belongs_to :author
alias_method :set_author, :author=
def author=(author)
author = Author.find_or_initialize_by_name(author) if author.is_a? String
set_author(author)
end
end