Истоки стека технологий
В определенный момент пришло моё время написать что-то отдаленно похожее на 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/html\nStatus: 200 ОК\n\n<!DOCTYPE html><html><h3 align=center style=\"color: green\"><strong>Success</strong></h3></html>"
else
request.out.print "Content-Type: text/html\nStatus: 403 Forbidden\n\n<!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-z\d\-]+(\.[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 как представлений. Надо ли приводить гайд на структуру проектов на основе вышеописанного покажет время.
Спасибо за внимание.
ЗЫ: Раз уж у многих матёрых web разработчиков пошла на эту статью реакция, то вот дисклеймер: описывается как работают ruby приложения в web. Без Rack. Без Rails. Ruby + AR -> FCGI -> Apache. Только то, что необходимо. Если вам мало уровней абстракции вашего кода от http, html и sql, то можно добавить сколько угодно слоёв. Это упрощает жизнь, но не стоит забывать про их существование.
Комментарии (31)
fuCtor
29.02.2016 15:47Зачем такие сложности??
Для подключения gem-ов как минимум есть bundler. Для запуска есть rack-сервера, которые завернут куда надо, некоторые даже явно конфигурировать не надо, просто добавить в зависимости.
использовать Ruby, так как я с ним довольно хорошо знаком
и в тоже время
if Address.find_by_id(address_id) == nil
Не однозначный конечно текст.deman_killer
29.02.2016 16:07-2Про bundler согласен. Только надо попробовать что с ним будет, когда Apache из своего окружения вызовет скрипт… можно туда зацепить bundler или нет.
Про код: а лучше писать неявно?Metus
29.02.2016 16:11Предполагаю, имелось ввиду что-то вроде этого:
if Address.find(address_id).nil?
fuCtor
29.02.2016 16:37Хотя бы да, но есть еще варианты
unless Address.find_by_id(address_id) errors.add(:address_id, "should points at exist address") end
или более наглядно
unless Address.exists?(address_id) errors.add(:address_id, "should points at exist address") end
Gris
29.02.2016 23:22Тогда уж еще проще
errors.add(:address_id, "should points at exist address") unless Address.exists?(address_id)
или валидацию написать
class Address < ActiveRecord::Base has_many :hosts, inverse_of: :address end class Host < ActiveRecord::Base belongs_to :address validates :address, presence: true, uniqueness: true end
тоже должно работать, без необходимости в кастомном валидаторе.deman_killer
29.02.2016 23:55-1has_many и belongs_to — не валидаторы. В этом и вся соль — они только определяют пачку вспомогательных методов.
printercu
01.03.2016 11:14Уже некоторое время можно
required: true
вместоpresence
валидатора:
belongs_to :address, inverse_of: :hosts, required: true
avdept
29.02.2016 16:36+5Автор посмотрел как создать блог за 15 и решил сделать свой сайт?
использовать Ruby, так как я с ним довольно хорошо знаком (а с php и python — нет)
увы вы с руби знакомы судя по всему не лучше чем с php\python, т.к. по коду это уровень человека который пишет на руби 2-3 месяца
ограничиться только необходимыми средствами разработки: ведь незачем тащить за собой JQuery, когда нужен всего один POST запрос
а то что рельсы тащат за собой кучу active-*** гемов, и еще кучу зависимостей — это ничего? Или то что jquery-ujs по дефолту идет в рельсовом гемфайле?
То есть, при использовании связи has_one не следует ожидать, что AR будет отслеживать соблюдение отношения 1:1 при добавлении записей: это на вашей совести.
Как раз таки AR проверяет что бы вы yе добавили еще одну запись при наличии оной.
Про apache и прочие танцы с бубном — выдает в вас пхпшника все же.deman_killer
29.02.2016 20:17-1увы вы с руби знакомы судя по всему не лучше чем с php\python, т.к. по коду это уровень человека который пишет на руби 2-3 месяца
Может и так. Пруф?
а то что рельсы тащат за собой кучу active-*** гемов, и еще кучу зависимостей — это ничего? Или то что jquery-ujs по дефолту идет в рельсовом гемфайле?
А я о чем? Как раз и использовал AR без рельс. Честно, вы читали что я написал?
Как раз таки AR проверяет что бы вы yе добавили еще одну запись при наличии оной
А вы попробуйте. Не проверяет он без валидатора ничего. has_ дает только методы доступа.
fzn7
29.02.2016 16:53+1Мать чесная… А Sinatra чем не понравился?
deman_killer
29.02.2016 20:23-2Хм… Ничем — стоит присмотреться.
Но он, опять же, использует Rake. Гляньте посты товарища fuCtor на эту тему выше.
j_wayne
01.03.2016 10:41+1Много раз разворачивали nginx + passenger + bundler + rvm для продакшена разных проектов. Не понимаю, чего тут можно бояться? До этого и Apache бывало использовали (если уж это железобетонное требование)
zirf
01.03.2016 15:12Вообще, с настройкой парится можно дома по выходным. А так масса вариантов помучиться 1 раз. 2 контейнера Docker, webserver+webapp и db. Сами приложения на хосте или в отдельном контейнере, как и базы. С небольшим джентльменским наборчиком выпуск приложения превращается в довольно простое мероприятие, в отличие. Или готовые PaaS использовать. Самое главное — четко разделить работу ИТ-инженера и разработчика. Это не должен делать один человек. А тут описание любительских мучений. Просто блины — комом это нормально. Задача комментаторов не только себя показывать, но и автору намекнуть что хотелось бы видеть.
Metus
А чем плохи unicorn, puma и прочие?
zirf
помогает понять, что рельсы — это гигантская сборка gem'ов, которые потом ещё и разворачивать придется на passenger
Понятно, что все не совсем так. Пользуйся автор ROR не пришлось бы таких странных установок делать и такие дивные require выпичатывать, совсем на другую установку непереносимые. Вот какой разработчик будет так мучиться при установленных рельсах?
$ rails new i_am_lazy
$ cd i_am_lazy
$ rails server
Metus
Переформулирую вопрос — чем rack-сервер хуже fsgi?
Чем rack-сервер хуже даёт понять, что рельсы — это ....?
deman_killer
Не могу ответить точно. Но rack-сервер не предназначен для production, не так ли?
fuCtor
rack сервер наоборот предназначен для продакшена. У вас может быть X узлов, на которых крутится сервер приложение, и один узел с балансировщиком (nginx), вот это как раз будет более боевая конфигурация чем Apache + FCGI.
deman_killer
Это все хорошо, только причем тут RoR. Скорее всего, X*Rack + nginx будет шустрее, чем fcgi+Apache… И всеравно зависит от ситуации. Только Rack можно притащить с собой и без Rails.
Вопрос только целесообразности.
Судя по этому Rack работает через CGI, FCGI и т.п. То есть всеравно получается Rack -> (CGI, FCGI) -> (Apache, nginx)
Metus
Почему не предназначен? puma, unicorn — это и есть серверы, работающие через rack-интерфейс. И их используют в production.
Для production не предназначен webrick, но это только один из многих — и все они взаимозаменяемы.
Они легко присоединяются к тому же nginx через сокет и мы получаем явное разделение: сервер-приложения и веб-сервер.
deman_killer
Каюсь, rack перепутал с webrick. Но, опять же, rack работает через стандартные интерфейсы web серверов.
deman_killer
Если не углубляться в "как", то рельсы прекрасно себя показывают. Речь не о том, что RoR сложно использовать, а о том, что не всегда оправдано нести с собой их целиком. Довольно много при поисках "что такое ActiveRecord" я находил проблем, связанных с непониманием принципов его работы: натыкаются на несоответствие реальности и ожиданий. Тут и приходится копать: а каким путем происходит "подтягивание" функционала рельсами. А если все работает прозрачно, то и ошибок меньше.
sl_bug
Если рельсы не оправданы, то есть sinatra. А зачем ваши мучения не ясно
estum
Собственно, рельсы и любой другой фреймворк тут вообще не причем, все что нужно — это rack, app-сервер (puma, unicorn или passenger) и nginx. Даже писать ничего особо не надо: пара строк в конфигах и готово.
Просто автор, видимо, не знает про rack и решил изобрести — даже не велосипед, нет — а колесо.
zirf
Ну я автора собственно и процитировал, дабы намекнуть что в общем все автоматизировано уже. Сервера приложений отличаются своими фичами, и фактор привычности, конечно. Web сервер по большому счету тоже все равно какой.
avdept
Квадратное причем.
deman_killer
FCGI — стандрат, поддерживаемый apache. Unicorn, passenger,… — отдельное ПО.
Работа с FCGI показалась мне прозрачнее.