Изменение формата сессии при переходе с Rails 3.0 на 3.2
Групон сейчас активно переводится с rails 3.0 на rails 3.2. Самая досадная неожиданность, которая встретилась в процессе переезда — это изменение формата хранения flash сообщений в сессии. Если бы при этом flash сообщение пропадало, то это было бы не страшно, но при декодировании сессии происходит исключение, и пользователь видит 500 ошибку до тех пор, пока не почистит куки. Конечно, пользователей, у которых в сессии будет flash сообщение в момент переключения с версии 3.0 на 3.2 будет не так и много, но это пользователи, потерянные навсегда.
Обновление на rails3 и проблемы, связанные с этим
На «Групоне» мы давно уже переходим с Rails 2.3 на Rails 3.0. На этой неделе перешли — полет нормальный. Но поскольку сразу переход у нас не получился, то мы переходили по частям. И тут возникла проблема, что в rails3 нужен новый devise (1.5.3), которому нужен warden (1.1.1). А в rails2 используется devise (1.0.9), которому нужен warden (0.10.7). И вот этот вот warden стал по-другому сериализовать сессию. А devise стал хранить remember_token в подписанной (signed) cookie. Более того, из самих рельс пропал класс ActionController::Flash::FlashHash, поэтому при десериализации сессии происходило неуловимое исключение в Marshal.load.
Все эти проблемы решены были кодом, которые приведен ниже. И еще похожий код был в rails2-ветке, который конвертил сессию третьих рельс во вторую. Пользуйтесь с осторожностью.
Проблемы с find_in_batches
Иногда мне кажется, что большинство инженерных историй похожи как две капли воды. Вот и эта история началась с того, что в одном большом отчете цифры не сходились раза в два.
Мысленно закурив трубку, как Шерлок Холмс, я взялся за дело. Вот таким образом выглядела модель, по которой считались цифры (я опускаю ненужные подробности).
class OfferDescription < ActiveRecord::Base
has_many :children, :class_name => 'OfferDescription', :foreign_key => :parent_id
end
В отчете данные группировались по родительским объектам OfferDescription
.
Именно у объектов, у которых были дети, в отчетах были все нули. Рассчитывал
отчеты приблизительно вот такой код.
Некоторые проблемы с cached_action в Rails 2.3
На Групоне у нас есть внутренняя страница, на которой динамически отображаются данные о продажах по различным городам. Результаты этой страницы зависят от параметров, которые передаются в метод show. А потом эта страница динамически обновляется с помощью ajax-запросов.
# dashboard.rb
class Admin::DashboardsController < Admin::BaseController
layout "admin"
def show
# Здесь сложные-сложные запросы к БД
if request.xhr?
render 'show', :layout => false
end
end
end
Поскольку запросы очень тяжелые, да и сама страничка непростая, то отдача ее сильно нагружала БД и app-сервера.
Переопределение Rails.logger и проблемы с ним
Часто один и тот же rails-проект используется и для web-части, и для каких-то других задач, выполняемых по расписанию или для создания демонов. В таких случаях иногда хочется, чтобы логи разных частей писались в разные файлы.
Готового способа для этого я не нашел. Например, посмотрим на такой пример:
$ ./script/console
Loading development environment (Rails 2.3.5)
>> ActiveRecord::Base.logger = Logger.new(STDOUT)
=> #<Logger:0xb192594 @progname=nil, @level=0, @default_formatter=#<Logger::Formatter:0xb19255c @datetime_format=nil>, formatternil, logdev#<Logger::LogDevice:0xb19247c @shift_size=nil, @shift_age=nil, @filename=nil, @dev=#<IO:<STDOUT>, mutex#<Logger::LogDevice::LogDeviceMutex:0xb192444 @mon_owner=nil, @mon_count=0, @mon_mutex=#<Mutex:0xb19240c>
>> User.first
=> #<User id: 1, email: "seeduser@scalaxy.ru" ...>
>>
И где же логи, спросите вы?
PgQ и Londiste
Хотя я так и не написал толком, как пользоваться PgQ и Londiste, но уже написал плагин, который облегчает его использование вместе с рельсами.
http://github.com/evtuhovich/pgq/tree/master
В README всё написано на плохом английском (с хорошим английским у меня плохо).
Совсем скоро я добавлю туда возможность прогонять миграции на master и slave базах данных одновременно. Тогда при очередной выкатке необходимо будет сделать только rake londiste:update в самом конце, после того, как все миграции прогонятся.
Проблема с проверкой уникальности какого-то поля в rails
Пусть в модели User у нас описана валидация для поля email
validates_uniqueness_of :email, :case_sensitive => false,
:message => i18n_proxy(:email_already_registered)
Следующий код генерирует вот такой запрос к базе данных:
SELECT * FROM users WHERE lower(email) = 'thmth' LIMIT 1
А на таблице users у нас индекс по полю email. В postgresql запрос, что вверху, не будет использовать индекс. Представьте, что будет, когда в таблице users десятки тысяч записей, а на каждое изменение любого поля в users вызывается такой запрос.
Именно это я наблюдал совсем недавно на нашей живой базе. Проблема решается
просто, например, убрать :case_sensetive
, а email всегда
предварительно переводить в нижние буквы.
Тесты и тестирование
Возможно, в сайте из 3 контроллеров и 15 страниц, тесты и не нужны. Я обычно не делаю тесты на маленьких проектах, которые пишу один.
В случае, если проект собирается быть большим, длиться долго, а команда состоит больше чем из одного человека, то тесты — крайне желательный инструмент для того, чтобы разработка не вышла из под контроля. Без тестов в какой-то момент невозможно внести серьезное изменение в код, потому что знаешь, что такое изменение заденет еще некоторые части и точно знаешь, что все зависимости точно не учтешь. В этот момент разработка превращается в постоянное исправление ошибок, при которых снова вносятся ошибки и так до бесконечности.
Проблемы с кэшированием
Все ситуации, о которых я здесь пишу, встретились мне в повседневной работе. Единственное, что я делаю — меняю название классов, чтобы не утруждать моего читателя незнакомой для него предметной областью.
Итак, у нас есть следующие модели:
# shop.rb
class Shop < ActiveRecord::Base
has_many :categories
cached_methods do
def wait_orders_count
Order.count :conditions => {:status_id => Order::WAIT}
end
def paid_orders_count
Order.count :conditions => {:status_id => Order::PAID}
end
def bad_orders_count
Order.count :conditions => {:status_id => Order::BAD}
end
end
end
class Category < ActiveRecord::Base
belongs_to :shop
has_many :products
end
class Product < ActiveRecord::Base
belongs_to :category
has_many :orders
end
class Order
belongs_to :product
end
Методы внутри cached_methods
выполняются в том случае, если значение для них не нашлось в кэше (например, в memcached).
До этого поля *_orders_count хранились в базе и мы добавляли к ним +1/-1 каждый раз, когда изменяли статус заказа
(Order). Если магазин большой, то таких обновлений будет очень много, что создает нагрузку на базу, поэтому их и вынесли
в кэш. Чтобы не думать, каждый раз при обновлении статуса заказа, мы полностью сбрасывали кэши для конкретного магазина.
PosgtreSql, миграции и огромные таблицы
Миграции в rails — это очень правильный инструмент. Правда, иногда случаются казусы, потому что конкретная БД перестает быть «сферическим конем в вакууме», как только количество данных и нагрузка на нее становится существенной.
Пусть у нас есть таблица posts, в которой 10 миллионов записей. И мы решили добавить в нее поле is_searchable.
$ script/generate migration add_is_searchable_to_posts
class AddIsSearchableToPosts < ActiveRecord::Migration
def self.up
add_column :posts, :is_searchable, :boolean, :default => true, :null => false
end
def self.down
remove_column :posts, :is_searchable
end
end
Если на базе development данных у вас немного, то миграция пройдет замечательно. На production базе она может занять несколько часов, блокируя таблицу posts. Заглянув в документацию по postgresql и немного подумав, можно переписать эту миграцию вот так: