2012-04-27 10 views
8

Estoy tratando de hacer que un setter de atributos en un modelo ActiveRecord ajuste su valor en la función text2ltree() postgres antes de que rails genere su consulta sql.Cómo hacer que el setter de atributos envíe valor a través de la función SQL

Por ejemplo,

post.path = "1.2.3" 
post.save 

debe generar algo así como

UPDATE posts SET PATH=text2ltree('1.2.3') WHERE id = 123 # or whatever 

Cuál es la mejor manera de hacer esto?

+0

Puede escribir una función Postgres (procedimiento almacenado) y luego simplemente llamar a 'SELECT myfunc ('1.2.3')'. Puedo proporcionar un ejemplo si estás interesado en esa ruta. –

Respuesta

2

EDIT: Para conseguir exactamente lo que busca más arriba, tendrá que utilizar esto para anular la incubadora por defecto en el archivo de modelo:

def path=(value) 
    self[:path] = connection.execute("SELECT text2ltree('#{value}');")[0][0] 
end 

A continuación, el código que tiene por encima de las obras.

Estoy interesado en aprender más sobre el funcionamiento interno de ActiveRecord y sus fundamentos de metaprogramación impenetrables, así que como ejercicio intenté lograr lo que describió en sus comentarios a continuación. He aquí un ejemplo que trabajó para mí (esto es todo en post.rb): salida

module DatabaseTransformation 
    extend ActiveSupport::Concern 

    module ClassMethods 
    def transformed_by_database(transformed_attributes = {}) 

     transformed_attributes.each do |attr_name, transformation| 

     define_method("#{attr_name}=") do |argument| 
      transformed_value = connection.execute("SELECT #{transformation}('#{argument}');")[0][0] 
      write_attribute(attr_name, transformed_value) 
     end 
     end 
    end 
    end 
end 

class Post < ActiveRecord::Base 
    attr_accessible :name, :path, :version 
    include DatabaseTransformation 
    transformed_by_database :name => "length" 

end 

Consola:

1.9.3p194 :001 > p = Post.new(:name => "foo") 
    (0.3ms) SELECT length('foo'); 
=> #<Post id: nil, name: 3, path: nil, version: nil, created_at: nil, updated_at: nil> 

En la vida real supongo que querría include el módulo en ActiveRecord: : Base, en un archivo en algún lugar anterior de la ruta de carga. También tendría que manejar adecuadamente el tipo de argumento que está pasando a la función de base de datos. Finalmente, descubrí que cada adaptador de base de datos implementa connection.execute, por lo que la forma de acceder al resultado puede ser diferente en Postgres (este ejemplo es SQLite3, donde el conjunto de resultados se devuelve como una matriz de hashes y la clave del primer registro de datos . es 0]

Este blog fue increíblemente útil:

http://www.fakingfantastic.com/2010/09/20/concerning-yourself-with-active-support-concern/

al igual que los carriles de guía para plug-in-autoría:

http://guides.rubyonrails.org/plugins.html

Además, por lo que vale, creo que en Postgres aún haría esto usando una migración para crear una regla de reescritura de consultas, pero esto resultó en una gran experiencia de aprendizaje. Espero que funcione y pueda dejar de pensar en cómo hacerlo ahora.

+1

Una forma completamente diferente de hacer esto que acabo de pensar sería usar el motor de reescritura de consultas de Postgresql. Un poco como usar un disparador. Debería configurar la regla para reescribir las actualizaciones de modo que el valor se pasara a través de la función. Lo bueno es que su modelo está limpio y solo hace una llamada a la base de datos. La desventaja es que no es una base de datos agnóstica, lo que obviamente no puede ser un problema en su caso. No estoy lo suficientemente familiarizado con la sintaxis para publicar un ejemplo, pero aquí hay un enlace a los documentos: http://www.postgresql.org/docs/8.4/static/sql-createrule.html –

+0

Creo que aquí debería haber una manera más eficiente de hacer esto que involucra el parche de mono ActiveRecord/Arel –

+0

Si por "eficiente" quiere decir rendimiento, el método de reescritura de la consulta es superior; No veo cómo puede implementar nada en Rails que evite dos llamadas al db. Si por "eficiente" te refieres a DRYer/más expresivo (asumiendo que este no es el único lugar donde deseas hacer esto), entonces estoy de acuerdo en que es una opción. –

Cuestiones relacionadas