Skip to content
← Wszystkie wpisy
7 min czytania Dawid Skłodowski

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:

  • require zapewnia, że klucz jest obecny i niepusty, podnosząc ActionController::ParameterMissing (400, nie 500), jeśli cały hash person jest nieobecny. permit białolistuje skalarne atrybuty w środku.

  • Atrybuty zagnieżdżone i kolekcje wymagają jawnych kształtów. has_many przez accepts_nested_attributes_for przepuszczasz 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 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_x i spółka zostały usunięte (pojedyncze find_by_x przetrwało, plus nowe find_by). Zastępstwem jest API relacji: where(x: ...), find_by(x: ...).
  • Default scopes i scope potrzebują wywoływalnego. scope :published, where(...) chce teraz lambdy: scope :published, -> { where(published: true) }.
  • update_attributes staje się update, a update_attribute (pojedyncze, pomija walidację) wciąż jest pułapką — preferuj update albo update_column i 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/concerns i app/controllers/concerns są pierwszoklasowe, a ActiveSupport::Concern to 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.