Posts Tagged ‘Ruby on Rails’

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

Суббота, Март 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

Вторник, Март 1st, 2011

Rails 3 вышел несколько месяцев назад, и одним из многих изменений было обновление API для генераторов и шаблонов приложений. Если вы еще не знаете, то теперь в Rails 3 используется Thor для этих целей, что даёт большую модульность и настраиваемость.

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

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

Вот так, например, выглядит шаблон, подготовленный Аароном Самнером:

# Создание rvmrc файла
create_file ".rvmrc", "rvm gemset use #{app_name}"

gem "haml-rails"
gem "sass"
# hpricot и ruby_parser используются гемом haml
gem "hpricot", :group => :development
gem "ruby_parser", :group => :development
gem "nifty-generators"
gem "simple_form"
gem "jquery-rails"

# Аутентификация и авторизация
gem "devise"
gem "cancan"

# rspec, factory girl, webrat, autotest для тестирования
gem "rails3-generators", :group => [ :development ]
gem "rspec-rails", :group => [ :development, :test ]
gem "factory_girl_rails", :group => [ :development, :test ]
gem "webrat", :group => :test
gem "ffaker", :group => :test
gem "autotest", :group => :test

run 'bundle install'

rake "db:create", :env => 'development'
rake "db:create", :env => 'test'

generate 'nifty:layout --haml'
remove_file 'app/views/layouts/application.html.erb' # вместо этого используется nifty layout
generate 'simple_form:install'
generate 'nifty:config'
remove_file 'public/javascripts/rails.js' # jquery-rails заменит стандартный prototype-ujs
generate 'jquery:install --ui'
generate 'rspec:install'
inject_into_file 'spec/spec_helper.rb', "\nrequire 'factory_girl'", :after => "require 'rspec/rails'"
inject_into_file 'config/application.rb', :after => "config.filter_parameters += [:password]" do
  <<-eos

    # Настройка генераторов
    config.generators do |g|
      g.stylesheets false
      g.form_builder :simple_form
      g.fixture_replacement :factory_girl, :dir => 'spec/factories'
    end
  eos
end
run "echo '--format documentation' >> .rspec"

# Настройка аутентификации и авторизации
generate "devise:install"
generate "devise User"
generate "devise:views"
run "db:migrate"
generate "cancan:ability"

# Удаляем ненужные файлы, копируем конфиг БД и добавляем его в игнор
remove_file 'public/index.html'
remove_file 'rm public/images/rails.png'
run 'cp config/database.yml config/database.example'
run "echo 'config/database.yml' >> .gitignore"

# Создаем git-репозиторий и делаем первый коммит
git :init
git :add => "."
git :commit => "-a -m 'create initial application'"

say <<-eos
  ============================================================================
  Your new Rails application is ready to go.

  Don't forget to scroll up for important messages from installed generators.
eos

Достаточно просто, не правда ли? Воспользоваться таким шаблоном можно так:

$ wget https://gist.github.com/848735.txt
$ rails new appname -m 848735.txt

Я сейчас создаю специальный гем, который поможет упростить создание новыйх приложений Rails 3 - этот гем называется playmo-rails, и уже можно посмотреть некоторые наработки. Мой гем так же использует Thor для генерации различных файлов. Этот гем создан для быстрого старта - можно создать новое приложения, добавляя в него нужные библиотеки и расширения, просто отвечая на вопросы установщика. И это очень удобно.

Пока playmo-rails умеет не так уж много:

  • Создает приложение и использованием Compass
  • Лайот приложения использует html5 boilerplate
  • Можно выбрать установку JQuery либо Mootools
  • Удаляет ненужные файлы из нового rails-приложения

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

Прекрасные контроллеры в 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

Отличная презентация об улучшениях в коде проекта на Rails 3

Вторник, Август 31st, 2010

Смотрим, как нужно писать код теперь. Мне презентаха очень понравилась!

Installing Ruby 1.9.2 and Rails 3 stable on Ubuntu

Вторник, Август 31st, 2010

Let’s install Ruby 1.9.2 and Rails 3 stable on Ubuntu. I’m going to use just one Ruby version so, this installation without RVM (Ruby Version Manager). I’m using Ubuntu 10.04, 32 bit version.

If you have not yet installed the following packages - install them:

$ sudo apt-get install gcc g++ build-essential libssl-dev libreadline5-dev zlib1g-dev linux-headers-generic libsqlite3-dev

Now download Ruby 1.9.2 sources, unpack them and install:

$ wget ftp://ftp.ruby-lang.org//pub/ruby/1.9/ruby-1.9.2-p0.tar.gz
$ tar -xvzf ruby-1.9.2-p0.tar.gz
$ cd ruby-1.9.2-p0/
$ ./configure --prefix=/usr/local/ruby
$ make && sudo make install

Add path to binary Ruby files.

$ sudo gedit /etc/environment

You need to add in the PATH variable that path - /usr/local/ruby/bin, should look something like this:

PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/ruby/bin"

Then run the source command for the file /etc/environment to apply changes.

$ source /etc/environment

Now check is Ruby installed properly:

$ ruby -v

You should see something like this: ruby 1.9.2p0 (2010-08-18 revision 29036) [x86_64-linux]
Now create a symbolic link for ruby and gem program

$ sudo ln -s /usr/local/ruby/bin/ruby /usr/local/bin/ruby
$ sudo ln -s /usr/local/ruby/bin/gem /usr/bin/gem

Ruby 1.9.2 is already includes Rubygems, so you do not have to install it.
Now install the required gem packages, including Rails 3.:

$ sudo gem install tzinfo builder memcache-client rack rack-test erubis mail text-format bundler thor i18n sqlite3-ruby
$ sudo gem install rack-mount --version=0.4.0
$ sudo gem install rails --version 3.0.0

Check Rails version:

$ rails -v

You should see the version number 3.0.0. Otherwise, try to execute command source /etc/environment and enter rails -v command once again.
Now you are ready to create a new Rails 3 application:

$ rails new myproject
cd myproject
rails server

UP: To update rails to latest version (3.0.3 for now), run:

sudo gem update rails

And change rails gem in your Gemfile to gem ‘rails’, ‘3.0.3′

Установка Ruby 1.9.2 и Rails 3 stable на Ubuntu

Вторник, Август 31st, 2010

Давайте установим Ruby 1.9.2 и Rails 3 stable на Ubuntu. Я использовал Ubuntu 10.04, 32-х битную версию. Здесь рассматривается установка Ruby без RVM, так что если будете следовать этому мануалу, то сначала удалите старую версию Ruby и её зависимости.

Если у вас еще не установлены следующие пакеты - установите их:

$ sudo apt-get install gcc g++ build-essential libssl-dev libreadline5-dev zlib1g-dev linux-headers-generic

Теперь скачаем исходники Ruby 1.9.2, распакуем их и установим:

$ wget ftp://ftp.ruby-lang.org//pub/ruby/1.9/ruby-1.9.2-p0.tar.gz
$ tar -xvzf ruby-1.9.2-p0.tar.gz
$ cd ruby-1.9.2-p0/
$ ./configure --prefix=/usr/local/ruby
$ make && sudo make install

Добавим путь к бинарным файлам Ruby.

$ sudo gedit /etc/environment

Вам нужно добавить в переменную PATH этот путь - /usr/local/ruby/bin, должно получиться примерно так:

PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:/usr/local/ruby/bin"

Затем выполним команду source для файла, чтобы применить изменения.

$ source /etc/environment

Теперь проверим, установился ли Ruby:

$ ruby -v

Вы должны увидеть примерно следующее: ruby 1.9.2p0 (2010-08-18 revision 29036) [x86_64-linux]
Теперь создадим символическую ссылку, чтобы программа gem работала

$ sudo ln -s /usr/local/ruby/bin/ruby /usr/local/bin/ruby
$ sudo ln -s /usr/local/ruby/bin/gem /usr/bin/gem

Ruby 1.9.2 уже содержит Rubygems, поэтому вам не придется его устанавливать.
Теперь установим нужные пакеты gem, включая rails 3.:

$ sudo gem install tzinfo builder memcache-client rack rack-test erubis mail text-format bundler thor i18n sqlite3-ruby
$ sudo gem install rack-mount --version=0.4.0
$ sudo gem install rails --version 3.0.0

Проверим версию rails:

$ rails -v

Вы должны увидеть номер версии 3.0.0. Если этого не произошло, по попробуйте выполнить команду source /etc/environment, затем запустите rails -v еще раз.
Теперь всё готово к созданию нового приложения rails:

$ rails new myproject
cd myproject
rails server

Установка Ruby и RoR на Ubuntu 10.04

Среда, Август 18th, 2010

Если вы используете Ubuntu 10.04 или 9.10, то следующие шаги по установке будут одинаковы для той и другой версии ОС. Чтобы установить Ruby on Rails, сначала вам нужно установить некоторые примочки, а так же сам Ruby.

Можно установить Ruby из репозитория, либо из исходного кода. Чтобы установить Ruby из репозитория, наберите в терминале следующую строку:

$ sudo apt-get install ruby-full build-essential

Если же вы хотите установить из исходников, то сначала нужно установить следующие пакеты:

$ sudo apt-get install build-essential libssl-dev libreadline5-dev zlib1g-dev

Теперь вам нужно скачать архив с исходными кодами Ruby:

$ wget ftp://ftp.ruby-lang.org/pub/ruby/stable-snapshot.tar.gz

Разархивируем файлы:

tar xzf stable-snapshot.tar.gz

Компилируем и устанавливаем Ruby:

$ cd ruby/
$ ./configure
$ make
$ sudo make install

Теперь запустите в консоли команду irb, и если вы не увидите никаких ошибок - поздравляю, Ruby установился успешно.

Перед тем, как установить Rails, вам нужно установить некоторые пакеты gem. Gem-пакеты - это упакованные приложения или библиотеки Ruby. Вы можете использовать команду gem чтобы установить различные бесплатные библиотеки, включая и Rails. Подробнее о rubygems можно почитать в мануале.

Устанавливаем rubygems следующей командой:

$ wget http://rubyforge.org/frs/download.php/45905/rubygems-1.3.2.tgz
$ tar xzvf rubygems-1.3.2.tgz
$ cd rubygems-1.3.2
$ sudo ruby setup.rb
$ sudo ln -s /usr/bin/gem1.8 /usr/bin/gem

Теперь при помощи команды gem устанавливаем Rails:

$ sudo gem install rails

Эта команда установит последнюю версию Rails, но если вы хотите установить одну из предыдущих версий, например 2.2.2, то воспользуйтесь следующей командой:

$ sudo gem install –version = 2.2.2 rails