2009-08-15 5 views
11

To nie jest dokładnie pytanie, raczej raport o tym, jak rozwiązałem problem z write_attribute, gdy atrybut jest obiektem, na Railsach 'Active Record. Mam nadzieję, że może to być użyteczne dla innych osób borykających się z tym samym problemem.Problem z korektorem ustawiającym w ActiveRecord

Pozwolę sobie wyjaśnić na przykładzie. Załóżmy, że masz dwie klasy, Book i Author:

class Book < ActiveRecord::Base 
    belongs_to :author 
end 

class Author < ActiveRecord::Base 
    has_many :books 
end 

bardzo proste. Ale z jakiegokolwiek powodu musisz zastąpić metodę author = na Book. Ponieważ jestem nowy w Rails, postępowałem zgodnie z sugestią Sam Ruby dotyczącą Agile Web Development z Railsami: używaj prywatnej metody attribute_writer. Tak więc, moja pierwsza próba to:

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 

Niestety, to nie działa. To, co otrzymuję od konsoli:

>> book = Book.new(:name => "Alice's Adventures in Wonderland", :pub_year => 1865) 
=> #<Book id: nil, name: "Alice's 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's Adventures in Wonderland", pub_year: 1865, author_id: nil, created_at: nil, updated_at: nil> 
>> book.author 
=> nil 

Wydaje się, że Rails nie rozpoznaje to jest obiekt i robi nic: po attribuition, autor nadal jest zerowa! Oczywiście, mógłbym spróbować write_attribute(:author_id, author.id), ale to nie pomaga, gdy autor nie jest jeszcze zapisany (wciąż nie ma id!) I potrzebuję, aby obiekty były zapisywane razem (autor musi być zapisany tylko jeśli książka jest ważna).

Po szukać dużo rozwiązania (i spróbować wielu innych rzeczy na próżno), znalazłem tę wiadomość: http://groups.google.com/group/rubyonrails-talk/browse_thread/thread/4fe057494c6e23e8, więc w końcu mogę miał jakiś działający kod:

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 

tym czasie, konsola była miło mi:

>> book = Book.new(:name => "Alice's Adventures in Wonderland", :pub_year => 1865) 
=> #<Book id: nil, name: "Alice's 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's 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> 

Sztuką jest tu alias_method_chain, że tworzy przechwytywania (w tym przypadku author_with_lookup) i alternatywnej nazwy do starego seter (author_without_lookup). Przyznaję, że zajęło to trochę czasu, aby zrozumieć tę aranżację i byłbym zadowolony, gdyby ktoś chciał wyjaśnić to szczegółowo, ale zaskoczyło mnie to, że nie ma informacji na temat tego rodzaju problemu. Muszę dużo google znaleźć tylko jeden post, który według tytułu wydawał początkowo niezwiązany z problemem. Jestem nowy w Rails, więc jak myślisz: czy to jest zła praktyka?

Odpowiedz

20

Polecam tworzenie wirtualnego atrybutu zamiast przesłonięcia metody 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 

Następnie można zrobić fajne rzeczy, np. Zastosować je do pola formularza.

<%= f.text_field :author_name %> 

Czy to zadziałało w Twojej sytuacji?

+0

myślałem, aby to, ale nie chciałem zduplikowane atrybuty. Metoda, którą zaproponowałem powyżej, działa bardzo dobrze; Po prostu chciałem się nim podzielić. Rzeczywiście mogę zrobić z nim sztuczkę text_field. Ale dzięki za odpowiedź! = D –

+2

Czy nie byłoby lepiej zastąpić metodę 'author_name' przez' delegate: name,: to =>: author,: prefix => true'? –

+2

@Adam, To z pewnością jest alternatywny sposób na zrobienie tego. Zwykle używam tylko 'delegate', gdy mamy do czynienia z wieloma metodami. Jeśli jest tylko jeden, wolę bezpośrednio zdefiniować metodę, ponieważ uważam, że jest bardziej przejrzysta. – ryanb

6

Po nadpisaniu akcesora należy ustawić rzeczywisty atrybut bazy danych dla write_attribute i self[:the_attribute]=, a nie nazwę wygenerowanego atrybutu, który nadpisuje. To działa dla mnie.

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

Gorąco polecam robić to, co sugeruje Ryan Bates; utwórz nowy atrybut author_name i pozostaw skojarzenie generowane metodami takimi, jakimi są. Mniej zamieszania, mniej zamieszania.

+0

Jak już powiedziałem, proponowana metoda działa tylko wtedy, gdy autor jest zapisany (tzn. Ma identyfikator), co nie jest moim przypadkiem. Mogę mieć nowego autora, który musi być zapisany tylko wtedy, gdy książka jest zapisana. –

+0

Przepisałem go trochę na podstawie Twojego komentarza. Czy teraz ma to więcej sensu? –

+0

Och, dzięki, teraz to działa tak jak oczekuję. Ale (pomimo wszystkich zaleceń) zachowam oryginalne rozwiązanie. Mimo to jest to dobra alternatywa, jeśli pewnego dnia zmienię zdanie. =] –

0

I rozwiązać ten problem przy użyciu 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