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