Tag: Rails3.1

Migracja z Rails 3.0.9 do Rails 3.1 RC5

Wstęp

Na fali RC5 nowych Railsów (3.1), postanowiłem zmigrować doń Susanoo (CMS pod Senpuu v5). Było to moje drugie podejście do przenosin. Pierwsze (jeszcze bodajże przed RC1) zakończyło się porażką. Głównie ze względu na to, że logika pod v5 nie była skończona i na przenosiny nie miałem zbyt dużo czasu.

Aktualne podejście na szczęście skończyło się dużo lepiej. Działa całość kodu i testów, jedyne problemy jakie występują (ale już niedługo), to niekompatybilność mojego gemu do osadzania obrazków w kodzie CSS (CSS image embedder). Nie powinno być z tym jednak większych problemów, ponieważ nowe Railsy umożliwiają wbicie się w Pipeline.

Wracając jednak do sedna sprawy, bardzo pomógł mi wpis Davida Rice'a dot. właśnie migracji. Duża część poniższych porad pochodzi właśnie z jego bloga.

Gemfile

Pierwszą rzeczą jaką musimy zaktualizować jest Gemfile w którym dodajemy (lub zamieniamy jeśli mieliśmy starsze wersje) następujące gemy:

gem 'rake', '~> 0.9.2'
gem "rails", "3.1.0.rc5"
gem 'mysql2', '0.3.6'
gem 'json'
gem 'sass'
gem 'coffee-script'
gem 'uglifier'
gem "sprockets", :git => 'https://github.com/sstephenson/sprockets.git'
gem 'jquery-rails'
gem "meta_search", '1.1.0.pre'
gem 'kaminari'
gem 'dynamic_form'
gem 'execjs'
gem 'therubyracer'

Oczywiście jest to kawałek gemfile'a z Susanoo, więc jeśli nie korzystacie z któregoś z gemów, to nie dodawajcie go do gemfile'a. Warto też pamiętać o tym, aby zaktualizować gemy (w miarę możliwości) do najnowszych wersji. Na przykład Mysql2 musi być z brancha 0.3.* aby współpracował z Railsami 3.1 (wersja RC5 wymaga wersji 0.3.6 mysql2).

config/boot.rb

Następnym krokiem jest zmiana pliku boot.rb. Usuwamy starą zawartość i wklejamy to co jest poniżej:

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

Po pierwsze na początku tego pliku dajemy:

require 'rake/dsl_definition'

Po drugie (to ważna decyzja!) jeśli nie chcemy korzystać z assetsów, tylko mieć (jak w 3.0) wszystkie JSy, Imgi i CSSy w public/ wpisujemy:

config.assets.enabled = false

W przeciwnym wypadku (app/assets), wpisujemy:

config.assets.enabled = true

Initializers

Jeśli korzystaliśmy z Sassa - usuwamy jego initializer (sass:plugin). Tak samo robimy ze wszystkimi innymi które nie są już potrzebne (bądź są niekompatybilne).

Routes w kilku plikach

W jednym z poprzednich wpisów, pokazywałem jak rozłożyć routsy w logiczny sposób. Jeśli uruchomimy nowe Railsy, zobaczymy taki oto warning:

DEPRECATION WARNING: 
config.paths.app.controller API is deprecated 
in favor of config.paths["app/controller"]

Aby go usunąć, zmieniamy linijkę:

config.paths.config.routes << route_file

na następującą:

config.paths['config/routes'] << route_file

ActionMailer

Kolejny warning będzie następujący:

DEPRECATION WARNING: content_type= is deprecated. 
Please pass :content_type as hash key to mail() instead.

Jego likwidujemy usuwając taki (lub podobny wpis) z pliku application.rb:

ActionMailer::Base.content_type = "text/html"

Assets - Pipeline

Jeśli nie włączyłeś assetsów - w tej chwili możesz odpalić swoją aplikację na Rails 3.1 RC5 :)
Jeśli jednak skusiła Cię wizja SCSSa i CoffeScriptu musisz wszystko przenieść do katalogu app/assets. Ja wybrałem trochę łatwiejszą drogę. Przeniosłem Javascripty i SCSSa, pozostawiłem jednak w katalogu public/assets wszystkie obrazki. Z niewiadomego dla mnie powodu, przeniesienie obrazków skutkuje baardzo powolnym ładowaniem się strony w trybie deweloperskim. Jak tylko sytuacja ta zostanie rozwiązana, nic nie stoi na przeszkodzie aby i obrazki leciały przez pipe'a.

# Przenosiny
mkdir app/assets
git mv public/images /assets
git mv public/javascripts app/assets/javascripts
git mv public/stylesheets app/assets/stylesheets

config/environments/production.rb

Dwie opcje dla produkcji (config/environments/production.rb):

config.assets.js_compressor  = :uglifier
config.assets.css_compressor = :scss

Ścieżki (referencje) do obrazków

Jak nietrudno się domyślić, wszelkie referencje wskazujące na /images/ nie będą działać. Z tego względu musimy zamienić w naszych plikach css/scss:

# Zamieniamy:
/images/
# Na:
/assets/

Manifesty dla plików CSS i JS

W manifeście umieszczamy informacje o tym jakie pliki mają zostać połączone w jeden, który zostanie zaserwowany klientowi.
Przykład dla JSa:

// This is a manifest file that'll be compiled into including all the files listed below.
// Add new JavaScript/Coffee code in separate files in this directory and they'll automatically
// be included in the compiled file accessible from http://example.com/assets/application.js
// It's not advisable to add code directly here, but if you do, it'll appear at the bottom of the
// the compiled file.
//
//= require jquery
//= require jquery_ujs
//= require_tree .

i wersja dla CSSa:

/*
 * This is a manifest file that'll automatically include all the stylesheets available in this directory
 * and any sub-directories. You're free to add application-wide styles to this file and they'll appear at
 * the top of the compiled file, but it's generally better to create a new file per style scope.
 *= require_self
 *= require_tree . 
*/

Warto pamiętać, że ścieżki podajemy od roota assetsów, czyli przykładowo

//= require "layouts/admin/costam.js"

W przypadku SCSSów możemy korzystać z dyrektywy @import. Musimy jednak pamiętać, że ścieżka będzie względna w stos. do położenia naszego pliku.

Dołączanie arkuszy styli do kodu

<%= stylesheet_link_tag    "sciezka/manifest" %>
<%= javascript_include_tag "sciezka/manifest" %>

Podsumowanie

To by było na tyle. Na pierwszy rzut oka wydaje się to proste, jednak jeśli mamy dużo stylów, plików JS oraz rzeczy "pobocznych" w jakiś sposób z nimi powiązanych, przeniesienie może nie być zbyt łatwe. Może okazać się też, że część gemów z których korzystamy nie jest kompatybilna z R3.1. Warto też zdawać sobie sprawę, że bez solidnego obłożenia kodu testami - taka zmiana może źle się dla nas skończyć. Taka zmiana jest też dobrą okazją do refaktoryzacji i uprzątnięcia kodu - a więc, do dzieła!

Problemy

Z poważniejszych to mam póki co jeden który występuje tylko w środowisku produkcyjnym, mianowicie taki oto piękny error:

cache error: File name too long - /home/blabla/sciezka/tmp/cache/http%3A%2F%2Flocalhost%3A3001%2Fassets%2Flayouts%2Fadmin%2Fdefault%2Fmodule%2Fads-190528e5830bd4c5019cd4da9bcdbfdb.css%3F20110812-2214-j2m5kl.lock

Wygląda na to, że nie do końca dobrze tworzone są nazwy w cache'u. Pożyjemy zobaczymy - widziałem już gdzieś po drodze ticket, tak więc ekipa jest świadoma błędu. Pewnie niedługo naprawią.

UUID-GUUID zamiast domyślnego ID w Ruby on Rails

Wstęp

W pewnym projekcie nad którym ostatnio pracuję, postanowiłem skorzystać z UUIDów (co to są UUIDy dowiesz się tutaj) zamiast wykorzystywać domyślne ID'ki. Realizacja tego zadania w Railsach wymaga zastosowania pewnych niewielkich zabiegów, jednak całość jest całkiem prosta.

Generowanie UUIDów

Do generowania UUIDów wykorzystamy gem uuidtools:

gem 'uuidtools'

Generowanie odbywa się z pomocą poniższej metody:

UUIDTools::UUID.random_create.to_s

Migracje

Aby wykorzystać UUIDy, zamiast standardowego pola ID skorzystamy z pola (a jakże) UUID. Aby tego dokonac, wyłączymy całkowicie generowanie IDków dla migracji. Stworzymy w tym celu klasę bazową dla naszych migracji (można oczywiście deklarować to w każdej migracji z osobna ale komu się chce). Tworzymy w katalogu db/ plik uuid_migration.rb zawierający jedną metodę (migracja ta jest migracją bazową więc nie ma up, down czy też (w R3.1) change:

class UuidMigration < ActiveRecord::Migration
  def create_uuid_table(name, options = {})
    create_table name, options.merge(:id => false) do |t|
      t.string :uuid, :limit => 36, :primary => true
      yield t
      t.timestamps
    end

    add_index name, :uuid, 'UNIQUE'
  end
end

Od teraz możemy wykorzystać naszą migrację edytując migracje danych modeli, np:

require File.expand_path(File.dirname(__FILE__))+'/../uuid_migration.rb'

class CreateProducts < UuidMigration
  def change
    create_uuid_table :products do |t|
      t.string   :name,  :null => false
      t.float    :price, :null => false
      t.timestamps
    end
  end
end

Taka migracja spowoduje, że oprócz pól zadeklarowanych powyżej, powstanie także (automatycznie) pole uuid oraz timestampy. Tworząc dużo modeli opartych na UUIDach, taka metoda jest wygodniejsza (nie zapomnimy np. przez przypadek wyłączyć automatycznie tworzonego pola id).

Klasa bazowa dla naszych modeli

Zanim zaczniemy, chciałbym zaznaczyć, że wykorzystamy klasę bazową zamiast modułu tylko dlatego, że w systemie nad którym pracuję, występuje takie rozwiązanie (powody pozostawiam dla siebie ;) ). Niemniej jednak, nic nie stoi na przeszkodzie aby przerobić rozwiązanie zaprezentowane poniżej wersję modułową.

W gruncie rzeczy cały "myk" sprowadza się do informowania klas o tym że zamiast id używamy uuida oraz do jego inicjowania. Inicjować uuida będziemy zaraz przed zapisem obiektu do bazy (before_create):

require 'uuidtools'

class UuidRecord::Base < ActiveRecord::Base

  self.abstract_class = true
  self.primary_key = :uuid

  before_create :generate_uuid

  private

  def generate_uuid
    self.uuid = UUIDTools::UUID.random_create.to_s
  end

end

Tworzymy klasę

Po utworzeniu naszej migracji oraz innych potrzebnych fragmentów kodu, możemy przystąpić do utworzenia naszej pierwszej klasy:

class Product < UuidRecord::Base

  set_table_name  :products
  set_primary_key :uuid

end

Wadą "uuidowania" jest to, że musimy deklarować nazwę tabeli (jeśli dziedziczymy, jako moduł includowany już nie), oraz że w każdym modelu (mimo że ustawiliśmy w nadrzędnym), musimy deklarować, że uuid jest naszym kluczem głównym:

set_table_name  :products
set_primary_key :uuid

Relacje pomiędzy obiektami

Opisując wykorzystanie UUIDów, warto wspomnieć także o tym, jak przebiega stosowanie ich w relacjach has, belongs_to, itd. Ogólnie rzecz ujmując, to przebiega... tak samo. Ale... należy pamiętać o tym, że nie mamy pola ID! Dlatego deklarując relacje, musimy podawać klucz obcy oraz klucz główny:

  belongs_to :product,
    :primary_key => :uuid,
    :foreign_key => :product_uuid

  has_many :ordereds,
    :primary_key => :uuid,
    :foreign_key => :order_uuid

Tyle jeśli chodzi o UUIDy. Ich wykorzystanie nie jest trudne, pod warunkiem, że będziemy pamiętać o tym co napisałem powyżej.

Copyright © 2025 Closer to Code

Theme by Anders NorenUp ↑