Archive for Январь, 2012

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

Вторник, Январь 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 помогает улучшить безопасность вашего приложения, помогает защитить данные, которые требуют защиты от случайного просмотра.

Информационное безобразие

Четверг, Январь 12th, 2012

Мы, программисты и айтишники, впрочем, как и люди остальных профессий, ежедневно получаем слишком много информации. Больше того, чем можем переварить. Не сталкивался ли ты с такой штукой, что у тебя в голове играет музыка, самая попсовая которая может быть, хотя ты и любитель рока? Эту музыку ты услышал, когда был в кафешке на обеденном перерыве. И вот она прочно обосновалась в твоих мозгах! Так же происходит с новостными сайтами, газетами, рекламой и телевидением. Однако нам не нужна эта тупая информация - все, что мы хотим узнать, мы можем узнать из книг, общения с нужными людьми, либо интернета (предварительно отключив баннеры).

Меня эта лишняя информация просто сделала уже не мной, я из тех, кто сильно реагирует на нее. Я решил так, что я отказываюсь от новостных и развлекательных сайтов (я их забанил у себя), ТВ я и так редко смотрю, если смотрю - то Дискавери. Перестал совсем брать листовки у раздавальщиков на улице, хотя раньше изредка брал. Срываю рекламу у себя у подъезда и в лифте, чтобы не видеть ее изо дня в день. Не смотрю на мониторы и рекламу в метро (слушаю плеер с закрытыми глазами). Стараюсь не обращать внимания на рекламные билборды на улицах. Перестал смотреть Ютуб. Но стал читать больше бумажных книг.

В общем, избавился от лишнего информационного шума, который ничего не дает, кроме депрессивного состояния. Какая мне разница, что где-то убили 98 человек? Че я должен что-ли говорить, оо бля, какой ужас, как так можно? Я же их не знаю, и мне вообще плевать, зачем мне эту новость знать? Это то же самое, что если бы тебе щас сказали, что на планете Ка-Пэкс убили 980 миллиардов капэксян - какие бы были твои чувства? Тут нужно применять специальный математический метод, который мой друг Иван предложил 8 лет назад - это метод наложения хуя. Как его применять - несложно разобраться самому.

Я щас руководствуюсь аналогией малых племен - у тебя в племени все хорошо, но если в племени, которое находится за 10 дней ходьбы от тебя что-то плохое случилось - зачем тебе знать об этом, запариваться и как-то переживать? Не хочу тратить свою энергию на это!

Знать нужно только о тех вещах, которые тебе нужны в твоей жизни или работе. Но об этих вещах ты должен узнать только тогда, когда сам этого захочешь.

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

Virtualbox guest ssh

Воскресенье, Январь 8th, 2012

Всё время забываю, как делать port-forwarding в Virtualbox для Ubuntu-host в Ubuntu-guest.

Итак. Для начала в настройках сети виртуальной машины устанавливаем Network Adapter как NAT. Делаем это при выключенной виртуальной машине.

Дальше смотрим виртуалки, которые есть:

$ VBoxManage list vms

И делаем, собственно, port forwarding:

$ VBoxManage modifyvm "Ubuntu 11.10" --natpf1 "guestssh,tcp,,2222,,22"

Ubuntu 11.10 - это имя виртуальной машины, для которой пробрасываем порты.

Всё. Дальше, запускаем виртуальную машину и соединяемся с ней:

$ ssh -p 2222 user@localhost

Готово.