User Tools

Site Tools


inf:ruby:rails_3

Table of Contents

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
=> #<Client id: nil, name: nil, code: nil>
>> 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.
inf/ruby/rails_3.txt · Last modified: 2021/02/16 09:56 (external edit)