2009-08-15 17 views
11

Esto no es exactamente una pregunta, es más bien un informe sobre cómo resolví un problema con write_attribute cuando el atributo es un objeto, en Rails 'Active Record. Espero que esto pueda ser útil para otros que enfrentan el mismo problema.Problema con la anulación setter en ActiveRecord

Déjenme explicarlo con un ejemplo. Suponga que tiene dos clases, Book y Author:

class Book < ActiveRecord::Base 
    belongs_to :author 
end 

class Author < ActiveRecord::Base 
    has_many :books 
end 

muy simple. Pero, por alguna razón, debe anular el author = método en Book. Como soy nuevo en Rails, he seguido la sugerencia de Sam Ruby sobre Desarrollo web ágil con Rails: use el método privado attribute_writer. Por lo tanto, mi primer intento fue:

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 

Lamentablemente, esto no funciona. Eso es lo que me pasa desde la consola:

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

Parece que los carriles no reconoce que es un objeto y no hace nada: después de la attribuition, autor sigue siendo nula! Por supuesto, podría probar write_attribute(:author_id, author.id), pero no ayuda cuando el autor aún no está guardado (¡aún no tiene una identificación!) Y necesito que los objetos se guarden juntos (el autor debe guardarse solo si el libro es válido).

Después de buscar mucho para una solución (y tratar muchas otras cosas en vano), me encontré con este mensaje: http://groups.google.com/group/rubyonrails-talk/browse_thread/thread/4fe057494c6e23e8, por lo que finalmente podría tenido algún código de trabajo:

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 

Esta vez, la consola era agradable para mí:

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

el truco aquí es el alias_method_chain, que crea un interceptor (en este caso author_with_lookup) y un nombre alternativo a la edad colocador (author_without_lookup). Confieso que me tomó un tiempo entender este arreglo y me alegraría que alguien se ocupe de explicarlo en detalle, pero lo que me sorprendió fue la falta de información sobre este tipo de problema. Tengo que buscar mucho en Google para encontrar una sola publicación, que por el título parecía inicialmente no relacionada con el problema. Soy nuevo en Rails, ¿qué opinas, chicos? ¿Es una mala práctica?

Respuesta

20

Recomiendo crear un atributo virtual en lugar de anular el método 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 

Entonces podría hacer cosas geniales como aplicarlo a un campo de formulario.

<%= f.text_field :author_name %> 

¿Funcionaría para su situación?

+0

pensé que hacer esto, pero yo no quería atributos duplicados. El método que propuse arriba está funcionando muy bien; Solo quería compartirlo. De hecho, puedo hacer el truco text_field con él. ¡Pero gracias por tu respuesta! = D –

+2

¿No sería más correcto reemplazar el método 'author_name' con' delegate: name,: to =>: author,: prefix => true'? –

+2

@ Adam, esa es ciertamente una forma alternativa de hacerlo. Usualmente solo uso 'delegate' cuando trato con múltiples métodos. Si solo hay uno, prefiero definir el método directamente porque creo que es más claro. – ryanb

6

Cuando reemplaza el descriptor de acceso, debe establecer un atributo de base de datos real para write_attribute y self[:the_attribute]=, y no el nombre del atributo generado por asociación que está anulando. Esto funciona para mí

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

Recomiendo encarecidamente hacer lo que Ryan Bates sugiere, sin embargo; cree un nuevo atributo author_name y deje los métodos generados por la asociación tal como están. Menos fuzz, menos confusión.

+0

Como dije anteriormente, el método que propuso funciona solo si el autor está guardado (es decir, tiene una identificación), que no es mi caso. Puedo tener un nuevo autor que se debe guardar solo cuando el libro se guarda también. –

+0

Lo reescribí un poco según tu comentario. ¿Tiene más sentido ahora? –

+0

Gracias, ahora esto funciona como espero. Pero (a pesar de todas las recomendaciones) mantendré la solución original. Sin embargo, es una buena alternativa si cambio de parecer algún día. =] –

0

He resuelto este problema utilizando 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 
Cuestiones relacionadas