¿Cómo funciona...? Kaminari
Link: https://rubygems.org/gems/kaminari
¿Qué hace la gema?
Provee algunos helper methods y class methods para paginar colecciones de objetos. Para evitar cargar grandes cantidades de objetos en memoria, una técnica muy común es el paginado, es decir: dividir la colección de objetos en "páginas" y mostrar una menor cantidad de elementos por vez para mejorar la performance tanto en velocidad como en consumo de memoria.
Existen 3 gemas populares para resolver esta problemática: Kaminari, WillPaginate y Pagy, estas últimas dos las vamos a estudiar más adelante. Cada una de estas gemas toma distintos caminos para lograr el mismo objetivo pero con distinto resultado final en cuanto a performance, customización, features, integración con otras gemas y en la "limpieza" del código.
¿Cómo se usa?
Los dos usos principales de la gema son: obtener una página dada una colección de objectos, y generar código html para navegar entra las páginas calculadas. (Al final vamos a hablar del caso especial de paginar un array en lugar de una relación de ActiveRecord)
¿Cómo lo hace?
Como todos los casos, el punto de entrada es el archivo en lib con el nombre de la gema:
En principio esto sólo require 3 archivos: el core de la gema, la extensión de actionview y la extensión de activerecord.
Pero tiene algo diferente a la gema anterior: dentro de lib tenemos sólo dos archivos, el que mostramos recién y kaminari/version.rb que sólo define la versión actual de la gema. Los archivos que requiere no están dentro de lib, tampoco están en el root del proyecto!
Para entender qué está pasando tenemos que ir un paso más atrás, al momento de correr bundle. La gema kaminari no provee la funcionalidad, es algo así como una "metagema" que en realidad depende de otras tres gemas (que están dentro del mismo proyecto en github). Veamos el gemspec:
Lo primero que hace el gemspec (recordemos que el archivo .gemspec es simplemente código ruby) es requerir version.rb que sí define la gema kaminari y luego agrega tres gemas como dependencias: kaminari-core, kaminari-actionview y kaminari-activerecord.
Entonces, volvamos al primer bloque de código que vimos para entender cómo funcionan estos require, kaminari.rb:
Al agregar la gema kaminari-core, ahora también tenemos kaminari-core/lib/ en el PATH y dentro de este directorio SI encontramos la carpeta kaminari con el archivo core.rb. Lo mismo ocurre para los otras dos líneas.
No vamos a entrar en todo el detalle de qué hace puntualmente cada gema porque hay muchas cosas que usa internamente que no hacen al objetivo de este blog y requeriría demasiados posts por gema.
Ahora que vimos esta curiosidad, vamos a centrarnos en los dos puntos que hablamos antes: el paginado y los links de navegación entre páginas. Para eso tenemos las gemas kaminari-activerecord y kaminari-actionview, respectivamente.
¿Cómo se agrega el método '.page'?
Veamos, qué hay en el archivo que requirió la gema kaminari al requerir kaminari/activerecord:
Lo primero que notamos es que la gema NO tiene un archivo lib/kaminari-activerecord.rb porque no se usa como punto de entrada.
Lo que hace este archivo es esperar a que se cargue ActiveRecord y le incluye el módulo Kaminari::ActiveRecordExtension. Este módulo es un Concern de Rails que se encarga de incluir, ahora si, el método '.page'.
Este módulo es otro Concern que, al ser incluido en ActiveRecord, define un método de clase. El método no es necesariamente `.page`, se puede configurar, por eso se está usando eval y el nombre del método se obtiene de la configuración 'page_method_name' de la gema.
Este método lo que hace es devolver una colección de ActiveRecord usando limit y offset para encontrar la página pedida, pero, además, la extiende llamando a .extending y en el bloque le incluye algunos módulos con varios métodos (.per, .total_count, .next_page, .last_page?, entre otros). Es por esto que siempre hay que usar .page antes de .per.
Nuestra colección ya está limitada a cierta cantidad, desde tal offset y tiene los métodos necesarios para saber cuántas páginas quedan, en qué página estamos, etc. Todos los métodos necesarios para generar la navegación.
¿Y el view helper 'paginate'?
La gema kaminari-actionview curiosamente no se encarga de definir los helpers ni las vistas, eso está en kaminari-core (en kaminari-core/lib/kaminari/helpers/paginator.rb). Esta gema lo que hace es sólo incluir esos helpers en ActionView para poder usarlos en las vistas:
Este patrón es similar al de la otra gema, espera a que se cargue ActionView y le incluye el módulo Kaminari::Helpers::HelperMethods que incluye el helper paginate pero además otros como link_to_previous_page, next_page_url, etc.
(En otro post quizás veamos cómo maneja la posibilidad de sobreescribir las vistas)
Conectando los cables
1- agregar la gema `gem 'kaminari'` y correr bundler
Esto agrega las otras 3 gemas como dependencias.
2- llamar el método `.page` en una relación de ActiveRecord que la convierte en una colección paginada y extendida gracias al código de kaminari-activerecord.
3- usar el view helper `paginate` para generar el html de la navegación, el método definido en kaminari-core pero incluido en ActionView por kaminari-actionview.
Extra: paginando arrays
Todo lo anterior aplica sólo a colecciones de ActiveRecord, pero la gema provee además un método de paginado de arrays puros de Ruby. Lo que hace es definir una nueva clase que hereda de Array pero que agrega todos los métodos necesarios para poder ser usada igual que la versión extendida de una colección de ActiveRecord. Agrega el método .page y el resto necesarios como .per, .total_count, etc. Esto se define en el siguiente código:
El archivo define el método de clase paginate_array en la clase Kaminari. Este método se usa para generar el objeto del tipo PaginatableArray y vemos que el método para paginar el array se crea usando class_eval con el nombre configurado, similar a la extensión de ActiveRecord.
Más adelante vamos a analizar WillPaginate y Pagy para luego poder hacer una comparación.
¿Qué hace la gema?
Provee algunos helper methods y class methods para paginar colecciones de objetos. Para evitar cargar grandes cantidades de objetos en memoria, una técnica muy común es el paginado, es decir: dividir la colección de objetos en "páginas" y mostrar una menor cantidad de elementos por vez para mejorar la performance tanto en velocidad como en consumo de memoria.
Existen 3 gemas populares para resolver esta problemática: Kaminari, WillPaginate y Pagy, estas últimas dos las vamos a estudiar más adelante. Cada una de estas gemas toma distintos caminos para lograr el mismo objetivo pero con distinto resultado final en cuanto a performance, customización, features, integración con otras gemas y en la "limpieza" del código.
¿Cómo se usa?
Los dos usos principales de la gema son: obtener una página dada una colección de objectos, y generar código html para navegar entra las páginas calculadas. (Al final vamos a hablar del caso especial de paginar un array en lugar de una relación de ActiveRecord)
¿Cómo lo hace?
Como todos los casos, el punto de entrada es el archivo en lib con el nombre de la gema:
# lib/kaminari.rb
require 'kaminari/core'
require 'kaminari/actionview'
require 'kaminari/activerecord'
En principio esto sólo require 3 archivos: el core de la gema, la extensión de actionview y la extensión de activerecord.
Pero tiene algo diferente a la gema anterior: dentro de lib tenemos sólo dos archivos, el que mostramos recién y kaminari/version.rb que sólo define la versión actual de la gema. Los archivos que requiere no están dentro de lib, tampoco están en el root del proyecto!
Para entender qué está pasando tenemos que ir un paso más atrás, al momento de correr bundle. La gema kaminari no provee la funcionalidad, es algo así como una "metagema" que en realidad depende de otras tres gemas (que están dentro del mismo proyecto en github). Veamos el gemspec:
# kaminari.gemspec
$:.push File.expand_path("../lib", __FILE__)
require "kaminari/version"
Gem::Specification.new do |spec|
# ...
spec.add_dependency 'kaminari-core', Kaminari::VERSION
spec.add_dependency 'kaminari-actionview', Kaminari::VERSION
spec.add_dependency 'kaminari-activerecord', Kaminari::VERSION
# ...
end
Lo primero que hace el gemspec (recordemos que el archivo .gemspec es simplemente código ruby) es requerir version.rb que sí define la gema kaminari y luego agrega tres gemas como dependencias: kaminari-core, kaminari-actionview y kaminari-activerecord.
Entonces, volvamos al primer bloque de código que vimos para entender cómo funcionan estos require, kaminari.rb:
# lib/kaminari.rb
require 'kaminari/core'
# otros require ...
Al agregar la gema kaminari-core, ahora también tenemos kaminari-core/lib/ en el PATH y dentro de este directorio SI encontramos la carpeta kaminari con el archivo core.rb. Lo mismo ocurre para los otras dos líneas.
No vamos a entrar en todo el detalle de qué hace puntualmente cada gema porque hay muchas cosas que usa internamente que no hacen al objetivo de este blog y requeriría demasiados posts por gema.
Ahora que vimos esta curiosidad, vamos a centrarnos en los dos puntos que hablamos antes: el paginado y los links de navegación entre páginas. Para eso tenemos las gemas kaminari-activerecord y kaminari-actionview, respectivamente.
¿Cómo se agrega el método '.page'?
Veamos, qué hay en el archivo que requirió la gema kaminari al requerir kaminari/activerecord:
# kaminari-activerecord/lib/kaminari/activerecord.rb
require "kaminari/activerecord/version"
require 'active_support/lazy_load_hooks'
ActiveSupport.on_load :active_record do
require 'kaminari/core'
require 'kaminari/activerecord/active_record_extension'
::ActiveRecord::Base.send :include, Kaminari::ActiveRecordExtension
end
Lo primero que notamos es que la gema NO tiene un archivo lib/kaminari-activerecord.rb porque no se usa como punto de entrada.
Lo que hace este archivo es esperar a que se cargue ActiveRecord y le incluye el módulo Kaminari::ActiveRecordExtension. Este módulo es un Concern de Rails que se encarga de incluir, ahora si, el método '.page'.
# kaminari-activerecord/lib/kaminari/activerecord/active_record_model_extension.rb
require 'kaminari/activerecord/active_record_relation_methods'
module Kaminari
module ActiveRecordModelExtension
extend ActiveSupport::Concern
included do
include Kaminari::ConfigurationMethods
eval <<-RUBY, nil, __FILE__, __LINE__ + 1
def self.#{Kaminari.config.page_method_name}(num = nil)
per_page = max_per_page && (default_per_page > max_per_page) ? max_per_page : default_per_page
limit(per_page).offset(per_page * ((num = num.to_i - 1) < 0 ? 0 : num)).extending do
include Kaminari::ActiveRecordRelationMethods
include Kaminari::PageScopeMethods
end
end
RUBY
end
end
end
Este módulo es otro Concern que, al ser incluido en ActiveRecord, define un método de clase. El método no es necesariamente `.page`, se puede configurar, por eso se está usando eval y el nombre del método se obtiene de la configuración 'page_method_name' de la gema.
Este método lo que hace es devolver una colección de ActiveRecord usando limit y offset para encontrar la página pedida, pero, además, la extiende llamando a .extending y en el bloque le incluye algunos módulos con varios métodos (.per, .total_count, .next_page, .last_page?, entre otros). Es por esto que siempre hay que usar .page antes de .per.
Nuestra colección ya está limitada a cierta cantidad, desde tal offset y tiene los métodos necesarios para saber cuántas páginas quedan, en qué página estamos, etc. Todos los métodos necesarios para generar la navegación.
¿Y el view helper 'paginate'?
La gema kaminari-actionview curiosamente no se encarga de definir los helpers ni las vistas, eso está en kaminari-core (en kaminari-core/lib/kaminari/helpers/paginator.rb). Esta gema lo que hace es sólo incluir esos helpers en ActionView para poder usarlos en las vistas:
# kaminari-actionview/lib/kaminari/actionview.rb
require "kaminari/actionview/version"
require 'active_support/lazy_load_hooks'
ActiveSupport.on_load :action_view do
require 'kaminari/helpers/helper_methods'
::ActionView::Base.send :include, Kaminari::Helpers::HelperMethods
require 'kaminari/actionview/action_view_extension'
end
Este patrón es similar al de la otra gema, espera a que se cargue ActionView y le incluye el módulo Kaminari::Helpers::HelperMethods que incluye el helper paginate pero además otros como link_to_previous_page, next_page_url, etc.
(En otro post quizás veamos cómo maneja la posibilidad de sobreescribir las vistas)
Conectando los cables
1- agregar la gema `gem 'kaminari'` y correr bundler
Esto agrega las otras 3 gemas como dependencias.
2- llamar el método `.page` en una relación de ActiveRecord que la convierte en una colección paginada y extendida gracias al código de kaminari-activerecord.
3- usar el view helper `paginate` para generar el html de la navegación, el método definido en kaminari-core pero incluido en ActionView por kaminari-actionview.
Extra: paginando arrays
Todo lo anterior aplica sólo a colecciones de ActiveRecord, pero la gema provee además un método de paginado de arrays puros de Ruby. Lo que hace es definir una nueva clase que hereda de Array pero que agrega todos los métodos necesarios para poder ser usada igual que la versión extendida de una colección de ActiveRecord. Agrega el método .page y el resto necesarios como .per, .total_count, etc. Esto se define en el siguiente código:
# kaminari-actionview/lib/kaminari/actionview.rb
module Kaminari
class PaginatableArray < Array
# más métodos...
class_eval <<-RUBY, __FILE__, __LINE__ + 1
def #{Kaminari.config.page_method_name}(num = 1)
offset(limit_value * ((num = num.to_i - 1) < 0 ? 0 : num))
end
RUBY
# más métodos...
end
def self.paginate_array(array, limit: nil, offset: nil, total_count: nil, padding: nil)
PaginatableArray.new array, limit: limit, offset: offset, total_count: total_count, padding: padding
end
end
El archivo define el método de clase paginate_array en la clase Kaminari. Este método se usa para generar el objeto del tipo PaginatableArray y vemos que el método para paginar el array se crea usando class_eval con el nombre configurado, similar a la extensión de ActiveRecord.
Más adelante vamos a analizar WillPaginate y Pagy para luego poder hacer una comparación.
Comentarios
Publicar un comentario