2009-08-15 28 views
11

Đây không phải là một câu hỏi, đó là một báo cáo về cách tôi giải quyết vấn đề với write_attribute khi thuộc tính là một đối tượng, trên Rails 'Active Record. Tôi hy vọng điều này có thể hữu ích cho những người khác phải đối mặt với cùng một vấn đề.Vấn đề với ghi đè setter trên ActiveRecord

Hãy để tôi giải thích bằng ví dụ. Giả sử bạn có hai lớp, BookAuthor:

class Book < ActiveRecord::Base 
    belongs_to :author 
end 

class Author < ActiveRecord::Base 
    has_many :books 
end 

Rất đơn giản. Nhưng, vì lý do gì đó, bạn cần ghi đè phương thức author = trên Book. Vì tôi mới sử dụng Rails, tôi đã theo đề xuất của Sam Ruby về Phát triển Web Agile với Rails: sử dụng phương thức riêng tư attribute_writer. Vì vậy, lần thử đầu tiên của tôi là:

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 

Thật không may, điều này không có tác dụng. Đó là những gì tôi nhận được từ bảng điều khiển:

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

Dường như Rails không nhận ra nó là một đối tượng và không có gì: sau khi attribuition, tác giả vẫn còn không! Tất nhiên, tôi có thể thử write_attribute(:author_id, author.id), nhưng nó không giúp đỡ khi tác giả chưa được lưu (nó vẫn không có id!) Và tôi cần các đối tượng được lưu lại với nhau (tác giả phải được lưu chỉ khi sách hợp lệ).

Sau khi tìm kiếm rất nhiều cho một giải pháp (và thử nhiều thứ khác một cách vô ích), tôi thấy tin nhắn này: http://groups.google.com/group/rubyonrails-talk/browse_thread/thread/4fe057494c6e23e8, vì vậy cuối cùng tôi có thể có một số mã làm việc:

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 

thời gian này, giao diện điều khiển là tốt với tôi:

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

Bí quyết ở đây là alias_method_chain, tạo ra một đánh chặn (trong trường hợp này author_with_lookup) và một cái tên thay thế cho các setter cũ (author_without_lookup). Tôi thú nhận phải mất một thời gian để hiểu sự sắp xếp này và tôi rất vui nếu có ai đó quan tâm giải thích chi tiết, nhưng điều làm tôi ngạc nhiên là thiếu thông tin về vấn đề này. Tôi phải google rất nhiều để tìm thấy chỉ là một bài, mà theo tiêu đề dường như ban đầu không liên quan đến vấn đề. Tôi mới làm quen với Rails, vậy bạn nghĩ gì về chàng trai: đây có phải là một hành động tồi tệ?

Trả lời

20

Tôi khuyên bạn nên tạo thuộc tính ảo thay vì ghi đè phương pháp 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 

Sau đó, bạn có thể làm những việc tuyệt vời như áp dụng nó vào trường biểu mẫu.

<%= f.text_field :author_name %> 

Điều này có phù hợp với hoàn cảnh của bạn không?

+0

Tôi nghĩ để làm điều này, nhưng tôi không muốn thuộc tính trùng lặp. Phương pháp tôi đề xuất ở trên đang hoạt động rất tốt; Tôi chỉ muốn chia sẻ nó. Tôi thực sự có thể làm cho các trick text_field với nó. Nhưng cảm ơn bạn đã trả lời! = D –

+2

Sẽ không chính xác hơn nếu thay thế phương thức 'author_name' bằng' delegate: name,: to =>: author,: prefix => true'? –

+2

@Adam, Đó chắc chắn là một cách thay thế để làm điều đó. Tôi thường chỉ sử dụng 'delegate' khi giao dịch với nhiều phương thức. Nếu chỉ có một tôi thích xác định phương pháp trực tiếp bởi vì tôi cảm thấy nó rõ ràng hơn. – ryanb

6

Khi bạn ghi đè trình truy cập, bạn phải đặt thuộc tính DB thực cho write_attributeself[:the_attribute]= chứ không phải tên thuộc tính do liên kết tạo mà bạn ghi đè. Điều này làm việc cho tôi.

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

Tôi thực sự khuyên bạn nên làm những gì Ryan Bates gợi ý; tạo một thuộc tính mới author_name và để lại các phương thức liên kết được tạo như chúng. Ít mờ, ít nhầm lẫn hơn.

+0

Như tôi đã nói ở trên, phương pháp bạn đề xuất chỉ hoạt động nếu tác giả được lưu (tức là có id), đó không phải là trường hợp của tôi. Tôi có thể có một tác giả mới chỉ được lưu khi cuốn sách được lưu. –

+0

Tôi đã viết lại một chút dựa trên nhận xét của bạn. Bây giờ nó có ý nghĩa hơn không? –

+0

Oh cảm ơn, bây giờ điều này hoạt động như tôi mong đợi. Nhưng (mặc dù tất cả các khuyến nghị) tôi sẽ giữ lại giải pháp ban đầu. Tuy nhiên, đó là một lựa chọn tốt nếu tôi thay đổi ý định một ngày nào đó. =] –

0

tôi giải quyết vấn đề này bằng 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 
Các vấn đề liên quan