Продолжается подписка на наши издания! Вы не забыли подписаться?

Работа с Grails

Автор: Скотт Дэвис
Источник: IBM developerWorks
Опубликовано: 08.07.2010
Версия текста: 1.1
Grails и унаследованные базы данных
Резервное копирование и восстановление данных
Трансформация USGS-данных
Импорт новых данных об аэропортах
Отключение dbCreate
Блоки статического отображения
Делаем унаследованную таблицу доступной только на чтение
Использование унаследованных Java-классов с файлами отображения Hibernate
Использование аннотаций Enterprise JavaBeans (EJB) 3 для Java-классов
Модель событий Grails
События сборки
Событие CreateFile
Обнаружение вызываемых событий сборки
Генерирование своих событий
Bootstrap
Добавление записей в БД в процессе загрузки
Отказоустойчивая вставка и удаление записей БД в BootStrap.groovy
Выполнение действий, специфичных для среды, в BootStrap.groovy
События на микроуровне
Создание обработчиков событий в доменных классах
Заключение
RESTful Grails
REST простыми словами
Сервис-ориентированные Web-сервисы
Ресурсно-ориентированные Web-сервисы
Реализация GETful Web-сервиса в Grails
Реализация RESTful Web-сервиса в Grails
Заключение

Grails и унаследованные базы данных

Grails Object Relational Mapping (GORM) API – одна из главных частей Web-фреймворка Grails. В статье «GORM: забавное название, серьезная технология» вы познакомились с основами GORM, включая простые отношения «один-ко-многим». Позже, в статье «Отношения многие-ко-многим с ложкой Ajax», вы использовали GORM, чтобы смоделировать более сложные отношения классов. Теперь вы увидите, как «ORM» в GORM гибко работает с именами таблиц и колонок в унаследованных БД, не соответствующих стандартным соглашениям по именованию, принятым в GORM.

Резервное копирование и восстановление данных

Всегда, когда вы работаете с данными, хранящимися в БД, важно иметь актуальную резервную копию. Мэрфи, тот, который «Законы Мэрфи», наверно, мой святой покровитель. Все, что может пойти не так, пойдет не так, значит, нужно готовиться к худшему.

Резервное копирование

Кроме резервного копирования целевой БД с помощью обычно используемого ПО, я предлагаю сделать вторую, текстовую, копию данных. Это позволит легко создавать для разработки и тестирования БД с одним и тем же набором данных, а также легко перемещать данные между серверами БД (например, мигрировать с MySQL на DB2 и обратно).

Еще раз, в этой серии мы разрабатываем приложение Trip Planner, планировщик поездок. Листинг 1 – это Gro­ovy-скрипт backupAirports.groovy, выполняющий резервное копирование записей из таблицы airport. С помощью трех выражений и менее чем 20 строк кода он подключается к БД, копирует все записи из таблицы и экспортирует их в виде XML.

Листинг 1. backupAirports.groovy
sql = groovy.sql.Sql.newInstance(
   "jdbc:mysql://localhost/trip?autoReconnect=true",
   "grails",
   "server",
   "com.mysql.jdbc.Driver")

x = new groovy.xml.MarkupBuilder()

x.airports
{
  sql.eachRow("select * from airport order by id")
  { 
    row -> airport(id:row.id)
    {
      version(row.version)
      name(row.name)
      city(row.city)
      state(row.state)
      country(row.country)
      iata(row.iata)
      lat(row.lat)
      lng(row.lng)
    }
  }
}

Первое выражение в листинге 1 создает новый объект groovy.sql.Sql. Это тонкий Groovy-фасад над стандартным пучком JDBC-классов, включающим Connection, Sta­tement и ResultSet. Вы, возможно, опознали четыре аргумента фабричного метода newInstance: строка JDBC-подключения, имя пользователя, пароль и JDBC-драйвер (это значения из grails-app/conf/Data­Sour­ce.gro­ovy).

Следующее выражение создает groovy.xml.Markup­Builder. Этот класс позволяет создавать XML-документы на лету.

Последнее выражение (начинающееся с x.airports) создает XML-дерево. Корневой элемент XML-докумен­та – это airports. Для каждой записи в БД создается элемент airport с атрибутом id. В элемент airport вложены элементы version, name и city (подробнее об использовании Sql и MarkupBuilder в Groovy см. Ресурсы).

В листинге 2 показан результирующий XML.

Листинг 2. Результирующий XML.
<airports>
  <airport id='1'>
    <version>2</version>
    <name>Denver International Airport</name>
    <city>Denver</city>
    <state>CO</state>
    <country>US</country>
    <iata>den</iata>
    <lat>39.8583188</lat>
    <lng>-104.6674674</lng>
  </airport>
  <airport id='2'>...</airport>
  <airport id='3'>...</airport>
</airports>

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

Заметьте, что этот скрипт никак не зависит от фреймворка Grails. Чтобы он работал, на вашей системе должен быть установлен Groovy (см. Ресурсы). В classpath нужно указать путь к JAR-файлу JDBC-драйвера. Это можно делать при запуске скрипта. На UNIX это делается так:

groovy -classpath /path/to/mysql.jar:. backupAirports.groovy

А на Windows:

groovy -classpath c:\path\to\mysql.jar;. backupAirports.groovy

Я достаточно часто использую MySQL, поэтому я копирую JAR в каталог .groovy/lib в своем домашнем каталоге (/Users/sdavis под UNIX, c:\Documents and Settings\sdavis под Windows). JAR-ы их этого каталога автоматически включаются в classpath при запуске Groovy-скриптов из командной строки.

Скрипт из листинга 1 выводит результат на экран. Чтобы сохранить данные в файл, перенаправьте вывод при исполнении скрипта:

groovy backupAirports.groovy > airports.xml

Восстановление данных

Вынуть данные из БД – это полдела. Не менее важно вернуть их обратно. Скрипт restoreAirports.groovy, показанный в листинге 3, читает XML, используя Groovy XmlParser, конструирует SQL-выражение insert и использует Groovy-объект Sql для выполнения этого выражения (подробнее об XmlParser см. Ресурсы).

Листинг 3. Groovy-скрипт для восстановления записей БД из XML
if(args.size())
{
   f = new File(args[0])
   println f

   sql = groovy.sql.Sql.newInstance(
      "jdbc:mysql://localhost/aboutgroovy?autoReconnect=true",
      "grails",
      "server",
      "com.mysql.jdbc.Driver")

   items = new groovy.util.XmlParser().parse(f)
   items.item.each{item ->
     println "${item.@id} -- ${item.title.text()}"
      sql.execute(
         "insert into item (version, title, short_description, description, 
                 url, type, date_posted, posted_by) values(?,?,?,?,?,?,?,?)",
         [0, item.title.text(), item.shortDescription.text(), 
                                item.description.text(), 
             item.url.text(), item.type.text(), item.datePosted.text(), 
             item.postedBy.text()]
         )
   }
}
else
{
   println "USAGE: itemsRestore [filename]"
}

Чтобы запустить этот скрипт, введите:

groovy restoreAirports.groovy airports.xml

Вспомните, что для того, чтобы связи между таблицами работали, поле первичного ключа на стороне «одного» должно соответствовать полю внешнего ключа на стороне «многих». Например, значение, хранимое в колонке id таблицы airport должно совпадать со значением в колонке arrival_airline_id таблицы flight.

Трансформация USGS-данных

USGS предоставляет данные об аэропортах в виде так называемого шейпфайла (shapefile). Это известный формат файла для обмена географическими данными. Шейпфайл состоит как минимум из трех файлов. .shp-файл содержит географические данные – в данном случае широту/долготу для каждого аэропорта. .shx-файл – это пространственный индекс. .dbf-файл – как вы можете догадаться – это старый добрый dBase-файл, который содержит все непространственные данные (в данном случае название аэропорта, IATA-код и так далее).

Я использовал Geospatial Data Abstraction Library (GDAL) – open-source набор утилит командной строки для работы с географическими данными – для конвертации шейпфайла в файл на Geography Markup Language (GML), используемом в этой статье. Конкретнее, я использую команду ogr2ogr -f "GML" airports.xml airprtx020.shp. С помощью GDAL вы также можете конвертировать данные в CSV-файл, GeoJSON, Keyhole Markup Language (KML) и другие форматы.

Чтобы гарантировать, что автоматически нумерованные поля id при восстановлении получат те же номера, сбросьте все таблицы перед их восстановлением. Это сбросит автонумерацию в 0, когда Grails при следующем запуске будет заново создавать таблицы.

Теперь, когда данные по аэропортам надежно сохранены в резервных копиях (и, возможно, данные из других таблиц тоже), вы готовы к экспериментам с новыми «унаследованными» данными. Вы озадачены? В следующем разделе все станет ясно.

Импорт новых данных об аэропортах

United States Geological Survey (USGS) публикует полный список аэропортов США, включающий IATA-коды и широту/долготу. Не удивительно, что поля USGS не соответствуют полям класса Airport, использовавшегося до сих пор. Можно изменить Grails-класс, чтобы он соответствовал таблице USGS, но это приведет к большим переделкам приложения. Вместо этого можно использовать пару других способов, которые позволят гладко отобразить существующий класс Airport на другую, новую, схему таблиц.

Для начала нужно импортировать «унаследованные» от USGS данные в БД. Запустите скрипт createUsgsAirports.groovy из листинга 4, чтобы создать новую таблицу (этот скрипт предполагает, что вы используете MySQL; поскольку разные СУБД используют несколько различающийся синтаксис создания таблиц, возможно, скрипт придется изменить в случае использования других СУБД).

Листинг 4. Создание таблицы USGS Airports
sql = groovy.sql.Sql.newInstance(
   "jdbc:mysql://localhost/trip?autoReconnect=true",
   "grails",
   "server",
   "com.mysql.jdbc.Driver")

ddl = """
CREATE TABLE usgs_airports (
  airport_id bigint(20) not null,
  locid varchar(4),
  feature varchar(80),
  airport_name varchar(80),
  state varchar(2),
  county varchar(50),
  latitude varchar(30),
  longitude varchar(30),
  primary key(airport_id)
);
"""

sql.execute(ddl)

Посмотрите на usgs-airports.xml из листинга 5. Это пример GML-формата. Этот XML несколько сложнее, чем простой XML из листинга 2, созданный скриптом резервного копирования. Каждый элемент содержится в пространстве имен, и элементы более глубоко вложены.

Листинг 5. USGS-аэропорты в GML
<?xml version="1.0" encoding="utf-8" ?>
<ogr:FeatureCollection
     xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
     xsi:schemaLocation="http://ogr.maptools.org/airports.xsd"
     xmlns:ogr="http://ogr.maptools.org/"
     xmlns:gml="http://www.opengis.net/gml">
                         
  <gml:featureMember>
    <ogr:airprtx020 fid="F0">
      <ogr:geometryProperty>
        <gml:Point>
          <gml:coordinates>-156.042831420898438,19.73573112487793</gml:coordinates>
        </gml:Point>
      </ogr:geometryProperty>
      <ogr:AREA>0.000</ogr:AREA>
      <ogr:PERIMETER>0.000</ogr:PERIMETER>
      <ogr:AIRPRTX020>1</ogr:AIRPRTX020>
      <ogr:LOCID>KOA</ogr:LOCID>
      <ogr:FEATURE>Airport</ogr:FEATURE>
      <ogr:NAME>Kona International At Keahole</ogr:NAME>
      <ogr:TOT_ENP>1271744</ogr:TOT_ENP>
      <ogr:STATE>HI</ogr:STATE>
      <ogr:COUNTY>Hawaii County</ogr:COUNTY>
      <ogr:FIPS>15001</ogr:FIPS>
      <ogr:STATE_FIPS>15</ogr:STATE_FIPS>
    </ogr:airprtx020>
  </gml:featureMember>

  <gml:featureMember>...</gml:featureMember>
  <gml:featureMember>...</gml:featureMember>
</ogr:FeatureCollection>  

Теперь создайте скрипт restoreUsgsAirports.groovy, показанный в листинге 6. Чтобы получить элементы с пространствами имен, нужно объявить пару переменных groovy.xml.Namespace. Вместо использования простой нотации с точками, используемой в предыдущем скрипте restoreAirport.groovy (листинг 3), нужно окружить элементы с пространствами имен квадратными скобками.

Листинг 6. Восстановление данных USGS по аэропортам в БД
if(args.size())
{
  f = new File(args[0])
  println f

  sql = groovy.sql.Sql.newInstance(
     "jdbc:mysql://localhost/trip?autoReconnect=true",
     "grails",
     "server",
     "com.mysql.jdbc.Driver")

  FeatureCollection = new groovy.util.XmlParser().parse(f)
  ogr = new groovy.xml.Namespace("http://ogr.maptools.org/")
  gml = new groovy.xml.Namespace("http://www.opengis.net/gml")
  
  FeatureCollection[gml.featureMember][ogr.airprtx020].each{airprtx020 ->
    println "${airprtx020[ogr.LOCID].text()} -- ${airprtx020[ogr.NAME].text()}"
    points = airprtx020[ogr.geometryProperty][gml.Point][gml.coordinates]
      .text().split(",")
    
     sql.execute(
        "insert into usgs_airports (
           airport_id, locid, feature, airport_name, state, 
             county, latitude, longitude) values(?,?,?,?,?,?,?,?)",
           [airprtx020[ogr.AIRPRTX020].text(), 
           airprtx020[ogr.LOCID].text(), 
           airprtx020[ogr.FEATURE].text(), 
           airprtx020[ogr.NAME].text(), 
           airprtx020[ogr.STATE].text(), 
           airprtx020[ogr.COUNTY].text(), 
           points[1], 
           points[0]]
         )    
  }
}
else{
   println "USAGE: restoreAirports [filename]"
}

Чтобы вставить в созданную таблицу данные из файла usgs_airports.xml, введите в командной строке:

groovy restoreUsgsAirports.groovy
usgs-airports.xml

Чтобы проверить, что это работает корректно, войдите в MySQL из командной строки и проверьте наличие данных, как показано в листинге 7:

Листинг 7. Проверка наличия данных USGS об аэропортах в БД
$ mysql --user=grails -p --database=trip

mysql> desc usgs_airports;
+--------------+-------------+------+-----+---------+-------+
| Field        | Type        | Null | Key | Default | Extra |
+--------------+-------------+------+-----+---------+-------+
| airport_id   | bigint(20)  | NO   | PRI |         |       | 
| locid        | varchar(4)  | YES  |     | NULL    |       | 
| feature      | varchar(80) | YES  |     | NULL    |       | 
| airport_name | varchar(80) | YES  |     | NULL    |       | 
| state        | varchar(2)  | YES  |     | NULL    |       | 
| county       | varchar(50) | YES  |     | NULL    |       | 
| latitude     | varchar(30) | YES  |     | NULL    |       | 
| longitude    | varchar(30) | YES  |     | NULL    |       | 
+--------------+-------------+------+-----+---------+-------+
8 rows in set (0.01 sec)

mysql> select count(*) from usgs_airports;
+----------+
| count(*) |
+----------+
|      901 | 
+----------+
1 row in set (0.44 sec)

mysql> select * from usgs_airports limit 1\G
*************************** 1. row ***************************
  airport_id: 1
       locid: KOA
     feature: Airport
airport_name: Kona International At Keahole
       state: HI
      county: Hawaii County
    latitude: 19.73573112487793
   longitude: -156.042831420898438

Отключение dbCreate

Теперь, когда унаследованная таблица на месте, осталось последнее: отключить переменную dbCreate в gra­ils-app/conf/DataSource.groovy. Вспомните, в «GORM: забавное название, серьезная технология» я писал, что эта переменная говорит GORM создать соответсвующую таблицу, если она не существует, и изменить существующие таблицы, если они не соответствуют доменным классам Grails. Если вы имеете дело с унаследованными таблицами, вы должны отключить это, чтобы GORM не разрушил схему, которую могут использовать другие приложения.

Хорошо было бы селективно включать dbCreate для некоторых таблиц и отключать для других. Увы, это глобальная настройка – «все или ничего». В ситуациях, когда имеется смесь новых и унаследованных таблиц, я позволяю GORM создать новые таблицы, затем отключаю dbCreate и импортирую мои собственные, унаследованные таблицы. Как видите, в таких ситуациях очень важно иметь хорошую стратегию резервирования и восстановления.

Блоки статического отображения

Первая стратегия отображения доменного класса на унаследованную таблицу, которую я покажу, - это использование статического блока отображения. Я использую их в большинстве случаев, поскольку они наиболее в духе Grails. Я обычно добавляю к доменным классам статические блоки constraints, так что добавление статических блоков mapping лежит вполне в русле остального.

Скопируйте файл grails-app/domain/Airport.groovy в gra­ils-app/domain/AirportMapping.groovy. Это имя служит только целям демонстрации. Вам понадобятся три класса, отображаемые на одну и ту же таблицу, поэтому вам нужен способ их уникального именования (в реальных приложениях такого, скорее всего, не случится).

Закомментируйте поля city и country, поскольку в новой таблице их нет. Удалите их также из блока constraints. Теперь добавьте статический блок mapping, чтобы связать имена Grails с именами в БД, как показано в листинге 8:

Листинг 8. AirportMapping.groovy
class AirportMapping
{
  static constraints = 
  {
    name()
    iata(maxSize:3)
    state(maxSize:2)
    lat()
    lng()
  }
  
  static mapping = 
  {
    table "usgs_airports"
    version false
    columns {
      id column: "airport_id"
      name column: "airport_name"
      iata column: "locid"
      state column: "state"
      lat column: "latitude"
      lng column: "longitude"              
    }
  }
  
  String name
  String iata
  //String city
  String state
  //String country = "US"
  String lat
  String lng
  
  String toString()
  {
    "${iata} - ${name}"
  }
}

Первое выражение в блоке mapping связывает класс AirportMapping с таблицей usgs_airports. Следующее выражение говорит Grails, что таблица не содержит колонки version (GORM обычно создает ее, чтобы способствовать оптимистической блокировке). Наконец, блок columns отображает имена Grails на имена БД.

Заметьте, что благодаря использованию этой техники вы можете игнорировать отдельные поля таблицы. В данном случае в доменном классе отсутствуют поля fe­ature и county. Чтобы добиться обратного – иметь в доменном классе поля, отсутствующие в таблице, нужно добавить статическую строку transients. Эта строка выглядит похоже на переменную belongsTo, используемую в отношениях один-ко-многим. Например, если бы в классе Airport было два поля, которые не нужно хранить в таблице, код выглядел бы так:

        static transients = ["tempField1", "tempField2"]

Показанный здесь блок mapping всего лишь краем задевает то, что можно сделать с помощью этой техники. Подробнее см. Ресурсы.

Делаем унаследованную таблицу доступной только на чтение

Введите grails generate-all AirportMapping, чтобы создать контроллер и GSP-представления. Поскольку эта таблица, в сущности, является справочником, откройте grails-app/controllers/AirportMappingController.groovy и ос­тавьте там только замыкания list и show. Удалите delete, edit, update, create и save (и не забудьте удалить delete, edit и save из переменной allowedMethods; можно полностью удалить строку или оставить пустые квадратные скобки).

Чтобы сделать представления доступными только для чтения, необходимо сделать пару небольших изменений. Прежде всего, удалите ссылку New AirportMapping в начале файла grails-app/views/airportMapping/list.gsp. Сделайте то же в grails-app/views/airportMap­ping/­show.­gsp. Наконец, удалите кнопки edit и delete в конце show.gsp.

Введите grails run-app, чтобы убедиться, что блок mapping работает. Вы должны увидеть страницу, показанную на рисунке 1.


Рисунок 1. Проверка работоспособности блока mapping.

Использование унаследованных Java-классов с файлами отображения Hibernate

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

Прежде чем в Java 1.5 появились аннотации, пользователи Hibernate создали XML-файлы отображения, или HBM-файлы. Если вспомнить, что GORM – это тонкий Groovy-фасад над Hibernate, неудивительно будет, что все старые приемы из Hibernate по-прежнему работают.

Для начала скопируйте унаследованные файлы с Java-кодом в src/java. Если вы используете пакеты, создайте по каталогу для каждого пакета. Например, файл AirportHbm.java, показанный в листинге 9, лежит в пакете org.davisworld.trip. Это значит, что полный путь к этому файлу должен быть src/java/org/davisworld/trip/Air­port­Hbm.java.

Листинг 9. AirportHbm.java
package org.davisworld.trip;

public class AirportHbm {
  private long id;
  private String name;
  private String iata;
  private String state;
  private String lat;
  private String lng;

  public long getId() {
    return id;
  }

  public void setId(long id) {
    this.id = id;
  }
  
  // место для остальных геттеров/сеттеров
  // all of the other getters/setters go here
}

Когда Java-файл на месте, можно создать рядом с ним «теневой» файл с названием AirportHbmConstra­ints.gro­ovy, показанный в листинге 10. В него можно поместить статический блок constraints, обычно находящийся в доменном классе. Удостоверьтесь, что он находится в том же пакете, что и Java-класс.

Листинг 10. AirportHbmConstraints.groovy
package org.davisworld.trip

static constraints = 
{
  name()
  iata(maxSize:3)
  state(maxSize:2)
  lat()
  lng()
}

Файлы в каталоге src будут компилироваться при исполнении приложения или создании WAR-файла для развертывания. Если Java-код уже скомпилирован, можно просто превратить его в JAR и поместить в каталог lib.

Теперь нужно настроить контроллер. Следуя соглашению по конфигурации, контроллер должен называться AirportHbmController.groovy. Поскольку Java-класс находится в пакете, вы можете либо поместить контроллер в тот же пакет, либо импортировать Java-класс в начало файла. Я предпочитаю импорт, как показано в листинге 11.

Листинг 11. AirportHbmController.groovy
import org.davisworld.trip.AirportHbm

class AirportHbmController 
{
  def scaffold = AirportHbm
}

Теперь скопируйте существующие HBM-файлы в grails-app/conf/hibernate. У вас должен получиться единый hibernate.cfg.xml, показанный в листинге 12, в котором указан каждый файл отображения, использованный для каждого класса. В данном случае должно иметься вхождение для файла AirportHbm.hbm.xml.

Листинг 12. hibernate.cfg.xml
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
        "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
        "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">

<hibernate-configuration>
    <session-factory>
        <mapping resource="AirportHbm.hbm.xml"/>
    </session-factory>
</hibernate-configuration>

Каждому классу может соответствовать собственный HBM-файл. Этот файл является XML-эквивалентом статического блока mapping, использованного выше. В листинге 13 показан файл AirportHbm.hbm.xml:

Листинг 13. AirportHbm.hbm.xml
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping>
    <class name="org.davisworld.trip.AirportHbm" table="usgs_airports">
        <id name="id" column="airport_id">
            <generator class="native"/>
         </id>          
        <property name="name" type="java.lang.String">
            <column name="airport_name" not-null="true" />
        </property>
        <property name="iata" type="java.lang.String">
            <column name="locid" not-null="true" />
        </property>
        <property name="state" />
        <property name="lat" column="latitude" />
        <property name="lng" column="longitude" />
    </class>
</hibernate-mapping>

Заметьте, что в ссылках на Java-класс указывается полное имя пакета. Остальные вхождения отображают Java-имена на имена из таблицы. Вхождения для полей name и iata демонстрируют длинную форму. Поскольку поле state совпадает в Java-коде и в таблице, можно сократить это вхождение; последние два поля – lat и lng – демонстрируют сокращенный синтаксис (подробнее о Hibernate-файлах отображениях см. Ресурсы).

Перезапустите Grails, если он все еще работает. Теперь вы должны видеть отображенные Hibernate данные по адресу http://localhost:8080/trip/airportHbm.

Использование аннотаций Enterprise JavaBeans (EJB) 3 для Java-классов

Как я уже говорил, в Java 1.5 появились аннотации. Аннотации позволяют добавить метаданные прямо в Java-класс, предваряя их префиксом @. В Groovy 1.0, вышедшем в декабре 2006 года, не поддерживались такие возможности Java 1.5, как аннотации. Все изменилось с выходом Groovy 1.5 годом позже. Это значит, что вы можете использовать в Grails-приложениях Java-файлы с EJB3-аннотациями.

Начнем с Java-файла с EJB3-аннотациями. Поместите файл AirportAnnotation.java, приведенный в листинге 14, в src/java/org.davisworld.trip, рядом с файлом AirportHbm.java:

Листинг 14. AirportAnnotation.java
package org.davisworld.trip;

import javax.persistence.*;

@Entity
@Table(name="usgs_airports")
public class AirportAnnotation 
{
  private long id;
  private String name;
  private String iata;
  private String state;
  private String lat;
  private String lng;

  @Id 
  @Column(name="airport_id", nullable=false)
  public long getId() 
  {
    return id;
  }

  @Column(name="airport_name", nullable=false)
  public String getName() 
  {
    return name;
  }

  @Column(name="locid", nullable=false)
  public String getIata() {
    return iata;
  }

  @Column(name="state", nullable=false)
  public String getState() 
  {
    return state;
  }

  @Column(name="latitude", nullable=false)
  public String getLat() {
    return lat;
  }

  @Column(name="longitude", nullable=false)
  public String getLng() 
  {
    return lng;
  }

  // Методы-сеттеры не имеют аннотаций .
  // Здесь они не показаны, но они должны быть в файле,
  // если вы хотите изменять значения.

}

Обратите внимание – в этот файл нужно импортировать пакет javax.persistence. @Entity и @Table аннотируют декларацию класса, отображая его на соответствующую таблицу базы данных. Остальные аннотации появляются над методами-геттерами каждого поля. Все поля должны иметь аннотацию @Column для отображения имени поля на имя колонки. У первичного ключа должна быть аннотация @ID.

Файл AirportAnnotationConstraints.groovy из листинга 15 не отличается от предыдущего примера, приведенного в листинге 10:

Листинг 15. AirportAnnotationConstraints.groovy
package org.davisworld.trip

static constraints = 
{
  name()
  iata(maxSize:3)
  state(maxSize:2)
  lat()
  lng()
}

AirportAnnotationController.groovy (листинг 16) выглядит как обычно:

Листинг 16. AirportAnnotationController.groovy
import org.davisworld.trip.AirportAnnotation

class AirportAnnotationController 
{
  def scaffold = AirportAnnotation
}

Здесь еще раз появляется файл hibernate.cfg.xml. На этот раз его синтаксис слегка отличается. Вместо того, чтобы указывать на HBM-файл, он указывает напрямую на класс, как показано в листинге 17.

Листинг 17. hibernate.cfg.xml
<?xml version='1.0' encoding='utf-8'?>
<!DOCTYPE hibernate-configuration PUBLIC
        "-//Hibernate/Hibernate Configuration DTD 3.0//EN"
        "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">

<hibernate-configuration>
    <session-factory>
        <mapping resource="AirportHbm.hbm.xml"/>
        <mapping class="org.davisworld.trip.AirportAnnotation"/>         
    </session-factory>
</hibernate-configuration>

Чтобы аннотации заработали, осталось сделать еще одно. Исходно Grails не сконфигурирован для работы с EJB3-аннотациями. В grails-app/conf/DataSource.groovy нужно импортировать специальный класс, как показано в листинге 18:

Листинг 18. DataSource.groovy
import org.codehaus.groovy.grails.orm.hibernate.cfg.GrailsAnnotationConfiguration
dataSource 
{
  configClass = GrailsAnnotationConfiguration.class
  
   pooled = false
   driverClassName = "com.mysql.jdbc.Driver"
   username = "grails"
   password = "server"
}

Импортировав org.codehaus.groovy.grails.orm.hiber­na­te.cfg.GrailsAnnotationConfiguration и позволив Spring вставить его в блок dataSource как configClass, Grails начинает поддерживать EJB3-аннотации так же, как HBM-файлы и собственные блоки отображения.

Если забыть про этот, последний, шаг (что со мной случается почти каждый раз при использовании EJB3-аннотаций в Grails), вы получите следующее сообщение об ошибке:

Листинг 19.
org.hibernate.MappingException: 
An AnnotationConfiguration instance is required to use 
<mapping class="org.davisworld.trip.AirportAnnotation"/>

Заключение

К этому моменту для вас не должно представлять трудностей отображение своих объектов на реляционные БД в Grails (не зря же, в конце концов, это называется GORM). Если вы можете легко сохранить и восстановить свои данные, у вас есть множество способов заставить Grails работать с нестандартными соглашениями по именованию, принятыми в унаследованных базах данных. Статические блоки mapping – самый простой путь решения проблемы, поскольку он наиболее в духе Grails. Но если у вас есть Java-классы, уже отображенные на унаследованную БД, нет смысла заново изобретать колесо. Неважно, используете вы HBM-файлы или более новые EJB3-аннотации, Grails сможет использовать уже сделанную работу и позволит перейти к другим задачам

В следующей статье вы сможете поиграть с моделью событий Grails. Все, от создания скриптов до индивидуальных Grails-артефактов (доменных классов, контроллеров и т.д.), генерирует события. Вы узнаете, как настраивать слушатели для перехвата этих событий и их обработки.

Модель событий Grails

Создание Web-сайта – это упражнение в разработке, основанной на событиях. Приложение сидит и тихо ждет, когда пользователь пришлет запрос. Оно возвращает объект и снова засыпает до следующего вызова. В дополнение к традиционному жизненному циклу HTTP-запросов и откликов Grails предоставляет ряд точек, позволяющих воспользоваться событийной моделью и задать собственное поведение.

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

ПРИМЕЧАНИЕ

Впервые Gant-скрипты упоминались в первой статье этой серии. Вспомните, что Gant – это тонкий Groovy-фасад для Apache Ant. Gant не реализует заново задачи Ant – он вызывает нижележащий Ant-код, максимизируя совместимость. Все что можно сделать в Ant, можно сделать и в Gant. Единственное различие состоит в том, что Gant-скрипты – это Groovy-скрипты, а не XML-файлы.

События сборки

Первый ваш шаг как Grails-разработчика – это напечатать grails create-app. Последнее, что вы вводите – grails run-app или grails war. Эти команды и все, что вы вводите между ними, генерируют события в ключевых точках процесса.

Посмотрите на каталог $GRAILS_HOME/scripts. Файлы в этом каталоге – это Gant-скрипты, соответствующие вводимым вами командам. Например, когда вы вводите grails clean, вызывается Clean.groovy.

Откройте Clean.groovy в текстовом редакторе. Первая цель – это default, см. листинг 20.

Листинг 20. Цель default в Clean.groovy
target ('default': "Cleans a Grails project") 
{
   clean()
   cleanTestReports()
}

Как видите, не слишком много. Здесь запускается цель clean, а затем – цель cleanTestReports. Идя по стеку вызовов, посмотрите на цель clean, показанную в листинге 21:

Листинг 21. Цель clean в Clean.groovy
target ( clean: "Implementation of clean") 
{
  event("CleanStart", [])
  depends(cleanCompiledSources, cleanGrailsApp, cleanWarFile)
  event("CleanEnd", [])
}

Если вы хотите изменить поведение команды clean, можете добавить сюда собственный код. Проблема такого подхода состоит в том, что эти изменения придется переносить при каждом обновлении Grails. Это также делает вашу среду более уязвимой при переносе с компьютера на компьютер (инсталляционные файлы Grails редко сохраняют в системах контроля версий, в отличие от кода приложений). Чтобы избежать синдрома «но на моей машине это работает», я предпочитаю сохранять изменения такого типа в проекте. Это гарантирует, что любое восстановление из системы контроля версий содержит все изменения, необходимые для успешной сборки. Это также помогает поддерживать все в согласованном виде при использовании сервера непрерывной интеграции, например, CruiseControl.

Заметьте, что цель clean генерирует пару событий. Событие CleanStart происходит до начала процесса, а CleanEnd – после завершения. Вы можете использовать эти события в проекте, сохраняя ваш пользовательский код с кодом проекта, и оставляя файлы Grails нетронутыми. Все, что нужно сделать – создать слушатель.

Создайте файл Events.groovy в каталоге скриптов вашего проекта. Введите в него код, приведенный в листинге 22.

Листинг 22. Добавляем слушатели событий в Events.groovy
eventCleanStart = 
{
  println "### About to clean"
}

eventCleanEnd = 
{
  println "### Cleaning complete"
}

Если теперь ввести grails clean, будет выдано нечто похожее на листинг 23:

Листинг 23. Консольный вывод с новыми комментариями
$ grails clean

Welcome to Grails 1.0.3 - http://grails.org/
Licensed under Apache Standard License 2.0
Grails home is set to: /opt/grails

Base Directory: /src/trip-planner2
Note: No plugin scripts found
Running script /opt/grails/scripts/Clean.groovy
Environment set to development
Found application events script
### About to clean
  [delete] Deleting: /Users/sdavis/.grails/1.0.3/projects/trip-planner2/resources/web.xml
  [delete] Deleting directory /Users/sdavis/.grails/1.0.3/projects/trip-planner2/classes
  [delete] Deleting directory /Users/sdavis/.grails/1.0.3/projects/trip-planner2/resources
### Cleaning complete

Конечно, можно не только писать простые сообщения в консоли, но и выполнять реальную работу. Возможно, требуется удалить какие-то лишние каталоги. Может, вы захотите «перезагрузить» XML-файлы, переписав уже существующие. Все, что можно сделать в Groovy (или в Java), можно сделать и так.

Событие CreateFile

Вот еще один пример событий. Каждый раз, когда вы вводите одну из команд create, генерируется событие Cre­atedFile. Посмотрите на scripts/Create­Domain­Cla­ss.gro­ovy (листинг 24):

Листинг 24. CreateDomainClass.groovy
Ant.property(environment:"env")
grailsHome = Ant.antProject.properties."env.GRAILS_HOME"

includeTargets << new File ( "${grailsHome}/scripts/Init.groovy" )  
includeTargets << new File( "${grailsHome}/scripts/CreateIntegrationTest.groovy")

target ('default': "Creates a new domain class") {
    depends(checkVersion)

   typeName = ""
   artifactName = "DomainClass"
   artifactPath = "grails-app/domain"
   createArtifact()
   createTestSuite() 
}

Здесь вы не увидите вызова события CreatedFile, но посмотрите на цель createArtifact в $GRA­ILS_HO­ME/scri­pts/Init.groovy (цель createTestSuite в $GRAILS_HO­ME/scripts/CreateIntegrationTest.groovy в конце также вызывает цель createArtifact из $GRAILS_HOME/scri­pts/Init.gro­ovy). В предпоследней строке createArtifact можно увидеть следующий вызов: event("CreatedFile", [artifactFile]).

Это событие имеет одно существенное отличие от события CleanStart: оно возвращает значение обработчику события. В данном случае это полный путь к только что созданному файлу (как вы вскоре увидите, второй аргумент – это список – вы можете вернуть столько разделенных запятыми значений, сколько захотите). Обработчик событий должен уметь перехватывать входящие значения.

Предположим, что вы хотите автоматически добавлять созданные файлы в систему контроля версий. В Groovy вы можете заключить все, что вы обычно вводите в командной строке, в кавычки, и вызвать execute() для этой строки. Добавьте в scripts/Events.groovy обработчик события (листинг 25).

Листинг 25. Автоматическое добавление артефактов в Subversion
eventCreatedFile = 
{
  fileName -> "svn add ${fileName}".execute()
  println "### ${fileName} was just added to Subversion."  
}

Теперь введите grails create-domain-class Hotel и посмотрите на результат. Если вы не используете Sub­version, эта команда просто тихо не выполнится. Если вы используете Subversion, введите svn status. Вы должны увидеть список добавленных файлов (доменный класс и соответствующий интеграционный тест).

Обнаружение вызываемых событий сборки

Самый быстрый путь узнать, какие события генерируются какими скриптами – поискать в скриптах Grails вызовы event(). В UNIX можно использовать grep для поиска строки event в Groovy-скриптах (листинг 26).

Листинг 26. Поиск строки event в Groovy-скриптах с помощью grep
$ grep "event(" *.groovy
Bootstrap.groovy:       event("AppLoadStart", ["Loading Grails Application"])
Bootstrap.groovy:       event("AppLoadEnd", ["Loading Grails Application"])
Bootstrap.groovy:       event("ConfigureAppStart", [grailsApp, appCtx])
Bootstrap.groovy:       event("ConfigureAppEnd", [grailsApp, appCtx])
BugReport.groovy:       event("StatusFinal", ["Created bug-report ZIP at ${zipName}"])

Теперь, когда вы знаете, что вызывается, вы можете создать соответствующие слушатели в scripts/Eve­nts.gro­ovy и выполнить глубокую настройку среды.

Генерирование своих событий

Конечно, теперь, когда вы знаете как это работает, ничто не мешает вам создать свои собственные события. Если вам нужно изменять скрипты из $GRA­ILS_HO­ME/scripts (что мы и собираемся делать, чтобы генерировать события), я очень рекомендую скопировать их в каталог scripts вашего проекта. Это значит, что измененный скрипт будет сохранен в системе контроля версий вместе со всем остальным. Grails спросит, какую версию скрипта использовать – из $GRAILS_HOME или из локального каталога scripts.

Скопируйте $GRAILS_HOME/scripts/Clean.groovy в локальный каталог scripts и добавьте после события Cle­anEnd следующее событие:

event("TestEvent", [new Date(), "Some Custom Value"])

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

Добавьте в scripts/Events.groovy замыкание из листинга 27.

Листинг 27. Перехват пользовательского события.
eventTestEvent = 
{
  timestamp, msg -> println "### ${msg} occurred at ${timestamp}"
}

Если ввести grails clean и выбрать локальную версию скрипта, вы увидите:

### Some Custom Value occurred at Wed Jul 09 08:27:04 MDT 2008

Bootstrap

Вы можете использовать не только события сборки, но и события приложения. Файл grails-app/conf/Boot­Strap.groovy исполняется при каждом запуске и остановке Grails. Откройте BootStrap.groovy в текстовом редакторе. При запуске вызывается замыкание init. При закрытии приложения вызывается замыкание destroy.

Для начала добавим в замечания простой текст, как показано в листинге 28.

Листинг 28. Начало работы с BootStrap.groovy
def init = 
{
  println "### Starting up"
}

def destroy = 
{
  println "### Shutting down"
}

Введите grails run-app для запуска приложения. В конце этого процесса должно появится сообщение ### Starting up.

Теперь нажмите CTRL+C. Видите сообщение ### Shutting Down? Я тоже не вижу. Дело в том, что CTRL+C бесцеремонно завершает работу сервера, не вызывая замыкание destroy. Можете поверить, что при завершении работы сервера приложений замыкание будет вызвано. Но вам не надо писать grails war и загружать WAR в Tomcat или IBM WebSphere, чтобы увидеть событие destroy.

Чтобы увидеть события init и destroy, запустите Grails в интерактивном режиме, написав в консоли interactive. Теперь введите run-app, чтобы запустить приложение, и exit, чтобы завершить работу сервера. Работа в интерактивном режиме существенно ускоряет процесс разработки, поскольку JVM в этом случае все время на ходу. В качестве бонуса, приложение завершается более мягко, чем в случае грубого CTRL+C.

Добавление записей в БД в процессе загрузки

Что можно сделать со скриптом BootStrap.groovy, кроме простого консольного вывода? Часто его используют для вставки записей в БД.

Для начала добавьте в созданный ранее класс Hotel поле name, как показано в листинге 29.

Листинг 29. Добавляем поле в класс Hotel
class Hotel
{
  String name
}

Теперь создадим HotelController, как показано в листинге 30.

Листинг 30. Создание HotelController
class HotelController 
{
  def scaffold = Hotel
}
ПРИМЕЧАНИЕ

Примечание: Если вы закомментировали переменную dbCreate в grails-app/conf/DataSource.groovy, как говорилось в статье «Grails и унаследованные базы данных», ее надо вернуть на место и дать ей значение update. Можно, конечно, вместо этого вручную синхронизировать таблицу Hotel и класс Hotel.

Теперь добавьте в BootStrap.groovy код из листинга 31.

Листинг 31. Сохранение и удаление записей в BootStrap.groovy
def init = 
{ 
  servletContext ->  
  new Hotel(name:"Marriott").save()
  new Hotel(name:"Sheraton").save()  
}

def destroy = 
{
  Hotel.findByName("Marriott").delete()
  Hotel.findByName("Sheraton").delete()  
}

В следующей паре примеров лучше держать открытой консоль MySQL и следить за БД. Введите mysql --user=grails -p --database=trip (пароль - server). Затем выполните следующие шаги:

  1. Запустите Grails, если он еще не запущен.
  2. Введите show tables;, чтобы убедиться, что таблица Hotel создана.
  3. Введите desc hotel;, чтобы увидеть колонки и типы данных.
  4. Введите select * from hotel;, чтобы убедиться, что записи вставлены.
  5. Введите delete from hotel;, чтобы удалить записи.

Отказоустойчивая вставка и удаление записей БД в BootStrap.groovy

Вы можете захотеть большей отказоустойчивости для вставки и удаления записей с помощью BootStrap.groovy. Если вы не проверяете перед вставкой, существует ли уже такая запись, в вашей БД могут появиться дубликаты. Если вы попытаетесь удалить несуществующую запись, вы получите исключение. В листинге 32 показано, как выполнять отказоустойчивые вставки и удаления.

Листинг 32. Отказоустойчивые вставки и удаления
def init = { servletContext ->  
  def hotel = Hotel.findByName("Marriott")    
  if(!hotel)
  {
    new Hotel(name:"Marriott").save()
  }
  
  hotel = Hotel.findByName("Sheraton")
  if(!hotel)
  {
    new Hotel(name:"Sheraton").save()
  }
}

def destroy = 
  {
  def hotel = Hotel.findByName("Marriott")
  if(hotel)
  {
    Hotel.findByName("Marriott").delete()
  }
  
  hotel = Hotel.findByName("Sheraton")
  if(hotel)
  {
    Hotel.findByName("Sheraton").delete()
  }
}

Если вы вызовете Hotel.findByName("Marriott"), и этот отель не существует в таблице, вам будет возвращен null-объект. Следующая строка, if(!hotel), равна true только для не-null значений. Это гарантирует, что новый Hotel будет сохранен только если он еще не существует. В замыкании destroy выполняются такие же проверки, чтобы убедиться, что вы не пытаетесь удалить несуществующие записи.

Выполнение действий, специфичных для среды, в BootStrap.groovy

ПРИМЕЧАНИЕ

switch в Groovy

Обратите внимание – выражение switch в Groovy более надежно, чем в Java. В Java-коде вы можете переключаться только на целочисленные значения. В Groovy можно переключаться и на строковые значения.

Если вы хотите, чтбы что-то происходило только если вы работаете в каком-то особо режиме, вам поможет класс GrailsUtil. Импортируйте в начало файла gra­ils.util.GrailsUtil. Статический метод Grails­Util.getEnviron­ment() (сокращенный до GrailsUtil.environment благодаря сокращенному синтаксису геттеров Groovy) позволяет выяснить, в каком режиме вы работаете. Объедините это знание с выражением switch, как показано в листинге 33, и вы получите зависимое от среды поведение при запуске Grails.

Листинг 33. Зависимые от среды действия в BootStrap.groovy
import grails.util.GrailsUtil

class BootStrap 
{

  def init = 
  { 
    servletContext ->
    switch(GrailsUtil.environment)
    {
      case "development":
        println "#### Development Mode (Start Up)"
        break
      case "test":
        println "#### Test Mode (Start Up)"
        break
      case "production":
        println "#### Production Mode (Start Up)"
        break
    }
  }

  def destroy = 
  {
    switch(GrailsUtil.environment)
    {
      case "development":
        println "#### Development Mode (Shut Down)"
        break
      case "test":
        println "#### Test Mode (Shut Down)"
        break
      case "production":
        println "#### Production Mode (Shut Down)"
        break
    }
  }
}

Теперь записи вставляются только в тестовом режиме. Но не останавливайтесь на этом. Я часто выношу свои тестовые данные в XML-файлы. Объедините то, что узнали здесь, со скриптами резервирования и восстановления из статьи «Grails и унаследованные базы данных» и вы получите мощную тестовую платформу.

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

События на микроуровне

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

Метка времени в доменных классах

Если создать пару особо именованных полей, GORM автоматически вставит в класс временную метку, как показано в листинге 34.

Листинг 34. Поля метки времени
class Hotel
{
  String name
  Date dateCreated 
  Date lastUpdated 
}

Как следует из имен, поле dateCreated заполняется при первой вставке данных в БД. Поле lastUpdated заполняется при каждом обновлении записи в БД,

Чтобы проверить, что эти поля скрыто заполняются, сделайте еще одну вещь: отключите их в представлениях create и edit. Это можно сделать, введя grails generate-views Hotel и удалив поля из файлов create.gsp и edit.gsp, но скаффолдинг позволяет быть несколько динамичнее. В статье «Изменяем вид Groovy Server Pages» я рассказывал, как изменять вид скаффолдинга с помощью grails install-templates. Теперь добавьте два поля временных меток в список excludedProps в шаблонах, как показано в листинге 35.

Листинг 35. Исключение полей временных меток из скаффолдинга по умолчанию
excludedProps = ['dateCreated','lastUpdated',
                 'version',
                 'id',
                   Events.ONLOAD_EVENT,
                   Events.BEFORE_DELETE_EVENT,
                   Events.BEFORE_INSERT_EVENT,
                   Events.BEFORE_UPDATE_EVENT]

Это исключит создание полей в представлениях create и edit, но оставит эти поля в представлениях list и show. Создайте Hotel или два и убедитесь, что поля автоматически обновляются.

Если ваше приложение уже использует эти имена полей, вы можете легко отключить эту возможность, как показано в листинге 36.

Листинг 36. Отключение временных меток
static mapping = 
{ 
  autoTimestamp false 
}

В «Grails и унаследованные базы данных» говорилось, что можно также указать version false, если вы хотите отключить автоматическое создание и обновление поля version, используемого для оптимистических блокировок.

Создание обработчиков событий в доменных классах

Кроме создания временных меток в доменных классах, можно использовать четыре перехватчика событий: befo­reInsert, befortUpdate, beforeDelete и onLoad.

Имена этих замыканий говорят сами за себя. Замыкание beforeInsert вызывается перед методом save(). Замыкание beforeUpdate вызывается перед методом update().Замыкание beforeDelete вызывается перед методом delete(). Наконец, onLoad вызывается, когда класс загружается из БД.

Предположим, что в вашей компании принята политика простановки временных меток для записей БД, и что эти поля у вас стандартно называются cr_time and up_time. У вас есть две возможности заставить Grails вести себя в соответствии с корпоративной политикой. Первая – использовать прием со статическим отображением, показанный в предыдущей статье, чтобы привязать имена полей, используемые по умолчанию Grails, к именам колонок, используемым по умолчанию в компании (листинг 37).

Листинг 37. Отображение полей временных меток.
class Hotel
{
  Date dateCreated
  Date lastUpdated
  
  static mapping = 
  {
    columns 
    {
      dateCreated column: "cr_time"
      lastUpdated column: "up_time"
    }
  }
}

Вторая возможность – именовать поля доменного класса в соответствии с корпоративным стандартом и создать замыкания beforeInsert и beforeUpdate, чтобы заполнять эти поля, как показано в листинге 38 (не забудьте сделать новые поля nullable – иначе метод save() в BootStrap.groovy не сработает).

Листинг 38. Замыкания beforeInsert и beforeUpdate
class Hotel
{
  static constraints = 
  {
    name()
    crTime(nullable:true)
    upTime(nullable:true)
  }

  String name
  Date crTime
  Date upTime

  def beforeInsert = 
  {
    crTime = new Date()
  }

  def beforeUpdate = 
  {
    upTime = new Date()
  }  
}

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

Что делать с этими событиями, как и со всеми рассмотренными выше, вы решите сами. Вспомните, как в «Grails и мобильный Web» вы создавали сервис Geo­coding для конвертации адресов в широту/долготу точек, чтобы отобразить на карте аэропорт. В этой статье этот сервис вызывается в замыканиях save и update в AirportController. Я бы попробовал переместить этот вызов сервиса в beforeInsert и beforeUpdate в классе Airport, чтобы все происходило автоматически и прозрачно.

Как можно распространить такое поведение на все классы? Я бы добавил эти поля и замыкания в используемый по умолчанию шаблон DomainClass в src/temp­lates.

Заключение

События в Grails помогают настраивать поведение приложение. Можно расширить процесс сборки, не изменяя стандартных Grails-скриптов, создав файл Events.groovy в каталоге scripts. Можно изменить процесс запуска и завершения приложения, добавляя собственный код в замыкания init и destroy в файле BootStrap.groovy. Наконец, добавляя такие замыкания, как beforeInsert и beforeUpdate, в доменный класс, можно реализовать такое поведение, как создание временных меток или геокодирование.

В следующей статье я расскажу о том, как использовать Grails для создания Web-сервисов, основанных на Representational State Transfer (REST). Вы увидите, как Grails поддерживает HTTP-методы GET, PUT, POST и DELETE, необходимые для поддержки Web-сервисов нового поколения, RESTful.

RESTful Grails

В этом месяце я собираюсь показать, как ваши Grails-приложения могут работать источником исходных данных – в частности, XML, – которые могут использовать другие Web-приложения. Я обычно называю это созданием Web-сервиса для Grails-приложения, но в наши этот термин перегружен скрытыми смыслами. Многие ассоциируют Web-сервисы с SOAP и полномасштабной сервис-ориентированной архитектурой (SOA). Если вам это нужно, два плагина для Grails позволяют создать SOAP-интерфейс для вашего приложения (см. Ресурсы). Но вместо того, чтобы использовать такую конкретную реализацию, как SOAP, я покажу вам, как возвращать Plain Old XML (POX), используя интерфейс, основанный на Representational State Transfer (REST).

Когда дело доходит до RESTful Web-сервисов, не менее важно понимать, почему, чем как. Докторская диссертация Рой Филдинг (см. Ресурсы) - источник сокращения REST – описывает два подхода к Web-сервисам: сервис-ориентированный и ресурсно-ориентирован­ный. Прежде чем я покажу вам код реализации вашей собственной RESTful ресурсно-ориентированной архитектуры (РОА), я поясню различие между этими двумя философиями дизайна, и расскажу о двух конкурирующих популярных определениях REST. В качестве награды за чтение рассуждений в первой части этой статьи, вы найдете много Grails-кода во второй.

REST простыми словами

Когда разработчики говорят о RESTful Web-сервисах, они обычно имеют в виду, что хотят предоставить простой, беспроблемный способ получения XML от их приложений. RESTful Web-сервисы обычно предоставляют URL, возвращающий XML в ответ на HTTP-запрос GET (чуть позже я дам более формальное определение REST, уточняющее это определение в одном тонком, но важном смысле).

Yahoo! предоставляет ряд RESTful Web-сервисов (см. Ресурсы), возвращающих POX в ответ на простой http-запрос GET. Наберите, например, http://api.search.yahoo.com/WebSearchService/V1/webSearch?appid=YahooDemo&query=beatles в адресной строке браузера. Вы получите в XML-виде те же результаты поиска, что обычно выводятся в виде HTML при задании beatles в качестве условия поиска на домашней странице Yahoo!.

Если бы (гипотетически) Yahoo! поддерживало SOAP-интерфейс (оно не поддерживает), SOAP-запрос возвратил бы те же самые данные, но создание такого запроса потребовало бы несколько больших усилий от разработчика. Запрашивающей стороне пришлось бы отправлять строго определенный XML-документ с секциями SOAP-заголовка и телом вместо простого набора пар имя/значение в строке запроса – причем отправлять запрос, используя HTTP POST вместо GET. После всей этой дополнительной работы ответ пришел бы в виде формального XML-документа, имеющего, как и запрос, SOAP-заголовок и тело, которые нужно удалить, чтобы добраться до результатов запроса. RESTful-подход к Web-сервисам обычно рассматривается как «менее церемонная» альтернатива сложности SOAP.

Некоторые тренды показывают, что RESTful к Web-сер­висам набирает популярность. Amazon.com предоставляет как RESTful, так и SOAP-сервисы. Исследования реального использования Web-сервисов показывают, что девять из десяти пользователей выбирают RESTful-интерфейс. Еще один примечательный пример – Google официально объявил свои SOAP Web-сервисы устаревшими еще в декабре 2006 года. Все его службы данных (собранные под крышей Google Data API) используют RESTful-подход.

Сервис-ориентированные Web-сервисы

Если бы разница между REST и SOAP сводилась к относительным достоинствам GET и POST, различить их было бы просто. Используемый HTTP-метод важен, но не по тем причинам, которые вы можете с ходу предположить. Чтобы полностью осознать разницу между REST и SOAP, требуется оценить глубинную семантику этих двух стратегий. SOAP воплощает сервис-ориентированный подход к Web-сервисам – тот, при котором методы (или команды) являются основным способом взаимодействия с сервисом. REST же использует ресурсно-ориентированный подход, в котором центральное место отводится объекту.

В SOA вызов сервиса выглядит как вызов удаленной процедуры (RPC). Гипотетически, если у вас есть Java-класс Weather с методом getForecast(String zipcode), вы можете легко выставить этот метод как Web-сервис. На самом деле у Yahoo! есть как раз такой Web-сервис. Введите в браузере http://weather.yahooapis.com/fore­castrss?p=94089, подставив в параметр p собственный ZIP-код. Сервис Yahoo! поддерживает второй пара­метр – u – принимающий "f" для Фаренгейта и "с" для Цельсия. Нетрудно представить перегрузку сигнатуры метода вашего гипотетического класса, чтобы он принимал второй параметр: getForecast("94089", "f").

Вернувшись к показанному выше запросу к Yahoo!, нетрудно представить, как переписать его в виде вызова метода.

http://api.search.yahoo.com/WebSearchService /V1/webSearch?appid 
  = YahooDemo&query=beatles 

легко превращается в

WebSearchService.web­Search("YahooDemo", "beatles").

Так если вызовы Yahoo! являются, в сущности, RPC-вызовами, не противоречит ли это моему предположению, что Yahoo!-сервисы – это RESTful-сервисы? К сожалению, да. Но я не одинок в своем заблуждении. Yahoo! также называет их RESTful-сервисами, хотя честно признает, что они не соответствуют строгому смыслу определения. В Yahoo! Web Services FAQ ответ на вопрос "Что такое REST?" звучит как "REST означает Representational State Transfer. Большая часть Yahoo! Web-сервисов использует 'REST-Like'-операции в стиле RPC через HTTP GET или POST..."

В REST продолжаются дебаты. Проблема в том, что нет никакой удачной фразы, сжато описывающей «основанные на RPC Web-сервисы, предпочитающие использование GET использованию POST, и простые URL-запросы XML-запросам». Некоторые называют это HTTP/POX или REST/RPC-сервисами. Другие называют их Low REST, чтобы отличать их от High REST Web-сервисов – сервисов, которые более точно соответствует исходному определению ресурсно-ориентированной архитектуры, данному Филдингом.

Неформально я называю сервисы, подобные сервисам Yahoo!'s, GETful-сервисами. Я не вкладываю в это уничижительного смысла – наоборот, я думаю, что в Yahoo! проделали огромную работу по созданию коллекции «бесцеремонных» Web-сервисов. Это название отражает главное достоинство RPC-сервисов Yahoo! – получение XML-результатов с помощью простого HTTP-запроса GET – без неверного употребления термина, введенного Филдингом.

Ресурсно-ориентированные Web-сервисы

ПРИМЕЧАНИЕ

POST vs. PUT

В REST-сообществе идут споры по поводу ролей POST и PUT при вставке новых ресурсов. Определение PUT в исходном RFC для HTTP 1.1 (где Филдинг был ведущим автором) говорит, что если ресурс не существует, сервер может создать его. " А если ресурс уже существует, "...содержащуюся в нем сущность следует рассматривать как модифицированную версию той, что находится на исходном сервере". Таким образом, если ресурс не существует, PUT равно INSERT. Если же ресурс существует, PUT равно UPDATE. Все было бы достаточно просто, если бы в определении POST не говорилось:

«POST должен обеспечить единый метод реализации следующих функций:

Аннотирование существующих ресурсов;

Размещение сообщений на досках объявлений, в телеконференциях, списках рассылки или аналогичных ресурсах;

Предоставление блоков данных, таких, как результат отправки форм, процессам, обрабатывающим данные;

Расширение баз данных через операцию добавления».

«Аннотирование существующих ресурсов» по идее подразумевает UPDATE, а «Размещение сообщений на досках объявлений» и «Расширение баз данных» должно бы подразумевать INSERT.

Учитывая тот факт, что все основные браузеры не поддерживают метод PUT при отправке данных HTML-форм (они поддерживают только GET и POST) трудно определить, что народная мудрость должна сказать о том, когда какие методы использовать.

Atom Publishing Protocol – популярный формат синдикации, следующий RESTful-принципам. Авторы RFC для Atom пытаются закончить споры вокруг POST и PUT с помощью определения:

«Atom Publishing Protocol использует HTTP-методы для работы с ресурсами следующим образом:

GET используется, чтобы получить представление известного Ресурса.

POST используется для создания новых, динамически именуемых, Ресурсов...

PUT используется для редактирования известных Ресурсов. Он не используется для создания Ресурса.

DELETE используется для удаления известных Ресурсов".

В этой статье я руководствуюсь подходом Atom, используя POST для вставки и PUT для обновления. Однако если вы решите сделать все наоборот, вы тоже будете в хорошей компании – в книге RESTful Web Services (см. Ресурсы) рекомендуется использовать PUT как INSERT.

Итак, что же требуется от сервиса, чтобы быть реально ресурсно-ориентированным? Все сводится к созданию хорошего URI (Uniform Resource Identifier) и использованию четырех HTTP-методов (GET, POST, PUT и DELETE) стандартизованным образом вместо одного (GET) в сочетании с вызовами пользовательских методов.

Возвращаясь к запросу Beatles, первый шаг к более формальному RESTful-интерфейсу – изменение URI. Вместо передачи Beatles в качестве аргумента метода webSearch, Beatles становится центральным ресурсом в URI. Например, URI статьи в Википедии, посвященной Beatles – http://en.wikipedia.org/wiki/Beatles.

Но что на самом деле отличает философии GETful и RESTful – это метод, используемый для возврата представления ресурса. RPC-интерфейс Yahoo! определяет ряд методов (webSearch, albumSearch, newsSearch и т.д.). Без чтения документации нельзя узнать имя вызываемого метода. В случае Yahoo! я могу следовать шаблону и предположить, что существуют методы song­Search, imageSearch и videoSearch, но никаких гарантий этому нет. Кроме того, другие Web-сайты могут использовать другие соглашения по именованию, и называть методы, например, findSong или songQuery. В случае Grails такие пользовательские действия, как aiport/list и airport/show, стандартны для приложения, но эти имена методов никак не стандартны для других Web-фрейм­ворков.

В отличие от этого, RESTful-подход всегда использует HTTP GET, чтобы возвратить представление запрашиваемого ресурса. Поэтому я знаю, что GET – это стандартный способ получения любого ресурса из Википедии (http://en.wikipedia.org/wiki/Beatles, http://en.wikipe­dia.org/wiki/United_Airlines или http://en.wikipedia.org/wi­ki/Peanut_butter_and_jelly_sandwich).

Сила стандартизации вызовов методов становится очевиднее, когда вы имеете дело с полным жизненным циклом ресурса, Create/Retrieve/Update/Delete (CRUD). RPC-интерфейс не предлагает стандартного способа создания нового ресурса. Пользовательский метод может называться create, new, insert, add или почти как угодно еще. В RESTful-интерфейсе отправка URI POST-запроса создает новый ресурс, PUT обновляет ресурс, а DELETE удаляет его (см. POST vs. PUT).

Теперь, когда вы лучше понимаете разницу между GETful и RESTful Web-сервисами, можно начать создание собственного сервиса на Grails. Я приведу примеры обоих подходов, но начну с простого POX-примера.

Реализация GETful Web-сервиса в Grails

Самый быстрый способ создать POX из Grails-приложения – импортировать пакет grails.converters.* и добавить пару новых замыканий, как показано в листинге 39:

Листинг 39. Простой XML-результат.
import grails.converters.*

class AirportController{
  def xmlList = 
  {
    render Airport.list() as XML
  }

  def xmlShow = 
  {
    render Airport.get(params.id) as XML
  }
  
  //... оставшаяся часть контроллера
}

Вы уже видели работу пакета grails.converters в статье «Отношения многие-ко-многим с ложкой Ajax». Этот пакет дает невероятно простую поддержку JavaScript Object Notation (JSON) и XML-вывода. На рисунке 2 показаны результаты вызова xmlList:


Рисунок 2. XML-вывод по умолчанию в Grails.

Генерируемый по умолчанию XML хорош для отладки, но, скорее всего, вы захотите несколько изменить формат. К счастью, метод render() предлагает Groovy Mark­upBuilder, который позволяет определять пользовательский XML «на лету» (см. Ресурсы). В листинге 40 показан код для настройки выдачи XML:

Листинг 40. Настройка генерирования XML.
def customXmlList = 
{
  def list = Airport.list()
  render(contentType:"text/xml")
  {
    airports
    {
      for(a in list)
      {
        airport(id:a.id, iata:a.iata)
        {
          "official-name"(a.name)
          city(a.city)
          state(a.state)
          country(a.country)
          location(latitude:a.lat, longitude:a.lng)
        }
      } 
    }
  }
}

На рисунке 3 показан результирующий XML:


Рисунок 3. Пользовательский XML, сгенерированный с помощью Groovy MarkupBuilder.

Обратите внимание, насколько код соответствует XML-результату. Можно задать любое имя элемента (airports, airport, city), независимо от того, соответствуют ли они именам полей класса. Если вы хотите создать имя элемента с дефисом (например, official-name) или добавит поддержку пространств имен, просто заключите имя элемента в кавычки. Атрибуты (такие как id и iata) определяются с использованием hashmap-синтаксиса Gro­ovy: key:value. Чтобы заполнить тело элемента, используйте значение без key.

Согласование содержания и заголовок Accept

Создать отдельные замыкания, возвращающие HTML- и XML-представления данных, довольно просто, но нельзя ли заставить одно замыкание выполнять обе задачи? Это возможно благодаря заголовку Accept, включаемому в HTTP-запрос. Этот кусочек метаданных в запросе говорит серверу: «У тебя может быть несколько представлений ресурса для этого URI – так мне нужно вот такое».

cURL – это удобная открытая HTTP-утилита командной строки (см. Ресурсы). Введите в командной строке curl http://localhost:9090/trip/airport/list, чтобы симулировать запрос браузера к списку аэропортов. Вы должны увидеть на экране ответ в формате HTML.

Теперь сделаем два маленьких изменения запроса. На этот раз сделаем не GET-, а HEAD-запрос. HEAD – это стандартный HTTP-метод, возвращающий только метаданные, но не тело ответа (он включен в спецификацию HTTP как раз для такой отладки, которой мы сейчас занимаемся). Кроме этого, задайте cURL режим verbose, чтобы видеть метаданные, как показано в листинге 41.

Листинг 41. Использование cURL для HTTP-отладки
$ curl --request HEAD --verbose http://localhost:9090/trip/airport/list
* About to connect() to localhost port 9090 (#0)
*   Trying ::1... connected
* Connected to localhost (::1) port 9090 (#0)
> HEAD /trip/airport/list HTTP/1.1
> User-Agent: curl/7.16.3 (powerpc-apple-darwin9.0) 
        libcurl/7.16.3 OpenSSL/0.9.7l zlib/1.2.3
> Host: localhost:9090
> Accept: */*
> 
< HTTP/1.1 200 OK
< Content-Language: en-US
< Content-Type: text/html; charset=utf-8
< Content-Length: 0
< Server: Jetty(6.1.4)
< 
* Connection #0 to host localhost left intact
* Closing connection #0

Обратите внимание на заголовок Accept в запросе. Когда клиент отправляет */*, он, в сущности, говорит: «Мне все равно, какой формат вы возвратите. Я все приму».

cURL позволяет переопределить это значение с помощью параметра --header. Введите curl --request HEAD --verbose --header Accept:text/xml http://local­host:9090/­trip/­airport/list, и вы убедитесь что теперь заголовок Accept запрашивает text/xml. Это MIME-тип ресурса.

Как же Grails реагирует на заголовок Accept на стороне сервера? Добавьте еще одно замыкание в AirportController, как показано в листинге 42:

Листинг 42. Действие debugAccept.
def debugAccept = 
{
  def clientRequest = request.getHeader("accept")
  def serverResponse = request.format
  render "Client: ${clientRequest}\nServer: ${serverResponse}\n"    
}

Первая строка листинга 4 выбирает из запроса заголовок Accept. Вторая строка показывает, как Grails интерпретирует запрос, и какой он отправит ответ.

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

Листинг 43. Изменение заголовка Accept в cURL.
$ curl  http://localhost:9090/trip/airport/debugAccept
Client: */*
Server: all

$ curl  --header Accept:text/xml http://localhost:9090/trip/airport/debugAccept
Client: text/xml
Server: xml

Откуда взялись значения all и xml? Посмотрите на grails-app/conf/Config.groovy. В начале файла вы увидите hashmap, использующий простые имена в качестве ключей (наподобие all и xml) и MIME-типы в качестве значений (см. листинг 44).

Листинг 44. Hashmap grails.mime.types в Config.groovy.
grails.mime.types = [ html: ['text/html','application/xhtml+xml'],
                      xml: ['text/xml', 'application/xml'],
                      text: 'text-plain',
                      js: 'text/javascript',
                      rss: 'application/rss+xml',
                      atom: 'application/atom+xml',
                      css: 'text/css',
                      csv: 'text/csv',
                      all: '*/*',
                      json: ['application/json','text/json'],
                      form: 'application/x-www-form-urlencoded',
                      multipartForm: 'multipart/form-data'
                    ]
ПРИМЕЧАНИЕ

Расширенное согласование содержания

Заголовок Accept типичного Web-браузера несколько сложнее упрощенного, использовавшегося нами в cURL. Например, заголовок Accept браузера Firefox 3.0.1 для Mac OS X 10.5.4 выглядит так:

text/html,application

/xhtml+xml,application/xml;

q=0.9,*/*;q=0.8

Это разделенный запятыми список с дополнительными атрибутами q, означающими предпочтение определенных типов MIME (значение q – сокращение от quail­ty – float-значение в диапазоне от 0,0 до 1,0). Поскольку application/xml имеет значение q, равное 0,9, Firefox предпочитает XML-данные всему остальному.

Вот заголовок Accept браузера Safari версии 3.1.2 для Mac OS X 10.5.4:

text/xml,application/xml,

application/xhtml+xml,

text/html;q=0.9,text/

plain;q=0.8,image/png,

*/*;q=0.5

MIME-тип text/html имеет значение q, равное 0,9, поэтому HTML является предпочтительным типом, за ним идет text/plain со значением 0.8 и */* со значением 0,5.

Подробнее см. Ресурсы.

Итак, теперь, когда вы знаете немного больше о согласовании содержания, вы можете добавить блок withFormat в list действие, чтобы возвращать надлежащий тип данных, основываясь на заголовке запроса Accept, как показано в листинге 45.

Листинг 45. Использование блока withFormat в действии.
def list = 
{
  if(!params.max) params.max = 10
  def list = Airport.list(params)
  withFormat
  {
    html
    {
      return [airportList:list]
    }
    xml
    {
      render list as XML
    }
  }
}

Последняя строка каждого блока формата должна содержать render, return или redirect – как и в обычном действии. Если заголовок Accept содержит */*, используется первое вхождение блока.

Настройка заголовка Accept в cURL – это хорошо, но можно также выполнить некоторое тестирование, настраивая URI. И http://localhost:8080/trip/airport/list.xml, и http://localhost:8080/trip/airport/list?format=xml представляют собой методы явно переопределить заголовок Accept. Поиграйте с cURL и различными URI чтобы проверить, что ваш блок withFormat работает как ожидалось.

Если вы хотите сделать такое поведение стандартным для Grails, не забудьте, что можно использовать grails install-templates и редактировать файлы в /src/templates.

Теперь, когда все строительные блоки на месте, последний кирпичик – это перейти от GETful-интерфейса к истинно RESTful-интерфейсу.

Реализация RESTful Web-сервиса в Grails

Для начала нужно обеспечить, что контроллер откликается на четыре базовых HTTP-метода. Вспомним, что замыкание index закрытие является точкой входа в контроллер, если пользователем не определены пользовательские действия, например, list или show. По умолчанию index выполняет переадресацию на действие list: def index = { redirect(action:list,params:params) } Замените этот код на код из листинга 46:

Листинг 46. Переключение на HTTP-метод.
def index = 
{ 
  switch(request.method)
  {
    case "POST":
      render "Create\n"
      break
    case "GET":
      render "Retrieve\n"
      break
    case "PUT":
      render "Update\n"
      break
    case "DELETE":
      render "Delete\n"
      break
  }   
}

Используйте cURL, как показано в листинге 57, чтобы убедиться, что выражение switch работает корректно:

Листинг 47. Использование cURL для всех четырех HTTP-методов.
$ curl --request POST http://localhost:9090/trip/airport
Create
$ curl --request GET http://localhost:9090/trip/airport
Retrieve
$ curl --request PUT http://localhost:9090/trip/airport
Update
$ curl --request DELETE http://localhost:9090/trip/airport
Delete

Реализация GET

Поскольку вы уже знаете, как возвращать XML, реализовать метод GET должно быть несложно. Есть, правда, одно «но». GET-запрос к http://localhost:9090/trip/airport должен возвратить список аэропортов. GET-запрос к http://localhost:9090/trip/airport/den должен возвращать экземпляр аэропорта с IATA-кодом den. Чтобы это сделать, вам нужно создать собственное отображение URL.

Откройте grails-app/conf/UrlMappings.groovy в текстовом редакторе. Отображение по умолчанию, /$cont­roller/$action?/$id?, должно выглядеть знакомо. URL http://localhost:9090/trip/airport/show/1 отображается на AiportController и действие show, а значение params.id равно 1. Заключительный знак вопроса, идущий после действия и ID, означает, что элемент URL не является обязательным.

Добавьте в блок static mappings строку, отображающую RESTful-запросы на AirportController, как показано в листинге 48. Я пока что жестко запрограммировал контроллер, поскольку в других контроллерах еще не реализована поддержка REST. Позже вы, возможно, замените часть URL, airport, на $controller.

Листинг 48. Создание отображения URL.
class UrlMappings 
{
  static mappings = 
  {
    "/$controller/$action?/$id?"
    {
      constraints { // apply constraints here
    }
  }
  "/rest/airport/$iata?"(controller:"airport",action:"index")
  "500"(view:'/error')
}

Это отображение гарантирует, что все URI, начинающиеся с /rest, перенаправляются в действие index (это избавляет от необходимости согласования содержания). Это также означает, что вы можете проверить наличие params.iata, чтобы определить, следует ли возвращать список или экземпляр.

Измените действие index так, как показано в листинге 49:

Листинг 49. Возвращаем XML из HTTP GET
def index = 
{ 
  switch(request.method)
  {
    case "POST":   //...
    case "GET":
      if(params.iata){render Airport.findByIata(params.iata) as XML}
      else{render Airport.list() as XML}          
      break
    case "PUT":    //...
    case "DELETE": //...
  }      
}

Введите в браузере http://localhost:9090/trip/rest/airport и http://localhost:9090/trip/rest/airport/den, чтобы убедиться, что отображение URL на месте.

ПРИМЕЧАНИЕ

Отображение URL по HTTP-методу

Можно использовать несколько иной подход к настройке RESTful-отображения URL. Можно перенаправить запросы отдельному действию, основанному на HTTP-запросе. Например, вот так можно отобразить GET, PUT, POST и DELETE на соответствующие (уже существующие) Grails-действия:

static mappings =

{

"/airport/$id"(controller:"airport")

{

action = [GET:"show", PUT:"update", DELETE:"delete", POST:"save"]

}

}

Реализация DELETE

Добавление поддержки DELETE не сильно отличается от добавления GET. В данном случае я, впрочем, хочу позволить удалять только отдельные аэропорты по их коду IATA. Если пользователь отправит HTTP DELETE без IATA-кода, я возвращу HTTP-код 400, Bad Request. А если пользователь отправит код IATA, который не получается найти, я возвращу вечно популярный код 404 Not Found. Только при удачном удалении я возвращу стандартный код 200 OK (дополнительную информацию о кодах статуса HTTP см. в разделе Ресурсы).

Добавьте код из листинга 50 в DELETE case в действии index:

Листинг 50. Реакция на HTTP DELETE
def index = 
{ 
  switch(request.method)
  {
    case "POST": //...
    case "GET":  //...
    case "PUT":  //...
    case "DELETE":
      if(params.iata)
      {
        def airport = Airport.findByIata(params.iata)
        if(airport)
        {
          airport.delete()
          render "Successfully Deleted."
        }
        else
        {
          response.status = 404 //Not Found
          render "${params.iata} not found."
        }
      }
      else
      {
        response.status = 400 //Bad Request
        render """DELETE request must include the IATA code
                  Example: /rest/airport/iata
        """
      }
      break
  }
}

Сперва попробуем удалить заведомо существующий аэропорт, как показано в листинге 51:

Листинг 51. Удаляем существующий аэропорт
Deleting a Good Airport</heading>
$ curl --verbose --request DELETE http://localhost:9090/trip/rest/airport/lga
> DELETE /trip/rest/airport/lga HTTP/1.1 
< HTTP/1.1 200 OK
Successfully Deleted.

Теперь попробуем удалить заведомо отсутствующий аэропорт, как показано в листинге 52:

Листинг 52. Пытаемся удалить несуществующий аэропорт
$ curl --verbose --request DELETE http://localhost:9090/trip/rest/airport/foo
> DELETE /trip/rest/airport/foo HTTP/1.1
< HTTP/1.1 404 Not Found
foo not found.

Наконец, попробуем выполнить запрос DELETE без IATA, как показано в листинге 53:

Листинг 53. Пытаемся удалить все аэропорты сразу
$ curl --verbose --request DELETE http://localhost:9090/trip/rest/airport
> DELETE /trip/rest/airport HTTP/1.1
< HTTP/1.1 400 Bad Request
DELETE request must include the IATA code
Example: /rest/airport/iata

Реализация POST

Следующая цель – вставка нового аэропорта. Создайте файл с названием simpleAirport.xml, как показано в листинге 54.

Листинг 54. simpleAirport.xml
<airport>
  <iata>oma</iata>
  <name>Eppley Airfield</name>
  <city>Omaha</city>
  <state>NE</state>
  <country>US</country>
  <lat>41.3019419</lat>
  <lng>-95.8939015</lng>
</airport>

Если XML-представление ресурса плоское (без глубокой вложенности элементов), и каждое элемента соответствует имени поля в классе, Grails может сконструировать новый класс прямо по XML. К корневому элементу XML-документа можно обращаться через params, как показано в листинге 55:

Листинг 55. Реакция на HTTP POST
def index = 
{ 
  switch(request.method)
  {
    case "POST":
      def airport = new Airport(params.airport)
      if(airport.save())
      {
        response.status = 201 // Создано
        render airport as XML
      }
      else
      {
        response.status = 500 //Internal Server Error
        render 
          "Could not create new Airport due to errors:\n ${airport.errors}"
      }
      break
    case "GET":    //...
    case "PUT":    //...
    case "DELETE": //...
  }      
}

XML должен быть плоским, поскольку params.airport является, по сути, HashMap (Grails скрыто выполняет за вас XML-HashMap). Это означает, что на самом деле вы используете конструктор именованных аргументов для Airport:

def airport = new Airport(iata:"oma", city:"Omaha", state:"NE")

Чтобы протестировать новый код, используйте cURL и отправьте с помощью POST файл simpleAirport.xml, как показано в листинге 56:

Листинг 56. Использование cURL для выполнения HTTP POST
$ curl --verbose --request POST --header "Content-Type: text/xml" --data 
      @simpleAirport.xml http://localhost:9090/trip/rest/airport
> POST /trip/rest/airport HTTP/1.1
> Content-Type: text/xml
> Content-Length: 176
> 
< HTTP/1.1 201 Created
< Content-Type: text/xml; charset=utf-8
<?xml version="1.0" encoding="utf-8"?><airport id="14">
  <arrivals>
    <null/>
  </arrivals>
  <city>Omaha</city>
  <country>US</country>
  <departures>
    <null/>
  </departures>
  <iata>oma</iata>
  <lat>41.3019419</lat>
  <lng>-95.8939015</lng>
  <name>Eppley Airfield</name>
  <state>NE</state>
</airport>

В случае более сложного XML нужно выполнять демаршаллинг вручную. Вспомните, например, тот XML-форматы, что вы определили выше? Создайте файл newAirport.xml, показанный в листинге 57.

Листинг 57. newAirport.xml
<airport iata="oma">
  <official-name>Eppley Airfield</official-name>
  <city>Omaha</city>
  <state>NE</state>
  <country>US</country>
  <location latitude="41.3019419" longitude="-95.8939015"/>
</airport>

Теперь в действии index замените строку def airport = new Airport(params.airport) кодом из листинга 58:

Листинг 58. Парсинг сложного XML
def airport = new Airport()
airport.iata = request.XML.@iata
airport.name = request.XML."official-name"
airport.city = request.XML.city
airport.state = request.XML.state
airport.country = request.XML.country
airport.lat = request.XML.location.@latitude
airport.lng = request.XML.location.@longitude

Объект request.XML – это groovy.util.XmlSlurper, который содержит сырой XML. Это на самом деле корневой элемент, так что можно запрашивать дочерние элементы по имени (request.XML.city). Если имя содержит дефис или пространство имен, заключите его в кавычки (request.XML."official-name"). Чтобы получить доступ к атрибутам элемента, используется символ @ (requ­est.XML.location.@latitude). Подробнее об XmlSlur­per см. Ресурсы.

И наконец, проверьте его с помощью cURL:

curl --request POST --header "Content-Type: text/xml" --data @newAirport.xml http://localhost:9090/trip/rest/airport

Реализация PUT

Последний HTTP-метод из тех, что нужно поддерживать – метод PUT. Код поддержки POST и PUT практически идентичен. Единственное различие состоит в том, что вместо построения классов непосредственно по XML, вам нужно запросить у Gorm уже существующие классы. Затем строка airport.properties = params.airport заменяет имеющиеся данные полей новыми данными из XML, как показано в листинге 59:

Листинг 59. Реакция на HTTP PUT
def index = 
{       
  switch(request.method)
  {
    case "POST":  //... 
    case "GET":   //...
    case "PUT":   
      def airport = Airport.findByIata(params.airport.iata)
      airport.properties = params.airport
      if(airport.save())
      {
        response.status = 200 // OK
        render airport as XML
      }
      else
      {
        response.status = 500 //Internal Server Error
        render 
          "Could not create new Airport due to errors:\n ${airport.errors}"
      }
      break
    case "DELETE": //...
  } 
}

Создайте файл editAirport.xml, показанный в листинге 60:

Листинг 60. editAirport.xml
<airport>
  <iata>oma</iata>
  <name>xxxEppley Airfield</name>
  <city>Omaha</city>
  <state>NE</state>
  <country>US</country>
  <lat>41.3019419</lat>
  <lng>-95.8939015</lng>
</airport>

Протестируйте результат с помощью cURL:

curl --verbose --request PUT --header "Content-Type: text/xml" 
  --data @editAirport.xml http://localhost:9090/trip/rest/airport. 

Заключение

Я охватил довольно много за короткий промежуток времени. You should now understand the difference between an SOA and an ROA. Теперь вы должны понимать разницу между SOA и ROA. Вы должны также знать, что не все RESTful Web-сервисы одинаковы. Некоторые из них являются GETful-сервисам – они используют http-запрос GET для RPC-подобных вызовов методов. Другие действительно ресурсно-ориентированы, и у них URI является ключом для доступа к ресурсу, а стандартные HTTP-методы GET POST PUT и DELETE обеспечивают полные CRUD-возможности. Какой подход вы ни выберете – GETful или RESTful, Grails предлагает надежную поддержку как для вывода XML, так и для запихивания его обратно.

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

Ресурсы
  1. Mastering Grails: другие статьи данной серии на англ. яз.: http://www.ibm.com/developerworks/views/java/libra­ry­view.jsp?search_by=mastering+grails.
  2. Web-сайт Grails: http://grails.codehaus.org/.
  3. Документация фреймворка Grails:
    http://gra­ils.org/doc/1.0.x/
  4. Groovy SQL – обучающий материал по работе с SQL с использованием Groovy: http://groovy.co­deha­us.org/Tutorial+6+-+Groovy+SQL
  5. GroovyMarkup: http://groovy.codehaus.org/GroovyMarkup.
  6. XmlParser – чтение XML с помощью Groovy Xml­Parser: http://groovy.codehaus.org/Rea­ding+XML+ us­ing+Groovy%27s+XmlParser
  7. GORM – Mapping DSL – отображение доменных классов Grails на имеющиеся схемы с помощью DSL: http://grails.org/GORM+-+Mapping+DSL
  8. Файлы отображения в Hibernate и EJB3-аннотации в Hibernate: http://www.hibernate.org/hib_docs/v3/referen­ce/en/html/tutorial.html#tutorial-firstapp-mapping и http://www.­hi­ber­nate.org/hib_docs/annotations/referen­ce/en/html/en­ti­ty.html.
  9. Practically Groovy: серия статей на страницах developerWorks, в которой исследуется практическое использование Groovy и рассматривается, когда и как его следует применять – http://www.ibm.com/de­veloper­works/ru/views/java/libra­ry­view.jsp?se­arch_by= practically+groovy:
  10. Groovy: Web-сайт проекта Groovy с дополнительной информацией – http://groovy.codehaus.org/
  11. AboutGroovy.com (EN): Web-сайт с последними новостями и ссылками на статьи о Groovy – http://gro­o­vy.codehaus.org/
  12. Groovy Recipes: Greasing the Wheels of Java (Pragmatic Bookshelf, March 2008): книга Скотта Дэвиса по Groovy и Grails – http://www.prag­prog.com/tit­les/sdgrvr
  13. "Build software with Gant" (Andrew Glover, developerWorks, May 2008) – http://www.ibm.com/developer­works/edu/j-dw-java-gant-i.html
  14. Архитектурный Architectural Styles and the Design of Network-based Software Architectures: (Roy Thomas Fielding, University of California at Irvine, 2000): докторская диссертация Филдинга, описывающая REST.
  15. Representational State Transfer – статья по REST в Википедии: http://en.wikipedia.org/wiki/Represen­tatio­nal_St­ate_Transfer.
  16. Google Data API: - http://code.google.com/apis/gdata/
  17. RESTful Web-Сервисы Services (Leonard Richardson and Sam Ruby, O'Reilly Media, 2007): Автор книги защищает использование PUT для INSERTs – http://orei­lly.com/catalog/9780596529260/
  18. Creating XML using Groovy's MarkupBuilder: пример создания XML c помощью Groovy MarkupBuilder – http://gro­­ovy.codehaus.org/Creating+XML+using+Gro­o­vy%27s+Mar­kupBuilder
  19. Пример чтения XML c помощью Groovy' XmlSlurper – http://groovy.codehaus.org/Reading+XML+using+Groovy%27s+XmlSlurper
  20. Создание REST-сервисов (Write REST services, J. Jeffrey Hanson, developerWorks, October 2007): создание REST-сервисов с использованием Java и Atom Publishing Protocol – http://www.ibm.com/deve­loper­works/edu/x-dw-x-restatompp.html
  21. Создание RESTful Web-сервисов (Build a RESTful Web service, Andrew Glover, developerWorks, July 2008): обучающий материал по REST и фреймворку Restlet – http://www.ibm.com/developerworks/edu/j-dw-java-rest-i.html
  22. Пересекая границы: REST on Rails ("Crossing borders: REST on Rails", Bruce Tate, developerWorks, August 2006): поддержка RESTful Web-сервисов в Ruby on Rails – http://www.ibm.com/develop­per­works/ja­va/lib­rary/j-cb08016/
  23. Код примеров к статье – http://www.ibm.com/de­ve­lo­perwo­rks/apps/download/in­dex.jsp?con­tent­id=320496&­file­name=j-grails07158.zip&method=http&locale=world­wide. Код содержит Grails-приложение, Groovy-скрипты и файл USGS Airport на GML.



Copyright © 1994-2016 ООО "К-Пресс"