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!
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"
config.generators do |g| g.template_engine :haml g.test_framework :rspec, :fixture => false
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).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
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ć.:default
metody column
)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
text
bazy danych jako serializowanej. Jakikolwiek obiekt zostanie przypisany atrybutowi tej kolumny, będzie zapisany w bazie jako YAML.text
ma maksymalny rozmiar 64K.class User < ActiveRecord::Base serialize :preferences, Hash end
serialized_attributes
nil
:def preferences read_attribute(:preferences) || write_attribute(:preferences, {}) end
>> c = Client.new => #<Client id: nil, name: nil, code: nil> >> c.new_record? => true >> c.persisted? => false
create
:c = Client.create(:name => "sQbase", :code => "SQB")
# 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])
>> first_girl.name => "Antonina Louis"
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 [...]
first_girl['name'] first_girl[:name]
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}
attribute_before_type_cast
find_by_
i find_all_by_
, po których następuje nazwa kolumny, wedle której chcemy szukać rekordu.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)
Girl.order("cuteness").find_all_by_height(176)
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")
find_or_initialize_by_
find_
są niekompatybilne z Arelem i dlatego coraz rzadziej używane.Girl.scoped_by_height("176").order(:cuteness) # irb >> Girl.methods.include?("scoped_by_height") => true >> Girl.methods.include?("scoped_by_weight") => false
find_by_sql
umożliwia otrzymanie obiektów za pomocą bezpośredniego zapytania SQL. Wykorzystywanie SQL'a w aplikacjach Railsowych jest jednak bardzo odradzane. count_by_sql
, która zwraca liczbę rekordów w bazie danych odpowiadającej zapytaniu.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
clear_query_cache
można zrobić to ręcznie.SELECT foo FROM bar WHERE id = 1 SELECT foo FROM bar WHERE id = 1 LIMIT 1
save
.save
wstawi rekord, jeśli on nie istnieje, lub uaktualni jego atrybuty, jeśli istnieje.save
zwróci true
, gdy operacja się powiedzie i false
w wypadku przeciwnym.save!
, która korzysta z wyjątków.update_attribute
i update_attributes
uaktualniają wartości, jednocześnie zapisując zmiany do bazy danych.update_attribute
nie podlega walidacji. Można doprowadzić do sytuacji, gdy nieprawidłowy obiekt zostanie zapisany w bazie. Callbacki również są pomijane.decrement
, increment
, toggle
. Każda posiada wersję z wykrzyknikiem, która dodatkowo uruchamia update_attribute
.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.: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
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.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"
readonly_attributes
.class Girl < ActiveRecord::Base attr_readonly :annoying_level end
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.destroy
skutkuje cichym błędem. Aby sprawdzić, czy dany obiekt jest usunięty, korzystamy z metody destroyed?
.Girl.delete(1) Girl.delete([2, 3])
ActiveRecord
:integer
o nazwie lock_version
do danej tabeli, ze standardową wartością 0
,ActiveRecord::StaleObjectError
,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
lock_version
musi zostać sprawdzone. Kolejnym minusem jest kiepskie doświadczenie użytkownika, ponieważ dowie się on o problemie po utracie danych.User.transaction do user = User.lock.first user.admin = true user.save! end
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)
,# 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
T
, F
, 0
, 1
). Railsy transparentnie poradzą sobie z tym problemem, jeśli podamy w warunku Rubowe true
lub false
,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)
User.order(:height) User.order('height asc')
ascending
jeśli się jej nie zdefiniuje lub skorzysta z symbolu,User.limit(10).offset(10)
User.includes(phones: [:country_codes, {region_codes: :regions}]
true
/false
: User.exists?(height: 176) # lub User.where(height: 176).exists?
arel_table
, w celu wygenerowania własnego SQL'a poprzez Arel: users = User.arel_Table users.where(users[:height].eq(176))
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
),«(*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ę,named_scope
, teraz scope
.class Timesheet < ActiveRecord::Base scope :submitted, where(:submitted => true) scope :underutilized, where('total_hours < 40')
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)}
User.delinquent
.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
current_user_id
, ID obecnie zalogowanego użytkownika.rake db:sessions:create
migracji.config/initializers/session_store.rb
:MyApplication::Application.config.session_store :active_record_store
Mamy trzy rodzaje cache'owania widoków:
caches_page
,public
.caches_action
,around_filter
,<% cache do %> <%= render 'entry', collection: @entries %> <% end %>
<% cache(fragment: 'entries') do %> ...
<% cache(@user.name + "_stats") do %> ...
fragment_exists?
: def index unless fragment_exists?(fragment: 'entries') @entries = Entry.all.limit(10) end end
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
expire_page(action: 'public', format: :xml)
,expire_fragment
: expire_fragment(fragment: 'entries')
Memcached
: expire_Fragment(%r{entries/.*})
Sweeper
to swego rodzaju Observer
, wyspecjalizowany w czyszczeniu zawartości pamięci podręcznej,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
class EntriesController < ApplicationController ... cache_sweeper :entry_sweeper, only: [:create, :update, :destroy] ... end
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ą.> Rails.cache.write(:actress, "Anne Hathaway") => true > Rails.cache.read :actress => "Anne Hathaway"
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
Cache-Control
,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),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.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,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
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.