Статьи

ОТДЫХ С Rails Часть 1

В этой первой части я покажу вам, как создавать RESTful-сервисы с использованием Rails. REST — это архитектурный стиль, созданный по образцу Интернета. По сути, это кодифицирует принципы и методы веб-серверов, которые приводят к созданию самой большой распределенной системы из когда-либо созданных. Для некоторых людей «распределение» связано с подключением — отправкой сообщений на удаленные серверы — мы также думаем о том, как крупномасштабные системы возникают из небольших сервисов, созданных независимо различными группами людей — распределенных в проектировании и реализации.

[img_assist | nid = 4838 | title = | desc = | link = url | url = http: //www.manning.com/mcanally/ | align = right | width = 139 | height = 174] Когда мы думаем о REST, мы думаем о том, чтобы следовать тем же самым принципам. Моделирование наших сервисов с точки зрения ресурсов, обеспечение их адресации в виде URL-адресов, связывание их путем ссылки с одного ресурса на другой, обработка представлений на основе типа контента, выполнение операций без сохранения состояния и т. Д. В следующих разделах мы покажем это на примере использования Rails. Вы также быстро поймете, почему мы выбрали Rails для этой задачи.

Эта статья основана на главе 5 из Ruby in Practice Джереми МакАнелли и Ассаф Аркин. Предоставлено Manning Publications . Все права защищены.

 
Ресурсы RESTful

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

ПРОБЛЕМА

Мы создаем менеджер задач, который наши сотрудники могут использовать для управления своими повседневными задачами. Мы также планируем несколько приложений и рабочих процессов, которые будут создавать и выполнять эти задачи. Как бы мы разработали наш диспетчер задач как веб-сервис, который могут использовать как люди, так и приложения?

РЕШЕНИЕ

Очевидно, что одна часть решения поддерживает программируемые веб-форматы, такие как XML и JSON, которые мы рассмотрим в следующем разделе. Прежде чем мы решим эту проблему, нам нужно понять, как структурировать наши ресурсы, чтобы мы могли использовать их из веб-браузеров и клиентских приложений.

Когда мы разрабатываем веб-сервис, наша цель — создать его один раз и поддерживать любое количество клиентов, которые хотят к нему подключиться. Чем больше клиентских приложений могут повторно использовать наш сервис, тем больше мы получаем от первоначальных усилий по созданию этого сервиса. Мы всегда в поиске тех принципов и методов, которые сделали бы наш сервис слабо связанным и готовым к повторному использованию.

В этом разделе мы собираемся сделать это, применив принципы REST к нашему менеджеру задач. Мы начнем с определения наиболее важных ресурсов, которые нам необходимо предоставить. У нас есть один, представляющий коллекцию задач, которые мы сделаем очевидными, используя путь / задачи URL. И поскольку мы также планируем работать над отдельными задачами, мы дадим каждой задаче свои отдельные ресурсы, и мы будем делать это иерархически, поместив каждую задачу в ресурс в форме / tasks / {id}.

Мы будем обрабатывать все это через TasksController, поэтому первое, что мы сделаем, это определим ресурс, чтобы Rails мог отобразить входящий запрос на нужный контроллер. Мы делаем это в файле config / rout.rb:

ActionController::Routing::Routes.draw do |map|
# Tasks resources handled by TasksController
map.resources :tasks
end

Получение списка всех задач в коллекции выполняется действием index:

class TasksController < ApplicationController
# GET on /tasks
# View: tasks/index.html.erb
def index
@tasks = Task.for_user(@user_id)
end
. . .
end

А для отдельных задач мы будем использовать действие show:

# GET on /tasks/{id}
# View: tasks/show.html.erb
def show
@task = Task.find(params[:id])
end

Что еще мы хотели бы сделать с задачей? Мы хотим изменить (обновить) его, и нам нужно предложить способ его удаления. Мы можем сделать все три на одном ресурсе. Мы можем использовать HTTP GET для получения задачи, PUT для обновления задачи и DELETE для ее отмены. Итак, давайте добавим еще два действия, которые воздействуют на задачу члена:

# PUT on /tasks/{id}
def update
@task = Task.find(params[:id])
@task.update_attributes! params[:task]
respond_to do |format|
format.html { redirect_to :action=>'index' }
format.xml { render :xml=>task } end
end
# DELETE on /tasks/{id}
def destroy
if task = Task.find_by_id(params[:id])
task.destroy
end
head :ok
end

Мы немного опередили себя. Прежде чем мы сможем сделать все это в задаче, нам нужен какой-то способ ее создания. Поскольку у нас есть ресурс, представляющий коллекцию задач, и каждая задача представлена ​​своим собственным ресурсом, мы будем использовать HTTP POST для создания новой задачи в коллекции:

# POST on /tasks def create
task = Task.create!(params[:task])
respond_to do |format|
format.html { redirect_to :action=>'index' }
format.xml { render :xml=>@task, :status=>:created, :location=>url_for(:action=>'show', :id=>task.id) } end
end

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

Мы еще не закончили. Мы хотим предоставить эту услугу как людям, так и приложениям. Наши сотрудники собираются использовать веб-браузер, они не собираются отправлять запросы POST или PUT, а делают это с помощью форм. Таким образом, нам нужны две формы, одна для создания новой задачи и одна для обновления существующей задачи. Мы можем разместить их в списке задач и в отдельном представлении задач соответственно. Для больших форм, и для наших задач потребуется несколько полей, занимающих большую часть страницы, мы хотели бы предложить их как отдельные страницы, связанные с существующими страницами представления, поэтому мы предложим два дополнительных ресурса.

Из списка задач мы будем ссылаться на отдельный ресурс, представляющий форму для создания новых задач, и будем продолжать проектировать наши ресурсы иерархически, назначая ему путь URL / tasks / new. Аналогично, мы свяжем каждую отдельную задачу с URL-адресом для просмотра и URL-адресом для его редактирования:

# GET on /tasks/new
# View: tasks/new.html.erb
def new
@task = Task.new
end
# GET on /tasks/{id}/edit
# View: tasks/edit.html.erb
def edit
@task = Task.find(params[:id])
end

Теперь становится понятнее, почему мы решили расположить ресурсы иерархически. Если вам нравится возиться с адресной строкой браузера, попробуйте следующее: откройте форму редактирования для данной задачи, скажем / tasks / 123 / edit и измените URL-адрес, чтобы перейти на один уровень вверх к представлению задач в / tasks / 123, и перейти на другой уровень к списку задач в / tasks. Помимо того, что это хороший трюк в браузере, эта настройка помогает разработчикам понять, как все ресурсы связаны друг с другом. Это тот случай, когда выбор интуитивно понятных URL стоит 1000 слов документации.
Итак, давайте сделаем паузу и рассмотрим, что у нас есть:

  1. GET запрос к / tasks возвращает список всех задач
  2. POST-запрос к / tasks создает новую задачу и перенаправляет обратно в список задач
  3. GET-запрос к / tasks / new возвращает форму, которую мы можем использовать для создания новой задачи, он отправляет POST в / tasks
  4. Запрос GET для / tasks / {id} возвращает одну задачу
  5. Запрос PUT для / tasks / {id} обновляет эту задачу
  6. Запрос DELETE для / tasks / {id} удаляет эту задачу
  7. Запрос GET для / tasks / {id} / edit возвращает форму, которую мы можем использовать для обновления существующей задачи, он помещает эти изменения в / tasks / {id}

Мы попали сюда не случайно. Мы намеренно выбрали эти ресурсы, поэтому все, что нам нужно сделать, — это отслеживать одну ссылку (URL) на список задач и одну ссылку на каждую отдельную задачу. Помог нам тот факт, что мы можем использовать все четыре метода HTTP, которые уже определяют семантику операций, которые мы можем выполнять с этими ресурсами. Обратите внимание, что, добавляя больше действий к нашим контроллерам, мы не изменили нашу конфигурацию маршрутизации. Эти соглашения являются вопросом практического смысла, и Rails также следует им, и поэтому наше однострочное определение ресурса охватывает всю эту логику, все, что нам нужно было сделать, это выполнить действия. Далее мы собираемся добавить пару действий, специфичных для нашего менеджера задач, и расширить наше определение ресурса, чтобы охватить их.

Первый ресурс, который мы добавим, предназначен для просмотра коллекции выполненных задач, и мы можем следовать тем же правилам, чтобы добавлять ресурсы для просмотра ожидающих задач, задач, запланированных на сегодня, задач с высоким приоритетом и так далее. Мы собираемся разместить его по пути URL / tasks / complete. Второй ресурс, который мы собираемся добавить, облегчит изменение приоритета задачи. Прямо сейчас внесение изменений в задачу требует обновления ресурса задачи. Мы хотим разработать простой элемент управления AJAX, который показывает пять цветных чисел, и щелчок по одному из этих номеров устанавливает приоритет задачи. Мы упростим задачу, предоставив ресурс для представления приоритета задачи, чтобы мы могли написать обработчик события onClick, который обновляет приоритет, отправляя ему новый номер приоритета. Мы считаем это частью ресурса задачи, поэтому мыЯ свяжу его с URL-адресом / tasks / {id} / priority. Итак, давайте сложим эти два ресурса вместе и создадим маршруты, показанные в листинге 1.

ActionController::Routing::Routes.draw do |map|
# Tasks resources handled by TasksController
map.resources :tasks, :collection => { :completed=>:get }, :member => { :priority=>:put } end

А теперь давайте добавим действия контроллера:

# GET on /tasks/completed
# View: tasks/completed.html.erb
def completed
@tasks = Task.completed_for_user(@user_id)
end
# PUT on /tasks/{id}/priority
def priority
@task = Task.find(params[:id])
@task.update_attributes! :priority=>request.body.to_i
head :ok
end

Это будет работать? Мы, конечно, на это надеемся, но не узнаем, пока не проверим. С определениями ресурсов Rails легко работать, но мы все равно время от времени допускаем ошибки и создаем что-то отличное от того, что намеревались. Итак, давайте исследуем наши определения маршрутов с помощью задачи «маршруты»:

$ rake routes

Вывод будет выглядеть примерно как листинг 2.

complete_tasks GET / задачи / завершенные {: Действие => «завершено»}
задачи GET /задачи {: Действие => «индекс»}
ПОСЛЕ /задачи {: Действие => «создать»}
new_task GET / задачи / новый {: Действие => «новый»}
завершение_перемещение PUT / Задачи /: идентификатор / завершение {: Действие => «завершение»}
edit_task GET / Задачи /: идентификатор / редактировать {: Действие => «редактировать»}
задача GET / Задачи /: идентификатор {: Действие => «показать»}
ПОЛОЖИТЬ / Задачи /: идентификатор {: Действие => «обновление»}
УДАЛИТЬ / Задачи /: идентификатор {: Действие => «уничтожить»}

Фактический вывод является более подробным, мы сократили его до узких страниц этой книги, проигнорировав имя контроллера (неудивительно, что это всегда «задачи»), и удалили маршруты форматирования, которые мы рассмотрим в следующем разделе. Мы сохранили остальное. Вы можете видеть, как каждый метод HTTP (второй столбец) и шаблон URL (третий столбец) отображаются на действие правого контроллера (самый правый столбец). Быстрый взгляд говорит нам все, что нам нужно знать. Крайний левый столбец заслуживает более подробного объяснения. Rails создает несколько удобных методов маршрутизации, которые мы можем использовать вместо универсального url_for. Например, поскольку нашему списку задач нужна ссылка на URL для формы создания задачи, мы можем написать это:

<%= link_to "Create new task", url_for(:controller=>'tasks', :action=>'new') %>

Или, используя именованный метод маршрута, сократите его до:

<%= link_to "Create new task", new_task_url %>

Мы можем поместить рядом с каждой задачей, появляющейся в списке задач, указав на вид этой задачи:

<%= link_to task.title, task_url(task) %>

Или ссылку для формы редактирования задачи:

<%= link_to "Edit this task", edit_task_url(task) %>

Мы закончили, поэтому давайте посмотрим, как выглядит наш контроллер со всеми действиями, собранными в одном файле. Когда мы это напишем, мы собираемся сделать несколько небольших изменений. Во-первых, мы будем использовать именованные маршруты вместо url_for. Во-вторых, мы добавим фильтр для загрузки задачи в контроллер, чтобы использовать действия, выполняемые над отдельными задачами. В листинге 3 показано, как выглядит получившийся контроллер.

class TasksController < ApplicationController
before_filter :set_task, :only=>[:show, :edit, :update, :complete]
def index
@tasks = Task.for_user(@user_id)
end
def completed
@tasks = Task.completed_for_user(@user_id)
end
def new
@task = Task.new
end
def create
task = Task.create!(params[:task])
respond_to do |format|
format.html { redirect_to tasks_url }
format.xml { render :xml=>task, :status=>:created, :location=>task_url(task) } | end
end
def show
end
def edit
end
def update
@task.update_attributes! params[:task]
respond_to do |format|
format.html { redirect_to tasks_url }
format.xml { render :xml=>@task } end
end
def priority
@task.update_attributes! :priority=>request.body.to_i
head :ok
end
def destroy
if task = Task.find_by_id(params[:id]) task.destroy end
head :ok
end
private
def set_task
@task = Task.find(params[:id])
end
end

ОБСУЖДЕНИЕ

Мы показали, как создать простой RESTful Web-сервис с использованием Rails. Есть еще несколько вещей, которые стоит отметить в этом примере и как мы использовали Rails для применения принципов REST. Одним из основных принципов REST является единый интерфейс. HTTP предоставляет несколько методов, которые вы можете использовать для каждого ресурса, четыре из которых мы здесь показываем: POST (создание), GET (чтение), PUT (обновление) и DELETE (удаление). У них четкая семантика, и все понимают их одинаково. Клиенты знают, что делает GET и чем он отличается от DELETE, серверы работают по-разному в POST и PUT, кэши знают, что они могут кэшировать ответ на GET, но должны аннулировать его при DELETE и т. Д. Вы также можете использовать это для создания более надежных приложений, например, PUT и DELETE являются идемпотентными методами, поэтому, если вы не сможете выполнить запрос, вы можете просто повторить его во второй раз.Единый интерфейс избавляет нас от необходимости заново изобретать и документировать эту семантику для каждого приложения, это помогает нам всегда делать одно и то же.

К сожалению, хотя мы получаем такое разнообразие для программируемого Интернета, веб-браузеры еще не догнали, а некоторые не могут должным образом обрабатывать PUT и DELETE. Обычный обходной путь — использовать POST для имитации PUT и DELETE, посылая реальный HTTP-метод в параметре _method. Rails понимает это соглашение, как и многие AJAX-библиотеки, такие как Prototype.js и jQuery, поэтому вы можете безопасно использовать их с Rails, чтобы сохранить ваши ресурсы RESTful. В нашем примере вы заметите, что при обновлении существующего ресурса (задачи) мы отвечаем на запрос PUT с кодом состояния по умолчанию 200 (ОК) и представлением обновленного ресурса в формате XML. С другой стороны, при создании нового ресурса мы отвечаем на запрос POST кодом состояния 201 (Создан), XML-представлением нового ресурса и заголовком Location.Последний сообщает клиентскому приложению, что мы только что создали новый ресурс, и где найти этот ресурс, например, чтобы позже обновить его. В обоих случаях мы возвращаем документ, который может отличаться от полученного нами, например, добавляя поля, такие как id, version и updated_at. В любом случае, мы используем полную семантику протокола HTTP, чтобы различать создание нового ресурса и обновление существующего.

Люди работают с различными формами приложений, поэтому, когда мы отвечаем на веб-браузер, мы беспокоимся о взаимодействии с пользователем. То, как работают браузеры, если мы просто отвечаем на POST-запрос с помощью рендера, а затем пользователь решает обновить страницу, браузер выполнит еще один POST-запрос, проблема с двойной передачей. Мы не хотим, чтобы это произошло, поэтому вместо этого мы перенаправляем. Нам также не нужно отправлять обратно представление ресурса или его местоположение, вместо этого мы решаем вернуть пользователя в списки задач.
Вам может быть интересно, что произойдет, если кто-то сделает запрос к / tasks / 456, но такой задачи не будет? Ясно, что это должно вернуть 404 (Not Found) ответа, но мы не показываем такого в нашем примере. Мы даем Rails понять это.

Когда мы вызываем Task.find, он генерирует исключение ActiveRecord :: RecordNotFound, если не может найти задачу с этим идентификатором. Rails перехватывает это исключение и сопоставляет его с кодом состояния 404 (не найдено). Поведение по умолчанию заключается в отправке обратно статической страницы, которую вы можете найти (и настроить для своего приложения) в public / 404.html.
Аналогично, если мы попытались создать или обновить задачу, отправив поле, которое она не понимает, например, XML-документ с элементом <address> (у наших задач нет поля адреса), Rails сгенерирует ActiveRecord: : RecordInvalid или ActiveRecord :: RecordNotSaved исключение. Затем он поймает это исключение и отобразит его в код состояния 422 (Unprocessable Entity).

Вы можете добавить свою собственную логику для ловли и обработки этих исключений, а также ввести свою собственную логику исключений и обработки. Взгляните на ActionController :: Rescue, в частности на метод rescue_from. Одним из способов, с помощью которого Rails упрощает развертывание, является забота обо всех этих деталях и применение поведения по умолчанию, поэтому вам не нужно беспокоиться об этом, если вы не хотите изменить его поведение. Другим примером является то, как Rails работает с неподдерживаемыми типами контента, возвращая 406 (не допустимо), которое мы приведем в действие в следующем разделе. Говоря о методах поиска, одной из распространенных ошибок, которые делают веб-разработчики, является сохранение копии объекта в сеансе, например:

Task.find_by_user(session[:user])

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

Task.find_by_user_id(session[:user_id])

Этот код работает лучше, если вы используете сеансы. При разработке приложений, использующих веб-сервис, намного проще работать с базовой аутентификацией HTTP, как мы показали в предыдущих разделах. Это проще в использовании, чем через пользовательскую форму входа в систему, а затем переносить сессионный cookie.
К счастью, написание контроллеров, поддерживающих оба средства аутентификации, является тривиальным вопросом. Просто добавьте фильтр, который может использовать HTTP Basic Authentication или сеанс, чтобы идентифицировать пользователя и сохранить его идентификатор в переменной экземпляра @user_id. Мы рекомендуем делать это в ApplicationController, поэтому мы не показываем этот фильтр в нашем примере.

Мы говорили о простоте отображения ресурсов для операций CRUD (создание, чтение, обновление, удаление). Сопоставление ресурсов — это еще одна область, в которой мы рекомендуем вам больше узнать. Вы можете сделать иерархические ресурсы на один шаг дальше и создать вложенные ресурсы, например, / books / 598 / chapters / 5. Вы можете использовать метод to_param для создания более удобных URL-адресов, например, / books / 598-ruby-in-practice / chapters / 5-web-services. Кроме того, взгляните на некоторые вспомогательные методы формы, которые сгенерируют правильную форму из объекта ActiveRecord, используя наиболее подходящий URL ресурса. Эта комбинация не только облегчит разработку веб-приложений, но и поможет вам с самого начала делать правильные вещи.

При создании веб-сервисов RESTful мы должны иметь дело с несколькими типами контента. Мы кратко коснулись этого вопроса, используя HTML для конечных пользователей и XML для приложений, а во второй части мы рассмотрим его подробнее, добавив поддержку JSON и Atom.