Hlavní navigace

Ruby on Rails: Co se nevešlo

Jan Molič

Závěrečný díl je o filtrech, kešování, migracích, pluginech a vůbec o všem, co se nevešlo do předchozích dílů. Stručně řečeno: nebojte, je to delší.

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/controller­s/admin/blogs_con­troller.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_sys­tem.rb:

module LoginSystem
  ...
  def login_required
    ...
  end
  ...

end 

Dále se podívejte do souboru app/controller­s/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::Ba­se), 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/controller­s/blogs_contro­ller.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/controller­s/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/controller­s/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://local­host:3000/admin/blo­gy/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/environ­ments/develop­ment.rb nastavte

...
config.action_controller.perform_caching = true
... 

Kde nastavit, co se má kešovat?

Samozřejmě v kontroléru.

app/controller­s/admin/blogs_con­troller.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://local­host:3000/admin/blog­s/show/9). Stránka se zakešovala. Pak jsme článek editovali, jenže na url http://localhos­t:3000/admin/blog­s/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/controller­s/admin/blogs_con­troller.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_swe­eper.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/environ­ment.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/data­base.yml.

db/migrate/001_cre­ate_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_ad­d_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::Mi­gration.

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.rubyonra­ils.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/u­ser.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.

Našli jste v článku chybu?

24. 1. 2011 12:50

xforce (neregistrovaný)

Webhosting založený na tomto frameworku naleznete na www.4smart.cz

22. 5. 2007 19:40

Patrik Jíra (neregistrovaný)
Zdravím, právě bylo spuštěno Ruby on Rails fórum (rails-forum.cz), pokud tedy hledáte radu, přijďte si pro ni.
Vitalia.cz: Bižuterie tisícinásobně překračuje povolené limity

Bižuterie tisícinásobně překračuje povolené limity

120na80.cz: 5 nejčastějších mýtů o kondomech

5 nejčastějších mýtů o kondomech

Lupa.cz: Kdo pochopí vtip, může jít do ČT vyvíjet weby

Kdo pochopí vtip, může jít do ČT vyvíjet weby

Lupa.cz: Propustili je z Avastu, už po nich sahá ESET

Propustili je z Avastu, už po nich sahá ESET

Podnikatel.cz: Přehledná titulka, průvodci, responzivita

Přehledná titulka, průvodci, responzivita

Vitalia.cz: Jsou čajové sáčky toxické?

Jsou čajové sáčky toxické?

Vitalia.cz: Když přijdete o oko, přijdete na rok o řidičák

Když přijdete o oko, přijdete na rok o řidičák

Měšec.cz: Kdy vám stát dá na stěhování 50 000 Kč?

Kdy vám stát dá na stěhování 50 000 Kč?

Vitalia.cz: Láska na vozíku: Přitažliví jsme pro tzv. pečovatelky

Láska na vozíku: Přitažliví jsme pro tzv. pečovatelky

DigiZone.cz: Recenze Westworld: zavraždit a...

Recenze Westworld: zavraždit a...

Vitalia.cz: Potvrzeno: Pobyt v lese je skvělý na imunitu

Potvrzeno: Pobyt v lese je skvělý na imunitu

Vitalia.cz: Spor o mortadelu: podle Lidlu falšovaná nebyla

Spor o mortadelu: podle Lidlu falšovaná nebyla

Měšec.cz: Jak vymáhat výživné zadarmo?

Jak vymáhat výživné zadarmo?

Vitalia.cz: Paštiky plné masa ho zatím neuživí

Paštiky plné masa ho zatím neuživí

Lupa.cz: Insolvenční řízení kvůli cookies? Vítejte v ČR

Insolvenční řízení kvůli cookies? Vítejte v ČR

Lupa.cz: Proč firmy málo chrání data? Chovají se logicky

Proč firmy málo chrání data? Chovají se logicky

Podnikatel.cz: Chaos u EET pokračuje. Jsou tu další návrhy

Chaos u EET pokračuje. Jsou tu další návrhy

Lupa.cz: Teletext je „internetem hipsterů“

Teletext je „internetem hipsterů“

Vitalia.cz: Nejlepší obranou při nachlazení je útok

Nejlepší obranou při nachlazení je útok

Podnikatel.cz: Podnikatelům dorazí varování od BSA

Podnikatelům dorazí varování od BSA