Combinando Scopes y Ransack gracias a Virtus & Siphon
Hay una solicitud de función común en la página de problemas de saqueo y es para integrar sus ámbitos de ActiveRecord habituales dentro de la búsqueda. De hecho, ransack es una herramienta fantástica para configurar rápidamente un formulario para seleccionar filas de tablas en función de los valores de las columnas. Pero se queda un poco corto cuando desea búsquedas relacionales más complejas. Por lo tanto, es casi natural que agregue un alcance en su formulario de búsqueda de ransack antes de darse cuenta de que no funciona y que no hay una forma clara de hacerlo.
Ransack no se lo llevará todo
El problema con esa solicitud de caracterÃstica atractiva y algo obvia es que el saqueo se basa en el encasillado que activerecord realiza en sus atributos, para coaccionar todos los valores de cadena del hash del parámetro en su valor real. Cuando params[:user][:age_gt] => "18"
irrumpe en su modelo, sabe que “18” debe ser un número entero (porque la columna correspondiente de la base de datos lo dice) y seguirá en consecuencia.
Por otro lado, su modelo no tiene ninguna razón para saber cuál es el tipo de argumento que enviarÃa a su alcance.
Toma esto :
scope :active, ->(bool) { where(active: bool) }
A diferencia de los atributos de ActiveRecord, no dice ningún lugar que bool
sea ​​booleano.
params[:user][:active] # => "false"
# and since "false" is just a string
!!"false" # => true
Sà … ¡Ups! Dado que solo obtiene cadenas de sus parámetros , necesita una capa de configuración para saber qué tipo es su argumento antes de que alcance su alcance.
Por supuesto, puede delegar el trabajo de coerción a todos y cada uno de los ámbitos haciendo que solo tomen cadenas. Luego convertirÃan los argumentos en el tipo correcto. Pero eso no es muy elegante: primero corre el riesgo de romper el código heredado y segundo está agregando otra capa de responsabilidad a activerecord que todos estamos tratando de desbloquear . Finalmente ransack ya está siendo un objeto Form y una extensión ActiveRecord, agregar más responsabilidad parece bien … irresponsable.
Por qué Siphon
Una vez que saqueaste tu base de datos, es posible que desees huir con tu auto, pero Dios mÃo, se ha quedado sin gasolina y todos los datos están en él … Asà que sacas ese pequeño tubo de plástico de tu bolsillo y metes en otro auto, succiona las primeras gotas y luego déjelo fluir en su automóvil … El sifón es una actividad muy discreta junto al saqueo y se muestra en el código base: la esencia es de alrededor de 50 lÃneas.
Entonces, Siphon es solo una pequeña joya de conveniencia similar a [has_scope] que aún es experimental, pero hace su trabajo de aplicar alcances a un modelo ActiveRecord gracias a un objeto de formulario (creado con Virtus ) que contiene la información coercitiva.
Ahora veámoslo en acción. Imagina que tienes el conjunto de datos canónico “Pedidos, productos y artÃculos”. ¿Cuál serÃa un caso en el que el saqueo por sà solo no cubre las condiciones que desea aplicar? Bueno, tuve que buscar ransack nuevamente porque no podÃa recordar dónde se quedó corto. Cubre las condiciones complejas de salida con ‘O’, ‘Y’, coincidencias, mayor que, incluso se une, etc. Si, por supuesto, podrÃa presentar consultas complejas para ilustrar la necesidad de un alcance, pero ¿cuál serÃa la situación más simple en la que ransack no lo cortarÃa y necesitarÃa un visor personalizado. Bueno, para uno, puede combinar columnas pero no puede combinar predicados (por ejemplo, iguales y mayores que). Entonces, si desea mostrar todos los pedidos obsoletos y necesita separar 2 columnas con diferentes tipos:
Con un alcance viene naturalmente (observe el ‘OR’):
class Order < ActiveRecord::Base
scope :stale, -> { where(["state = 'onhold' OR submitted_at < ?", 2.weeks.ago]) }
end
Solo con saqueo:
= search_form_for @q do |f|
= f.text_field :description_or_name_cont #=> Ok but...
= f.text_field :state_eq_or_submitted_at_gt #=> Impossible.
… ¡estás jodido!
Y si desea ponerlos en diferentes campos y confiar en que el usuario hará la combinación correcta
= search_form_for @q do |f|
= f.text_field :state_eq
= f.date_field :submitted_at_gt
… todavÃa estás jodido porque diferentes campos solo hacen conjunciones exclusivas (también conocido como: condición1 Y condición2) no disyunciones (también conocido como: condición1 O condición2).
Bien, apuntemos, sigamos aplicando ámbitos dentro de un formulario.
Sifón en acción
Los alcances:
# order.rb
class Order < ActiveRecord::Base
scope :stale, ->(duration) { where(["state='onhold' OR (state != 'done' AND updated_at < ?)", duration.ago]) }
scope :unpaid -> { where(paid: false) }
end
La forma :
= form_for @order_form do |f|
= f.label :stale, "Stale since more than"
= f.select :stale, [["1 week", 1.week], ["3 weeks", 3.weeks], ["3 months", 3.months]], include_blank: true
= f.label :unpaid
= f.check_box :unpaid
El objeto de formulario:
# order_form.rb
class OrderForm
include Virtus.model
include ActiveModel::Model
#
# attribute are the named scopes and their value are :
# - either the value you pass a scope whith arguments
# - either a Siphon::Nil value to apply (or not) on a scope whith no argument
#
attribute :stale, Integer
attribute :unpaid, Siphon::Nil
end
Aaaaand … Sifón TADA:
# orders_controller.rb
def search
@order_form = OrderForm.new(params[:order_form])
@orders = siphon(Order.scoped).scope(@order_form)
end
Es posible que desee leer algunas ideas sobre lo que hace el sifón o profundicemos en ello …
Saquea mano a mano con Siphon & Virtus
La idea principal es separar los campos de saqueo de los campos de sifón / alcance y, por lo tanto, anidar uno de ellos. Asà que anidemos los campos de ransack en el parámetro q (ya que es la convención de ransack) y dejemos los ámbitos en la parte superior:
-# admin/products/index.html
= form_for @product_search, url: "/admin/products", method: 'GET' do |f|
= f.label "has_orders"
= f.select :has_orders, [true, false], include_blank: true
-#
-# And the ransack part is right here...
-#
= f.fields_for @product_search.q, as: :q do |ransack|
= ransack.select :category_id_eq, Category.grouped_options
ok, ahora sostiene los alcances y tiene la bondad del saqueo. Necesitamos encontrar una manera, ahora, de distribuir esos datos al objeto de formulario. Asà que primero deje que ProductSearch lo trague en el controlador:params[:product_search]
params[:product_search][:q]
# products_controller.rb
def index
@product_search = ProductSearch.new(params[:product_search])
@products ||= @product_search.result.page(params[:page])
end
Y ahora lo esencial:
# product_search.rb
class ProductSearch
include Virtus.model
include ActiveModel::Model
# These are scopes for the siphon part
attribute :has_orders, Boolean
attribute :sort_by, String
# The q attribute is holding the ransack object
attr_accessor :q
def initialize(params = {})
@params = params || {}
super
@q = Product.search( @params.fetch("q") { Hash.new } )
end
# siphon takes self since its the formobject
def siphoned
Siphon::Base.new(Product.scoped).scope( self )
end
# and here we merge everything
def result
Product.scoped.merge(q.result).merge(siphoned)
end
end
Como ves aquà Virtus manejará todos los atributos del sifón de forma automática (gracias a lo super
cual realmente merece su nombre aquÃ). Luego la lÃnea:
@q = Product.search( @params.fetch("q") { Hash.new } )
… asignará un objeto de formulario Ransack a q que valientemente mantendrá los valores en:
= f.fields_for @product_search.q, as: :q do |ransack|
Luego, al llamarlo , obtendrá una ActiveRelation que fusionará con la otra ActiveRelation proporcionada por sifón:@q.result
Siphon::Base.new(Product.scoped).scope( self )
Y voilà , el controlador simplemente recolecta todos los frutos del trabajador Form Object:
@products ||= @product_search.result.page(params[:page])
… y puede seguir aplicando más alcance (como la paginación), sigue siendo su buena ActiveRelation regular …
Para terminar rápidamente: ¡no hay magia y simplemente funciona!
Siéntase libre de hacerme preguntas o sugerir cosas para mejorar el artÃculo.
Estaré encantado de actualizarlo.