Rails 4 w produkcji: strong parameters, Turbolinks i caching „matrioszki”
Rails 4.0 właśnie się ukazał. Oto rzetelne, prosto z okopów spojrzenie na to, co wdrażamy na realnych projektach — strong parameters, kompromis Turbolinks, caching „matrioszki”, pierwszoklasowy Postgres i pułapki aktualizacji.
Rails 4.0 ukazał się pod koniec czerwca, a my przenieśliśmy już na niego kilka projektów. To mniejsze wydanie niż swego czasu 3.0 — nie ma jednej, poruszającej ziemię zmiany jak fuzja 2.x-do-3.0 z Merbem — ale zaskakująco wiele nowych domyślnych ustawień dotyka tego, jak piszesz codzienny kod, a kilka z nich po cichu zmieni kształt twojej aplikacji. Oto pragmatyczne spojrzenie z okopów: co wdrażamy ochoczo, co wyłączamy i jakie pułapki aktualizacji kosztowały nas po popołudniu.
Strong Parameters: ochrona mass-assignment dorasta
Główną zmianą w codziennym kodzie jest to, że ochrona przed mass-assignment
przeniosła się z modelu do kontrolera. Stare attr_accessible i attr_protected
zniknęły z rdzenia — żyją dalej w gemie protected_attributes, jeśli naprawdę ich
potrzebujesz podczas migracji — a w ich miejsce białą listę parametrów tworzysz
tam, gdzie żądanie faktycznie trafia:
class PeopleController < ApplicationController
def create
@person = Person.create(person_params)
# ...
end
def update
@person = Person.find(params[:id])
@person.update(person_params) # uwaga: `update`, nie `update_attributes`
end
private
def person_params
params.require(:person).permit(:name, :email, :bio, role_ids: [])
end
end
„Dlaczego” liczy się tu bardziej niż mechanika. W starym schemacie to, co
użytkownik mógł ustawić, było właściwością modelu, zadeklarowaną raz, daleko od
konkretnego żądania. W chwili, gdy potrzebowałeś, by admin ustawiał pola, których
zwykły użytkownik nie mógł, sięgałeś po attr_accessible :role, as: :admin i rolę
:as przewleczoną przez każdy save — niezgrabne, łatwe do pomylenia i niewidoczne
w miejscu wywołania. Strong Parameters przerzuca odpowiedzialność na kontroler,
czyli warstwę, która faktycznie zna kontekst: kto wykonuje to żądanie i co
wolno mu zmienić.
Kilka rzeczy wartych wiedzy, gdy zaczniesz tego używać na poważnie:
-
requirezapewnia, że klucz jest obecny i niepusty, podnoszącActionController::ParameterMissing(400, nie 500), jeśli cały hashpersonjest nieobecny.permitbiałolistuje skalarne atrybuty w środku. -
Atrybuty zagnieżdżone i kolekcje wymagają jawnych kształtów.
has_manyprzezaccepts_nested_attributes_forprzepuszczasz zagnieżdżonym hashem, a wartości tablicowe potrzebują końcowego[]:params.require(:project).permit( :name, tag_ids: [], tasks_attributes: [:id, :title, :done, :_destroy] ) -
Cokolwiek nieprzepuszczone jest usuwane, a w trybie deweloperskim dostajesz linię w logu mówiącą, które klucze odfiltrowano — czytaj ją, bo po cichu porzucony atrybut jest inaczej mylącym błędem.
-
params.require(:person).permit!przepuszcza wszystko. Istnieje; traktuj jako zapach kodu, bo to dokładnie ta dziura, którą Strong Parameters miało zamknąć.
Czyta się to lepiej niż rozrzucone po modelach attr_accessible, biała lista jest
tuż obok akcji, która rozumie kontekst, a tryb awarii (400 na brakującym
parametrze) jest znacznie przyjaźniejszy niż ciche mass-assignment. Podoba nam się
to bez zastrzeżeń.
Turbolinks: szybko domyślnie, ale poznaj kompromis
Turbolinks to najbardziej kontrowersyjny domyślny element wydania i ten, który
najczęściej wyłączamy. Pomysł jest naprawdę sprytny: zamiast pełnego załadowania
strony przy każdym kliknięciu linku, Turbolinks przechwytuje kliknięcie, pobiera
nową stronę przez XHR, podmienia <body> i <title> oraz utrzymuje to samo
środowisko JavaScript przy życiu przez całą nawigację. Ponieważ przeglądarka nigdy
nie parsuje ponownie twojego CSS i JS, strony bogate w treść, renderowane po
stronie serwera, wydają się niemal natychmiastowe. We właściwej aplikacji to
realne, darmowe przyspieszenie.
Haczyk to część, którą ludzie pomijają: „utrzymuje to samo środowisko JavaScript
przy życiu” oznacza, że strona nigdy się naprawdę nie przeładowuje, więc zdarzenia,
na których zawsze polegałeś, przestają odpalać się tak, jak oczekujesz. Klasyczna
awaria to $(document).ready — odpala się raz, przy pierwszym prawdziwym
załadowaniu, i nigdy więcej, gdy turbolinkujesz między stronami. Każdy kod
podpinający handlery na ready po cichu przestaje działać po pierwszej wizycie, a
potem tajemniczo znów działa po twardym odświeżeniu — co jest doprowadzającym do
szału błędem do ścigania.
Masz dwie uczciwe opcje. Zaakceptuj to — podpinaj przez delegację zdarzeń, by
handlery przeżyły podmiany body, i nasłuchuj własnych zdarzeń cyklu życia Turbolinks
zamiast ready:
// zamiast $(document).ready(...)
$(document).on("page:load", function () {
initSomeWidget();
});
// delegacja przeżywa podmianę body:
$(document).on("click", ".js-toggle", function () { /* ... */ });
Albo, w aplikacji mocno opartej na JS, gdzie ciągle walczysz z cyklem życia, zrezygnuj:
# Gemfile — usuń turbolinks całkowicie albo zostaw i wyłączaj strony
# per-link przez data-no-turbolink na kotwicy lub kontenerze
gem "turbolinks"
Jest też ostra krawędź wokół pamięci: ponieważ środowisko trwa, handlery podpięte do
document kumulują się przy każdej wizycie, jeśli nie uważasz, a wyciek, który
pełne załadowanie strony by zmiotło, teraz utrzymuje się przez całe życie karty.
Nasza zasada: zostaw Turbolinks dla stron w większości statycznych, renderowanych po stronie serwera, gdzie błyszczy, i wyłącz je dla wszystkiego z dużą ilością własnego jQuery albo złożonym cyklem życia po stronie klienta. Chodzi o to, by Turbolinks było świadomą decyzją, a nie domyślnym ustawieniem, które zasadzi się na ciebie na produkcji tydzień po starcie.
Caching „matrioszki”: funkcja, z której cieszymy się najbardziej
Zagnieżdżone cache’owanie fragmentów z automatycznym unieważnianiem kluczy —
caching „matrioszki” (Russian-doll), napędzany nowym cache_digests — to zmiana, z
której wyciągnęliśmy najwięcej. Wzorzec to: cache’uj kolekcję, cache’uj każdy
element w środku i pozwól kluczom się zagnieżdżać, tak by przy zmianie jednego
elementu wygasł tylko jego fragment i jego przodkowie, a rodzeństwo zostało ciepłe:
<% cache @project do %>
<h1><%= @project.name %></h1>
<% @project.todos.each do |todo| %>
<% cache todo do %>
<%= render todo %>
<% end %>
<% end %>
<% end %>
Działają tu dwa mechanizmy i oba warto zrozumieć.
Po pierwsze, klucz cache’a zawiera digest szablonu. Framework haszuje zawartość widoku (i jego zależności — renderowanych przez niego partiali) i wplata ten hash w klucz cache’a. Zysk jest ogromny: gdy edytujesz partial, jego digest się zmienia, więc stary fragment po prostu nigdy więcej nie jest wyszukiwany. To zabija najbardziej irytujący błąd cache’owania — nieaktualne widoki serwowane po deployu, bo markup się zmienił, a klucz cache’a nie. Nie czyścisz już cache’y ręcznie przy deployu; przestarzałe fragmenty po prostu się starzeją.
Po drugie, touch: true propaguje świeżość w górę drzewa. Aby zewnętrzny
fragment wygasł, gdy zmieni się wewnętrzny rekord, updated_at rodzica musi się
przesunąć, gdy przesuwa się dziecko. Wpinasz to przez belongs_to ... touch: true:
class Todo < ActiveRecord::Base
belongs_to :project, touch: true # zapis todo podbija project.updated_at
end
Teraz aktualizacja jednego todo dotyka jego projektu, klucz cache’a projektu się
zmienia, jego fragment wygasa i renderuje się od nowa — ale każdy inny fragment
todo jest czytany prosto z cache’a. Ze składowaniem w pamięci (Memcached albo Redis
przez dalli) przed stroną bogatą w treść przyspieszenie jest dramatyczne i — w
przeciwieństwie do Turbolinks — nie ma żadnej wady behawioralnej do rozważenia.
PostgreSQL traktowany pierwszoklasowo
Rails 4 dostarcza natywne wsparcie dla całej gamy typów PostgreSQL, które wcześniej
wymagały własnej serializacji lub gemu: hstore, tablic, inet/cidr, uuid i
json. W projektach, w których i tak mocno opieramy się na Postgresie, usuwa to
stertę kodu klejącego.
class AddPreferencesToUsers < ActiveRecord::Migration
def change
add_column :users, :preferences, :hstore
add_column :users, :tag_list, :string, array: true, default: []
add_index :users, :preferences, using: :gin
end
end
user.preferences = { "theme" => "dark", "locale" => "en" }
user.tag_list = ["ruby", "rails"]
user.save
Kolumna klucz-wartość hstore z indeksem GIN to pragmatyczny środek między sztywnymi
kolumnami a sięganiem po bazę dokumentową — pola niby-schemaless, które wciąż żyją w
prawdziwym składzie relacyjnym, na którym możesz joinować i zakładać transakcje.
Kolumny tablicowe usuwają całą gamę trywialnych tabel łączących. To początek
długiego trendu, w którym Postgres wchłania funkcje, dla których ludzie kiedyś
odchodzili od Railsów, a Rails 4 to moment, gdy staje się to wygodne.
Pułapki aktualizacji
Kilka rzeczy ugryzło nas przy przenoszeniu realnych aplikacji, żadna śmiertelna, ale wszystkie warte wiedzy:
- Threadsafe jest teraz domyślne. Rails 4 działa w trybie threadsafe od ręki.
Jeśli masz kod niebezpieczny wątkowo albo zmienny stan na poziomie klasy, to tutaj
się ujawni — przejrzyj inicjalizatory i wszelkie
@@zmienne_klasowe. - Dynamiczne findery zniknęły.
find_all_by_x,find_last_by_xi spółka zostały usunięte (pojedynczefind_by_xprzetrwało, plus nowefind_by). Zastępstwem jest API relacji:where(x: ...),find_by(x: ...). - Default scopes i
scopepotrzebują wywoływalnego.scope :published, where(...)chce teraz lambdy:scope :published, -> { where(published: true) }. update_attributesstaje sięupdate, aupdate_attribute(pojedyncze, pomija walidację) wciąż jest pułapką — preferujupdatealboupdate_columni wiedz, które co pomija.- Ruby 1.9.3 to podłoga, a 2.0 to miejsce, gdzie chcesz być dla GC i usprawnień argumentów nazwanych.
- Concerns dostają dom.
app/models/concernsiapp/controllers/concernssą pierwszoklasowe, aActiveSupport::Concernto idiomatyczny sposób współdzielenia zachowań — szturchnięcie ku kompozycji zamiast grubych klas bazowych.
Werdykt
Rails 4 to łatwa rekomendacja. Same Strong Parameters i caching „matrioszki”
zwracają wysiłek: pierwsze czyni twój model bezpieczeństwa czytelnym i lokalnym,
drugie czyni wydajność widoków niemal darmową, gdy podepniesz touch:.
Pierwszoklasowe typy Postgresa to cichy luksus, który usuwa kod. Jedyne, przez co
nie należy przejść we śnie, to Turbolinks — to realna wygrana na właściwych stronach
i realny ból głowy na niewłaściwych, więc podejmij świadomą decyzję per aplikacja,
zamiast pozwolić, by domyślne ustawienie zaskoczyło cię na produkcji. Zrób to,
przejrzyj inicjalizatory pod kątem przełącznika threadsafe, a przejście będzie
gładkie.