Filtry
Co jsou zač? Jedná se o metody kontroléru, které jsou automaticky zavolány před voláním nebo po volání nějaké akce. Pomocí filtrů řešíme věci, které nechceme psát pořád dokola v každé akci. Filtry využívá například přihlašovací systém – před každou akcí je zavolána metoda, jež nepřihlášeného uživatele přesměruje na přihlašovací stránku.
before a after filtry
Podívejme se na ten přihlašovací systém zblízka.
app/controllers/admin/blogs_controller.rb:
class Admin::BlogsController < ApplicationController
layout 'admin'
before_filter :login_required
...
end
Zde vidíte ono „kouzlo“. Metoda se jmenuje login_required a před každou akcí testuje, zda je uživatel přihlášen, případně ho přesměruje na přihlašovací stránku. Odkud se ale login_required v kontroléru bere?
Podíváme-li se do adresáře lib, uvidíme v něm soubor login_system.rb, který tam vyrobil login generátor. Mimo jiné obsahuje právě metodu login_required.
lib/login_system.rb:
module LoginSystem
...
def login_required
...
end
...
end
Dále se podívejte do souboru app/controllers/application.rb:
require_dependency "login_system"
class ApplicationController < ActionController::Base
include LoginSystem
end
Modul LoginSystem je v application.rb includován. Co to znamená? V Ruby můžeme rozšiřovat třídy několika způsoby. Jednak zděděním jiné třídy, kdy uvedeme v záhlaví rodičovskou třídu (zdědění ActionController::Base), nebo vytvořením tzv. mixinu. Mixin se z třídy stane pomocí include, kdy jsou do třídy namíchány metody z modulu.
Soubor application.rb je načítán před načtením souborů kontrolérů a slouží jako default pro všechny. Podíváme-li se do libovolného kontroléru, opravdu zjistíme, že jeho třída dědí tuto defaultní. Tím je login systém, tudíž i metoda login_required, přístupná ve všech kontrolérech.
app/controllers/blogs_controller.rb
class BlogsController < ApplicationController
...
end
Jak již bylo řečeno, pomocí filtrů řešíme věci, které nechceme psát pořád dokola v každé akci. Tak třeba nastavení defaultních proměnných pro všechny akce můžeme provést takto:
class FooController < ApplicationController
before_filter :defaults
def defaults
@users = session[:users]
@days = %w( po ut st ct pa so ne )
end
def index
end
def show
end
end
Kromě before_filter existuje samozřejmě after_filter, jehož chování je, předpokládám, zřejmé.
Jak ale docílit toho, aby filtr fungoval jen pro konkrétní akce? Kupříkladu akce zobrazující přihlašovací stránku přece nemůže vyžadovat přihlášeného uživatele? Filtry lze naštěstí omezit jen na vybrané akce za pomoci parametrů :except (= kromě) a :only (= jen vyjmenované). Příklady:
class FooController < ApplicationController
before_filter :login_required, :except=>'login'
before_filter :login_required, :except=>[ 'login', 'signup' ]
before_filter :login_required, :only=>'index'
before_filter :login_required, :only=>[ 'index', 'list', 'show' ]
end
around filtr
Třetím typem filtru je around filtr. Učebnicovým příkladem je zjištění doby provádění nějaké akce v kontroléru. Around filtr je třída se dvěma metodami – before a after (staticky volanými).
app/controllers/application.rb:
class TotalTimeFilter
def before( controller )
@start_time = Time.now
end
def after( controller )
time_total = Time.now - @start_time
controller.logger.debug "Akce #{controller.action_name} trvala #{time_total} s."
end
end
app/controllers/foo_controller.rb:
class FooController < ApplicationController
around_filter TotalTimeFilter.new
end
Použijeme-li around filtr v kontroléru Foo, obalí každou akci. Před akcí bude zavolána jeho metoda before, po akci metoda after. A nejen to. Around filtrů je možné použít více, obalují pak nejen akce, ale i sebe navzájem.
class FooController < ApplicationController
around_filter A.new, B.new, C.new
end
Výsledkem bude provedení filtrů a akce v tomto pořadí
A.before
B.before
C.before
akce
C.after
B.after
A.after
Parametry :except a :only však nelze u around filtrů použít.
Počáteční kontrola
Before filtru využívá metoda verify, která zjednodušuje, co bychom jinak museli psát na mnoha řádcích.
class FooController < ApplicationController
verify :only => :list,
:session => :blog,
:redirect_to => { :action=>'index' }
verify :except => :list,
:method => :get,
:flash => :msg,
:add_flash=>[ :notice=>'musite se prihlasit' ],
:params => [ :id, :blog_id ],
:redirect_to => { :action=>'login', :controller=>'users' }
def index
end
def list
end
...
end
V tomto kontroléru bude akce list zavolána pouze tehdy, existuje-li v session klíč :blog. Když ne, bude uživatel přesměrován na akci index.
Druhé verify slouží pro ostatní akce. Testuje, jestli požadavek přišel formou GET, ve flashi je zárověň nastaven klíč :msg (například nějaká hláška z předchozí stránky) a v parametrech je jak :id, tak :blog_id. Jinak uživatele přesměruje na přihlašovací stránku, přičemž nastaví zprávu (flash[:notice]).
Kešování
Jestliže se vygenerovaná stránka nemění, je rychlejší ji uložit do keše než ji generovat pořád znovu. Na podobném principu funguje kešovací proxy server, který si stránku s určitou url zapamatuje a pak ji po nějakou dobu vrací z keše místo toho, že by požadavky předával www serveru. Proxy server však obvykle nerozumí obsahu a stránky expirují po určité době, což není vždy vhodné.
Rails mají proto keš vlastní. Můžeme díky tomu určit, za jakých okolností data expirují. Rails umějí kešovat na třech úrovních; kešují buď celé stránky, jednotlivé akce nebo fragmenty.
Kešování fragmentů popisovat nebudu, ve zkratce jde o kešování částí šablon. Osobně je nepoužívám, protože jsem zatím netvořil aplikaci, která by je svou náročností vyžadovala.
Prvním a nejjednodušším způsobem je kešování celých stránek, podobně, jak to dělá proxy server. Jakmile je stránka poprvé vygenerována, výsledek se uloží do keše a příště, je-li volána táž URL, je z keše rovnou vrácena. Je to rychlé, ale…
…ale ne vždy použitelné. V případě administračního rozhraní máme například stránku s URL „http://localhost:3000/admin/blogy/list“. Táž URL se však ne vždy chová stejně – podle toho, zda je uživatel přihlášen či ne. Pokud by byl uživatel přihlášen a přišel na tuto URL, stránka by se nakešovala. Poté by se odhlásil, jenže na této URL by i po odhlášení našel „přihlášený“ obsah. Proč? Kešujeme-li celé stránky, kompletně se tím obejde přihlašovací mechanismus, tedy filtry. Stránka je rovnou vrácena z keše, aniž by bylo řízení předáno kontroléru.
V takových případech se lépe hodí kešování na úrovni akcí, což je druhý způsob kešování. Rozdílem je, že neobejdeme kontrolér, jsou provedeny filtry, a teprve poté se rozhodne, zda akci provést či vrátit nakešovaná data. Za chvíli si ukážeme, jak kešování nastavit.
kešujeme…
V režimu development je kešování vypnuto, spusťte tedy testovací webrick server v režimu production. Buď nastavte proměnnou prostředí RAILS_ENV na production
mig> RAILS_ENV="production" script/server
nebo spusťte webrick s parametrem -e production
mig> script/server -e production
Případně v souboru config/environments/development.rb nastavte
...
config.action_controller.perform_caching = true
...
Kde nastavit, co se má kešovat?
Samozřejmě v kontroléru.
app/controllers/admin/blogs_controller.rb:
class Admin::BlogsController < ApplicationController
before_filter :login_required
# caches_page :index, :list, :show
caches_action :index, :list, :show
...
end
caches_page použijeme, když chceme kešovat celé stránky. Jak již bylo řečeno, v případě administračního rozhraní je vhodnější caches_action.
Donucení keše k expiraci stránky
Předpokládejme, že jsme se podívali na článek s id=9 (http://localhost:3000/admin/blogs/show/9). Stránka se zakešovala. Pak jsme článek editovali, jenže na url http://localhost:3000/admin/blogs/show/9 se pořád zobrazuje beze změn. Je nutné stránku z keše odstranit, k čemuž zavoláme metodu expire_action (případně expire_page, kešujeme-li celé stránky). Zavoláme ji po editaci článku s parametry, jenž identifikují stránku v keši.
app/controllers/admin/blogs_controller.rb:
class Admin::BlogsController < ApplicationController
...
def update
expire_action :action=>'list', :id=>9
end
...
end
Sweepers
Občas se týž model využívá ve více kontrolérech. Pak v každém z kontrolérů musíme hlídat, kdy má být co odstraněno z keše. Abychom to uhlídali a kontroléry se nestaly nepřehlednými, pomůže nám sweeper.
Sweeper je observer. A co je observer? Pozorovatel. Je to speciální objekt, který se naváže na jiný objekt a začne ho „pozorovat“. To znamená, že při volání metod pozorovaného objektu jsou automaticky volány i patřičné metody pozorovatele. Tím lze transparentně oddělit části kódu z objektu či objektů do observeru. Sweeper odděluje části starající se o expiraci keše z kontrolérů. V kontrolérech pak pouze nadefinujeme, který sweeper má kontrolér pozorovat.
app/models/blog_sweeper.rb:
class BlogSweeper < ActionController::Caching::Sweeper
observe Blog
def after_create( blog )
my_expire_action :list
end
def after_update( blog )
my_expire_action :list
my_expire_action :show, :id=>blog.id
end
def after_destroy( blog )
my_expire_action :list
end
private
def my_expire_action( action, id=nil )
# akce expiruje v obou kontrolerech
expire_action( :controller=>'blogs', :action=>action, :id=>id )
expire_action( :controller=>'admin/blogs', :action=>action, :id=>id )
end
end
V obou kontrolérech nastavíme, který sweeper je má pozorovat.
class Admin::BlogsController < ApplicationController
before_filter :login_required
caches_action :index, :list, :show
cache_sweeper :blogs_sweeper
def create
...
end
class BlogsController < ApplicationController
caches_action :index, :list, :show
cache_sweeper :blogs_sweeper
def create
...
end
Kde je keš uložena?
K ukládání keše slouží obdobný mechanismus jako k ukládání session. Keš je defaultně ukládána do souborů v adresáři public, soubory jsou pojmenovány podle URL stránky, kterou kešují. Stejně jako u session je více možností, jak data ukládat, například do databáze či je posílat přes DRb na vzdálený server.
Myslím, že budete chtít pouze nastavit jiný adresář k ukládání souborů, editujte tedy config/environment.rb:
...
ActionController::Base.page_cache_directory = 'jiny/adresar'
...
Jako se Rails nestarají o odmazávání souborů session, nestarají se ani o odstraňování souborů keše. Staré soubory odstraňujte třeba z cronu, vlastně tím vyřešíte expiraci na základě stáří keše.
Migrace
Migrace nemá nic společného ani s filtry, ani s keší. Uvádím ji zde proto, že jde o dobrý nástroj. K čemu slouží? Kdykoli jsme zatím zakládali databázové tabulky, činili jsme tak pomocí mysql konzole a souboru se sql dotazem, případně nějakým GUI adminem. Pracuje-li na jedné aplikaci ve více lidech, každý z vás má nejspíš svou lokální kopii jak souborů, tak i databáze. Soubory se synchonizují jednoduše (svn, csv), ale jak synchronizovat databázi? Vyrobíme migraci!
mig> script/generate migration CreateFoo
create db/migrate
create db/migrate/001_create_foo.rb
Soubor migrace editujme podle potřeby. V následujícím příkladě pomocí migrace založíme novou tabulku „foo“. Nic zajímavého, jde o obdobu CREATE TABLE foo (…), avšak s výhodou databázové nezávislosti, protože je k tomu využito ActiveRecord. Navíc, konfigurace se bere standardně z config/database.yml.
db/migrate/001_create_foo.rb:
class CreateFoo < ActiveRecord::Migration
def self.up
create_table :foo do |t|
t.column :title, :string
t.column :body, :text
t.column :lock_version, :integer, :default => 1
t.column :created_at, :datetime
end
end
def self.down
drop_table :foo
end
end
Metoda down slouží k navrácení stavu před migrací.
Migraci však nejprve provedeme. Proměnná prostředí RAILS_ENV opět řídí, ve kterých databázích se má migrace provést.
mig> rake migrate
mig> RAILS_ENV="production" rake migrate
Abychom si ukázali, jak funguje vrácení stavu před migrací, vytvoříme ještě jednu migraci, která upraví sloupce tabulky foo.
mig> script/generate migration AddIdToFoo
exists db/migrate
create db/migrate/002_add_id_to_foo.rb
db/migrate/002_add_id_to_foo.rb:
class FooChanges < ActiveRecord::Migration
def self.up
add_column :foo, :id, :integer, :default => 1
rename_column :foo, :body, :txt
end
def self.down
remove_column :foo, :id
rename_column :txt, :body
end
end
Rake provede jen ty migrace, které ještě nebyly provedeny…
mig> rake migrate
mig> RAILS_ENV="production" rake migrate
Totéž může udělat kdokoli se svou lokální databází, takže jsou databáze jakoby synchronizovány.
Vrátit se ke stavu před migrací či migracemi se lze snadno, spuštěním migrate s argumentem version. Budou provedeny všechny patřičné metody down všech migrací s vyšší verzí.
mig> rake migrate version=1
Ostatní metody popisující změny databáze (change_column, add_index, …) jsou popsány v Rails API, sekce Class ActiveRecord::Migration.
Pluginy
Možná to nejzajímavější jsem si nechal úplně na konec – jsou to pluginy, jimiž lze Rails takřka neomezeně rozšiřovat a měnit. V podstatě nejde o nic zvláštního, většinu věcí totiž umožňuje Ruby ze své dynamické podstaty. Pro příklad uvedu třídu MojeTrida se dvěma metodami foo a bar.
class MojeTrida
def foo
"puvodni"
end
def bar
"puvodni"
end
end
Tuto třídu nyní rozšíříme o metodu print!. Jak? Jestliže nadefinujeme třídu znovu, původní metody v ní zůstanou a nové metody budou přidány. Navíc pokud nadefinujeme již existující metodu znovu, nebude vyvolána chyba, nýbrž bude předefinována metoda.
class MojeTrida
def foo
"preddefinovana"
end
def print!
puts foo # tiskne "preddefinovana"
puts bar # tiskne "puvodni"
end
end
Přesně totéž dělají pluginy. Jsou nainstalovány v adresáři vendor/plugins a Rails je nahrají při startu. Dělají to tak, že v každém podadresáři nahrají skript init.rb, jenž se postará o inicializaci svého pluginu. Pluginy nadefinují nové třídy či předefinují stávající základ Rails.
Dostupné pluginy naleznete na wiki.rubyonrails.org a nainstalujete jednoduše – stačí znát jejich URL:
mig> script/plugin install http://svn.assembla.com/svn/appshare/breakout/vendor/plugins/guid/
Upozorňuji, že dva pluginy mohou měnit základ Rails v témže místě, čímž Rails přestanou fungovat. Pluginy by sice měly být napsány tak, aby k tomu nedošlo, nicméně pokud byste například nainstalovali dvě rozšíření ActiveRecord zároveň, lze nějaký konflikt předpokládat.
Příklad?
Používám třeba „Uses Guid Plugin“ v modulu User, protože nechci, aby měli uživatelé jednoduchá číselná id. Plugin jsem nainstaloval a do modelu User přidal jedinou řádku – usesguid.
app/modules/user.rb:
class User
usesguid
...
end
ID Nového záznamu je automaticky 22znakové (pokud je ovšem sloupec v tabulce typu VARCHAR).
mig> script/console
>> g = GuidTest.new
=> #<GuidTest:0xb7468e68 @attributes={"id"=>"bXbezUSE0r2OkwaayBY4jf"}, @new_record=true>
Zajímavé jsou pluginy generující různé grafy, zejména přes CSS (bez použití obrázků). Jak je nainstalovat i rozchodit je na stránkách rozebráno více než názorně, což ostatně platí i o jiných pluginech, takže se jimi nebudu zabývat.
Závěrem
Doufám, že se mi podařilo naplnit cíl seriálu a popsat základy Rails tak, aby je pochopil kdokoli, kdo někdy programoval, a to nejen v Ruby. AJAX, který jsem měl původně v úmyslu zmínit, by sám o sobě vydal asi na více dílů, takže bych byl rád, kdyby o něm někdo něco napsal. Totéž platí o WebServices, ActionMaileru a jiných Rails komponentách. Přesto si myslím, že jejich rozchození zvládne každý, kdo zvládl tento seriál. Obvykle jde o instalaci nějakého pluginu, přidání něčeho do kontroléru či modelu a tak podobně. A to je vše, přátelé. Alespoň prozatím.