====== 1. Rails Environment and Configuration ======
===== 1.2. Startup and Application Settings =====
Uruchamiając środowisko Railsów (na przykład za pomocą polecenia ''rails server''), najpierw przetwarzane są trzy pliki:
* ''boot.rb'', konfiguruje Bundlera i ładuje ścieżki,
require 'rubygems'
# Set up gems listed in the Gemfile.
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
* ''application.rb'', ładuje gemy, konfiguruje aplikację,
require File.expand_path('../boot', __FILE__)
require 'rails/all'
if defined?(Bundler)
# If you precompile assets before deploying to production, use this line
Bundler.require *Rails.groups(:assets => %w(development test))
# If you want your assets lazily compiled in production, use this line
# Bundler.require(:default, :assets, Rails.env)
end
module TestApp
class Application < Rails::Application
# Settings in config/environments/* take precedence over those specified here.
# Application configuration should go into files in config/initializers
# -- all .rb files in that directory are automatically loaded.
# Custom directories with classes and modules you want to be autoloadable.
# config.autoload_paths += %W(#{config.root}/extras)
# Only load the plugins named here, in the order given (default is alphabetical).
# :all can be used as a placeholder for all plugins not explicitly named.
# config.plugins = [ :exception_notification, :ssl_requirement, :all ]
# Activate observers that should always be running.
# config.active_record.observers = :cacher, :garbage_collector, :forum_observer
# Set Time.zone default to the specified zone and make Active Record auto-convert to this zone.
# Run "rake -D time" for a list of tasks for finding time zone names. Default is UTC.
# config.time_zone = 'Central Time (US & Canada)'
# The default locale is :en and all translations from config/locales/*.rb,yml are auto loaded.
# config.i18n.load_path += Dir[Rails.root.join('my', 'locales', '*.{rb,yml}').to_s]
# config.i18n.default_locale = :de
# Configure the default encoding used in templates for Ruby 1.9.
config.encoding = "utf-8"
# Configure sensitive parameters which will be filtered from the log file.
config.filter_parameters += [:password]
# Enable the asset pipeline
config.assets.enabled = true
# Version of your assets, change this if you want to expire all your assets
config.assets.version = '1.0'
end
end
* ''environment.rb'', uruchamia initializery.
# Load the rails application
require File.expand_path('../application', __FILE__)
# Initialize the rails application
TestApp::Application.initialize!
==== 1.2.1. application.rb ====
* Standardowo w pliku tym ładuje się Railsy w całości, poprzez ''require 'rails/all'''. Możliwe jest jednak wybranie tylko kilku komponentów:
require "active_model/railtie"
require "active_record/railtie"
require "action_controller/railtie"
require "action_view/railtie"
require "action_mailer/railtie"
require "active_resource/railtie"
* Dla naszej aplikacji utworzony został osobny moduł, po to, by umożliwić uruchomienie kilku aplikacji Railsowych pod jednym procesem.
* Railsy ładują pluginy alfabetycznie.
* Railsowe skrypty generatorów korzystają z pewnych standardowych ustawień. Można to zmienić:
config.generators do |g|
g.template_engine :haml
g.test_framework :rspec, :fixture => false
==== 1.2.2. Initializers ====
* W katalogu ''config/initializers'' znaleźć można pliki konfiguracyjne dla naszej aplikacji. Są one ustawiane po załadowaniu wszystkich frameworków, pluginów i gemów (na przykład ustawień tych gemów).
* Standardowo Railsy tworzą pięć initializerów:
* Backtrace Silencers - zmienia wygląd backtrace'ów, usuwając część linii.
* Inflections - W Railsach istnieje klasa o nazwie ''Inflector''. Jest ona odpowiedzialna za zamianę wyrazów z liczby pojedynczej do mnogiej i //vice versa//. Można tam dodać wyrazy, z którymi ''Inflector'' sobie nie radzi.
ActiveSupport::Inflector.inflections do |inflect|
inflect.irregular 'pensum', 'pensa'
end
* MIME types - można tu dodać nowe typy MIME, na które ma odpowiadać ''respond_to''.
* ''session_store.rb'' - w pliku ustawiony jest tajny klucz, służący do weryfikacji ciasteczek.
* ''session_store.rb'' - istnieje możliwość przechowywania sesji w bazie danych zamiast w ciasteczkach. W tym pliku można to skonfigurować.
==== 1.2.3. Dodatkowa konfiguracja ====
====== 5. Working with Active Record ======
===== 5.3. Defining Attributes =====
==== 5.3.1. Default Attribute Values ====
* Najczęściej, standardowe wartości atrybutów (kolumn) ustawia się w modelu. (można również w migracji - opcja '':default'' metody ''column'')
* Instancje modeli mają kilka metod (''write_attribute'' i ''read_attribute'') aby nadpisywać standardowe akcesory.
Zwracanie kategorii, a w gdy jest ona ''nil'', zwrócenie ''n/a'':
class TimesheetEntry < ActiveRecord::Base
def category
read_attribute(:category) || 'n/a'
end
end
==== 5.3.2. Serialized Attributes ====
* Jest możliwość oznaczenia kolumny ''text'' bazy danych jako serializowanej. Jakikolwiek obiekt zostanie przypisany atrybutowi tej kolumny, będzie zapisany w bazie jako YAML.
* Kolumna o typie ''text'' ma maksymalny rozmiar 64K.
* Często przechowuje się za pomocą takiego mechanizmu hash obiektów związanych z ustawieniami użytkownika (po co tworzyć na to osobną tabelę?)
class User < ActiveRecord::Base
serialize :preferences, Hash
end
* Dostęp do listy serializowanych atrybutów kontrolowany jest poprzez ''serialized_attributes''
* Aby ustawić wartość domyślną atrybutów serializowanych, musimy uciec się do przeładowania akcesora atrybutu, tak by ustawiał on wartość domyślną, gdy atrybut jest ''nil'':
def preferences
read_attribute(:preferences) || write_attribute(:preferences, {})
end
===== 5.4. CRUD: Creating, Reading, Updating, Deleting =====
==== 5.4.1. Creating New Active Record Instances ====
* Obiekty mogą być instancjonowane jako puste lub z ustawionymi już atrybutami, ale jeszcze nie zapisane do bazy:
>> c = Client.new
=> #
>> c.new_record?
=> true
>> c.persisted?
=> false
* Można od razu zapisywać obiekty do bazy danych za pomocą metody ''create'':
c = Client.create(:name => "sQbase", :code => "SQB")
==== 5.4.2. Reading Active Record Objects ====
# Pobranie obiektu o danym id
first_girl = Girl.find(1)
# Pobranie ostatniego obiektu
last_girl_standing = Girl.last
# Pobranie wszystkich (w00t!) dziewcząt
sqbells_life = Girl.all
# Przedziały również i tu działają
v3rts_life = Girl.find([1, 2])
==== 5.4.3. Reading and Writing Attributes ====
* Po znalezieniu obiektu, do jego atrybutów dostać się można na kilka sposobów:
>> first_girl.name
=> "Antonina Louis"
* Możemy łatwo modyfikować zwracane wartości:
def name
read_attribute(:name).reverse
end
# irb
>> first_girl.name
=> "siuoL aninotnA"
# trzeba uważać na nieskończoną rekurencję!
def name
self.name.reverse
end
# irb
>> first_girl.name
SystemStackError: stack level too deep [...]
* Do atrybutów dostać można się również za pomocą notacji hashowej:
first_girl['name']
first_girl[:name]
* Wiele metod Railsowych akceptuje parametry w postaci symbolu i łańcucha znaków. Ogólna reguła mówi, że z symboli korzystamy, gdy chodzi o nazwę czegoś a z łańcucha znaków w przypadku jakichś wartości.
* Istnieje również metoda ''attributes'', zwracająca Hash wszystkich atrybutów. Przydaje się to, gdy chcemy na przykład przesłać je wszystkie za jednym razem innej funkcji. Należy pamiętać, że modyfikacja Hasha nie wpłynie na atrybuty obiektu rodzimego.
>> first_girl.attributes
=> {"name"=>"Antonina Louis", "phone"=>"n/a", "id"=>1}
>> atts = first.girl.attributes
>> atts["name"] = "Kasia Vitton"
=> "Kasia Vitton"
>> first_girl.attributes
=> {"name"=>"Antonina Louis", "phone"=>"n/a", "id"=>1}
==== 5.4.4. Accessing and Manipulating Attributes Before They Are Typecast ====
* Czasem chcemy uzyskać dostęp do surowych danych, pobranych bezpośrednio z bazy danych, bez rzutowania. Możemy tego dokonać za pomocą akcesorów ''attribute_before_type_cast''
==== 5.4.7. Dynamic Attribute-Based Finders ====
* W celu ułatwienia poszukiwań, istnieją dynamicznie generowane metody ''find_by_'' i ''find_all_by_'', po których następuje nazwa kolumny, wedle której chcemy szukać rekordu.
* Można szukać również wedle wielu kolumn.
Girl.find_by_height(176)
Girl.find_by_height_and_weight(176, 60)
# to samo, z pomocą klauzuli where
Girl.where("height = ? AND weight = ?", height, weight)
* Metody można również łączyć z zakresami (//scopes//) i relacjami. Metody muszą wtedy być na końcu zapytania.
Girl.order("cuteness").find_all_by_height(176)
* Metoda ''find_or_create_by_'', która zwróci obiekt, jeśli on istnieje, a jeżeli nie, utworzy go.
Girl.find_or_create_by_height_and_weight_and_name("176", "60", "Zuzanna")
* Jeśli chcemy znaleźć lub utworzyć obiekt, ale nie zapisywać go, używamy metody ''find_or_initialize_by_''
* Wszystkie metody ''find_'' są niekompatybilne z Arelem i dlatego coraz rzadziej używane.
==== 5.4.8. Dynamic Scopes ====
* Dynamiczne zakresy są podobne do dynamicznych metod z rozdziału 5.4.7. Są jednak kompatybilne z Arelem i dlatego preferowane w aplikacjach wykorzystujących Rails 3.
* Zakresy są generowane w razie potrzeby.
Girl.scoped_by_height("176").order(:cuteness)
# irb
>> Girl.methods.include?("scoped_by_height")
=> true
>> Girl.methods.include?("scoped_by_weight")
=> false
==== 5.4.9. Custom SQL Queries ====
* Metoda ''find_by_sql'' umożliwia otrzymanie obiektów za pomocą bezpośredniego zapytania SQL. Wykorzystywanie SQL'a w aplikacjach Railsowych jest jednak bardzo odradzane.
* Podobnie jest z metodą ''count_by_sql'', która zwraca liczbę rekordów w bazie danych odpowiadającej zapytaniu.
==== 5.4.10. Query Cache ====
* Railsy próbują optymalizować swoją wydajność poprzez keszowanie zapytań. Jest to hash przechowywany w obecnym wątku, jeden dla każdego połączenia z bazą danych. Gdy korzystamy z metod find (lub jakichkolwiek innych, powodujących zapytania SELECT) wyniki zapisywane są w tym hashu razem z zapytaniem SQL. Jeśli takie samo zapytanie zostanie wysłane ponownie, (w krótkim, jak mniemam, odstępie czasu), to zamiast ponownie pytać serwer bazy danych, dane zostaną pobrane z hasha.
* Można ręcznie uaktywnić keszowanie zapytań za pomocą bloku ''cache'':
User.cache do
puts User.first
puts User.first
puts User.first
end
# development.log
User Load (1.0ms) SELECT * FROM users LIMIT 1
CACHE (0.0ms) SELECT * FROM users LIMIT 1
CACHE (0.0ms) SELECT * FROM users LIMIT 1
* Operacje zapisu i usuwania rekordów powodują wyczyszczenie keszu.
* Za pomocą metody ''clear_query_cache'' można zrobić to ręcznie.
* Domyślnie, kesz zapytań Active Recordu jest włączony dla przetwarzania akcji kontrolera.
* Różne zapytania zwracające te same wyniki są jednak keszowane jako osobne.
SELECT foo FROM bar WHERE id = 1
SELECT foo FROM bar WHERE id = 1 LIMIT 1
* Powyższe zapytania uznane zostaną jako dwa osobne.
==== 5.4.13. Updating a Particular Instance ====
* Najlatwiej uaktualnic obiekt AR poprzez zmianę jego atrybutów a następnie wywołanie metody ''save''.
* Metoda ''save'' wstawi rekord, jeśli on nie istnieje, lub uaktualni jego atrybuty, jeśli istnieje.
* Metoda ''save'' zwróci ''true'', gdy operacja się powiedzie i ''false'' w wypadku przeciwnym.
* Istnieje również metoda ''save!'', która korzysta z wyjątków.
==== 5.4.14. Updating Specific Attributes ====
* Metody ''update_attribute'' i ''update_attributes'' uaktualniają wartości, jednocześnie zapisując zmiany do bazy danych.
* Obiekt uaktualniany za pomocą metody ''update_attribute'' **nie** podlega walidacji. Można doprowadzić do sytuacji, gdy nieprawidłowy obiekt zostanie zapisany w bazie. Callbacki również są pomijane.
==== 5.4.15. Convenience Updaters ====
* Railsy oferują kilka wygodnych metod, takich jak: ''decrement'', ''increment'', ''toggle''. Każda posiada wersję z wykrzyknikiem, która dodatkowo uruchamia ''update_attribute''.
==== 5.4.16. Touching Records ====
* Czasem chcemy tylko "dotknąć" rekordu by uaktualnić daty w polach, żeby wskazać, że rekord był wyświetlany. Korzystamy wtedy z metody ''touch''. Gdy wywołamy ją bez żadnych parametrów, uaktualnione zostanie pole ''updated_at'', bez uruchamiania jakiejkolwiek walidacji czy callbacków. Jeśli podamy nazwę kolumny jako parametr, uaktualniona zostanie również i ona.
* Opcję '':touch => true'' można dodać do asocjacji ''belongs_to''. Wtedy, gdy obiekt dziecko zostanie "dotknięty", daty uaktualniane będą również u rodzica.
class User < ActiveRecord::Base
belongs_to :client, :touch => true
end
# irb
>> user.touch #=> user.client.touch jest również uruchamiane
==== 5.4.17. Controlling Access to Attributes ====
* Konstruktory i metody ''update'', które przyjmują hashe do wykonania masowego przypisania atrybutów są podatne na ataki hakerów, gdy korzysta się z nich w połączeniu z hashem ''params'' w metodach kontrolera.
* Chcąc chronić atrybuty danej klasy, mamy do dyspozycji dwie metody, które kontrolują dostęp do tych atrybutów:
* ''attr_accessible'' przyjmuje jako argument listę atrybutów, które będą dostępne do masowego przypisania.
* ''attr_protected'' działa odwrotnie. Atrubuty podane metodzie jako argumenty będą chronione przed masowym przypisaniem.
class Girl < ActiveRecord::Base
attr_protected = :phone_number
end
girl = Girl.new(:name => "Zuzanna", :phone_number => "605 945 344")
girl.phone_number # => nil
girl.attributes = { "phone_number" => "605 945 344" }
girl.phone_number # => nil
# atrybut można ustawić tylko tak
girl.phone_number = "605 945 344"
girl.phone_number # => "605 945 344"
==== 5.4.18. Readonly Attributes ====
* Atrybuty ustawione jako tylko do odczytu, mogą być ustawiane tylko wtedy, gdy obiekt nie jest jeszcze zapisany
* W przypadku próby nadpisania takiego atrybutu po zapisaniu, wartość pozostaje niezmieniona. Nie jest również wyświetlany komunikat o błędzie.
* Listę takich atrybutów można otrzymać za pomocą metody ''readonly_attributes''.
class Girl < ActiveRecord::Base
attr_readonly :annoying_level
end
==== 5.4.19. Deleting and Destroying ====
* Chcąc usunąć rekord z bazy danych mamy dwie możliwości:
* ''delete'' - używa SQL'a bezpośrednio, nie ładuje obiektu przez co jest szybsza.
* ''destroy'' - ładuje obiekt, następnie go usuwa i uniemożliwia jego edycję. Gdy chcemy wykorzystać callback ''before_destroy'' lub gdy mamy jakieś zależności - obiekty dzieci, które powinny być usunięte wraz z usunięciem rodzica.
* Próba zapisania obiektu po jego usunięciu metodą ''destroy'' skutkuje cichym błędem. Aby sprawdzić, czy dany obiekt jest usunięty, korzystamy z metody ''destroyed?''.
* Metody mogą być również używane jako metody klasy, podając jako parametr id lub ich tablicę.
Girl.delete(1)
Girl.delete([2, 3])
===== 5.5. Database Locking =====
* Gdy istnieje możliwość, że te same dane będą modyfikowane przez więcej niż jednego użytkownika, mogą pojawić się kolizje,
* dwa podejścia do unikania kolizji są zaimplementowane w ''ActiveRecord'':
* blokada optymistyczna (ang. //optimistic locking//),
* blokada pesymistyczna (ang. //pesimistic locking//),
* istnieją inne rozwiązania, jak zablokowanie całych tabel.
==== 5.5.1. Optimistic Locking ====
* Strategia opiera się na wykrywaniu i rozwiązywaniu kolizji jeśli się one pojawią,
* zalecana w sytuacjach gdy kolizje będą pojawiały się rzadko,
* pomimo nazwy, dane nie są nigdy blokowane,
* aby umożliwić optymistyczne blokowanie, należy dodać kolumnę ''integer'' o nazwie ''lock_version'' do danej tabeli, ze standardową wartością ''0'',
* jeśli ten sam rekord zostanie załadowany w dwóch różnych instancjach modeli i różnie zapisany, pierwsza instancja wygra uaktualnienie a druga podniesie wyjątek ''ActiveRecord::StaleObjectError'',
* wyjątek trzeba obsłużyć, na przykład tak:
def update
user = User.find(params[:id])
user.update_attributes(params[:timesheet])
rescue ActiveRecord::StaleObjectError
flash[:error] = "User data was modified while you were editing it."
redirect_to [:edit, user]
end
* pesymistyczna blokada jest prosta do zaimplementowania, nie wymaga specjalnych funkcjonalności od bazy danych, jest jednak wolniejsza, ponieważ ''lock_version'' musi zostać sprawdzone. Kolejnym minusem jest kiepskie doświadczenie użytkownika, ponieważ dowie się on o problemie po utracie danych.
==== 5.5.2. Pessimistic Locking ====
* Pesymistyczne blokowanie wymaga specjalnych funkcji od bazy danych,
* blokuje konkretne wiersze w bazie danych podczas operacji uaktualniania danych, uniemożliwiając innemu użytkownikowi ich odczyt,
* współpracuje z transakcjami:
User.transaction do
user = User.lock.first
user.admin = true
user.save!
end
* można zamknąć istniejącą już instancję modelu za pomocą metody ''lock!'', która tak naprawdę wykonuje ''reload(lock: true)'', a więc nie powinno się tego robić na instancji do której wprowadzono zmiany, bo zostaną one odrzucone. Aby usunąć blokadę: ''lock!(false)'',
* hipotetycznie istnieją sytuacje, w których (na przykład z powodu zawieszenia procesu Railsów) blokada nie zostanie usunięta aż do czasu zakończenia lub time-outu połączenia z bazą danych.
===== 5.6. Where Clauses =====
==== 5.6.1. where (*conditions) ====
* warunki mogą być przekazane jako string lub hash,
* parametry są automatycznie sprawdzane pod kątem wstrzykiwania SQL:
# Hash style
User.where(name: "Marek")
User.where(name: ["Marek", "Jarek", "Darek"])
# String style
User.where('name LIKE ? AND surname = ?', "%#{terms}%", surname)
User.where('name IN (?)', ["Marek"], "Jarek", "Darek")
# Bind variables
User.where("name = :name AND surname = :surname", name: "Jarek", surname: "Gruk")
# Można wykorzystać zmienną wielokrotnie
User.where("name LIKE :name OR surname LIKE :name", name: '%are%')
# Jeśli potrzebujemy tylko sprawdzania równości z wykorzystaniem SQL-owego AND
User.where(name: name, surname: surname).first
* wartości boolowskie są zapisywane różnie przez różne bazy danych (niektóre mają natywnie wartości boolowskie, inne korzystają z pojedynczego znaku ''T'', ''F'', ''0'', ''1''). Railsy transparentnie poradzą sobie z tym problemem, jeśli podamy w warunku Rubowe ''true'' lub ''false'',
* trzeba być ostrożnym, przekazując ''nil'' jako zmienną:
> User.where('email = ?', nil)
=> SELECT * FROM users WHERE (email = NULL)
> User.where(email: nil)
=> SELECT * FROM users WHERE (users.email IS NULL)
==== 5.6.2. order(*clauses) ====
* Metoda przyjmuje jeden lub więcej symboli (reprezentujących kolumny), bądź fragmenty SQL'a:
User.order(:height)
User.order('height asc')
* przyjmuje opcję ''ascending'' jeśli się jej nie zdefiniuje lub skorzysta z symbolu,
* w przypadku gdy nie umieścimy omawianej klauzuli, dostaniemy dane w "jakimś" porządku (niekoniecznie uporządkowanych po id).
==== 5.6.3. limit(number) i offset(number) ====
User.limit(10).offset(10)
==== 5.6.8. includes(*associations) ====
* Eliminuje problem "N+1" zapytań, "chętnie" ładując zależności (//eager loading//):
User.includes(phones: [:country_codes, {region_codes: :regions}]
==== 5.6.10. readonly ====
* Ustawia zwracane obiekty w trybie "tylko do odczytu". Można zmieniać ich atrybuty, ale zmiany nie zostaną zapisane w bazie danych.
==== 5.6.11. exists? ====
* Sprawdza, czy dany rekord znajduje się w bazie danych, zwracając ''true''/''false'':
User.exists?(height: 176)
# lub
User.where(height: 176).exists?
==== 5.6.12. arel_table ====
* Można dostać się bezpośrednio do tabeli danej klasy za pomocą ''arel_table'', w celu wygenerowania własnego SQL'a poprzez Arel:
users = User.arel_Table
users.where(users[:height].eq(176))
====== 7. Active Record Associations ======
===== 7.1. The Association Hierarchy =====
* Asocjacje pojawiają się zazwyczaj jako metody modelu. Ukrywają się jednak często jako zwykłe obiekty Rubiego. ''user.addresses'' zwróci nam obiekt klasy ''Array'', ale tak naprawdę to instancje ''AssociationProxy'' lub którejś z jej podklas (przykładowo dla ''has_many'' będzie to ''HasManyAssociation''),
===== 7.2. One-to-Many Relationships =====
* ''<<(*records)'' i ''create(attributes = {})'' dodadzą jeden lub kilka obiektów do właściciela kolekcji, zachowują się jednak inaczej. ''<<'' jest transakcjonalny a ''create'' nie. ''<<'' wywołuje callbacki (''before_add'' i ''after_add'') a ''create'' nie. Zwracają też różne rzeczy - ''<<'' zwraca tablicę lub ''false'' gdy nie uda mu się dodać jakiegoś elementu, ''create'' zwraca nowo utworzoną instancję,
*
====== 9. Advanced Active Record ======
===== 9.1. Scopes =====
* Zakresy umożliwiają definiowanie i łączenie kryteriów zapytań w sposób umożliwiający zapisanie i ponowne ich użycie.
* Wcześniej (Rails 2, sprawdzić!) ''named_scope'', teraz ''scope''.
class Timesheet < ActiveRecord::Base
scope :submitted, where(:submitted => true)
scope :underutilized, where('total_hours < 40')
* Jeśli już przed uruchomieniem znane są wszystkie parametry, można korzystać z Arelowskich ''where'', ''order'', ''limit''. Jeśli nie znamy wszystkich parametrów dopóki nie uruchomimy aplikacji, należy jako drugi parametr podać lambdę. Będzie wtedy przeliczana z każdym jej wywołaniem.
class User < ActiveRecord::Base
scope :delinquent, lambda{where('timesheets_updated_at < ?', 1.week.ago)}
* Korzystać z takiego zakresu możemy jak ze zwykłą metodą: ''User.delinquent''.
==== 9.1.1. Scope Parameters ====
====== 13. Session Management ======
* HTTP jest protokołem bezstanowym.
* Bez sesji niemożliwym byłoby powiązanie dwóch żądań HTTP. Konieczne byłoby uwierzytelnianie na każdej stronie.
* W Railsach, każdemu nowemu użytkownikowi nowa sesja przypisywana jest automatycznie. Przy wizycie, ciasteczko z ID sesji wysyłane jest przeglądarce.
sqbell@sqbell-netbook:~$ curl 10.0.0.10:3000 -I
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8
X-Ua-Compatible: IE=Edge
Etag: "492b3c7c9fd2d950e1f2ada5a46651b3"
Cache-Control: max-age=0, private, must-revalidate
X-Runtime: 0.100905
Content-Length: 0
Server: WEBrick/1.3.1 (Ruby/1.9.2/2011-07-09)
Date: Fri, 30 Sep 2011 15:45:08 GMT
Connection: Keep-Alive
Set-Cookie: _depot_session=BAh7CEkiD3Nlc3Npb25faWQGOgZFRkkiJWE1N2I1NWQ0NTdkY2UzN2E2MTkxNTM4ZDRlZTE5Zj
Y0BjsAVEkiDGNvdW50ZXIGOwBGaQZJIhBfY3NyZl90b2tlbgY7AEZJIjFxeE1DOHhBMFVqN2RjMGZ0NCtOdWp4blRrelRSQUJHSW1
zY3NuRGdqT1g4PQY7AEY%3D--9cddc91db772a3ab182baf74f504ed390a165569; path=/; HttpOnly
* Od tego momentu, każde żądanie wysłane przez przeglądarkę będzie zawierać taki ID.
* Dobrą praktyką Rails jest przechowywanie jak najmniej danych za pomocą sesji.
===== 13.1. What to Store in the Session =====
* Liczby, krótkie ciągi znaków można przechowywać w sesji, obiektów natomiast nie powinno się w sesji umieszczać.
* Większość aplikacji Railsowych przechowuje w sesji ''current_user_id'', ID obecnie zalogowanego użytkownika.
* Nie powinno się przechowywać krytycznych danych w sesji. Użytkownik w każdej chwili może zamknąć przeglądarkę, lub wyczyścić ciasteczka, kasując tym samym sesję.
* Od wersji 3, sesje są leniwie ładowane, co oznacza, że jeśli nie odnosimy się do nich w kontrolerze, nie są ładowane i nie spowalniają działania aplikacji.
===== 13.3. Storage Mechanisms =====
* Istnieją różne mechanizmy przechowywania sesji. Domyślnym dla Railsów jest zapisanie sesji w ciasteczku, co jednocześnie ogranicza ilość danych do 4KB.
* Wiąże się to jednocześnie z pewnymi względami bezpieczeństwa (session-reply attacks - http://en.wikipedia.org/wiki/Replay_attack).
==== 13.3.1. Active Record Session Store ====
* Sesje można przechowywać w bazie danych.
* Odpowiednią tabelę można utworzyć za pomocą wygenerowanej poleceniem: ''rake db:sessions:create'' migracji.
* Trzeba również dodać linię do pliku ''config/initializers/session_store.rb'':
MyApplication::Application.config.session_store :active_record_store
==== 13.3.2. Memcache Session Storage ====
====== 17. Caching and Performance ======
===== 17.1. View Caching =====
Mamy trzy rodzaje cache'owania widoków:
* //page caching//, gdzie wszystko co zwróci dana akcja kontrolera zostanie zapisane a późniejsze żądania nie będą wymagały jakiejkolwiek reakcji ze strony Railsów,
* //action caching//, podobnie jak w przypadku //page caching//, ale Railsy są już zaangażowane w obsługę żądań, filtry w kontrolerach są uruchamiane,
* //fragment caching//, różne części strony są zapisywane.
==== 17.1.2. Page Caching ====
* Uruchamiany za pomocą metody makra ''caches_page'',
* dla Railsów wygląda to jak statyczna strona HTML w katalogu ''public''.
==== 17.1.3. Action Caching ====
* Uruchamiany za pomocą metody ''caches_action'',
* prawie taki sam jak //page caching//, ale uruchamia filtry kontrolerów,
* zaimplementowane z wykorzystaniem technik //Fragment Caching// oraz filtra ''around_filter'',
* zawartość jest zapisywana w pamięci podręcznej na podstawie klucza opartego na hoście i ścieżce,
* różne formaty danych (XML, JSON, HTML) są traktowane jako osobne żądania i są przechowywane w pamięci podręcznej osobno.
==== 17.1.4. Fragment Caching ====
* Umożliwia zapisanie części renderowanej strony i serwowanie ich dla przyszłych żądań bez konieczności ich ponownego renderowania,
* ustawiany na poziomie widoku:
<% cache do %>
<%= render 'entry', collection: @entries %>
<% end %>
* konieczne jest podawanie nazwy danego fragmentu, jeśli mamy więcej fragmentów w danym widoku, ponieważ fragmenty są identyfikowane na podstawie hosta i ścieżki:
<% cache(fragment: 'entries') do %>
...
* istnieje możliwość utworzenia globalnych fragmentów, tj. nie przypisanych do jednego adresu URL. Jest to przydatne, gdy dany fragment pojawia się w kilku widokach różnych akcji różnych kontrolerów:
<% cache(@user.name + "_stats") do %>
...
* gdy dany fragment jest już dodany do pamięci podręcznej, nie ma potrzeby wysyłania żądań do bazy danych dla tego fragmentu. Korzystamy z metody ''fragment_exists?'':
def index
unless fragment_exists?(fragment: 'entries')
@entries = Entry.all.limit(10)
end
end
==== 17.1.5. Wygasanie zawartości pamięci podręcznej ====
* Przechowując zawartość w pamięci podręcznej, trzeba rozważyć wszystkie możliwości, które sprawią, że ta zawartość nie będzie aktualna,
* metody ''expire_page'' i ''expire_action'' usuwają zawartość pamięci podręcznej, która będzie zregenerowana przy następnym żądaniu, należy pamiętać o poprawnej identyfikacji zawartości:
# Zazwyczaj ''update'' i ''destroy'' również dezaktualizują zawartość pamięci podręcznej
def create
...
if @entry.save
expire_page action: 'public'
redirect_to entries_path(@entry)
end
...
end
* różne reprezentacje tego samego zasobu (HTTP, XML, etc) wymagają również osobnego czyszczenia: ''expire_page(action: 'public', format: :xml)'',
* aby usunąć fragment, należy skorzystać z metody ''expire_fragment'': expire_fragment(fragment: 'entries')
* jeżeli została zastosowana paginacja, lub zasoby są bardziej skomplikowane, konieczne być może zastosowanie wyrażeń regularnych, należy jednak pamiętać, **że nie działają one z ''Memcached''**: expire_Fragment(%r{entries/.*})
==== 17.1.6. Automatic Cache Expiry with Sweepers ====
* Klasa ''Sweeper'' to swego rodzaju ''Observer'', wyspecjalizowany w czyszczeniu zawartości pamięci podręcznej,
* pisząc zamiatacza, określamy listę modeli, jakie ma obserwować:
class EntrySweeper < ActionController::Caching::Sweeper
observe Entry
def expire_cached_content(entry)
expire_page controller: 'entries', action: 'public'
expire_fragment(%r{entries/.*})
expire_Fragment(fragment: (entry.user.name + "_stats"))
end
alias_method :after_save, :expire_cached_content
alias_method :after_destroy, :expire_cached_content
end
* tak napisanego zamiatacza musimy podać w kontrolerze:
class EntriesController < ApplicationController
...
cache_sweeper :entry_sweeper, only: [:create, :update, :destroy]
...
end
==== 17.1.9. Cache Storage ====
Mamy do wykorzystanie trzy różne opcje przechowywania pamięci podręcznej:
* ''ActiveSupport::Cache::FileStore'' - przechowuje zawartość na dysku, udostępnia fragmenty wszystkim procesom,
* ''ActiveSupport::Cache::MemoryStore'' - przechowuje fragmenty w pamięci, w efekcie czego może konsumować bardzo duże ilości pamięci, w przypadku kiepskich strategii wygaśnięcia,
* ''ActiveSupport::Cache::MemCacheStore'' - przechowuje fragmenty za pomocą ''Memcached'', opcja uznawana za najlepszą.
===== 17.2. General Caching =====
* Zawsze można korzystać z ustawionego mechanizmu pamięci podręcznej:
> Rails.cache.write(:actress, "Anne Hathaway")
=> true
> Rails.cache.read :actress
=> "Anne Hathaway"
==== 17.2.1. Eliminating Extra Database Lookups ====
* Często korzysta się z pamięci podręcznej do przechowywania częstych zapytań do bazy danych. Poniżej, metoda ''fetch'' najpierw sprawdza w pamięci podręcznej, a dopiero później pyta bazę danych:
class User < ActiveRecord:Base
def self.fetch(id)
Rails.cache.fetch("user_#{id}") { User.find(id) }
end
def after_save
Rails.cache.write("user_#{id}", self)
end
def after_destroy
Rails.cache.delete("city_#{id}")
end
end
===== 17.3. Control Web Caching =====
* Dostępne są metody do kontroli nagłówków HTTP 1.1 ''Cache-Control'',
* standardowo, ustawiana jest instrukcja ''private'', która mówi, że żądanie jest przeznaczone dla konkretnego użytkownika i tym samym nie powinno być zapisane w pamięci podręcznej pośredników (//web proxies//),
* ustawienie ''public'' umożliwia pośrednikom przechowywanie zawartości w pamięci podręcznej, nie powinno być stosowane w przypadku stron, na których istnieją elementy dla konkretnego użytkownika,
* ''expires_in'' - metoda nadpisuje nagłówek ''Cache-Control'':
expires_in 3.hours, 'max-stale' => 5.hours, public: true
* ''expires_now'' ustawia nagłówek ''Cache-Control'' na ''no-cache'', czyli bez zachowywania w pamięci podręcznej.
===== 17.4. ETags =====
* ETagi, //entity tags//, to technika, dzięki której nie trzeba w ogóle wysyłać zawartości do klienta, jeśli od ostatniej wizyty nic na serwerze się nie zmieniło,
* poprawnie zaimplementowane, są jedną z najlepszych technik zwiększających wydajność na stronach o dużym ruchu,
* standardowo, renderowanie automatycznie dodaje nagłówek ''ETag'' do każdej ''200 OK'' odpowiedzi. Wartość tej zmiennej to hash MD5 ciała odpowiedzi. Jeśli następne żądanie będzie miało taki sam ''Etag'', odpowiedzią będzie ''304 Not Modified'', a ciało odpowiedzi będzie puste,
* ''RFC 2616'' zaleca wysyłanie zarówno silnego ''ETag'' jak i wartości ''Last-Modified''. Ta ostatnia nie jest dodawana przez Railsy automatycznie,
* metoda ''fresh_when'' ustawia ''ETag'' i/lub ''Last-Modified'' i renderuje odpowiedź ''304 Not Modified'' jeśli żądanie jest "świeże". Świeżość jest obliczna za pomocą metody ''cache_key'' obiektu lub kolekcji obiektów przekazywanej metodzie jako opcja '':etag'':
def show
@article = Article.find(params[:id])
fresh_when(etag: @article, last_modified: @article.created_at.utc, public: true)
end
* metoda ''stale?'' ustawia ''ETag'' i/lub ''Last-Modified'' odpowiedzi i porównuje je z żądaniem klienta (wykorzystując metodę ''fresh_When''). Jeśli nie pasują do siebie, żądanie jest określane jako nieświeże i powinno być wygenerowane od nowa. Ta metoda jest wykorzystywana zamiast ''fresh_when'', jeśli dodatkowa logika jest potrzebna w kontrolerze do renderowania widoku.