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

    В продолжении статьи ”Первое знакомство с 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. Пока писал последние строчки наткнулся на интересный сайт со скринкастами о рельсах.
    Поделиться публикацией

    Похожие публикации

    Комментарии 16

      0
      Спасибо за статью, не дня без кода!
      • НЛО прилетело и опубликовало эту надпись здесь
          0
          Спасибо!
          • НЛО прилетело и опубликовало эту надпись здесь
              0
              Спасибо! (тест НЛО)
        0
        Отличная статья. Только вот описания работы с AJAX-ом так и не увидел :-(
          0
          Preview реализовано с его помощью.
            0
            Кнопка preview создаётся с помощью helper'а submit_to_remote, который генерирует вот такой код:

            <input name="preview" onclick="new Ajax.Updater('preview', '/articles;preview', {asynchronous:true, evalScripts:true, onComplete:function(request){location.hash = '#preview'}, parameters:Form.serialize(this.form)}); return false;" type="button" value="Preview" />

            Ajax.Updater это класс из библиотеки Prototype, он отправляет на сервер (url: /articles;preview) асинхронный запрос и вставляет ответ с сервера в элемент <div id="preview" />.

            В качестве параметров запроса берутся значения из формы: parameters: Form.serialize(this.form)
            Form этот класс из prototype, он помогает работать с формами.

            По завершении запроса выполняется location.hash = '#preview', чтобы браузер проскролился на элемент preview.
              0
              Спасибо. Теперь понятно где искать.
            0
            Спасибо. Открыл для себя много нового.
              0
              Спасибо, вот так невзначай и руби выучу :) Вот только как хабру не хватает подсветки кода - жуть просто.
                0
                если есть среди читающих приближенные к администрации — попросите добавить на сайт http://code.google.com/p/syntaxhighlight…

                очень хорошая подсветка, у меня весь код в статьях корректно размечен
                0
                Гуд!! Большое спасибо за статью!
                  +1

                  Обнаружил небольшую ошибку-не-ошибку (-:

                  Я о функции Article.find_by_title.
                  Не знаю, как получится в комментариях, но в статье вышло забавно: Article.findbytitle. Видите, как красиво, курсивом, оформилось «by» в названии? Так вот, на самом деле, там имеется в виду следущее:

                  «Article», точка (.), «find», знак подчёркивания (_), «by», знак подчёркивания, «title»

                  Просто добрый хабрапарсер вспомнил вики-синтаксис и решил показать, что он его знает (-:

                  Это я пишу, чтоб у новичков проблем не возникло (-;

                    0
                    Точно, должно быть find_by_title. Это не хабр, это я проглядел.
                    Markdown форматирует курсивом слово что между знаками подчёркивания, а я забыл сделать это кодом чтобы он оставил как есть.
                    Спасибо!

                  Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                  Самое читаемое