~ 40x Faster!
~ 36x Lighter!
~ 35x Simpler!
~ 1,410x More Efficient!
Der Ansatz ist im Vergleich zu will_paginate etwas anders. So fällt auf, dass die erzeugten Active-Record-Relations nicht erweitert werden, sondern die eigentliche Collection und Paginierungs-Metadaten getrennt werden. Mein Interesse ist geweckt.
Pagy-Gem installieren:
bundle add pagy
# Installing pagy 6.0.0
An zentraler Stelle für Controller includen…
class ApplicationController < ActionController::Base
include Pagy::Backend
end
Und als Helper verfügbar machen.
module ApplicationHelper
include Pagy::Frontend
end
Inititalizer config/initializers/pagy.rb anlegen um z.B. die Standard-Seitengröße festzulegen, Bootstrap-basierte Helper zu aktivieren und die Sprache festzulegen (Vorlage mit allen Einstellungsmöglichkeiten).
# Instance variables
# See https://ddnexus.github.io/pagy/docs/api/pagy#instance-variables
Pagy::DEFAULT[:page] = 1 # default
Pagy::DEFAULT[:items] = 10 # default
Pagy::DEFAULT[:outset] = 0 # default
# Bootstrap extra: Add nav, nav_js and combo_nav_js helpers and templates for Bootstrap pagination
# See https://ddnexus.github.io/pagy/docs/extras/bootstrap
require 'pagy/extras/bootstrap'
# I18n
Pagy::I18n.load(locale: 'de')
# When you are done setting your own default freeze it, so it will not get changed accidentally
Pagy::DEFAULT.freeze
Im Controller dann die entsprechende Methode verwenden, die ein Array mit zwei Werten zurückgibt.
# GET /users
def index
@pagy, @users = pagy(User.all)
end
Im passenden View dann den die Liste der User und den Paginator anzeigen. Leider ist die Rückgabe von Pagy nicht html_safe und muss entsprechend eingefügt werden (im Beispiel HAML).
= render @users
!= pagy_bootstrap_nav(@pagy)
Dies ist natürlich nur ein erster Ansatz. Das Gem kommt mit vielen nativen Erweiterungen und hat vor allem im Bereich Performance so einiges zu bieten. So ließe sich z.B. ein Endless-Scroller ohne aufwendige Eintragszählung umsetzen oder eine Suche mit Searchkick/Elasticsearch realisieren.
]]>Für den einfacheren Einstieg erzeuge ich innerhalb einer bestehenden Rails-App eine neue Engine:
rails plugin new m3 --full
Diese wird zu Testzwecken im Gemfile des Elternprojekts verlinkt:
gem 'm3', path: 'm3'
Im Gemspec-File m3.gemspec steht bereits eine Rails-Abhängigkeit, die ich um die gewünschten Gems erweitere:
s.add_dependency "rails", "~> 4.2.4" s.add_dependency "simple_form", "~> 3.2.0" s.add_dependency "draper", "~> 2.1.0" s.add_dependency "cancancan", "~> 1.13.1"
Damit diese beim Start der Elternapplikation zur Verfügung stehen, müssen diese vom Gem geladen werden. Dies sollte in der Engine-Datei gemacht werden:
# <gem>/lib/<gem>/engine.rb require 'cancan' require 'draper' require 'simple_form' module M3 class Engine < ::Rails::Engine # ...
Die Elternapplikation kann nun wie gewohnt auf die Gems zugreifen.
Erweiterte Konfigurationen für Simple-Form werden in zugehörigen Initializern festgelegt, die man ebenfalls einfach in der Engine anlegen kann:
# <gem>/config/initializers/simple_form.rb SimpleForm.setup do |config| # ...
# <gem>/config/initializers/simple_form_bootstrap.rb SimpleForm.setup do |config| # ...
Grundlegende Controller-Konfigurationen habe ich in einen Application-Controller mit Namespace gepackt:
# <gem>/app/controllers/m3/application_controller.rb class M3::ApplicationController < ActionController::Base # ...
Hier kann man auch wie gewohnt Concerns verwenden, diese werden ebenfalls automatisch gefunden.
Die fertigen Konfigurationen können dann vom Application-Controller der Elternapplikation verwendet werden:
# <app>/app/controllers/application_controller.rb class ApplicationController < M3::ApplicationController # ...
Auch in einer Engine landet unter lib so einiges, was nicht vom Autoloading abgedeckt wird. Diese könnten wie Gem-Abhängigkeiten explizit in der Engine-Datei geladen werden. Diese Lösung ist jedoch ein unschöner Workaround und sei hier nur für den Notfall aufgeführt.
# <gem>/lib/<gem>/engine.rb # Workaround require_dependency File.expand_path("../../../app/models/m3", __FILE__) require_dependency File.expand_path("../../../lib/m3/router", __FILE__)
Wie man sieht lade ich hier auch den Namespace M3, da Autoloading an dieser Stelle noch nicht funktioniert und die Lib-Klasse Routes entweder aufgrund des fehlenden Namespaces knallt oder diesen als leeres Modul anlegt. Letzteres kann zu einem schwer zu findenden Fehler führen, da die Modul-Datei im Verzeichnis app dann gar nicht mehr geladen wird.
Wie angedeutet ist dieser Ansatz wenig elegant. Die Require-Statements sollten besser zu einem Zeitpunkt ausgeführt werden, an dem Autoloading bereits zur Verfügung steht. Dann passiert vieles automatisch und übrige Requires können ohne File-Path-Expansions notiert werden:
# <gem>/lib/m3/router.rb require_dependency 'm3' class M3::Router # ...
# <gem>/config/routes.rb require_dependency 'm3/router' Rails.application.routes.draw do M3::Router.draw_routes(self) end]]>
$ bundle install ... Source does not contain any versions of 'gem (>= 0) ruby'
Das Problem scheint hier tatsächlich zu sein, dass die Gem-Spezifikation noch Todos enthält:
Gem::Specification.new do |s| # ... s.summary = "TODO: Summary of Gem." s.description = "TODO: Description of Gem." end
Entferne ich diese, so läuft die Installation durch.
Ironischerweise warnt Bundler sogar, dass das Gem Todos oder Fixmes in der Spezifikation enthält. Dass es das Verzeichnis deshalb nicht ordnungsgemäß ausliest erschließt sich aber nicht. Hier wäre eine Fehlermeldung statt der Warnung geeigneter. Möglicherweise ist es auch nur ein Bug, eine fehlende Beschreibung rechtfertigt kein Ignorieren eines Gems.
]]># userengine/app/models/user class User < ActiveRecord::Base belongs_to :user_group end
# railsapp/app/models/user require User::Engine.root.join('app', 'models', 'user') class User < ActiveRecord::Base belongs_to :user_group end
Dies wird vor allem während der Entwicklung zu Problemen führen, da Rails bei einer Änderung die Klasse zurücksetzt und nur die geänderte Datei neu lädt. Die andere Klassendefinition geht verloren, was zu merkwürdigen Ergebnissen führen kann.
Die Lösung für Rails ist ein spezielles Require, welches sich mit Autoloading verträgt:
# railsapp/app/models/user require_dependency User::Engine.root.join('app', 'models', 'user') # ...
Im Rails-Guide wird Single-Table-Inheritance als weiteres Einsatzgebiet genannt.
]]>Mit CanCan hat man auch den 2.0-Branch geforkt, leider blieb dieser bisher unangetastet. Die neue Version sollte vor allem die von mir seit langem erwartete Unterstützung für Attribute mitbringen. So lässt sich nicht mehr nur definieren und prüfen ob ein Model insgesamt, sondern auch welche Attribute geändert werden dürfen:
can :update, :projects, [:name, :priority]
Der Verfasser der Readme vertröstet derweil:
I am currently focusing on the 1.x branch for the immediate future, making sure it is up to date as well as ensuring compatibility with Rails 4+. I will take a look into the 2.x branch and try to see what improvements, reorganizations and redesigns Ryan was attempting and go forward from there.
Bleibt zu hoffen, dass einer der Entwickler die Zeit für einen tieferen Blick findet.
]]>.versions.conf
als Standard für das aktuelle Verzeichnis festlegen:
rvm use --create --versions-conf ruby-2.1@gemset_name
In aller Regel benötige ich auch Bundler:
gem install bundler]]>