Pull to refresh

Знакомство с Ruby on Rails (часть 2)

Reading time11 min
Views14K
В продолжении статьи ”Первое знакомство с Ruby on Rails” мы научимся работать с базой данных, и создадим каталог статей.
Узнаем как написать плагин, попробуем использовать AJAX и рассмотрим некоторые проблемы при развёртывании приложения на хостинге.

Начнем с базы данных.



Я работаю с MySQL, поэтому примеры установки будут для неё.

Пользователям Windows нужно скачать и установить MySQL-5.0.

Пользователям Linux (Ubuntu) еще проще:

<code class='sh' lang='sh'>$>sudo apt-get install mysql-server-5.0 libmysql-ruby</code>


После установки проверим что сервер работает:

<code class='sh' lang='sh'>$>mysqladmin ping -u root
mysqld is alive</code>


Настало время создать нужные базы данных. Потребуется их две – одна для разработки и одна для тестирования:

<code class='sh' lang='sh'>$>mysqladmin create example_development -u root
$>mysqladmin create example_test -u root</code>


Теперь давайте посмотрим на файл config/database.yml. Тут находятся параметры соединения с базой данных. Обычно ничего менять не требуется, по умолчанию mysql создаёт пользователя root со всеми правами и без пароля.

Осталось протестировать соединение приложения с базой данных. Идём в папку с приложением и набираем

<code class='sh' lang='sh'>$>rake</code>


Если появляются сообщения об ошибках, значит где-то вы ошиблись в настройках соединения, проверьте их.

Введение в работу с БД.



Связь объектов и баз данных в рельсах осуществляется с помощью OR меппера, который называется ActiveRecord. Он занимается отображением полей из таблицы БД в поля объекта, валидацией объектов перед сохранением, генерацией кода для представления связей между объектами.

Чтобы создать новую модель достаточно наследоваться от класса ActiveRecord::Base

<code class='ruby' lang='ruby'>class Article < ActiveRecord::Base
end</code>


По умолчанию ActiveRecord будет работать с таблицей названной также как класс, только во множественном числе. В нашем случае Articles.

Ко всем полям таблицы можно получить доступ с помощью методов с тем же названием:

<code class='ruby' lang='ruby'>#Пусть в таблице Articles есть поле title
Article.create(:title => 'Hello World!')
article = Article.find(1)
print article.title
#=> Hello World!</code>


ActiveRecord наглядно демонстрирует суть принципа “Convention over Configuration” – не требуется писать код для того, чтобы программа заработала, код нужен только когда программа должна работать не как обычно. Например:

  • нужно использовать таблицу с другим именем – добавляем в класс строчку set_table_name "mytablename"
  • в таблице криво названы поля – пожалуй лучше написать для полей методы доступа с нормальными названиями
  • один объект отображается на несколько таблиц – придётся писать свой ORM :)


ActiveRecord предоставляет много полезных функций, вот некоторые из них:

  • Article.find(id) – найти статью по id (PrimaryKey в БД, обычно это Integer)
  • Article.find(:all) – выбрать все статьи
  • Article.find_by_title('Hello World!') – найти статью с заголовком “Hello World!”
  • Article.create(:title => 'Hello World!') – создать статью и сохранить в БД
  • article.update(:title => 'Goodbye World!') – обновить статью в БД
  • article.destroy – удалить статью из БД


Что бы посмотреть документацию по ActiveRecord и другим установленным гемам нужно запустить

<code class='sh' lang='sh'>`$>gem_server`</code>


и открыть в браузере

http://localhost:8808/

Теперь давайте попробуем создать каталог.



Мы будем использовать script/generate чтобы создать модель, контроллер и вьюшки для каталога.

<code>$>ruby script/generate scaffold_resource article title:string body_format:string body:text</code>


Я не буду включать в текст исходный код, чтобы не раздувать статью, его можно посмотреть online, я буду описывать для чего нужны различные куски кода.

Рельсы сгенерировали несколько файлов, посмотрим на некоторые из них:

  • app/models/article.rb – модель для статьи
  • app/controllers/articles\_controller.rb – контроллер для управления каталогом статей
  • config/routes.rb – добавлена строчка map.resources :articles
  • app/views/articles/... – вьюшки для создания, редактирования и просмотра статей
  • app/views/layouts/articles.rhtml – шаблон страниц для работы к каталогом
  • db/migrate/001_create_articles.rb – создание таблицы для статей в базе данных


С моделью думаю всё понятно, посмотрим на контроллер. У контроллера есть 7 методов:

  • index – страница отображает всю коллекцию статей
  • show – страница отображает одну статью
  • new – страница для создания новой статьи
  • edit – страница для редактирования существующей статьи
  • create – обработчик поста формы создания новой статьи
  • update – обработчик поста формы редактирования статьи
  • destroy – обработчик запроса удаления статьи


Строчка map.resources :articles в файле config/routes.rb добавляет нужные правила маршрутизации урлов. Вот как выглядят созданные урлы:

  • /articles (GET) – index
  • /articles/:id (GET) – show (:id – идентификатор статьи)
  • /articles;new (GET) – new
  • /articles/:id;edit (GET) – edit
  • /articles (POST) – create
  • /articles/:id (PUT) – update
  • /articles/:id (DELETE) – destroy


PUT и DELETE это методы HTTP, как GET и POST.
Таким образом получилось уместить все необходимые для управления коллекцией методы в небольшой и понятный для пользователя набор урлов.

Принцип разбиения приложения на наборы ресурсов и предоставления универсального формата доступа к ресурсам (способа построения урлов) называется REST. Идея в том, чтобы использовать для работы с ресурсами протокол без состояния (вся необходимая информация содержится в урле), что улучшит масштабируемость приложения и упростит кеширование.

Поскольку ресурс однозначно идентифицируется урлом, основная работа приложения сводится к двум задачам – отдавать пользователю странички с указанным в урле ресурсом и проверять права доступа если ресурс не общедоступный. Поэтому писать и сопровождать веб приложения в стиле REST очень просто.

Посмотрим на результат, запускаем сервер

<code class='sh' lang='sh'>`$>ruby script/server` </code>


и идем

http://localhost:3000/articles

Я получил вот такую ошибку:

<code>Mysql::Error: Table 'example_development.articles' doesn't exist: SELECT * FROM articles</code>


В базе данных нет таблицы articles, надо бы её создать.

Изменения таблиц БД в рельсах делаются через механизм миграций. Одну миграцию рельсы сгенерировали для нас – создание таблицы articles (db/migrate/001_create_articles.rb). Нужно применить её к базе данных, идём в папку с приложением и запускаем

<code>$>rake db:migrate</code>


В результате миграции в базе данных была создана таблица articles, и теперь мы можем нажать в браузере F5 и поиграть с приложением.

Несколько слов о Rake.



Rake это замена утилит типа make, ant, maven.
Чтобы узнать что Rake может сделать для нас выполним следующее:

<code class='sh' lang='sh'>$>rake -T</code>


Получим длинный список задач, которые Rake умеет делать. Задачи пишутся на Ruby и находятся в файле Rakefile. Изначально файл содержит только стандартный набор задач, которые подключаются с помощью require 'tasks/rails'. Вот так например выглядит описание задачи db:migrate:

<code class='ruby' lang='ruby'>desc "Migrate the database through scripts in db/migrate. Target specific version with VERSION=x"
task :migrate => :environment do
  ActiveRecord::Migrator.migrate("db/migrate/", ENV["VERSION"] ? ENV["VERSION"].to_i : nil)
  Rake::Task["db:schema:dump"].invoke if ActiveRecord::Base.schema_format == :ruby
end</code>


Прелесть Rake в том, что описание задачи это обычный код на Ruby. Это сильно упрощает добавление новых задач по сравнению с ant или maven.

У Мартина Фаулера есть отличная статья о Rake (на английском).

Будем знакомиться с Rake по мере необходимости. Сейчас нам уже известно что задача db:migrate запускает миграции, при чем можно как применять так и откатывать изменения. Если мы посмотрим в файлик db/migrate/001_create_articles.rb, то увидим что у класса CreateArticles есть два метода: up и down. Эти методы вызываются когда миграция применяется и откатывается соответственно. Цифры 001 в названии файла это порядковый номер миграции, он используется для определения очерёдности применения миграций, при этом рельсы хранят в базе данных её версию, чтобы не применять одну миграцию несколько раз. Чтобы мигрировать базу до определенной версии нужно запустить db:migrate с параметром VERSION:

<code>$>rake db:migrate VERSION=0</code>


В результате база мигрирует до нулевой версии, когда еще не было создано ни одной таблицы. Это удобный способ очистить базу после экспериментов с приложением. Потом можно снова вызвать db:migrate без параметров, в итоге будут применены все миграции.

Каталог это здорово, но статьи не форматируются.



Надеюсь вы уже посмотрели на каталог и убедились в этом. Время заняться главной задачей приложения – форматированием статей.

У нас уже есть код для форматирования, теперь нужно понять как его использовать. В первой версии у нас был только контроллер, поэтому код располагался прямо в нем, теперь появилась модель Article, но если мы расположим код в модели, то жестко свяжем форматирование с конкретной моделью, а это приведет к тому, что мы не сможем повторно использовать код, хотя он никак не зависит от модели которую будет форматировать.

Будем писать плагин.



В рельсах это естественный путь добавления функциональности к приложению. При этом код форматирования будет отделен от модели, что позволит использовать его в других приложениях.

Хочется чтобы плагин обеспечил поддержку форматирования без лишних слов, например так:

<code>class Article < ActiveRecord::Base
  acts_as_formatted :body
end</code>


При этом исходный текст находится в поле body, а отформатированиый текст статьи можно получить с помощью метода body_as_html. Формат, в котором написана статья, находится в поле body_format.

Начнем. Прежде всего доверим рельсам создать для нас скелет плагина:

<code>$>ruby script/generate plugin acts_as_formatted</code>


В папке vendor/plugins появился наш плагин. Что внутри:

  • lib – в этой папке размещается код
  • lib/acts_as_formatted.rb – тут будет код плагина
  • tasks – плагин может добавлять задачи для Rake, они появятся в общем списке
  • test – плагин должен быть хорошо протестирован
  • init.rb – этот файл выполняется при загрузке, отсюда включаются файлы плагина, которые лежат в lib
  • install.rb, uninstall.rb – эти файлы выполняются при установке и удалении плагина, нам они не потребуются
  • Rakefile – файл с задачами Rake для плагина (запуск тестов и генерация документации)


Теперь осталось написать код для форматирования и добавить поддержку в ActiveRecord.

Как это сделать? Задача сводится к тому, чтобы добавить метод acts_as_formatted к ActiveRecord::Base. А в этом методе сгенерировать код, необходимый для поддержки форматирования. Для этого нам понадобится знать как это можно сделать в Ruby.

Как в Ruby добавить функциональность к существующему классу.



В Ruby все является обьектом, в этой простенькой программе

<code class='ruby' lang='ruby'>print "Hello World!"</code>


вызывается метод обьекта. У какого обьекта? Это объект типа модуль (аналог namespace, package), глобальный модуль называется Kernel. Модули похожи на классы, отличаются тем, что могут содержать только методы, константы и другие модули и классы. При этом руби позволяет подмешивать (mixin) модули в другие модули и классы, это делается с помощью методов extend и include у модулей и классов, например:

<code class='ruby' lang='ruby'>class MyClass
  extend Enumerable
end</code>


или

<code class='ruby' lang='ruby'>class MyClass
end

MyClass.extend(Enumerable)</code>


При этом в классе MyClass появятся все методы, константы, классы и модули, определенные в модуле Enumerable.

Пишем плагин.



Практически весь код плагина будет в файле acts_as_formatted.rb.

Плагин состоит из двух модулей:

  • ActiveRecord::Acts::ActsAsFormatted::Formatting – код отвечающий за форматирование
  • ActiveRecord::Acts::ActsAsFormatted::ClassMethods – единственный в нем метод – acts_as_formatted, этот модуль добавим к ActiveRecord::Base


Посмотрим что делает метод acts_as_formatted.
Сначала узнаем какие форматы поддерживаются и какие поля будут использоваться (в нашем случае body, body_format, body_as_html), затем добавляем правило валидации, чтобы проверить что поле формата содержит допустимый формат (обьект невозможно сохранить если не прошла валидация), и добавляем классу два метода для получения поддерживаемых форматов и отформатированного поля (supported_formats и body_as_html).

Вся работа по форматированию происходит в модуле ActiveRecord::Acts::ActsAsFormatted::Formatting. Здесь есть методы которые форматируют текст: format, format_markdown и format_textile, и метод supported_formats, который определяет поддерживаемые форматы, иcходя из методов, которые есть в модуле.

Теперь в init.rb добавим код инициализации:

<code class='ruby' lang='ruby'>require 'acts_as_formatted'

ActiveRecord::Base.extend(ActiveRecord::Acts::ActsAsFormatted::ClassMethods)</code>


и можно пробовать плагин в деле. Нужно поправить вьюшки, чтобы отображать отформатированный текст.

Причешем вьюшки и сделаем preview



Прежде всего стоит избавиться от дублирования кода в new и edit формах и создать одну форму, которую можно использовать создания и редактирования статей. Единственное отличие форм – урл и метод отправки. Поэтому удобно сделать вспомогательный метод, который будет определять урл и метод в зависимости от того, как используется форма, для редактирования или для создания статьи.

Для вспомогательных методов рельсы создают модули-помощники (helpers), разместим код в app/helpers/application_helper.rb. Метод edit_form_for определяет была ли модель уже сохранена и, в зависимости от этого, генерирует форму для создания или обновления модели. Методы submit_edit и cancel_edit создают кнопку для отправки формы и линку для возврата из формы.

Теперь создадим вьюшку для формы. Повторно используемые куски вьюшек в рельсах называются partials. Названия файлов partials начинаются с подчёркивания. Обычно они находятся там же где вьюшки, которые их используют.

После такого рефакторинга код вьюшек new и edit становится совсем простым и сводится к одной строчке:

<code class='ruby' lang='ruby'><%= render :partial => 'article', :object => @article %></code>


Осталось добавить preview. Для этого в контроллере создадим метод preview, который будет возвращать отформатированный текст статьи.

<code class='ruby' lang='ruby'>def preview
  article = Article.new(params[:article])
  render_text article.body_as_html
end</code>


Затем добавим правило в таблицу маршрутизации.

<code class='ruby' lang='ruby'>map.resources :articles,
              :collection => { :preview => :any }</code>


Вторая строчка добавляет правило для урла /articles;preview. :collection означает что будет использоваться урл коллекции (/articles), поскольку не важно для какой конкретно статьи генерируется preview. Вместо :collection можно использовать :member, тогда урл будет для конкретной статьи (/articles/:id), в нашем случае это не позволит делать preview создаваемых статей. :any означает что для вызова можно использовать любой HTTP метод, в нашем случае будут использоваться POST при создании и PUT при редактировании.

Теперь добавим поддержку на форму. Чтобы не загромождать вьюшку кодом сделаем вспомогательный метод. Этот метод создаёт кнопку, при нажатии на которую форма асинхронно отправляется на сервер и ответ отображается в переданном в метод элементе. Остаётся добавить кнопку и элемент для отображения отформатированной статьи во вьюшку.

Поскольку кнопка preview использует библиотеку prototype для асинхронной отправки запросов на сервер, нужно добавить её загрузку в шаблон страницы.

<code class='ruby' lang='ruby'><%= javascript_include_tag 'prototype' %></code>


На этом функциональность второй версии можно считать завершённой.

Теперь можно убрать код, оставшийся с первой версии.

<code>script/destroy controller input preview</code>


И напоследок.

Немного об установке приложения у хостера.



Может случиться так, что дома у вас все работает, а на хостинге категорически отказывается. Причин может быть много, но наиболее частая из них – не хватает каких-то гемов, или у хостера они не той версии. Это касается как самих рельсов, так и гемов, от которых зависит ваше приложение.

Сначала разберемся с зависимостью от рельсов. Перед тем как заливать ваше приложение на хостинг очень полезно сделать следующее:

<code>$>rake rails:freeze:gems</code>


В результате в папке vendor/rails появится копия рельсов с которой вы разрабатываете ваше приложение, и сервер будет использовать её, так что беспокоиться о том, какая версия есть у хостера, больше не потребуется.

Помимо рельсов, приложение часто зависит еще от каких-то гемов, в нашем слечае это RedCloth и Maruku. Для решения проблем с этими зависимостями Dr Nic написал замечательный плагин – Gems on Rails. Работает он по такому же принципу – делает локальные копии гемов. Давайте его установим и научимся использовать:

<code>$>gem install gemsonrails</code>


Идем в папку с приложением и запускаем

<code>$>gemsonrails
Installed gems_on_rails 0.6.4 to ./vendor/plugins/gemsonrails</code>


Теперь у нас установлен плагин Gems on Rails, который добавил полезные задачи для Rake.

<code>$>rake -T
...
rake gems:freeze   # Freeze a RubyGem into this Rails application; init.rb will be loaded on startup.
rake gems:link     # Link a RubyGem into this Rails application; init.rb will be loaded on startup.
rake gems:unfreeze # Unfreeze/unlink a RubyGem from this Rails application
...</code>


  • gems:link – добавляет в vendor/gems код, который загружает гем при загрузке приложения, если гема нет, то приложение не загрузится (удобно узнавать об отсутствии гемов сразу, а не во время работы)
  • gems:freeze – делает локальную копию гема, именно эта копия будет использоваться в приложении
  • gems:unfreeze – удаляет локальную копию и код сгенерированный gems:link


Давайте сделаем локалные копии гемов, нужных нашему приложению:

<code>$>rake gems:freeze GEM=maruku
$>rake gems:freeze GEM=redcloth</code>


У меня в папке vendor/gems появились папки maruku-0.5.6 и RedCloth-3.0.4.

На этом все. Задавайте вопросы, читайте документацию и книгу о рельсах.

Главное пишите код!



PS. Пока писал последние строчки наткнулся на интересный сайт со скринкастами о рельсах.
Tags:
Hubs:
+18
Comments16

Articles