Истоки стека технологий
В определенный момент пришло моё время написать что-то отдаленно похожее на web сайт. Самый обычный сайт: главная страница с отображением на ней трёх табличек из БД и парой форм для заполнения оных контентом. Такие мной были поставлены начальные требовнаия. Думаю, у каждого при написании первого сайта возникает вопрос: какие инструменты для этого использовать? Для меня были довольно принципиальны критерии:
- использовать Ruby, так как я с ним довольно хорошо знаком (а с php и python — нет)
- использовать объектную модель насколько это возможно
- ограничиться только необходимыми средствами разработки: ведь незачем тащить за собой JQuery, когда нужен всего один POST запрос
Соответственно, взгляд упал на Ruby on Rails: это полноценный framework, довольно популярный и, как ни странно, содержит в себе Ruby.
И тот "подправлен" заботливыми рельсами. Я имею ввиду, что весь встречающийся синтаксис мог внезапно оказаться C++. И никто бы не заметил — всё скрывается framework'ом.
И принялся я его изучать, чего и вам желаю. Хороший гайд: помогает понять, что рельсы — это гигантская сборка gem'ов, которые потом ещё и разворачивать придется на passenger. То есть, кроме мороки с bundler'ом или rvm (чего в итоге не избежать), придется ещё и passenger стыковать с Apache или nginx. Меня эти перспективы напугали и, дочитав таки tutorial, я начал искать чем бы RoR заменить, оставив при этом от него только необходимое. Для меня все ограничилось Ruby и ActiveRecord. Первые поиски пути выполнения Ruby кода на Apache показали, что есть mod_ruby: этот и этот.
Вот на это API и пишутся модули, позвоялющие выполнять скрипты PHP, Python и Ruby. Это не CGI, что сулит повышенную производительность. Вот какие модули я имею ввиду.
Однако, первые же опыты привели меня к такой и вот такой ситуации.
Конечно, не бог весть какие проблемы: всё решаемо, но тут я посмотрел на даты последних коммитов, увидел заветное "пол года назад" и отказался от этой идей.
После этого мой выбор пал на FCGI как довольно перспективное продолжение CGI. Почему не CGI? А потому-что. Т.е. для FCGI у Ruby есть gem, который позволяет обрабатывать запросы не в сыром виде CGI, а посредством интерфейса гема. Смотрится удобно, но об этом позже. Ну вроде всё: есть Apache, есть FCGI, Ruby… И, так как у меня есть небольшой backend в виде БД, а работать с тяжелым mysql или аналогами не хотелось, решил я прихватить ActiveRecord из RoR себе в виде файла sqlite3 БД.
Заинтересовало? Добро пожаловать под кат.
Разбор компонент
Составив такой стек, выделил я себе серверок с CentOS и приступил к воплощению идей в жизнь.
Apache
Всё начинается с Web сервера. В моём случае всё оказалось довольно просто: yum install epel-release; yum install apache mod_fcgid fcgi-devel mod_ssl gnutls-utils
.
Такая портянка пакетов обусловлена нашей жаждой оспользовать gem ruby-fcgi… И, конечно тем, что почти все пакеты в EPEL. По старой админской привычке я пропарсил конфиг Apache… И нашел там кучу совершенно бесполезных модулей, чего и вам советую!
<VirtualHost mysite:443>
#common options
ServerName mysite:443
#loging
LogLevel info
ErrorLog logs/mysite-error_log
CustomLog logs/mysite-access_log common
#main dir
DocumentRoot /var/www/html/
<Directory /var/www/html/>
Options ExecCGI
DirectoryIndex index.rb.fcgi
AllowOverride None
#Access
Allow from all
#LDAP
AuthLDAPUrl <>
AuthLDAPBindDN <>
AuthLDAPBindPassword <>
#Authorization
AuthType Basic
AuthBasicProvider ldap
AuthName "Input your domain login name and password"
Require ldap-attribute <>
Require ldap-attribute <>
</Directory>
#Scripts timeouts
FcgidIOTimeout 300
#SSL
SSLEngine on
SSLProtocol TLSv1
SSLCertificateFile /var/www/html/certs/ca_cert.pem
SSLCertificateKeyFile /var/www/html/certs/ca_key.pem
</VirtualHost>
<> помечены опущенные параметры
Само собой, имя mysite прописано в DNS сервере, mod_ssl установлен.
mod_fcgid
Как вы должны были заметить, вместе с Apache мы ставим mod_fcgid, который и есть интерфейс для встраивания скриптов для обработки запросов.
При установке этот модуль создал конфиг в /etc/httpd/conf.d/fcgid, содержащий:
LoadModule fcgid_module modules/mod_fcgid.so
AddHandler fcgid-script fcg fcgi fpl
FcgidIPCDir /var/run/mod_fcgid
FcgidProcessTableFile /var/run/mod_fcgid/fcgid_shm
Интерес здесь представляет только строка AddHandler fcgid-script fcg fcgi fpl
, которую неплохо бы из соображений безпасности заменить на AddHandler rb.fcgi
.
Так мы ограничиваем исполнение FCGID только скриптов с расширением rb.fcgi.
Ruby
Само собой, надо ставить Ruby. Но не в коем случае не из репозитория: многие печали буду. А конкретнее: придется прописывать полные пути до gem'ов при их подключении.
А потом перепрописывать при обновлении. Так что ставим RVM:
sudo su -
gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3
curl -sSL https://get.rvm.io | bash -s stable --ruby=2.2.1
source /etc/profile.d/rvm.sh
После выполнения этого в системе появится rvm и ruby… и ещё кое что.
FCGI gem
Дело дошло до настройки FCGI. Если как обычно, то gem install fcgi
. Только в этом случае gem установится в текущий gemset в rvm. А для того, чтобы в скриптах, которые будет выполнять Apache, gem'ы были видны, надо устанавливать их в глобальный gemset. То есть, rvm gemset use global; gem install fcgi
. И далее, если я говорю "устанавливаем gem", то имеется в виду нечто подобное.
Буквально это нам даёт право писать require "sqlite3"
в FCGI скриптах для Apache. Однако, интерпретатор в скриптах по-прежнему придется указывать #!/usr/local/rvm/rubies/ruby-2.2.1/bin/ruby
— мне этого не удалось избежать.
Теперь, потерев руки, можно делать файл /var/www/html/index.rb.fcgi
и писать в него чтото типа:
#!/usr/local/rvm/rubies/ruby-2.2.1/bin/ruby
require "fcgi"
FCGI.each { |request|
case request.env["REQUEST_METHOD"]
when 'GET'
request.out.print "Content-Type: text/htmlnStatus: 200 ОКnn<!DOCTYPE html><html><h3 align=center style="color: green"><strong>Success</strong></h3></html>"
else
request.out.print "Content-Type: text/htmlnStatus: 403 Forbiddennn<!DOCTYPE html><html><h3 align=center style="color: red"><strong>Access denied</strong></h3></html>"
end
request.finish
}
Довольно простой скрипт: выдает страничку с 'Success' на GET запрос и страничку с 'Access denied' на любой другой.
Т.е. fcgi gem предоставляет нам класс FCGI, который, будучи использованный в FCGI скриптах, может принимать запросы самостоятельно.
Мной это изучено по документации проекта.
Мы получаем при запросе объект request, который имеет:
- env поле: содержит всю информацию о запросе: тип запроса, параметы запроса, тип брузера, пользователь и многое другое. Все это можно (и нужно) использовать при обработке запроса;
- in поле представляет собой тело запроса. То есть, насколько мне стало ясно, env — это представление заголовка, а in — буквально тело HTTP запроса;
- out поле служит для записи в него всего, что надо отдать в ответ на запрос: полная FCGI-свобода действий, что видно по отправке статуса 200 и 403 выше.
in и out — обычные объекты типа IO, что какбы намекает.
Одна из задач выполнена: мы получаем чистый Ruby для разработки сайтов. Можно использовать все, что только есть в интернетах, для генерации html.
ActiveRecord gem
Вот тут, внезапно, пронадобилось использовать БД как backend. И, раз уж мы на Ruby, воспользуемся Rails фишкой в виде ActiveRecord.
Не скрою, я пользовался этой статьёй, но у меня есть что добавить. А, дабы не путать вас в ссылках на неё, опишу всё по порядку.
Устанавливаем gem: gem install activerecord sqlite3
— для работы с sqlite3 адаптером БД.
Подключение БД
Для работы с БД необходимо 'создать соединение' с БД. Это выглядит както так:
require 'active_record'
require 'sqlite3'
ActiveRecord::Base.establish_connection(
:adapter => 'sqlite3',
:database => "/var/www/html/db/mysite.db"
)
Ничего сверхъестественного и ничего нового они не превносят — просто указание на расположение БД и её формат. Эти строки надо добавить при инициализации любого fcgi скрипта для подключения в его окружение БД.
Миграции
Миграции (в терминологии Rails) — это метод автоматизаци разворачивания схемы БД при разработке, тестировании, внедрении,… По сути, это скрипты, которые запускаются для создания другими людьми БД, используемой сайтом.
Пример такого скрипта:
#!/usr/local/rvm/rubies/ruby-2.2.1/bin/ruby
require 'active_record'
require 'sqlite3'
ActiveRecord::Base.establish_connection(
:adapter => 'sqlite3',
:database => "/var/www/html/db/mysite.db"
)
class CreateUsers < ActiveRecord::Migration
def up
create_table :users do |t|
t.string :name
t.string :email
t.string :group
t.timestamps null: false
end
end
end
CreateUsers.migrate(:up)
Этот скрипт создает таблицу users, содержащую 5 полей — id, name, email, group и timestamp.
Существует довольно много действий, осуществимых с БД путем миграций, но не будем заострять на этом внимание. Все умеют читать мануалы. А для нас главное понять, что для создания и модификации БД необходимо написать ряд скриптов, а не носить везде с собой файл sqlite со схемой. Эти файлы никуда не включаются — это отдельная часть проекта и при работе сайта она не используется.
Валидации
Казалось бы, БД есть — почему бы не использовать её. Но не все так просто: неплохо бы определиться с тем, что наша БД может содержать, а что категорически нет.
Это подразуммевает, что у нас на руках есть схема таблиц БД с обозначенными полями, их типами, допустимыми значениями и связями между ними.
Цель — вывести ActiveRecord на чистую воду.
Здесь и начинается то, что упускает большинство руководств по использванию ActiveRecord. Не буду томить, скрипт валидаций:
class Host < ActiveRecord::Base
belongs_to :user, :inverse_of => :hosts, :validate => true
validates :address, :presence => true, :uniqueness => true
validates :user_id, :presence => true
validate :address_and_user_should_exists,
def address_and_user_should_exists
if Address.find_by_id(address_id) == nil
errors.add(:address_id, "should points at exist address")
end
if User.find_by_id(user_id) == nil
errors.add(:user_id, "should points at exist user")
end
end
validates :name, length: { minimum: 2, maximum: 20 }, presence: true, :format => { :with => /S(S|-)*S[^z]/i }, :uniqueness => true
validates :purpose, length: { minimum: 4, maximum: 100 }, presence: true, :format => { :with => /[^$^&`]+/i }
validates :description, length: { maximum: 700 }, presence: false, :format => { :with => /[^$^&`]*/i }
end
class User < ActiveRecord::Base
has_many :hosts, :inverse_of => :user, :dependent => :destroy
def self.valid_groups
["test", "probe" "etc"]
end
validates :name, length: { minimum: 2, maximum: 30 }, presence: true, :format => { :with => /S(S| )*S[^z]/i }, :uniqueness => true
validates :email, length: { minimum: 5, maximum: 40 }, presence: true, :format => { :with => /A[w+-.]+@[a-zd-]+(.[a-z]+)*.[a-z]+z/i }
validates :group, length: { minimum: 2, maximum: 30 }, presence: true, :format => { :with => /(w|-)+/i },
:inclusion => { :in => valid_groups, :message => "%{value} is not a valid group. Select one from drop-down hint."}
end
require и подключение БД опущены
Он предназначен для определения допустимых значений, записываемых в БД. В приведенном скрипте:
- описаны две таблицы: users и hosts (согласно нотации Activerecord, классу User соответствует таблица users и т.п.). Они представлены классами, наследующими ActiveRecord:Base.
- описаны отношения между полями этих таблиц в виде директив
has_many
иbelongs_to
- описаны сами валидаторы значений полей — это все методы класса (self.) и все вызовы `validate`
Начнем разбор всего, что написано в валидаторе. Строка class User < ActiveRecord::Base
означает, что в окружении у нас теперь есть класс User, который представляет собой (связан с) таблицей users БД, которую мы где-то выше подключили. Так, вызов User.find_by_id
позволяет производить поиск записей по id в таблице. И ничего больше. То есть, никакой магии нет — просто возвращается запись, если она есть.
Далее в обоих классах идёт строка, описывающая связи между таблицами.
Например, has_many :hosts, :inverse_of => :user, :dependent => :destroy
означает, что:
- для записи этой таблицы можно ожидать, что она имеет ноль или несколько связанных записей в таблице hosts
- является логической парой связи user, опиcанной в User
- при удалении записи из этой таблицы будут удалены связанные с ней записи
Здесь и кроется основная тайна ActiveRecord: связи AR не являются связями в БД. В БД их попросту нет.
То есть, при использовании связи has_one не следует ожидать, что AR будет отслеживать соблюдение отношения 1:1 при добавлении записей: это на вашей совести.
А ActiveRecord только гарантирует, что объявление связи позволит находить связанные записи.
И, наконец, идёт список валидаторов полей:
validates :user_id, :presence => true
validate :address_and_user_should_exists,
def address_and_user_should_exists
if Address.find_by_id(address_id) == nil
errors.add(:address_id, "should points at exist address")
end
if User.find_by_id(user_id) == nil
errors.add(:user_id, "should points at exist user")
end
end
validates :name, length: { minimum: 2, maximum: 20 }, presence: true, :format => { :with => /S(S|-)*S[^z]/i }, :uniqueness => true
Именно здесь, к примеру, реализуется то, что для указанных связанных полей должны существовать соответствующие записи в таблицах.
Думаю, значение кода понятно. если нет, то вот на мой вкус наилучшее средство.
Там можно узнать как пользоваться валидаторами, в том числе и в своих коварных целях.
К чему я веду: ActiveRecord упрощает жизнь тем, что не приходится иметь дело с sql синтаксисом или чем-то подобным. Однако, вам придется реализовать всю логику работы схемы БД самостоятельно.
Это неплохо: можно проверить что угодно на соотвестсвие чему угодно при добавлении, изменении и удалении записей БД. Но вся эта реализация на совести разработчика.
Сухой остаток
После написания всех упомянутых выше вещей мы получаем:
- Доступ к записям БД посредством вызова
User.find_by(:name => "Vasya")
,User.first.name
,User.all.each {...}
- Возможность создания и удаления записей (с выполнением наших валидаций):
User.create(:name => "Oleg", ...)
,User.first.destroy
- Возможность получения связанных записей:
User.first.hosts
(причем, если связь 1:1, то было быUser.first.host
). Поконкретнее про связи здесь.
Вместо заключения
На выходе мы получили исполнение Ruby скриптов по запросам Apache с вкраплениями ActiveRecord. В такой стек хорошо вписывается MVC из RubyOnRails путем использования валидаций как модели, Ruby + FCGI как контроллера и Ruby генераторов html как представлений. Надо ли приводить гайд на структуру проектов на основе вышеописанного покажет время.
Спасибо за внимание.
Автор: deman_killer