! ?

Работа с Grails

Автор: Скотт Дэвис
Опубликовано: 09.07.2010
Версия текста: 1.1

Возможности Groovy
Установка Grails
Web-сервер и база данных в комплекте
Создание первого Grails-приложения
Создание контроллера и видов
Работающее приложение
Заключение
Определение ORM
Создание отношения один-ко-многим
Каскадное удаление
Конфликты портов
Голые объекты
Проверки данных
Grails ORM DSL
Понимание DataSource.groovy
Изменение БД
Заключение
Представление Grails-приложения
GSP 101
Теги Grails
Собственные библиотеки тегов
Тестирование TagLib
Сложные пользовательские теги
Частичные шаблоны
Изменение скаффолдинга по умолчанию
Заключение

ПРИМЕЧАНИЕ

Об этой серии статей

Grails - это современная инфраструктура для разработки Web-приложений, который сочетает известные Java-технологии, такие как Spring и Hibernate, с современными приемами, такими как "соглашение по конфигурации". Написанный на Groovy, Grails прозрачно интегрируется с существующим Java-кодом, добавляя гибкость и динамику языков сценариев. После изучения Grails ваши взгляды на разработку Web-приложений изменятся бесповоротно.

1. Cоздание первого Grails-приложения

Знакомство с Grails я начну с другой бесплатной инфраструктуры для разработки Web-приложений: Ruby on Rails. Когда Rails появился, он увлек множество разработчиков. Возможности скаффолдинга, заложенные в Rails, позволяли запустить новый проект за меньшее время, чем раньше. Идея "соглашений по конфигурации" (convention over configuration), лежащая в основе Rails, позволяет приложению "собирать" себя самому, основываясь на разумных схемах именования, а не на громоздких и подверженных ошибкам конфигурационных XML-файлах. Возможности метапрограммирования Ruby позволяют объектам "магически" наследовать методы и поля, которые им требуются во время работы, без загромождения исходного кода.

Инфраструктур Rails заслужил и по-прежнему заслуживает того одобрения, которое он получил, но он ставит Java-разработчиков перед трудными вопросами. Стоит ли отказываться от известной Java-платформы из-за обещаний новой платформы? И что делать с существующим кодом Java, существующими корпоративными серверами и штатом опытных Java-программистов?

Здесь на сцену выходит Grails. Grails предоставляет те же возможности разработки, что и Rails, при этом сохраняя прочную связь с проверенными Java-технологиями. Но Grails – это не просто "еще один клон" Rails, перенесенный на платформу Java. Grails усвоил уроки, полученные в Rails, и соединил их с современными веяниями в Java-программировании. Это скорее развитие Rails, чем просто перенос на новую платформу.

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

Возможности Groovy

Так же, как Rails глубоко привязан к языку программирования Ruby, Grails не смог бы существовать без возможностей Groovy (см. раздел Ресурсы). Groovy – это динамический язык, работающий в JVM и прозрачно интегрирующийся с языком Java. Если вы читали многолетний цикл статей Practically Groovy на Web-сайте developerWorks, то вы уже знакомы с возможностями этого языка. Если же нет, то ничего страшного, в процессе изучения Grails придется узнать очень много и о Groovy. Это не должно быть сложно, так как Groovy был специально спроектирован так, чтобы понравиться Java-разработчикам.

Например, Groovy позволяет существенно сократить количество Java-кода, который обычно приходится писать. Не требуется писать get и set-методы для доступа к полям, так как Groovy предоставляет их автоматически. Не нужно писать конструкции типа for Iterator i = list.iterator() для выполнения цикла по списку элементов; конструкция list.each делает то же самое более лаконично и наглядно. Проще говоря, Groovy – это то, как выглядел бы язык Java, если бы его написали в двадцать первом веке.

Groovy никогда бы не привлек Java-программистов, если бы для того, чтобы воспользоваться его преимуществами, приходилось полностью переписывать приложения. К счастью, Groovy прозрачно интегрируется с существующим кодом. Язык Groovy не заменяет язык Java, а усовершенствует его. Groovy можно быстро изучить за один день, так как Groovy-код – это Java-код. Эти два языка настолько совместимы, что можно переименовать работающий файл .java в файл .groovy (например, файл Person.java переименовать в Person.groovy), и получить абсолютно правильный (и исполняемый) файл Groovy, хотя он и не будет использовать никаких синтаксических преимуществ, предоставляемых Groovy.

Такая глубокая совместимость между языками Groovy и Java означает, что Grails не нужно "изобретать колесо", когда речь идет о ключевых внутренних технологиях. Вместо этого он позволяет взглянуть на знакомые Java-библиотеки с точки зрения Groovy. Сценарии тестирования JUnit типа TestCase заключаются в Groovy-оболочку и представляются как объекты типа GroovyTestCase. Grails представляет новую точку зрения на сценарии сборки Ant посредством GANT – версии Ant, реализованной исключительно на Groovy. Grails помещает Hibernate за "тонкий" Groovy-фасад, называемый GORM (Grails Object Relational Mapping – расширение Grails для объектно-реляционного преобразования). Это только три примера того, как Grails позволяет использовать весь опыт, накопленный в Java, одновременно предоставляя преимущества современных приемов Web-разработки.

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

Установка Grails

Все, что требуется для запуска Grails-приложения, заключено в один ZIP-файл. Все зависимые библиотеки – Groovy, Spring и Hibernate и многие другие – уже установлены на место и готовы к использованию. Для установки Grails необходимо:

  1. Скачать и распаковать архив grails.zip с Web-сайта Grails (см. раздел Ресурсы).
  2. Установить переменную среды GRAILS_HOME.
  3. Добавить путь $GRAILS_HOME/bin в переменную среды PATH.

Конечно, необходимо иметь установленную версию JDK. (Grails хорош, но все-таки не настолько хорош). Grails 1.0 может запускаться на Java 1.4, 1.5 и 1.6. Если неизвестно, какая версия установлена, введите java -version в командной строке. Если необходимо, скачайте и установите версию JDK, совместимую с Grails (см. раздел Ресурсы).

После выполнения действий по установке необходимо ввести в командной строке grails -version для проверки правильности установки. Если выводится следующее приветственное сообщение, значит, все настроено правильно:

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

Web-сервер и база данных в комплекте

ПРИМЕЧАНИЕ

Использование бесплатных продуктов

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

Интересно, что для запуска Grails-приложений не требуется отдельно установленного Web-сервера. Grails поставляется со своим встроенным сервлет-контейнером – Jetty. Достаточно ввести grails run-app, и приложение будет запущено в контейнере Jetty (см. раздел Ресурсы) без необходимости проходить через обычные этапы установки. Запуск Grails-приложения на существующем корпоративном сервере приложений также не представляет проблем. Если ввести команду grails war, будет создан стандартный файл, который можно установить на Tomcat, JBoss, Geronimo, WebSphere или любой другой сервлет-контейнер, совместимый с Java EE 2.4.

Также не требуется отдельно установленной базы данных. Grails поставляется с базой данных HSQLDB (см. раздел Ресурсы), написанной полностью на Java. Наличие базы данных, уже готовой к использованию, дает существенную прибавку к продуктивности. Использовать другую СУБД, например, MySQL, PostgreSQL, Oracle Database или DB2 также несложно благодаря Hibernate и GORM. Если имеется JAR-файл с JDBC-драйвером и настройки для стандартного подключения, то достаточно внести одно изменение в файл DataSource.groovy, чтобы переключить приложение на использование другой базы данных.

Создание первого Grails-приложения

Мне приходится много путешествовать - по меньшей мере 40 поездок в год. Я обнаружил, что календари оказывают мне огромную помощь, говоря мне, когда я должен быть где-то, но при этом не сообщают, где находится это место. У онлайновых карт противоположная проблема: они отлично справляются с вопросом "где", но не с вопросом "когда". Поэтому в этой и паре последующих статей этой серии будет подготовлено специальное Grails-приложение, которое поможет решать вопросы "где" и "когда" при планировании поездки.

ПРИМЕЧАНИЕ

Осторожно, спойлер!

Вам не кажется, что в следующих статьях этой серии будет рассматриваться совместное использование Grails с Google Calendar и Google Maps? Мне кажется....

Для начала необходимо войти в пустой каталог и ввести команду grails create-app trip-planner. После вспышки активности можно будет увидеть каталог trip-planner. Подобно Maven, Rails и AppFuse, Grails создает стандартную структуру каталогов для пользователя. Если вам кажется, что это безнадежно ограничивает вас, а с инфраструктурой невозможно работать, если вы не можете педантично создать собственное дерево каталогов, то, скорее всего, большого удовольствия от работы с Grails вы не получите. Соглашение, первая составляющая принципа "соглашение по конфигурации", позволяет взять любое Grails-приложение и немедленно понять, какие компоненты имеются и где они хранятся.

Далее необходимо перейти в каталог trip-planner и ввести команду grails create-domain-class Trip. Если все пройдет нормально, появятся два файла: grails-app/do­main/Trip.groovy и grails-app/test/integration/Trip­Tests.gro­ovy. Тестирование будет рассматриваться в следующей статье. Пока же сфокусируемся на доменном классе, который начинается, как показано в листинге 1:

Листинг 1. Доменный класс, сгенерированный Grails
class Trip
{

}

Пока смотреть не на что, но мы это исправим, добавив поля в класс Trip, как показано в листинге 2:

Листинг 2. Класс Trip с добавленными полями
class Trip 
{
  String name
  String city
  Date startDate
  Date endDate
  String purpose
  String notes
}

Как уже говорилось, не нужно беспокоиться о создании get() и set()-методов, так как Groovy динамически сгенерирует их. Класс Trip также содержит много новых и полезных динамических методов, названия которых говорят сами за себя:

Все эти и другие методы находятся в полном вашем распоряжении. Отметим, что класс Trip не расширяет родительский класс и не реализует "волшебный" интерфейс. Благодаря возможностям метапрограммирования Groovy эти методы просто появляются в соответствующем месте в соответствующих классах. (Эти методы, связанные с сохранением данных в базу данных, получают только классы в каталоге grails-app/domain).

Создание контроллера и видов

Создание класса для доменной области - это только первый шаг. Каждой модели для полноты картины требуется хороший контроллер и несколько видов. Предполагается, что читатель уже знаком с шаблоном MVC (Model-View-Controller – Модель-Представление-Конт­ро­ллер) (см. раздел Ресурсы). Введите в командной строке: grails generate-all Trip, чтобы создать класс grails-app/controllers/TripController.groovy и соответствующий набор GSP-страниц (Groovy Server Pages - серверные страницы Groovy) в каталоге grails-app/views/Trip. Для каждого действия типа list в контроллере есть соответствующий файл list.gsp. Для действия create есть файл create.gsp. Здесь на практике становятся видны преимущества "соглашения по конфигурации": не требуется никаких XML-файлов для установления соответствия элементов. Каждый класс доменной области имеет пару в виде контроллера с соответствующим именем. При желании эту конфигурацию, основывающуюся на именах, можно обойти, но в большинстве ситуаций достаточно просто следовать соглашению, и приложение сразу заработает.

Рассмотрим файл grails-app/cont­rol­ler/Trip­Cont­rol­ler.gro­ovy, показанный в листинге 3:

Листинг 3. Класс TripController
class TripController 
{
  ...
  def list = 
  {
    if(!params.max) params.max = 10
    [ tripList: Trip.list( params ) ]
  }
  ...
}

Первое, что скорее всего заметят Java-программисты, это то, как много действий выполняет такой компактный код. Например, рассмотрим действие list. Самое важное происходит на последней строке этого метода. Grails возвращает коллекцию типа HashMap, содержащую единственный элемент с именем tripList. (Последняя строка в методе Groovy - это неявное выражение return. При желании можно напечатать слово return явно). Элемент tripList - это объект типа ArrayList, содержащий объекты типа Trip, извлеченные из базы данных методом Trip.list(). Обычно этот метод возвращает все записи из таблицы. Строка, расположенная над этой строкой, говорит: "если кто-нибудь передаст в URL параметр с именем max, необходимо на основании его значения ограничить количество возвращенных объектов Trip. Если такого параметра нет, то ограничить количество объектов Trip десятью". URL-адрес http://local­host:80­80/trip-planner/trip/list вызывает эту функциональность. Например, URL-адрес http://localhost:8080/trip-plan­ner/trip/list?max=3 покажет только три поездки вместо обычных десяти. Если еще имеются поездки, которые можно показать, то Grails автоматически создаст ссылки для перехода на страницы со следующими и предыдущими наборами элементов.

Так где же используется эта коллекция типа HashMap? Рассмотрим файл grails-app/views/list.gsp, показанный в листинге 4:

Листинг 4. Файл list.gsp
<g:each in="${tripList}" status="i" var="trip">
  <tr class="${(i % 2) == 0 ? 'odd' : 'even'}">
    <td>
      <g:link action="show" id="${trip.id}">${trip.id?.encodeAsHTML()}</g:link>
    </td>
  </tr>
</g:each>

Файл list.gsp – это по сути простой HTML документ с небольшим количеством GroovyTagLibs (библиотек Groovy-тегов). Все, перед чем стоит префикс g:, является Groovy-тегом. В листинге 4 тег <g:each> проходит по всем объектам Trip в коллекции tripList типа ArrayList и строит таблицу, соответствующую синтаксису HTML.

Чтобы понять, как работают контроллеры, необходимо разобраться с "тремя R": return (вернуть), redirect (перенаправить) и render (сформировать графическое отображение). Некоторые типы действий используют преимущества неявного выражения return, чтобы возвращать данные на GSP-страницу с таким же именем. Другие действия выполняют перенаправление. Например, действие типа index вызывается, если пользователь не указал тип действия в URL:

def index = { redirect(action:list,params:params) }

В этом случае класс TripController выполняет перенаправление к действию list, передавая при этом все параметры (или объект типа QueryString (строка запроса)) в коллекции params типа HashMap.

Наконец, действие типа save (см. листинг 5) не имеет соответствующей страницы save.gsp. Оно перенаправляет пользователя на страницу действия show, если запись была сохранена в базу данных без ошибок. В противном случае оно отображает страницу create.gsp, где можно увидеть возникшие ошибки и попробовать выполнить действие снова.

Листинг 5. Действие типа save
def save = 
{
  def trip = new Trip(params)
  if(!trip.hasErrors() && trip.save()) 
  {
    flash.message = "Trip ${trip.id} created"
    redirect(action:show,id:trip.id)
  }
  else 
  {
    render(view:'create',model:[trip:trip])
  }
}

Однако вместо того чтобы просто обсуждать, как работает Grails, стоит увидеть его в действии.

Работающее приложение

Введите grails run-app в командной строке. После набора сообщений Log4J, выведенных в консоль, должно быть показано сообщение с таким текстом:

Server running. Browse to http://localhost:8080/trip-planner

Если на порту 8080 уже находится запущенный сервер, в результате сбоя будет выведен дамп ядра с сообщением:

Server failed to start: java.net.BindException: Address already in use

Есть два способа легко изменить порт, на котором работает Jetty. Можно выполнить это изменение буквально на лету, введя команду grails -Dserver.port=9090 run-app. Чтобы сделать это изменение постоянным, необходимо в файле $GRAILS_HOME/scripts/Init.groovy найти строку, которая начинается с serverPort, и изменить ее значение:

serverPort = System.getProperty('server.port') ? 
             System.getProperty('server.port').toInteger() : 9090

После того как Grails был запущен на выбранном порту, можно ввести URL в Web-браузер. При этом должна появиться страница приветствия со списком всех контроллеров, как показано на рисунке 1:


Рисунок 1. Экран приветствия Grails-приложения.

Если после этого нажать на ссылку TripController, то вам будет представлено полноценное CRUD-при­ло­жение (Create, Read, Update, Delete – создать, считать, обновить, удалить), с которым уже можно работать.

Новые поездки создаются с помощью страницы, изображенной на рисунке 2:


Рисунок 2. Страница Create Trip (создать поездку).

Редактирование поездок производится с помощью страницы, изображенной на рисунке 3:


Рисунок 3. Страница Trip List (список поездок).

Так сколько же времени потребовалось на создание и запуск приложения? И сколько для этого потребовалось строк кода? Узнать об этом можно следующим способом:

  1. Нажать Ctrl-C для выключения Grails.
  2. Ввести grails stats.

На экране будут напечатаны следующие данные:

  +----------------------+-------+-------+
  | Name                 | Files |  LOC  |
  +----------------------+-------+-------+
  | Controllers          |     1 |    66 | 
  | Domain Classes       |     1 |     8 | 
  | Integration Tests    |     1 |     4 | 
  +----------------------+-------+-------+
  | Totals               |     3 |    78 | 
  +----------------------+-------+-------+

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

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

class TripController
{
  def scaffold = Trip
}

Эта единственная строка кода указывает Grails сделать то же самое, что было сделано с предыдущим контроллером, с одним исключением: сгенерировать все действия list, save и edit динамически - прямо в памяти во время выполнения. Три строки кода вместо 66 приводят к абсолютно такому же поведению приложения.

Снова введите grails run-app. Да, все введенные данные пропали, но это не страшно. Выключите Grails комбинацией клавиш Ctrl-C и напечатайте в командной строке grails prod run-app. Приложение будет запущено в рабочем режиме, что означает, что данные будут сохраняться между перезапусками сервера. Снова пройдем через TripController и сохраним несколько записей. В поведении приложения не заметно никакой разницы. Понимание того, что все, что показывается в Web-браузере, обеспечивается 15 строчками кода, дает представление о силе и возможностях Grails.

Заключение

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

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

Пока же рекомендую поупражняться с Groovy и Grails и уверяю, что ваши взгляды на Web-разработку радикально изменятся.

2. GORM: забавное название, серьезная технология

Эта часть посвящена другой области применения Grails: персистентности с использованием Grails Object Relational Mapping (GORM) API. Я начну с описания того, что такое объектно-реляционное отображение в Grails и с создания отношения один-ко-многим. Затем вы узнаете о проверках данных, гарантирующих, что ваше приложение не будет страдать синдромом «мусор на входе/мусор на выходе». Вы увидите язык предметной области GORM в действии, и он позволит вам настроить сохранение ваших POGO (plain old Groovy objects). Наконец, вы увидите, как просто переключиться с одной реляционной СУБД на другую. Подойдет любая СУБД, для которой имеется JDBC-драйвер и диалект Hibernate.

Определение ORM

Реляционные СУБД возникли в конце 70-х, но разработчики ПО по сей день бьются над эффективным извлечением и записью данных. Сегодняшнее ПО основано не на реляционных принципах, используемых в популярных СУБД, а на объектно-ориентированных.

Целый класс программ – ORM – служит для упрощения перемещения данных между БД и кодом. Hibernate, TopLink и Java Persistence API (JPA) – три популярные Java API, служащие этой цели (см. Ресурсы), хотя совершенным не является ни один из них. Проблема настолько персистентна (каламбур случайный), что имеет собственное крылатое выражение «объектно-реляционная несовместимость» (object-relational impedance mismatch, см. Ресурсы).

GORM – это тонкий Groovy-фасад над Hibernate. Это значит, что все ваши Hibernate-приемы будут работать – например, поддерживаются HBM файлы отображения и аннотации – но в этой статье речь пойдет об интересных возможностях самого GORM.

Создание отношения один-ко-многим

Легко недооценить сложности сохранения POGO в поля БД. На самом деле, если POGO отображается на одну таблицу, все довольно просто – свойства POGO точно соответствуют колонкам таблицы. Но если чуть-чуть усложнить объектную модель, например, ввести два POGO, ссылающихся друг на друга, все сразу станет намного сложнее.

Посмотрите, например, на Web-сайт Trip Planner, который мы начали в прошлой статье. Неудивительно, что POGO Trip играет в приложении важную роль. Откройте в текстовом редакторе grails-app/domain/Trip.groovy (листинг 1):

Листинг 1. Класс Trip
class Trip 
{ 
  String name
  String city
  Date startDate
  Date endDate
  String purpose
  String notes
}

Каждый из атрибутов в Листинге 1 легко и просто отображается на соответствующее поле таблицы Trip. В прошлой статье говорилось, что все POGO, которые хранятся в каталоге grails-app/domain, получают дополнительную таблицу, которая автоматически создается при запуске Grails. По умолчанию Grails использует встроенную БД HSQLDB, но к концу этой статьи вы сможете использовать любую реляционную СУБД по вашему выбору.

Поездка часто включает перелет, поэтому есть смысл создать класс Airline (показанный в листинге 2):

Листинг 2. Класс Airline.
class Airline
{ 
  String name
  String url
  String frequentFlyer
  String notes
}

Теперь вы захотите связать эти два класса. Чтобы запланировать перелет в Чикаго рейсом Xyz Airlines, вы представляете его в Groovy так же, как в Java-коде – добавляя в класс Trip свойство Airline (см. листинг 3). Эта техника называется композицией объектов (см. Ресурсы).

Листинг 3. Добавление свойства Airline в класс Trip.
class Trip 
{ 
  String name
  String city
  ...
  Airline airline
}

Все это здорово в программной модели, но в реляционных БД используется несколько иной подход. Каждая запись в таблице обладает уникальным ID, или первичным ключом. Добавление поля airline_id в таблицу Trip позволит связать одну запись с другой (в данном случае запись "Xyz Airlines" с записью "Chicago trip"). Это называется отношением один-ко-многим: с одной авиалинией может быть ассоциировано множество поездок (примеры отношений один-ко-многим и многие-ко-многим можено найти в онлайн-документации Grails, см. Ресурсы).

У предложенной схемы БД есть всего одна проблема. Вы можете иметь успешно нормализованную БД (см. Ресурсы), но теперь колонки таблицы рассинхронизированы с программной моделью. Если заменить поле Airline полем AirlineId, то вы допустите «просачивание» деталей реализации (факта сохранения POGO в БД) в объектную модель. Джоэль Спольски называет это Законом дырявых абстракций (см. http://rus­si­an.jo­el­on­software.com/Articles/LeakyAbstractions.html).

GORM помогает смягчить проблему дырявых абстракций, позволяя представить объектную модель способом, имеющим смысл для Groovy. Он скрыто занимается вопросами реляционной БД за вас, но, как вы сейчас увидите, можно легко переопределить настройки по умолчанию. GORM – это не непрозрачный слой, скрывающий от вас детали БД, а полупрозрачный – он пытается делать все правильно сам, но не встает у вас на пути, если вы захотите изменить его поведение. Это позволяет вам получить лучшее из двух миров.

Вы уже добавили свойство Airline в POGO Trip. Чтобы завершить отношение один-ко-многим, добавьте в POGO Airline настройку hasMany, как показано в листинге 4:

Листинг 4. Создание отношения один-ко-многим в Airline
class Airline 
{ 
  static hasMany = [trip:Trip]

  String name
  String url
  String frequentFlyer
  String notes
}

Каскадное удаление

В имеющейся модели вы можете получить «висячие» записи в БД: при удалении Airline останутся записи Trip, ссылающиеся на несуществующих родителей. Чтобы исключить это, можно добавить коллекцию типа hash­map static belongsTo в класс, описывающий сторону «многих».

Статическая настройка hasMany – это Groovy коллекция типа hashmap: ключ – это trip; значение – класс Trip. Если вы хотите создать еще одно отношение один-ко-многим для класса Airline, можно написать разделенный запятыми список пар ключ/значение в квадратных скобках.

Теперь создадим маленький класс AirlineController (см. Листинг 5) в grails-app/controllers, чтобы увидеть новое отношение один-ко-многим в действии:

Листинг 5. Класс AirlineController
class AirlineController 
{ 
  def scaffold = Airline
}

Вспомним из прошлой статьи, что def scaffold говорит Grails динамически во время исполнения создать базовые методы list(), save()и edit(). Он также говорит Grails динамически создать представления GroovyServer Page (GSP). И TripController, и AirlineController содержат def scaffold. Если после ввода grails generate-all у вас сохранились каталоги trip или airline в grails-app/views, их нужно удалить. В этом примере мы проверяем, что Grails создает контроллеры и представления динамически.

Теперь, когда доменные классы и контроллеры на месте, запустите Grails. Введите grails prod run-app, чтобы запустить приложение в рабочем режиме. Если все в порядке, вы увидите следующее сообщение:

Server running. Browse to http://localhost:8080/trip-planner

Конфликты портов

Если на порту 8080 уже работает другое приложение, измените порт так, как было описано в предыдущей статье. Я запускаю экземпляры Grails на порту 9090, чтобы исключить эту проблему сразу.

В браузере вы увидите ссылки на AirlineController и TripController. Щелкните по AirlineController и заполните форму для Xyz Airlines, как показано на рисунке 1.


Рисунок 1. Отношение один-ко-многим со стороны «одного».

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

Теперь создадим новую поездку, как показано на рисунке 2. Обратите внимание на выпадающий список Air­line. В нем будут содержаться все записи из таблицы Airline. Не беспокойтесь о «протекании» первичного ключа – в следующем разделе будет сказано, как добавить более осмысленные надписи.


Рисунок 2. Отношение один-ко-многим со стороны «многих».

Голые объекты

Вы уже увидели, как добавление хинта в Airline POGO (статический hasMany) влияет как на способ скрытого создания таблицы и на представление, генерируемое на пользовательской стороне. В Grails широко используется паттерн naked objects для декорирования доменных объектов (см. Ресурсы). Вводя такую информацию непосредственно в POGO, вы исключаете потребность во внешних конфигурационных XML-файлах. То, что вся информация хранится в одном месте – большое благо для производительности.

Например, освободиться от «протекающего» первичного ключа, показываемого в выпадающем списке, не сложнее, чем добавить метод toString в класс Airline, как показано в листинге 6.

Листинг 6. Добавление метода toString в Airline
class Airline 
{ 
  static hasMany = [trip:Trip]

  String name
  String url
  String frequentFlyer
  String notes
    
  String toString()
  {
    return name
  }
}

С этого момента в выпадающем списке отображается название авиалинии. Но тут есть весьма интересный момент: если Grails запущен, все, что нужно, чтобы изменения начали работать – сохранить Airline.groovy. Создайте новый Trip в браузере, чтобы увидеть, как это работает. Поскольку представления генерируются динамически, можно быстро переключаться между браузером и редактором, пока не будет создано желаемое представление – перезагрузка сервера не потребуется.

Теперь исправим порядок полей, сейчас расположенных в алфавитном порядке. Для этого нужно добавить в POGO еще одну конфигурацию: блок static constraints. Добавьте в этот блок поля в порядке, показанном в листинге 7 (эти ограничения влияют только на представление, а не на порядок колонок в таблице).

Листинг 7. Изменение порядка полей в Airline
class Airline 
{ 
  static constraints = 
  {
    name()
    url()
    frequentFlyer()
    notes()  
  }

  static hasMany = [trip:Trip]
    
  String name
  String url
  String frequentFlyer
  String notes
  
  String toString(){
    return name
  }
}

Сохраните изменения в файле Airline.groovy и создайте новую авиалинию в браузере. Теперь поля должны появиться в порядке, указанном в листинге 7, как показано на рисунке 3.


Рисунок 3. Измененный порядок полей.

Прежде чем извиниться за то, что я заставил вас дважды писать названия полей в POGO, позвольте объяснить, что есть хорошая причина вынести их в отдельный блок. Эти скобки в static constraints в листинге 7 недолго останутся пустыми.

Проверки данных

Кроме указания порядка следования полей, блок static constraints позволяет также разместить некие правила проверки. Например, можно ввести ограничения по длине для String-полей (по умолчанию – 255 символов). Можно обеспечить соответствие строковых значений неким образцам (например, для е-mail- или Web-адреса). Можно даже сделать поля обязательными или необязательными. Полный список правил проверки можно найти в онлайн-документации Grails.

В листинге 8 показан класс Airline с правилами проверки в блоке ограничений.

Листинг 8. Проверка данных в Airline
class Airline 
{ 
  static constraints = 
  {
    name(blank:false, maxSize:100)
    url(url:true)
    frequentFlyer(blank:true)
    notes(maxSize:1500)  
  }

  static hasMany = [trip:Trip]
    
  String name
  String url
  String frequentFlyer
  String notes

  String toString(){
    return name
  }
}

Сохраните изменения в файле Airline.groovy и и создайте новую авиалинию в браузере. При нарушении правил проверки вы получите предупреждение, показанное на рисунке 4.

Сообщения об ошибках можно изменить, они хранятся в файле messages.properties в каталоге grails-app/i18n. Заметьте, что сообщения по умолчанию написаны на нескольких языках.

Большинство ограничений в листинге 8 влияют только на слой представления, но пара влияет также и на слой персистентности. Например, колонка name в БД сейчас имеет ширину в 100 символов. Поле notes, кроме того, что стало не полем ввода, а текстовой областью (это делается для полей длиннее 255 символов) в представлении, из колонки VARCHAR становится колонкой TEXT, CLOB или BLOB.Все это зависит от используемой СУБД и диалекта Hibernate, который – теперь это, возможно, уже не вызовет удивления – можно переопределить.


Grails ORM DSL

Переопределить настройки Hibernate по умолчанию можно, используя любой из обычных методов конфигурирования: файлы отображения HBM или аннотации. Но Grails предлагает третий путь, следующий стилю naked objects. Просто добавьте в POGO блок static mapping , чтобы переопределить имена таблиц и полей, используемые по умолчанию, как показано в листинге 9.

Листинг 9. Использование GORM DSL
class Airline 
{ 
  static mapping = 
  {
    table 'some_other_table_name'
    columns {
      name column:'airline_name'
      url column:'link'
      frequentFlyer column:'ff_id'
    }
  }

  static constraints = 
  {
    name(blank:false, maxSize:100)
    url(url:true)
    frequentFlyer(blank:true)
    notes(maxSize:1500)  
  }

  static hasMany = [trip:Trip]
    
  String name
  String url
  String frequentFlyer
  String notes

  String toString()
  {
    return name
  }
}

Блок отображения особенно полезен, если у вас есть старые таблицы, которые вы собираетесь использовать в новом Grails-приложении. Здесь я касаюсь только основных возможностей, но ORM DSL позволяет сделать намного больше, чем простое отображение имен таблиц и полей. Вы можете переопределить типы данных для каждой колонки. Вы можете изменить стратегию создания первичного ключа или даже определить композитный первичный ключ. Вы можете изменить настройки кэширования Hibernate, настроить поля, используемые для ассоциирования с внешним ключом и так далее.

Важно помнить, что все эти настройки сосредоточены в одном месте: POGO.

Понимание DataSource.groovy

Все, что мы делали до сих пор, вертелось вокруг изменения индивидуальных классов. Теперь отступим на шаг и выполним некие глобальные изменения. Конфигурация БД, используемая всеми доменными классами, хранятся в файле grails-app/conf/DataSource.groovy, показанном в листинге 10. Откройте этот файл в текстовом редакторе:

Листинг 10. DataSource.groovy
dataSource 
{
  pooled = false
  driverClassName = "org.hsqldb.jdbcDriver"
  username = "sa"
  password = ""
}
hibernate 
{
  cache.use_second_level_cache=true
  cache.use_query_cache=true
  cache.provider_class='org.hibernate.cache.EhCacheProvider'
}
// Настройки окружения
environments 
{
  development 
  {
    dataSource 
    {
      dbCreate = "create-drop" // one of 'create', 'create-drop','update'
      url = "jdbc:hsqldb:mem:devDB"
    }
  }
  test 
  {
    dataSource 
    {
      dbCreate = "update"
      url = "jdbc:hsqldb:mem:testDb"
    }
  }
  production 
  {
    dataSource 
    {
      dbCreate = "update"
      url = "jdbc:hsqldb:file:prodDb;shutdown=true"
    }
  }
}

Блок dataSource содержит и позволяет изменить driverClassName, username и password, используемые для подключения к БД. Блок hibernate позволяет изменять настройки кэширования (не трогайте их, если вы не эксперт в Hibernate). Но по-настоящему интересные вещи находятся в блоке environments.

Вспомните, что Grails может работать в трех режимах: разработки, тестирования и рабочем. Команда grails prod run-app приказывает Grails использовать настройки БД из блока production. Если для каждого из режимов вы хотите использовать свои значения username и password, просто скопируйте эти настройки из блока dataSource в блоки environment и измените значения. Настройки из блока environment отменяют настройки из блока dataSource.

Настройка url – это строка JDBC-подключения. Заметьте, что в рабочем режиме HSQLDB использует файловое хранилище данных. В режимах development и test HSQLDB хранит данные в памяти. В предыдущей статье я говорил, что если вы хотите сохранить записи Trip при перезапуске сервера, нужно использовать режим production. Теперь вы знаете, как добиться этого в режимах development и test – просто скопируйте настройки url из production. Конечно, проблему исчезающих записей можно решить и заставив Grails работать с DB2, MySQL или другой традиционной СУБД (несколько позже вы увидите настройки для работы с DB2 и MySQL).

Значение dbCreate – еще одно из вызывающих разное поведение в разных окружениях. Это псевдоним для нижележащей настройки hibernate.hbm2ddl.auto, которая говорит, как Hibernate скрыто управляет таблицами. Установив dbCreate в create-drop , вы говорите Hibernate создавать таблицы при старте и удалять их при закрытии приложения. Если изменить это значение на create, Hibernate создаст новые таблицы и изменит существующие при необходимости, но все записи будут удалены при перезапуске. Значение по умолчанию для режима production — update — сохраняет все данные при перезапуске, а также создает или изменяет таблицы при необходимости.

Если вы используете Grails с уже существующей БД, я очень рекомендую закомментировать dbCreate. В этом случае Hibernate не будет касаться схемы БД. Это означает, конечно, что вам придется самому синхронизировать модель данных и нижележащую БД, но это кардинально сократит поток посланий от DBA, требующих ответа, кто изменяет таблицы БД без разрешения.

Добавить собственное окружение также несложно. Например, у вашей компании может быть бета-программа. Просто создайте в DataSource.groovy блок beta рядом с остальными (блок environments можно добавить также в grails-app/conf/Config.groovy для настроек, не связанных с БД). Чтобы запустить Grails в режиме beta, введите type grails -Dgrails.env=beta run-app.

Изменение БД

Если позволить Hibernate управлять таблицами, используя настройки dbCreate, то указать Grails новую БД можно быстро, в три шага: создать БД и учетную запись, скопировать JDBC-драйвер в каталог lib и изменить настройки в DataSource.groovy.

Инструкции по созданию БД и учетной записи сильно зависят от продукта. Для DB2 есть пошаговая онлайн-инструкция (см. Ресурсы). После создания БД и учетной записи измените DataSource.groovy, чтобы использовать значения, приведенные в листинге 11.

Листинг 11. DB2-настройки для DataSource.groovy
  driverClassName = "com.ibm.db2.jcc.DB2Driver"
  username = "db2admin"
  password = "db2admin"
  url = "jdbc:db2://localhost:50000/trip"

Если у вас установлен MySQL, используйте шаги, показанные в листинге 12, чтобы войти как пользователь root и создать БД trip:

Листинг 12. Создание БД MySQL
$ mysql --user=root
mysql> create database trip;
mysql> use trip;
mysql> grant all on trip.* to grails@localhost identified by 'server';
mysql> flush privileges;
mysql> exit
$ mysql --user=grails -p --database=trip

Теперь, когда учетная запись и БД созданы, исправьте DataSource.groovy:

Листинг 13. Настройки DataSource.groovy для MySQL
  driverClassName = "com.mysql.jdbc.Driver"
  username = "grails"
  password = "server"
  url = "jdbc:mysql://localhost:3306/trip?autoreconnect=true"

Когда БД создана, JAR JDBC-драйвера скопирован в каталог lib, а значения в DataSource.groovy исправлены, введите еще раз grails run-app. Теперь Grails будет использовать БД, отличную от HSQLDB.

Заключение

Это завершает нашу прогулку по GORM. Теперь вы должны хорошо представлять себе, что такое ORM, как управлять проверками и отношениями таблиц, и как заменить HSQLDB на нужную вам БД.

Следующая часть серии будет посвящена Web-слою. Вы подробнее узнаете о GSP и различных TagLibs Groovy. Вы увидите, как разбить GSP на partials, то есть частичные шаблоныфрагменты разметки, которые можно использовать многократно на разных страницах. Наконец, вы узнаете, как изменить исходные шаблоны, используемые в представлениях.

3. Изменяем вид Groovy Server Pages

Первые две части этой серии познакомили вас с основными строительными блоками Web-фреймворка Grails. Я рассказал – с повторами – что Grails основан на архитектурном паттерне Модель-Представление-Контроллер (Model-View-Controller, MVC), и что Grails использует соглашения по конфигурации для связывания воедино частей фреймворка. В Grails используются интуитивно понятные имена файлов и каталогов вместо старого, более подверженного ошибкам, метода ручного перечисления таких связей во внешних конфигурационных файлах. Например, в первой статье вы видели, что контроллеры имеют суффикс Controller и хранятся в каталоге grails-app/controller. Во второй статье вы узнали, что модели предметной области можно найти в каталоге grails-app/domain.

В этой части серии я завершу MVC-триптих рассказом о представлениях в Grails. Представления (как вы можете предположить) хранятся в каталоге grails-app/views. Но в истории о представлениях кроется куда больше, чем интуитивно очевидное имя каталога. Я расскажу о Groovy Server Pages (GSP) и дам вам указания на много альтернативных возможностей представления. Вы узнаете о стандартных библиотеках тегов Grails (TagLibs) и выясните, насколько просто создать собственную библиотеку тегов. Наконец, научу настраивать стандартные шаблоны для генерируемых представлений, тем самым балансируя между удобством автоматически создаваемых представлений и желанием отойти от предоставляемого по умолчанию облика Grails-приложений.

Представление Grails-приложения

Для слоя представления в Grails используется GSP. Groovy в Groovy Server Pages идентифицирует не только нижележащую технологию, но также и язык, который вы можете использовать, если захотите быстро написать скриптлет-другой. В этом смысле все очень похоже на технологию Java Server Pages (JSP), позволяющую подмешать немного Java-кода в Web-страницу, и на RHTML (основную технологию представления в Ruby on Rails), где можно втиснуть немного Ruby-кода между HTML-тегов.

Конечно, скриптлеты долго осуждались Java-со­обществом. Они ведут к самой низкой форме многократного использования – copy-and-paste – и прочим проявлениям морально-технологической низости («потому что вы можете» и «потому что вы должны» - это совсем разные вещи). Буква G в GSP должна напоминать добропорядочным Java-гражданам о языке реализации, и ни о чем больше. Groovy TagLibs и частичные шаблоны дают более передовой способ совместного использования кода и поведения Web-страницами.

GSP – это основа взгляда Grails на мир MVC, в центре которого лежат страницы. Страница – это главная единица измерения. Страница List предлагает ссылку на страницу Show. Страница Show позволяет перейти на страницу Edit, и так далее. Неважно, кто вы – умудренный Struts-разработчик или юный Ruby-энтузиаст, вы в любом случае знакомы с этим типом жизненного цикла Web.

Я говорю об этом потому, что в последние годы произошел настоящий кембрийский взрыв технологий представления, не основанных на страницах (см. Ресурсы). Умами завладевают такие компонентно-ориенти­ро­ванные Web-фреймворки, как JavaServer Faces (JSF) и Tapestry. Революция Ajax породила множество таких JavaScript-решений, как библиотеки Dojo и Yahoo! UI (YUI). Такие платформы Rich Internet Application (RIA), как Adobe Flash и Google Web Toolkit (GWT) обещают удобство Web-разработки и более богатый, свойственный настольным приложениям, интерфейс пользователя. К счастью, Grails может легко справиться со всеми этими технологиями представления.

Весь смысл разделения аспектов в MVC состоит в том, что оно позволяет легко выбрать для Web-приложения любое представление. Инфраструктура плагинов в Grails позволяет выбрать одну из многих альтернатив GSP, находящихся на расстоянии grails install-plugin. Многие из этих плагинов разработаны сообществом разработчиков для людей, желающих использовать Grails с их любимой технологией слоя представления.

Хотя у Grails нет родных, автоматических привязок к JSP, ничто не мешает использовать их вместе. Grails-приложение – это стандартное приложение Java EE, так что можно поместить в каталог lib нужный JAR, добавить нужные настройки в конфигурационный файл WEB-INF/web.xml, и написать приложение, как обычно. Grails-приложение развертывается в стандартном контейнере сервлетов, так что Grails поддерживает JSP не хуже, чем GSP. Существуют Grails-плагины для Echo2 и Wicket (компонентно-ориентированных Web-фреймворков), и ничто не мешает создать плагины для JSF или Tapestry.

Аналогично, шаги для подключения Ajax-фреймворков, таких, как Dojo или YUI, к Grails не отличаются от того, что вы делаете обычно: просто скопируйте их JavaScript-библиотеки в каталог web-app/js. Prototype и Scriptaculous устанавливаются с Grails по умолчанию. RichUI-пла­гин использует наилучший подход, выбирая UI-виджеты из разных Ajax-библиотек.

Если посмотреть на список плагинов, найдется поддержка для таких RIA-клиентов, как Flex, OpenLazlo, GWT и ZK. Очевидно, выбор альтернативных решений для представлений в Grails неограничен. Но давайте поговорим о собственной технологии представления Grails – GSP.

GSP 101

Узнать GSP-страницу можно по нескольким признакам. Расширение .gsp – это страшное предательство, как и частое использование тегов, начинающихся с <g:. Фактически, GSP-страница – это не более чем стандартный HTML с некоторой примесью Grails-тегов для динамичности контента. Некоторые из альтернативных технологий представления, упомянутых в предыдущем разделе, являются непрозрачными слоями абстракции, скрывающими детали HTML, CSS и JavaScript за слоем Java, ActionScript или какого-то другого языка программирования. GSP – это тонкий Groovy-фасад для стандартного HTML, позволяющий легко выйти за пределы фреймворка и использовать Web-технологии напрямую там, где это, по вашему мнению, нужно.

Но вам непросто будет отыскать GSP в приложении Trip Planner в его современном виде. (В первых двух статьях серии мы начали разработку приложения Trip Planner. Если вы не читали этих статей, самое время прерваться и прочитать их). Сейчас вы используете в представлениях динамический скаффолдинг, и каталог trip-planner/grails-app/views пуст. Откройте файл grails-app/controller/TripController.groovy, показанный в листинге 1, в текстовом редакторе, чтобы увидеть команду использования динамического скаффолдинга:

Листинг 1. Класс TripController
class TripController
{
  def scaffold = Trip
}

Строка def scaffold = Trip говорит Grails динамически генерировать GSP во время исполнения. Это хорошо для автоматической синхронизации представлений при изменении модели предметной области, но в процессе изучения фреймворка смотреть здесь не на что.

Введите grails generate-all Trip в корне каталога trip-planner directory. Ответьте y, когда вас спросят, хотите ли вы переопределить существующий контроллер. (Можно ответить a, то есть all, чтобы избежать повторяющихся вопросов). Теперь вы увидите полный класс TripController с замыканиями create, edit, list и show (среди прочих). Вы также увидите каталог grails-app/views/trip с четырьмя GSP: create.gsp, edit.gsp, list.gsp и show.gsp.

Здесь работает соглашение по конфигурации. При открытии http://localhost:9090/trip-planner/trip/list вы просите контроллер заполнить список объектов модели предметной области Trip и передать его представлению trip/list.gsp. Посмотрите еще раз в текстовом редакторе на TripController.groovy:

Листинг 2.Полностью заполненный класс TripController
class TripController
{
  ...
  def list = 
  {
    if(!params.max) params.max = 10
    [ tripList: Trip.list( params ) ]
  }
  ...
}

Это короткое замыкание выбирает 10 записей Trip из БД, конвертирует их в POGO и сохраняет в ArrayList под названием tripList. Затем страница list.gsp проходит по этому списку, строка за строкой формируя HTML-таблицу.

В следующем разделе рассказывается о популярных тегах Grails, включая тег <g:each>, используемый для показа каждого Trip на Web-странице.

Теги Grails

<g:each> - это широко используемый тег Grails. Он перебирает позиции списка по одной. Чтобы посмотреть на него в действии, откройте grails-app/views/trip/list.gsp (Листинг 3) в текстовом редакторе:

Листинг 3. Представление list.gsp
<g:each in="${tripList}" status="i" var="trip">
  <tr class="${(i % 2) == 0 ? 'even' : 'odd'}">
    <td>
      <link action="show" id="${trip.id}">${trip.id?.encodeAsHTML()}</g:link>
    </td>
    <td>${trip.airline?.encodeAsHTML()}</td>
    <td>${trip.name?.encodeAsHTML()}</td>
    <td>${trip.city?.encodeAsHTML()}</td>
    <td>${trip.startDate?.encodeAsHTML()}</td>
    <td>${trip.endDate?.encodeAsHTML()}</td>
  </tr>
</g:each>

Атрибут status тега <g:each> - это простое поле-счетчик (его значение используется на следующей строке в троичном выражении, задающем CSS-стиль even или odd). Атрибут var позволяет именовать переменные, используемые для хранения текущего элемента. Если изменить имя на foo, потребуется изменить следующую строку на ${foo.airline?.encodeAsHTML()} и так далее (оператор «.?» – это способ Groovy избегать Null­PointerException; это сокращение, заменяющее «вызывать метод encodeAsHTML() только если airline не равно null, иначе просто возвратить пустую строку»).

Другой распространенный Grails-тег – <g:link>. Как вы можете предположить, он создает HTML-ссылки <a href>. Ничто не мешает использовать сам тег <a href>, но этот удобный тег принимает такие удобные атрибуты как action, id и controller. Если вы хотите передать только значение href без окружающих тегов anchor, можно использовать <g:createLink>. В верхней части list.gsp вы можете видеть третий тег, возвращающий ссылку: <g:createLinkTo>. Этот тег принимает атрибуты dir и file вместо логических атрибутов controller, action и id. В листинге 4 показано применение тегов link и createLinkTo:

Листинг 4. Теги link и createLinkTo
<div class="nav">
  <span class="menuButton">
    <a class="home" href="${createLinkTo(dir:'')}">Home</a>
  </span>
  <span class="menuButton">
    <link class="create" action="create">New Trip</g:link>
  </span>
</div>

Обратите внимание, что вы можете использовать теги Grails в двух разных, взаимозаменяемых, формах – либо как теги в угловых скобках, либо как вызовы методов в фигурных скобках. Нотация с использованием фигурных скобок (формально известная как Expression Language или EL-синтаксис) лучше подходит для случаев, когда метод может быть встроен в атрибуты другого тега.

Несколькими строками ниже в list.gsp (листинг 5) вы можете видеть еще один популярный Grails-тег <g:if>. В данном случае он говорит «если атрибут flash.message не равен null, показать его».

Листинг 5. Тег <g:if>
<h1>Trip List</h1>
<if test="${flash.message}">
  <div class="message">${flash.message}</div>
</g:if>

При просмотре сгенерированных представлений вы увидите действие многих других Grails-тегов. Тег <g:paginate> выводит ссылки "previous" и "next", если БД содержит больше записей Trip, чем 10 выведенных на экран. Тег <g:sortable> позволяет сортировать содержимое колонок, щелкнув по их заголовку. Просмотр других GSP-страниц покажет теги, относящиеся к HTML-for­мам, например, <g:form> и <g:submit>. Онлайн-документация Grails содержит все имеющиеся теги и примеры их использования.

Собственные библиотеки тегов

Стандартные теги Grails, конечно, полезны, но вы, возможно, столкнетесь с ситуацией, в которой потребуются ваши собственные теги. Многие опытные Java-разработчики (включая меня) публично заявляют: «Да, собственные TagLib – в данном случае подходящее архитектурное решение», а затем ускользают и пишут вместо этого скриптлеты, когда думают, что их никто не видит. Написание собственной JSP TagLib требует стольких дополнительных усилий, что скриптлет зачастую выигрывает, так как является путем наименьшего сопротивления. Это неправильный порядок действий, но это (к сожалению) самый простой способ.

Скриплетам – нет! Это хаки в само первом смысле этого слова. Они нарушают основанную на тегах парадигму HTML и вставляют сырой код прямо в представление. Плох даже не столько сам код, плохо отсутствие инкапсуляции и возможности многократного использования. Единственный способ многократно использовать скриплет – это copy-and-paste. Это приводит к ошибкам, к разрастанию и повторам в коде. Я уж и не говорю о возможностях тестирования скритлетов.

Должен признаться, что я и сам написал больше JSP-скриптлетов, чем мне хотелось бы. JSP Standard Tag Library (JSTL) долго пыталась уберечь меня от неверных путей, но написание собственных JSP-тегов всегда было отдельной проблемой. К тому времени, когда я написал собственный JSP-тег на Java, скомпилировал его и разобрался с приведением Tag Library Descriptor (TLD) в нужный формат и размещением его в нужном месте, я уже совершенно забыл причину, заставившую меня писать этот самый тег. Что же до написания тестов, проверяющих мой новый JSP-тег – скажу только, что намерения у меня были самые благие.

В противоположность этому, писать собственные библиотеки TagLib на Grails очень просто. Framework делает это нужное дело, включая написание тестов, простым. Например, мне часто требуется разместить стандартный текст о копирайте внизу Web-страницы. Там должно быть написано © 2002 - 2008, FakeCo Inc. All Rights Reserved. Дело в том, что второй год должен быть текущим годом. В листинге 6 показано, как это делается с помощью скриптлета:

Листинг 6. Скриптлет.
<div id="copyright">
&copy; 2002 - ${Calendar.getInstance().get(Calendar.YEAR)}, 
    FakeCo Inc. All Rights Reserved.
</div>

Теперь, когда вы знаете, как подставить текущий год, давайте создадим тег, делающий то же самое. Для начала введите grails create-tag-lib Date. При этом будут созданы два файла: grails-app/taglib/DateTagLib.groovy (TagLib) и grails-app/test/integration/Date­TagLibTests.gro­ovy (тест). Добавьте в DateTagLib.groovy содержимое листинга 7:

Листинг 7. Простой пользовательский Grails-тег.
class DateTagLib 
{
  def thisYear = 
  {
    out << Calendar.getInstance().get(Calendar.YEAR)
  }
}

Листинг 7 создает тег <g:thisYear>. Как вы можете увидеть, год пишется напрямую в выходной поток. Листинг 8 демонстрирует применение нового тега.

Листинг 8. Использование нового тега.
<div id="copyright">
&copy; 2002 - <g:thisYear />, FakeCo Inc. All Rights Reserved.
</div>

Вы можете подумать, что все уже готово. Я же честно скажу вам, что это еще только полдела.

Тестирование TagLib

Несмотря на то, что сейчас все выглядит хорошо, нужно все-таки написать тест, чтобы проверить, что этот тег в будущем не сломается. Michael Feathers, автор Working Effectively with Legacy Code, говорит, что любой код без теста – устаревший. Чтобы убедиться, что мистер Feathers не прикалывается над вами, добавьте в DateTagLibTests.groovy код из листинга 9:

Листинг 9. Тест для пользовательского тега.
class DateTagLibTests extends GroovyTestCase 
{
  def dateTagLib

  void setUp()
  {
    dateTagLib = new DateTagLib()
  }

  void testThisYear() 
  {
    String expected = Calendar.getInstance().get(Calendar.YEAR)
    assertEquals("the years don't match", expected, dateTagLib.thisYear())
  }
}

GroovyTestCase – это тонкий Groovy-фасад над TestCase из JUnit 3.x. Создание теста для простого однострочного тега может показаться избыточным, но вы удивитесь, узнав, сколько раз такие одиночные строки становились источниками проблем. Написать тест несложно, и лучше обезопасить себя, чем потом жалеть. Введите grails test-app, чтобы запустить тест. Если все в порядке, вы увидите сообщение, показанное в листинге 10.

Листинг 10. Успешное завершение теста в Grails.
-------------------------------------------------------
Running 2 Integration Tests...
Running test DateTagLibTests...
                    testThisYear...SUCCESS
Running test TripTests...
                    testSomething...SUCCESS
Integration Tests Completed in 506ms
-------------------------------------------------------

Если вас смущает появление TripTests, не удивляйтесь. Когда вы ввели grails create-domain-class Trip, тест был сгенерирован за вас. На само деле, каждая Grails-команда create генерирует соответствующий тест. Да, тестирование настолько важно в современной разработке ПО. Если у вас до сих пор нет навыков написания тестов, позвольте Grails мягко подтолкнуть вас в нужном направлении. Вы не пожалеете.


Рисунок 1. Отчет Unit-теста.

Команда grails test-app, кроме запуска тестов, создает HTML-отчет. Откройте test/reports/html/index.html в браузере, чтобы увидеть стандартный отчет JUnit, показанный на рисунке 1.

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

Сложные пользовательские теги

Более сложные теги могут работать с атрибутами и с телом тега. Например, решение для копирайта, как оно сейчас выглядит, требует слишком многих copy/paste, на мой вкус. Хотелось бы поместить все имеющееся поведение в реально многократно используемый тег, например, такой: <g:copyright startYear="2002">FakeCo Inc.</g:copyright>. Код показан в листинге 11..

Листинг 11. Grails-тег, работающий с атрибутами и с телом тега.
class DateTagLib 
{
  def thisYear = 
  {
    out << Calendar.getInstance().get(Calendar.YEAR)
  }

  def copyright = 
  { 
    attrs, body ->
    out << "<div id='copyright'>"
    out << "&copy; ${attrs['startYear']} - ${thisYear()}, ${body()}"
    out << " All Rights Reserved."
    out << "</div>"
  }
}

Заметьте, что attrs – это HashMap атрибутов тега. Я использую его здесь для перехвата атрибута startYear. Я вызываю thisYear как замыкание (это такой же вызов замыкания, какой я мог бы сделать на GSP-странице в фигурных скобках). Аналогично, body передается в тег как замыкание, так что я вызываю его так же, как любой другой тег. Это дает мне возможность вкладывать теги на произвольную глубину.

Вы, возможно, обратили внимание на то, что пользовательская TagLib использует то же пространство имен g:, что и стандартная библиотека тегов Grails. Если вы хотите поместить свою библиотеку в собственное пространство имен, добавьте в DateTagLib.groovy static namespace = 'trip'. Тег после этого будет выглядеть так:

<trip:copyright startYear="2002">FakeCo Inc.</trip:copyright>.

Частичные шаблоны

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

Частичные шаблоны в документации Grails официально называются шаблонами. Единственная проблема состоит в том, что термин «шаблон» перегружен и означает в Grails две совсем разные вещи. Как вы увидите в следующем разделе для изменения используемых по умолчанию представлений вам нужно изменить их шаблоны. Изменения этих шаблонов могут включать частичные шаблоны, о которых идет речь в этом разделе. Чтобы уменьшить путаницу, я позаимствую часть терминологии из сообщества Rails и буду называть эти вещи частичными шаблонами.

Частичный шаблон – это отрывок GSP-кода, который может совместно использоваться несколькими Web-страницами. Представьте, например, что мне нужен стандартный нижний колонтитул для всех страниц. Для этого я создаю частичный шаблон _footer.gsp (листинг 12). То, что имя файла начинается с подчеркивания – подсказка фреймворку (и разработчику), что это незавершенный GSP. Если я помещу этот файл в каталог grails-app/views/trip directory, он будет виден только представлениям Trip. Чтобы он был виден всем страницам, я помещаю его в каталог grails-app/views.

Листинг 12. Частичный шаблон Grails.
<div id="footer">
  <g:copyright startYear='2002'>FakeCo, Inc.</g:copyright>

  <div id="powered-by">
    <img src="${createLinkTo(dir:'images', file:'grails-powered.jpg')}" />
  </div>
</div>

Как видите, частичный шаблон позволяет использовать синтаксис HTML/GSP. Пользовательские TagLib, напротив, пишутся на Groovy. Еще скажу, что библиотеки тегов в общем лучше для инкапсуляции микро-поведения, а частичные шаблоны – для многократного использования элементов раскладки страниц.

Чтобы этот пример работал так, как здесь, нужно скачать кнопку "Powered by Grails" в каталог grails-app/web-app/images.

В листинге 13 показано, как включить созданный колонтитул в нижнюю часть страницы list.gsp:

Листинг 13. Использование частичного шаблона.
<html><body>
...
<g:render template="/footer" />
</body></html>

Заметьте, что при использовании шаблона подчеркивание не используется. Если вы сохранили _footer.gsp в каталоге trip, нужно убрать и слэш. Думайте об этом так: каталог grails-app/views является корнем иерархии представлений.

Изменение скаффолдинга по умолчанию

Теперь, когда у вас есть хорошие, тестируемые, многократно используемые компоненты, можно сделать их частью скаффолдинга по умолчанию. Вспомните, что именно динамически генерируется, если поместить в контроллер def scaffold = Foo. Scaffolding по умолчанию также служит источником GSP, создаваемых при вводе команды grails generate-views Trip или grails generate-all Trip.

Чтобы изменить скаффолдинг по умолчанию, введите grails install-templates. Это добавит в проект новый каталог src/templates/ В нем вы найдете три каталога: artifacts, scaffolding и war.

Каталог artifacts содержит шаблоны для различных Groovy-классов, например, Controller, DomainClass, Tag­Lib. Если, например, вы хотите, чтобы все ваши контроллеры расширяли абстрактный родительский класс, вы можете выполнить изменения здесь. Все новые контроллеры будут основываться на измененном коде шаблона (некоторые добавляют def scaffold = @artifact.na­me@, чтобы динамический скаффолдинг был поведением по умолчанию для всех контроллеров).

Каталог war содержит файл web.xml, знакомый всем Java EE-разработчикам. Если вы хотите добавить собственные параметры, фильтры или сервлеты, то это делается здесь (энтузиасты JSF). Если ввести grails war, web.xml из этого каталога будет включен в конечный WAR.

Каталог скаффолдинга содержит сырье для динамически генерируемых представлений. Откройте list.gsp и добавьте <g:render template="/footer" /> в конец файла. Поскольку эти шаблоны используются всеми представлениями, используйте глобальные частичные шаблоны.

Теперь, когда вы изменили представление List, пора проверить, подействовали ли эти изменения. Изменения используемых по умолчанию шаблонов – один из немногих случаев, требующих перезапуска сервера. После перезапуска Grails откройте в браузере http://lo­calhost:9090/trip-planner/airline/list. Если вы используете в AirlineController скаффолдинг по умолчанию, нижняя часть страницы должна измениться.

Заключение

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

В следующем месяце знакомство с Web-инфраструктурой Grails будет сфокусировано на Ajax. Способность делать "микро"-запросы HTTP без перезагрузки всей страницы — это секретный ингредиент, используемый в Google Maps, Flickr и многих других популярных Web-сайтах. Мы применим немного этой магии в Grails. А именно, мы создадим связи "многие-ко-многим" и воспользуемся Ajax, чтобы сделать работу пользователей естественной и приятной.

А пока — удачи в освоении Grails!

Ресурсы


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

Copyright 1994-2016 "-"