Posts Tagged ‘rails 3’

Кое-что из моей работы

Вторник, Январь 17th, 2012

Мне тут понадобилось написать функциональность для сайта, которая представляет из себя Консультантов, которым можно задавать вопросы.

О чем я хочу рассказать:

  • Как использовать state_machine в моделях ActiveRecord
  • Как использовать counter_cache “с условиями”

Итак, распишем задачу более подробно:

  • Будет две модели, Consultor и ConsultorQuestion
  • ConsultorQuestion может быть опубликованным и неопубликованным. Используем state_machine.
  • По-умолчанию показываются только опубликованные вопросы. Используем default_scope.
  • У Consultor будет счетчик counter_cache, с количеством опубликованных вопросов

Так как контроллеры никому особо не интересны, я их не буду описывать. Начну сразу с моделей.

Модель Consultor (консультант):

# Консультант наследуется от Пользователя (STI).
class Consultor < User
  has_many :questions, :class_name => 'ConsultorQuestion', :dependent => :destroy

  default_scope where('type = ?', 'Consultor')
end

Модель Consultor использует STI (Single Table Inheritance) - наследуется от модели User, так как консультант это такой же пользователь, как и все остальные. Я создал отдельную модель для консультантов потому, что у консультантов есть вопросы (has_many :questions), а у обычных пользователей их нет. Незачем засорять модель User ненужными связями.

Я также определил default_scope, чтобы всегда по-умолчанию выбирались только консультанты, исключая других пользователей. С default_scope сразу начинаются проблемы, так как этот скоуп добавляет условие в каждый SQL-запрос к таблице consultors. И счетчик counter_cache перестаёт работать. Но об этом позже.

Об использовании state_machine

Давайте посмотрим на модель ConsultorQuestion. Я приведу сразу полную и законченную версию модели, конечно убрав оттуда то, что к делу не относится:

class ConsultorQuestion < ActiveRecord::Base
  state_machine :state, :initial => :unpublished do
    # Состояния
    state :unpublished
    state :published

    # Коллбэки переходов
    after_transition :unpublished => :published,   :do => :inc_counter_cache
    after_transition :published   => :unpublished, :do => :dec_counter_cache

    # События
    event :publish do
      transition :unpublished => :published
    end

    event :unpublish do
      transition :published => :unpublished
    end
  end

  # Перед удалением снимаем с публикации. Если состояние изменится
  # на unpublished, то автоматически произойдет декремент counter_cache
  before_destroy { unpublish! }

  belongs_to :consultor
  attr_protected :state, :consultor_id

  validates :questioner_name, :questioner_email, :question,  :presence => true
  default_scope with_state(:published).order('created_at DESC')

  def inc_counter_cache
    Consultor.unscoped.increment_counter('questions_count', self.consultor.id)
  end

  def dec_counter_cache
    Consultor.unscoped.decrement_counter('questions_count', self.consultor.id)
  end
end

Первое, на что обращаешь внимание, это state_machine. У вопроса могут быть два состояния: опубликован или не опубликован. state_machine очень облегчает переходы от состояния к состоянию, хотя это можно сделать и без неё. У вас получится странный код с кучей условий. Я попытаюсь изобразить, что может получиться, если не использовать state_machine:

class ConsultorQuestion < ActiveRecord::Base
  scope :published, where('state = ?', :published)
  scope :unpublished, where('state = ?', :unpublished)

  default_scope published.order('created_at DESC')

  before_save   :set_counter_cache
  after_destroy :set_counter_cache_for_destroy

  belongs_to :consultor
  attr_protected :state, :consultor_id

  validates :questioner_name, :questioner_email, :question,  :presence => true

  def published?
    state == 'published'
  end

  def unpublished?
    state == 'unpublished'
  end

  def state_was?(value)
    state_was == value.to_s
  end

  def set_counter_cache
    # Для добавляемых записей со статусом published
    if new_record? && published?
      # инкрементируем счетчик
      method = :increment_counter
    end

    # Для изменяемых записей.
    # Если запись была снята с публикации, декрементируем счетчик
    # Здесь можно воспользоваться aasm и переходом состояния (transition)
    if persisted? && state_was?('published') && unpublished?
      method = :decrement_counter
    end

    # Если запись была опубликована, декрементируем счетчик
    if persisted? && state_was?('unpublished') && published?
      method = :increment_counter
    end

    # Обновляем счетчик
    Consultor.unscoped.send(method, 'questions_count', self.consultor.id) if method
  end

  # Для удаляемых записей, которые были опубликованы
  def set_counter_cache_for_destroy
    Consultor.unscoped.decrement_counter('questions_count', self.consultor.id) if published?
  end
end

Ужасно, не правда ли? Зацените, сколько условий в коде и сравните это с предыдущим листингом, где нет ни одного условия и все реализуется при помощи коллбэков! В этом и есть сила state_machine - в вызове коллбэков при смене состояния. Чтобы сменить состояние, нам нужно всего лишь вызвать событие publish! или unpublish! в нужный момент. В нашем же случае мы используем коллбэки чтобы менять значение счетчика counter_cache. Помните, в условии было сказано, что нам нужно подсчитывать только опубликованные вопросы?

О счётчике counter_cache

Вы уже заметили в коде вызовы Consultor.unscoped.decrement_counter и Consultor.unscoped.increment_counter. Так мы меняем значение счетчика counter_cache вручную. Почему? Это стало необходимо потому, что мы используем default_scope в модели и этот скоуп добавляет своё условие к запросу, который обновляет счетчик counter_cache. В результате, счетчик не то что не обновляется, а даже происходит ошибка при выполнении SQL-запроса. Чтобы это исправить, мы вызываем через unscoped стандартные методы increment_counter и decrement_counter. Unscoped отменяет действие default_scope и запрос выполняется без ошибки.

Еще хочется заметить, что метод sizе берет значение из колонки counter_cache. Чтобы получить реальное количество записей в таблице, можно использовать метод length или метод count.

Немного о default_scope

Стоит с осторожностью пользоваться default_scope и не забывать, что он добавляет условие к каждому запросу. Поэтому, что-то может сломаться или работать не так, как вы ожидаете. Но зато, default_scope помогает улучшить безопасность вашего приложения, помогает защитить данные, которые требуют защиты от случайного просмотра.

Proper catching controller-level exceptions in Rails

Вторник, Июнь 21st, 2011

How to handle the exception in the Rails controller best way? Well, everyone knows that:

rescue_from ActiveRecord::RecordNotFound, :with => :page_404

Excellent. But what if in addition we want to handle all other exceptions to show the pretty 500 error page on production? Immediately we’ve got the idea to write:

rescue_from Exception, :with => :internal_error

Here’s the idea that we do not need to handle each exception separately, as well as all exceptions are derived from Exception so we can handle this Exception and calm down. But no such luck.

Let’s take a look what we have:

rescue_from ActiveRecord::RecordNotFound, :with => :page_404
rescue_from Exception, :with => :internal_error

At first glance, all is well. But this will not work as expected. The fact that the ActiveRecord::RecordNotFound exception is inherited from Exception, so the latest rescue_from call will fire for Exception too, so when an any exception raised, :internal_error will be called and this call which will block :page_404 call. So we never see 404 page :)

So, how to handle exceptions properly? I came to this decision:

# coding: utf-8
class ApplicationController < ActionController::Base
  # Catch all exceptions at a stretch
  rescue_from Exception, :with => :handle_exceptions

private

  # Handle exceptions
  def handle_exceptions(e)
    case e

    # Handling exception of CanCan gem
    when CanCan::AccessDenied
      authenticate_user!

    # Handling exception when record not found
    when ActiveRecord::RecordNotFound
      not_found

    # Or handle any other exceptions
    else
      internal_error(e)
    end
  end

  def not_found
    # Render 404 error page
    render 'application/not_found', :status => 404
  end

  def internal_error(exception)
    if RAILS_ENV == 'production'
      # Here you can send e-mail to developer or notify Hoptoad
      # ...

      # Render a pretty page for production with an error message
      render :layout   => 'layouts/internal_error',
             :template => 'application/internal_error',
             :status   => 500
    else
      # Forward exception that we were able to see a standard error page with stack for development.
      # With no arguments, it re-raises the recent exception.
      raise
    end
  end
end

There’s all clear from the example. I just added a branching for handling exceptions to avoid ambiguity as in the second example. Someone may say that we can just add rescue_from Exception at the top, but it will not save you if your colleague unwittingly add the desired rescue_from even higher. Therefore, I prefer for branching as the most obvious and intuitive solution.

UPD:
That article was mentioned in RubyShow Episode 171

Правильный перехват исключений уровня контроллера в Rails

Понедельник, Июнь 20th, 2011

Как красиво обработать исключение в контроллере Rails? Ну это все знают:

rescue_from ActiveRecord::RecordNotFound, :with => :page_404

Отлично. А что, если мы захотим вдобавок обработать все остальные исключения, чтобы показать красивую 500-ю страничку на продакшене? Сразу приходит мысль написать так:

rescue_from Exception, :with => :internal_error

Тут идея в том, что нам не нужно обрабатывать каждое исключение в отдельности, а так как все исключения наследуются от Exception, то мы можем обработать именно Exception и успокоиться. Но не тут-то было.

Давайте снова взглянем на то, что у нас получилось:

rescue_from ActiveRecord::RecordNotFound, :with => :page_404
rescue_from Exception, :with => :internal_error

На первый взгляд все хорошо. Но этот код не будет работать так, как от него ожидается. Дело в том, что исключение ActiveRecord::RecordNotFound наследуется от Exception, поэтому последнее объявление rescue_from будет срабатывать и для него, таким образом, при возникновении любого исключения, будет вызываться метод :internal_error, вызов которого будет перекрывать вызов :page_404. Так мы никогда не увидим страничку 404 :)

Как тогда правильно обрабатывать исключения? Я пришел к такому решению:

# coding: utf-8
class ApplicationController < ActionController::Base
  # Перехватываем все исключения подряд
  rescue_from Exception, :with => :handle_exceptions

private

  # "Разруливаем" исключения
  def handle_exceptions(e)
    case e

    # Обрабатываем исключение от гема CanCan
    when CanCan::AccessDenied
      authenticate_user!

    # Обрабатываем исключение при не найденной записи
    when ActiveRecord::RecordNotFound
      not_found

    # Или обрабатываем все остальные исключения
    else
      internal_error(e)
    end
  end

  def not_found
    # Страничка с ошибкой 404
    render 'application/not_found', :status => 404
  end

  def internal_error(exception)
    if RAILS_ENV == 'production'
      # Тут можно уведомить hoptoad или разработчика письмом об ошибке
      # ...

      # Рендерим красивую страничку с сообщением об ошибке для продакшена
      render :layout   => 'layouts/internal_error',
             :template => 'application/internal_error',
             :status   => 500
    else
      # А для девелопмента пробрасываем исключение дальше,
      # чтобы мы смогли увидеть стандартную страничку об ошибке для разработчиков.
      # При вызове raise без аргументов ruby автоматически "перекинет" последний эксепшен.
      raise
    end
  end
end

Тут всё понятно из примера. Я просто добавил ветвление при обработке исключений, чтобы избежать неоднозначностей, как во втором примере. В комментариех правильно заметили, что можно поместить перехват исключения Exception в самый верх, однако это не спасет вас, если ваш коллега по незнанию добавит перехват нужного ему эксепшена еще выше. Поэтому я выступаю именно за ветвление, как наиболее очевидное и понятное решение.

Кстати, напомню, что в Rails 3 перехватить исключение роутера ActionController::RoutingError при помощи rescue_from пока что невозможно.

Rails 3.1.beta: Could not find a JavaScript runtime

Четверг, Май 12th, 2011

A tiny note for saving time.
If you’re trying new rails 3.1.beta under Ubuntu you can see this error:

Could not find a JavaScript runtime. See https://github.com/sstephenson/execjs for a list of available runtimes.

The solution is quite simple. Just add gem ‘therubyracer’ into your Gemfile (it’s for Ruby MRI). Done.

Also, you can install another javascript runtimes. But who cares?

Свой шаблон для генератора контроллеров в Rails 3

Среда, Апрель 20th, 2011

Как известно, в Rails 3 можно использовать новый стиль кодирования, основанный на применении респондеров (Responders). То есть, форматы ответов задаются при помощи ключевого слова respond_to, а в экшенах используется respond_with.

Однако, нисмотря на то, что в таком стиле вы можете писать свои контроллеры уже сейчас, rails-генераторы (scaffold, controller и resource) все еще генерируют контроллеры на основе старого шаблона, в котором используется старый стиль кодирования. Раньше мне приходилось вручную переводить вновь сгенерированные контроллеры в новый стиль, пока я не переопределил стандартный шаблон контроллера.

В Rails 3 теперь можно удобно переопределить стандартный шаблон генератора контроллеров. Самый простой способ, это просто положить готовый шаблон (он приведен в конце статьи) в файл:

yourapp/lib/templates/rails/scaffold_controller/controller.rb

В этом случае, шаблон будет автоматически “подхватываться” при генерировании контроллера.

Второй способ может понадобиться, если нужно переопределить шаблон из гема или плагина. Для этого нужно сначала прописать в файле lib/yourgemname.rb вашего гема следующее:

require 'rails/all'

module Yourgemname
  class Railtie < ::Rails::Railtie
    config.generators.scaffold_controller = :yourgemname_controller
  end
end

Этим мы указываем, что генератор контроллеров должен использовать наш генератор. Файл генератора должен располагаться по пути lib/generators/rails/yourgemname_controller_generator.rb Теперь давайте посмотрим, как выглядит наш собственный генератор:

require 'rails/generators/rails/scaffold_controller/scaffold_controller_generator'

module Rails
  module Generators
    class YourgemnameControllerGenerator < ScaffoldControllerGenerator
      source_root File.expand_path("../templates", __FILE__)
    end
  end
end

Здесь все просто - мы наследуемся от базового класса генератора и указываем новый путь к шаблонам. Шаблон должен располагаться по пути lib/generators/rails/templates/controller.rb. Теперь посмотрим, как выглядит сам шаблон в стиле Rails 3:

class <%= controller_class_name %>Controller < ApplicationController
<% unless options[:singleton] -%>
  # GET /<%= table_name %>
  # GET /<%= table_name %>.xml
  def index
    @<%= table_name %> = <%= orm_class.all(class_name) %>
    respond_with(@<%= table_name %>)
  end
<% end -%>

  # GET /<%= table_name %>/1
  # GET /<%= table_name %>/1.xml
  def show
    @<%= file_name %> = <%= orm_class.find(class_name, "params[:id]") %>
    respond_with(@<%= file_name %>)
  end

  # GET /<%= table_name %>/new
  # GET /<%= table_name %>/new.xml
  def new
    @<%= file_name %> = <%= orm_class.build(class_name) %>
    respond_with(@<%= file_name %>)
  end

  # GET /<%= table_name %>/1/edit
  def edit
    @<%= file_name %> = <%= orm_class.find(class_name, "params[:id]") %>
  end

  # POST /<%= table_name %>
  # POST /<%= table_name %>.xml
  def create
    @<%= file_name %> = <%= orm_class.build(class_name, "params[:#{file_name}]") %>
    flash[:notice] = '<%= class_name %> was successfully created.' if @<%= orm_instance.save %>
    respond_with(@<%= file_name %>)
  end

  # PUT /<%= table_name %>/1
  # PUT /<%= table_name %>/1.xml
  def update
    @<%= file_name %> = <%= orm_class.find(class_name, "params[:id]") %>
    flash[:notice] = '<%= class_name %> was successfully updated.' if @<%= orm_instance.update_attributes("params[:#{file_name}]") %>
    respond_with(@<%= file_name %>)
  end

  # DELETE /<%= table_name %>/1
  # DELETE /<%= table_name %>/1.xml
  def destroy
    @<%= file_name %> = <%= orm_class.find(class_name, "params[:id]") %>
    @<%= orm_instance.destroy %>
    respond_with(@<%= file_name %>)
  end
end

Вот и все. Теперь при генерировании нового контроллера будет использоваться наш шаблон.

P.S: Хочу предупредить, что в Rails 3.1 (которая edge на момент написания статьи) это не будет работать в некоторых случаях, так как в этой версии есть небольшие изменения в шаблоне, а в приведенном мною шаблоне они не учитываются.

Подробнее о хранении товаров с различными свойствами в БД

Суббота, Март 5th, 2011

В прошлой статье меня попросили подробнее рассказать, как устроено хранение товаров с различными свойствами в проекте, о котором я упомянул.

Первый класс - это класс, который обеспечивает бестабличную ActiveRecord модель, если от него отнаследоваться. Он лежит в app/lib/tableless.rb

class Tableless
  include ActiveModel::Validations
  include ActiveModel::Conversion
  extend ActiveModel::Naming

  def initialize(props = {})
    props.each do |name, value|
      send("#{name}=", value)
    end
  end

  def persisted?
    false
  end
end

Этот код лежит в app/models/product_kinds/base.rb. Код обеспечивает поддержку групп свойств и свойств для моделей товаров (:group, :prop), а так же содержит базовую модель Base с двумя общими для всех типов товаров свойствами :name и :price.

Все остальные классы типов товаров должны наследоваться именно от Base. Смотрим код:

module ProductKinds
  module Props
    class PropOptions
      attr_accessor :as, :default, :collection

      def add(options)
        options.each_pair do |option, value|
          send "#{option}=".to_sym, value
        end
      end
    end

    class Prop
      attr_accessor :name

      def options
        @options
      end

      def options=(options)
        @options = PropOptions.new
        @options.add(options)
      end
    end

    module ClassMethods
      def self.extended(base)
        class_eval { attr_accessor :props, :current_group, :options }
      end

      # Set up group
      def group(name, &block)
        self.props       ||= {}
        self.options     ||= {}
        self.current_group = name;

        if block_given?
          instance_eval(&block)
        end
      end

      def prop(name, *options)
        self.props[self.current_group] ||= []

        prop = Prop.new
        prop.name    = name;
        prop.options = options.extract_options!

        self.props[self.current_group] << prop
        attr_accessor name
      end

      def grouped_prop_names
        if self == ProductKinds::Base
          self.props
        else
          properties = superclass.grouped_prop_names

          # Dirty, but works
          properties[:common] ||= []
          properties[:other]  ||= []
          self.props[:common] ||= []
          self.props[:other]  ||= []

          h = {
            :common => properties[:common] + self.props[:common],
            :other  => properties[:other] + self.props[:other]
          }

          h
        end
      end
    end

    module InstanceMethods
      def grouped_prop_names
        self.class.grouped_prop_names
      end
    end
  end

  class Base < Tableless
    extend  Props::ClassMethods
    include Props::InstanceMethods

    group :common do
      prop :name,  :as => :string
      prop :price, :as => :numeric
    end

    validates :name, :presence => true
    validates :price, :presence => true, :numericality => true
  end
end

Так мы описываем тип товара “Автомобиль”. Этот код лежит в app/models/product_kinds/automobile.rb

module ProductKinds
  class Automobile < Base
    group :common do
      prop :color, :as => :string
    end

    group :other do
      prop :max_speed, :as => :numeric
    end

    validates :color,     :presence => true
    validates :max_speed, :presence => true, :numericality => true
  end
end

Вот модель, описывающая тип товара. В БД у нас есть табличка со всеми существующими типами товаров. Этот код лежит в app/models/product_kind.rb

class ProductKind < ActiveRecord::Base
  validates :name, :presence => true, :uniqueness => true
  validates :classname, :presence => true, :uniqueness => true

  has_many :products

  def self.find_model_by_kind(kind)
    self.require_model_by_kind(kind)
    eval("ProductKinds::#{kind.camelize}")
  end

  def self.require_model_by_kind(kind)
    ProductKind.find_by_classname!(kind) # Проверяем существование
    require File.dirname(__FILE__) + "/product_kinds/#{kind}"
  end
end

А это - непосредственно модель, описывающая сам товар. У товара есть свойство :props, в котором в сериализованном виде хранится объект со свойствами товара, например, тот же Automobile. Мы видим геттер и сеттер для props, которые сериализуют и десериализую объект и, соответственно, строку:

require File.dirname(__FILE__) + '/product_kinds/base'

class Product < ActiveRecord::Base
  validates :props,           :presence => true
  validates :product_kind_id, :presence => true

  belongs_to :product_kind

  def props=(model)
    write_attribute(:props, Marshal::dump(model))
  end

  # Restore concrete product model from :props attr
  def props
    return @props_cache unless @props_cache.nil?
    props = read_attribute(:props)

    unless new_record?
      ProductKind.require_model_by_kind(product_kind.classname)
    end

    @props_cache = Marshal::restore(props) if props
    @props_cache
  end
end

Используется в контроллере это примерно так (создание нового товара):

class ProductsController < ApplicationController
  respond_to :html

  def create
    @kind          = params[:product][:kind]
    @product       = Product.new
    @props         = ProductKind.find_model_by_kind(@kind).new(params[:product][:props])
    @product.props           = @props
    @product.product_kind_id = ProductKind.find_by_classname!(@kind).id

    if @props.valid? && @product.valid?
      @product.save!

      flash[:notice] = 'Product was successfully created.'
      redirect_to new_product_image_path(@product)
    else
      render :new
    end
  end
end

P.S.: Знаю, что некоторые куски кода далеко не оптимальны и можно написать лучше и проще. Все в ваших руках - этот код только для примера, как можно сделать :)

Надеюсь, я сумел разъяснить некоторые детали, которые читатели сего блога хотели знать.

Бестабличные модели в Rails 3 и как их можно использовать

Четверг, Март 3rd, 2011

По-умолчанию модель ActiveRecord маппится на таблицу в БД. Но, бывают случаи, когда нужна модель без привязки к таблице - так удобно генерировать формы и вообще - где еще хранить бизнес-логику, как не в модели?

В Rails 3 ребята подошли к этому с умом. Разработчики вынесли часть логики ActiveRecord в отдельные модули и классы, обеспечив этим слабую связанность компонентов. Теперь ничто не мешает подмешать нужную логику в свой класс. Давайте посмотрим, как может выглядеть такая бестабличная модель:

class Tableless
  # Подмешиваем валидаторы, методы преобразований
  # и методы соглашения об именовании для ActiveRecord
  include ActiveModel::Validations
  include ActiveModel::Conversion
  extend ActiveModel::Naming

  # Полезняшка, чтобы создавать свойства из конструктора
  def initialize(props = {})
    props.each do |name, value|
      send("#{name}=", value)
    end
  end

  # Переопределяем метод persisted?, чтобы он всегда
  # возвращал false. Это сообщает ActiveRecord,
  # что данные модели не сохраняются в БД.
  def persisted?
    false
  end
end

Всё, теперь можно спокойно наследовать модели от класса Tableless - вы получите нормальную модель с валидацией, но без привязки к БД:

class Person < Tableless
  validates :name, :presence => true
  attr_accessible :name
end

Где подобное может пригодиться? Ну, например в форме отправки письма с сайта. Мне это пригодилось, когда нужно было сделать функциональность на сайте, которая позволяла бы создавать различные типы товаров.

У каждого товара, например у мобильного телефона или автомобиля, могут быть как общие свойства (такие как цена или масса), так и свои собственные, как наличие bluetooth для телефона или максимальная скорость для автомобиля.

Создавать отдельную таблицу под каждый тип товара мне совсем не впирало, поэтому, я решил что-то придумать. В итоге, я сделал одну таблицу, в которой в текстовом поле props хранилась сериализованная модель, и в отдельном поле был указан тип товара. Под каждый тип товара создавалась соответствующая этому типу модель со своими и общими свойствами.

Получилось очень удобно - теперь создать новый тип товара стало действительно быстро и просто, к тому же из моделей типов товара стало очень удобно генерировать формы и списки свойств, а еще каждый тип товара был обеспечен удобной рельсовой валидацией. Вот пример двух таких моделей, для сотового телефона и автомобиля:

module ProductKinds
  class Cellphone < Base
    group :common do
      prop :color,  :as => :string
      prop :weight, :as => :numeric
    end

    validates :color,  :presence => true
    validates :weight, :presence => true, :numericality => true
  end
end

module ProductKinds
  class Automobile < Base
    group :common do
      prop :color, :as => :string
    end

    group :other do
      prop :max_speed, :as => :numeric
    end

    validates :color,     :presence => true
    validates :max_speed, :presence => true, :numericality => true
  end
end

Классы моделей наследуются не напрямую от Tableless, а от класса базовой модели Base, она же в свою очередь наследуется от Tableless.

Это связано с тем, что необходимо было наличие общих валидируемых свойств (цена) для всех дочерних моделей. Еще вы можете заметить вызов методов group и prop - они подмешиваются в модели Base. Эти методы определяют свойства товара и разбивают эти свойства по группам. Надеюсь, из кода моделей все понятно. Я не буду тут выкладывать код базового класса моделей Base, так как там нет ничего экстраординарного, но если кому потребуется, то попросите в комментах.

Конечно, у такого подхода (хранение сериализованных моделей в БД) есть и минусы. Например, если код модели изменится, то вам нужно будет пройтись по всем сериализованным моделям в БД и обновить их в соответствии с новым кодом. Это, конечно же, можно и нужно автоматизировать в виде rake-задачи.

Второй минус это поиск. Вы не сможете напрямую искать по БД (кто-то еще так ищет, серьезно?) при помощи SQL запросов, но по-моему, это не так уж важно, когда есть такие офигенные инструменты как sphinx и thinking_sphinx. Можно отдавать данные сфинксу в виде XML, рассериализовав модели - не так уж трудно добавить такой функционал.

Хотя, я считаю, плюсов тут гораздо больше. Вроде это все, о чем я хотел рассказать.

Обработка ActionController::RoutingError в Rails 3

Среда, Март 2nd, 2011

Раньше, до третьих рельс, исключение маршрутизатора можно было красиво и спокойно перехватить, чтобы показать страницу с ошибкой 404. Достаточно было поместить в ApplicationController следующее:

rescue_from ActionController::RoutingError, :with => :error_not_found

Ну, и определить метод error_not_found, который бы рендерил страницу с 404 ошибкой. Но в Rails 3 произошло изменение - стал использоваться гем rack-mount, который используется в Rails как middleware, то есть как прослойка между вашим приложением и веб-сервером. И исключение RoutingError переехало именно в middleware, так что теперь его не получится отловить в контроллере, так как это исключение отлавливается и рендерится еще до вызова контроллера.

Разработчик Rails, а именно Хосе Валим, назначил этой проблеме статус low и поместил тикет в milestone 3.1, что означает, что решение разработчики родят только в версии Rails 3.1. На момент написания этой статьи текущая версия - 3.0.5, так что ждать еще достаточно долго. Сейчас разработчики думают над тем, как сделать обработку ошибок маршрутизации по-нормальному. Но Хосе предложил временное решение - использовать globbing в маршрутах. Нужно просто в вашем routes.rb в самом конце прописать это:

match "*path", :to => "application#error_not_found"

И добавить метод home#routing_error по вашему вкусу. Мой таков:


class ApplicationController < ActionController::Base
  def error_not_found
    render :template => "/error/404.html.erb", :status => 404
  end
end

Остальные эксепшены, вроде ActionController::UnknownController перехватываются нормально при помощи rescue_from и их можно преспокойно обрабатывать в ApplicationController, например.

Да, еще есть вот такое решение. С использованием гема vidibus-routing_error можно работать с исключением RoutingError как прежде, через rescue_from.

Прекрасные контроллеры в Rails 3

Пятница, Сентябрь 3rd, 2010

Зацените, кто еще не видел, насколько меньше кода стало в контроллерах Rails 3! Теперь можно сказать, что контроллеры стали по-настоящему соответствующими принципу DRY. И всё это благодаря респондерам. В общем, мне нравится:

class ProductsController < ApplicationController
  respond_to :html, :xml  

  def index
    respond_with(@products = Product.all)
  end  

  def show
    respond_with(@product = Product.find(params[:id]))
  end  

  def new
    respond_with(@product = Product.new)
  end  

  def create
    @product = Product.new(params[:product])
    flash[:notice] = "Successfully created product." if @product.save
    respond_with(@product)
  end  

  def edit
    respond_with(@product = Product.find(params[:id]))
  end  

  def update
    @product = Product.find(params[:id])
    flash[:notice] = "Successfully updated product."
      if @product.update_attributes(params[:product])

    respond_with(@product)
  end  

  def destroy
    @product = Product.find(params[:id])
    @product.destroy
    flash[:notice] = "Successfully destroyed product."
    respond_with(@product)
  end
end  

В респондерах теперь содержится вся та магия по отдаче ответа клиенту, которая раньше находилась в контроллере, в блоке, передаваемому методу respond_to. Сейчас стало удобно контролировать отдачу типов контента - ну нужно править каждый метод контроллера, если вы захотите добавить или убрать тип контента. Кстати, метод respond_to поддерживает ключи :only и :except, чтобы контролировать типы контента для методов.
Самое главное - это все работает, и работает замечательно! Я сейчас пишу проект на Rails 3 и использую всю эту красоту. В общем - радости нет предела)

Рекомендую почитать вот эту статейку на английском: Controllers in Rails 3