Создание баннерной системы на rails часть 1.

Posted on March 19, 2009
В связи с выходом нового rails 2.3.2 появилось множество возможностей ускорить отклик приложения.
Одним из таких способов является Rails Metal. Благодаря нему возможно обрабатывать запросы в обход всей машинерии, что дает огромное преимущество в скорости работы.
Для примера я решил создать приложение для баннерной сети. Приложение крайне просто и используется только как пример.
Вначале создадим новое приложение с помощью шаблона:

git :init

plugin 'restful-authentication', :git => 'git://github.com/technoweenie/restful-authentication.git', :submodule => true
plugin 'haml', :git => "git://github.com/nex3/haml.git", :submodule => true
plugin 'paperclip', :git => "git://github.com/thoughtbot/paperclip.git", :submodule => true
plugin 'acts_as_taggable_redux', :git => 'git://github.com/geemus/acts_as_taggable_redux.git', :submodule => true
plugin 'will_paginate', :git => 'git://github.com/mislav/will_paginate.git', :submodule => true
plugin 'role_requirement', :git => 'git://github.com/timcharper/role_requirement.git', :submodule => true


run "touch tmp/.gitignore log/.gitignore vendor/.gitignore"
run %{find . -type d -empty | grep -v "vendor" | grep -v ".git" | grep -v "tmp" | xargs -I xxx touch xxx/.gitignore}
file '.gitignore', <<-END
.DS_Store
log/*.log
tmp/**/*
config/database.yml
db/*.sqlite3
END
run "rm README"
run "rm public/index.html"
run "rm public/favicon.ico"
run "rm public/robots.txt"


generate("authenticated", "user session")
generate("roles", "Role User")

rake('db:create')
rake('db:sessions:create')
rake('acts_as_taggable:db:create')

initializer 'session_store.rb', <<-END
        ActionController::Base.session = { :session_key => '_#{(1..6).map { |x| (65 + rand(26)).chr }.join}_session', :secret => '#{(1..40).map { |x| (65 + rand(26)).chr }.join}' }
        ActionController::Base.session_store = :active_record_store
END


git :submodule => "init"

git :add => '.'
git :commit => "-a -m 'Initial commit'"

с помощью комманды

rails . -m ../template.txt

После этого можно создать обработчик запроса баннера (показ) и обработчик клика на баннер.


script/generate metal get_banner
Появившийся после этого файл app/metal/get_banner.rb выглядит так:

class GetBanner
  def self.call(env)
    if env["PATH_INFO"] =~ /^\/get_banner/
      [200, {"Content-Type" => "text/html"}, "Hello, World!"]
    else
      [404, {"Content-Type" => "text/html"}, "Not Found"]
    end
  end
end

Теперь стоит подумать об организации базы данных для того, что-бы наше преимущество в скорости обработки запроса не съедалось базой данных.
У нас имеется набор объектов WebSite (площадок), набор рекламных проектов (Projects) и набор баннеров в рекламных проектах (Banners). При этом каждый проект имеет тэги и каждая площадка тоже имеет набор тэгов. Нам необходимо создать сущность которая будет звеном между сайтом и баннером. Для того-чтобы выбор баннеров был простым запросом к БД, например:


SELECT *, MD5(RAND()) as rand  FROM sites_banners WHERE site_id=1 ORDER BY rand LIMIT 0,1
А так-же необходимо все показы и клики записывать в другую таблицу с дальнейшим обсчетом по деньгам.

Создадим базовые модели:


script/generate resource website url:string user_id:integer enabled:boolean
script/generate resource project user_id:integer name:string enabled:boolean
script/generate resource banner user_id:integer image_file_name:string image_content_type:string image_file_size:integer project_id:integer enabled:boolean
И отредактируем файлы миграции для установления значения enabled в true. (Мы заботимся о наших пользователях ;))

После этого наши модели будут выглядеть примерно так:


class Banner < ActiveRecord::Base
  belongs_to :project
  belongs_to :user
  has_attached_file :image
  validates_attachment_content_type :image, :content_type => ['image/jpeg', 'image/gif']
end

class Project < ActiveRecord::Base
  has_many :banners
  belongs_to :user
  validates_presence_of :name
end


class Website < ActiveRecord::Base
  belongs_to :user
  validates_presence_of :url
  validates_format_of :url, :with =>  /^(http|https):\/\/[a-z0-9]+([\-\.]{1}[a-z0-9]+)*\.[a-z]{2,5}(([0-9]{1,5})?\/.*)?$/ix
end

Так-же необходимо добавить в модель юзера строки:

has_many :websites
has_many :projects
has_many :banners

Теперь перейдем к работе со статистикой, для этого создадим модель BannerView


script/generate model banner_view website_id:integer banner_id:integer request_ip:string referrer:string
Именно в нее и будет писаться вся статистика по просмотру баннеров. В дальнейшем будет производится обработка данных этой таблицы с целью извлечения из нее необходимых данных с удалением старых. Для ускорения работы модель не имеет ORM связей.

Теперь перейдем к нашему metal раздатчику баннеров. В этой части статьи будем использовать самый примитивный выбор баннера - случайный из всех
поэтому его код будет выглядеть так:


require(File.dirname(__FILE__) + "/../../config/environment") unless defined?(Rails)

class GetBanner
  def self.call(env)
    if env["PATH_INFO"] =~ /^\/get_banner\/(\d+)/
      
      banner = Banner.find :first, :order => 'rand()'
      
      unless banner.nil?
        BannerView.create(
          :website_id => $1,
          :request_ip => env['REMOTE_ADDR'],
          :banner_id => banner.id,
          :referrer => env['HTTP_REFERER']
        )
        return [301, {"location" => banner.image.url}, []]
      end
      [404, {"Content-Type" => "text/html"}, ["Not availible"]]
    else
      [404, {"Content-Type" => "text/html"}, ["Not Found"]]
    end
  end
end
Эта операция делает 2 довольно быстрых SQL запроса.

В следующей статье я напишу биллинг и более "качественный" выбор баннера