Изучение Grails: Использование собственных URI и кодеков в приложениях Grails

Замена первичных ключей наглядными идентификаторами в URI

В этой статье серии Изучение Grails Скотт Дэвис рассказывает о том, как можно изменять стандартные идентификаторы ресурсов (URI), автоматически генерируемые Grails для Web-страниц. Использование наглядных адресных ссылок вместо первичных ключей в URI позволяет пользователям легче запоминать путь к нужным ресурсам.

Скотт Дэвис, главный редактор, AboutGroovy.com

Скотт Дэвис (Scott Davis) является международно признанным автором, лектором и разработчиком программного обеспечения. Среди его книг: Groovy Recipes: Greasing the Wheels of Java, GIS for Web Developers: Adding Where to Your Application, The Google Maps API и JBoss At Work.



27.10.2010

В предыдущей статье под названием Освежите ваше приложение Grails рассказывалось о косметических изменениях в интерфейсе Blogito – блог-сайта на основе Grails. Темой настоящей статьи является возможность изменения центрального связующего звена любого Web-приложения – URI, использующегося для навигации между страницами. Эта возможность имеет особое значение для Web-блогов, таких как Blogito, поскольку перманентные ссылки на конкретные статьи постоянно передаются между пользователями в Интернете подобно визитным карточкам. Их эффективность напрямую зависит от их наглядности.

Для генерации наглядных URI мы изменим код контроллера, в частности, файл UrlMappings.groovy, в котором создадим новые пути (pathways). В конце статьи будет показан пример создания собственного кодека, который облегчит задачу генерации нужных URI.

Основы URI

Буква U в аббревиатуре URI означает унифицированный (uniform), однако с тем же успехом она могла бы обозначать уникальный (unique, см. раздел Ресурсы). Например, если бы URI http://www.ibm.com/developerworks не указывал на страницу с этой статьей однозначным образом, то от него было бы немного пользы. Кроме того, URI играет роль запоминающегося идентификатора ресурса. На ту же страницу можно попасть и по ссылке http://129.42.56.216, однако вряд ли найдется много желающих запоминать численные IP-адреса сайтов в Web.

Таким образом, URI по крайней мере обязаны быть уникальными, а в идеале – легко запоминающимися (в заметке Спор вокруг непрозрачных URI приведена обратная точка зрения насчет последнего утверждения). Инфраструктура Grails однозначно удовлетворяет первому требованию, так как комбинация имен контроллера и замыкания, а также первичного ключа записи в базе данных гарантированно будет уникальной в каждом URI. Например, URI для первой записи в базе данных будет выглядеть как http://localhost:9090/blogito/entry/show/1.

Об этой серии

Grails – это современная инфраструктура для разработки Web-приложений, сочетающая использование знакомых Java-технологий, например, Spring и Hibernate, с такими популярными в настоящее время принципами, как соглашения по конфигурированию (convention over configuration). Будучи написанной на языке Groovy, Grails позволяет легко интегрировать старый Java-код в новые приложения, предоставляя гибкие возможности динамического скриптового языка. Изучение Grails навсегда изменит ваш взгляд на Web-разработку.

Использование первичных ключей в URI представляет собой вполне допустимое решение, но, тем не менее, оно оскорбляет мое чувство прекрасного по двум причинам. Во-первых, это выставляет напоказ детали внутренней реализации приложения, поскольку артефакты базы данных становятся частью фасада Web-приложения. Google, Amazon и eBay тоже содержат базы данных, однако вы никогда не найдете их отражения в URI. Вторая причина, по которой не следует использовать первичные ключи в URI, имеет более семантическую природу. Читателям блога Джейн Смит (Jane Smith) гораздо легче идентифицировать ее по строке jsmith, чем по числу 12. По той же причине использование заголовков записей вместо первичных ключей в URI делает их значительно более запоминающимися.

Создание класса User

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

Спор вокруг непрозрачных URI

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

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

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

Для начала выполните команду grails create-domain-class User в командной строке. Далее добавьте фрагмент кода в листинге 1 в файл grails-app/domain/User.groovy.

Листинг 1. Класс User
class User {
  static constraints = {
    login(unique:true)
    password(password:true)
    name()
  }
  
  static hasMany = [entries:Entry]
  
  String login
  String password
  String name
  
  String toString(){
    name
  }
}

Поля login и password говорят сами за себя: они отвечают за аутентификацию пользователя. Поле name служит для отображения: например, строка "Jane Smith" будет показываться для пользователя с именем jsmith. Как видите, классы User и Entry связаны отношением типа "один-ко-многим".

Чтобы окончательно связать эти классы, добавьте поле static belongsTo в файл grails-app/domain/Entry.groovy (листинг 2).

Листинг 2. Добавление связи типа "один-ко-многим" в класс Entry
class Entry {
  static belongsTo = [author:User]
  //остальной код опущен
}

Обратите внимание на то, насколько легко переименовать поле в процессе описания отношения. Теперь в классе User есть поле entries, а в классе Entry – поле author.

Как правило, в этот момент вы бы создали соответствующий класс UserController, который бы предоставлял полноценный интерфейс для управления экземплярами User. Однако пока это не представляет большого интереса, поэтому мы просто оставим несколько экземпляров User в качестве заглушек, а в следующей статье серии завершим полноценную реализацию механизма авторизации и аутентификации. Далее добавим несколько новых пользователей с помощью grails-app/conf/BootStrap.groovy, как показано в листинге 3.

Листинг 3. Создание тестовых экземпляров User в BootStrap.groovy
import grails.util.GrailsUtil

class BootStrap {

  def init = { servletContext ->
    switch(GrailsUtil.environment){
      case "development":
        def jdoe = new User(login:"jdoe", password:"password", name:"John Doe")
        def e1 = new Entry(title:"Grails 1.1 beta is out", 
           summary:"Check out the new features")
        def e2 = new Entry(title:"Just Released - Groovy 1.6 beta 2", 
           summary:"It is looking good.")
        jdoe.addToEntries(e1)
        jdoe.addToEntries(e2)
        jdoe.save()
        
        def jsmith = new User(login:"jsmith", password:"wordpass", name:"Jane Smith")
        def e3 = new Entry(title:"Codecs in Grails", summary:"See Mastering Grails")
        def e4 = new Entry(title:"Testing with Groovy", summary:"See Practically Groovy")
        jsmith.addToEntries(e3)
        jsmith.addToEntries(e4)
        jsmith.save()              
      break

      case "production":
      break
    }

  }
  def destroy = {
  }
}

Обратите внимание на присвоение записей экземплярам User. Вам не нужно заботиться о таких деталях, как первичные или внешние ключи. API объектно-реляционного отображения Grails (GORM) берет эти вопросы на себя, позволяя вам сосредоточиться на объектах.

Далее необходимо слегка подкорректировать частичный шаблон grails-app/views/entry/_entry.gsp, созданный в предыдущей статье. Имя автора записи должно отображаться рядом с полем Entry.lastUpdated (листинг 4).

Листинг 4. Добавление автора в шаблон _entry.gsp
<div class="entry">
  <span class="entry-date">
    <g:longDate>${entryInstance.lastUpdated}</g:longDate> : ${entryInstance.author}
  </span>
  <h2><g:link action="show" id="${entryInstance.id}">${entryInstance.title}
    </g:link></h2>                  
  <p>${entryInstance.summary}</p>
</div>

Выражение ${entryInstance.author} вызывает метод toString() класса User. Вместо него можно использовать ${entryInstance.author.name}, в котором явно указывается, какое поле необходимо отобразить. Подобный синтаксис позволяет обращаться к полям классов, находящихся на произвольной глубине в иерархии.

Пришло время посмотреть на все сделанные изменения в действии. Выполните команду grails run-app и откройте страницу по адресу http://localhost:9090/blogito/ в Web-браузере. Результат должен выглядеть как на рисунке 1.

Рисунок 1. Записи, созданные от имени только что добавленного автора
Записи, созданные от имени только что добавленного автора

Теперь, когда Blogito позволяет нескольким пользователям создавать записи, пришло время реализовать возможность фильтрации записей по имени автора.


Вывод записей по имени автора

Наша задача заключается в поддержке URI наподобие http://localhost:9090/blogito/entry/list/jdoe, в которых вместо первичного ключа для идентификации автора используется свойство User.login. Кроме того, придется также несколько изменить разбиение записей на страницы.

Автоматически генерируемый EntryController.list не предоставляет возможностей для фильтрации записей по имени автора. Реализация по умолчанию замыкания list приведена в листинге 5.

Листинг 5. Стандартная реализация замыкания list
def list = {
    if(!params.max) params.max = 10
    [ entryInstanceList: Entry.list( params ) ]
}

Данное замыкание необходимо расширить, приняв во внимание, что в конце URI может быть передано имя пользователя. Откройте файл grails-app/controllers/EntryController.groovy и добавьте новое замыкание list, показанное в листинге 6.

Листинг 6. Вывод записей, созданных конкретным автором
class EntryController {
  def scaffold = Entry
  
  def list = {
      if(!params.max) params.max = 10
      flash.id = params.id
      if(!params.id) params.id = "No User Supplied"

      def entryList
      def entryCount
      def author = User.findByLogin(params.id)
      if(author){
        def query = { eq('author', author) }
        entryList = Entry.createCriteria().list(params, query)        
        entryCount = Entry.createCriteria().count(query)
      }else{
        entryList = Entry.list( params )
        entryCount = Entry.count()
      }
      
      [ entryInstanceList:entryList, entryCount:entryCount  ]
  }  
}

Первое, на что следует обратить внимание - это присвоение значений по умолчанию свойствам params.max и params.id в случае, если соответствующие параметры не были переданы пользователем. Пока не обращайте внимания на flash.id - он будет обсуждаться позже, когда мы перейдем к вопросам постраничного вывода.

Значением params.id, как правило, является целое число (первичный ключ, если быть точным). Вам уже встречались URI наподобие /entry/show/1 и entry/edit/2. В принципе можно было описать отображение в grails-app/conf/UrlMappings.groovy таким образом, чтобы возвращался более наглядный идентификатор, вроде params.name или params.login, но пока мы оставим все как есть, поскольку в текущей реализации часть URI, следующая за типом действия (action), автоматически сохраняется в params.id. В листинге 7 показано отображение по умолчанию в URLMapper.groovy, возвращающее params.id.

Листинг 7. Отображение по умолчанию в классе UrlMappings.groovy
class UrlMappings {
    static mappings = {
      "/$controller/$action?/$id?"{
	      constraints {}
	  }
    //остальной код опущен
	}
}

Это значение не является первичным ключом класса User, поэтому нельзя получить ссылку на автора обычным образом, т.е. при помощи User.get(params.id). Вместо этого придется использовать метод User.findByLogin(params.id).

Если экземпляр User найден, то далее создается выражение query, являющееся примером использования Hibernate Criteria Builder (см. раздел Ресурсы). В данном примере список должен включать только записи, созданные конкретным автором. Как и ранее, обратите внимание на то, как GORM позволяет абстрагироваться от первичных и внешних ключей, позволяя сосредоточиться на работе с объектами.

Если params.id не соответствует ни одному автору, то возвращается полный список записей (см. строку entryList = Entry.list( params )).

Обратите внимание, что значение entryCount вычисляется явным образом. Автоматически генерируемые страницы GSP (GroovyServer Pages), как правило, вызывают Entry.count() внутри тега <g:paginate>. Однако в данном случае обратно передается отфильтрованный список, поэтому данное значение должно вычисляться в контроллере и сохраняться в переменной.

Сохранение значения params.id в flash.id позволяет приложению передавать условия поиска внутрь тега <g:paginate>. Измените <g:paginate> так, чтобы он использовал значение переменной entryCount и параметров в области видимости flash (листинг 8).

Листинг 8. Изменение list.gsp для реализации нестандартного постраничного вывода
<div class="paginateButtons">
  <g:paginate total="${entryCount}" params="${flash}"/>
</div>

Перезапустите Grails и откройте страницу http://localhost:9090/blogito/entry/list/jsmith в браузере. Результат должен выглядеть как на рисунке 2.

Рисунок 2. Показ записей, сделанных конкретным автором
Показ записей, сделанных конкретным автором

Для того чтобы убедиться, что постраничный вывод работает правильно, наберите http://localhost:9090/blogito/entry/list/jsmith?max=1 в адресной строке браузера. Затем нажмите на кнопки Previous и Next, удостоверившись, что отображаются только записи, сделанные Jane (рисунок 3).

Рисунок 3. Тестирование постраничного вывода записей
Тестирование постраничного вывода записей

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


Поддержка пользовательских URI

UrlMappings.groovy предоставляет гибкие возможности для создания новых URI. Предположим, в дополнение к уже работающим ссылкам типа http://localhost:9090/blogito/entry/list/jsmith пользователи вдруг попросили также поддерживать URI наподобие http://localhost:9090/blogito/blog/jsmith too. Никаких проблем! Добавьте новое отображение, показанное в листинге 9, в UrlMappings.groovy.

Листинг 9. Добавление специального отображения в класс UrlMappings.groovy
class UrlMappings {
    static mappings = {
      "/$controller/$action?/$id?"{
	      constraints {
			 // добавьте сюда нужные ограничения
		  }
	  }
	  "/"(controller:"entry")
	  "/blog/$id"(controller:"entry", action="list")
	  "500"(view:'/error')
	}
}

Теперь запросы с URI, начинающиеся с /blog, будут перенаправляться контроллеру Entry с типом действия list. Хотя $user или $login более наглядны, в данном случае стоит сохранить значение $id в соответствии с соглашением Grails, поскольку при этом "/$controller/$action?/$id?" и "/blog/$id"(controller:"entry", action="list") смогут указывать на один конечный ресурс.

Откройте страницу по адресу http://localhost:9090/blogito/blog/jsmith в Web-браузере, чтобы удостовериться, что отображение работает.

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


Создание собственного кодека

Использование свойства User.login в URI вместо User.id не представляет трудностей, поскольку его значения не могут включать символы пробела и табуляции. Правда, на данный момент приложение не содержит правил для проверки этого условия, но таковые несложно добавить (см. раздел Ресурсы).

Допустим, нам потребовалось заменить Entry.id на Entry.title в URI записей. Заголовки записей практически наверняка будут включать пробелы. Для решения этой проблемы можно добавить еще одно поле в класс Entry и заставить пользователей вводить версии заголовков без пробелов. Это не самый удачный вариант, поскольку, во-первых, требует дополнительных усилий со стороны пользователей, а во-вторых, неявно подразумевает создание правила для проверки значений нового свойства. Гораздо лучшим решением будет автоматическое преобразование пробелов в символы подчеркивания в заголовках записей. Grails поддерживает такую возможность через механизм специализированных кодеков (сокращенно от "кодировщик-декодировщик").

Создайте класс grails-app/utils/UnderscoreCodec и добавьте в него код, приведенный в листинге 10.

Листинг 10. Пример пользовательского кодека
class UnderscoreCodec {
  static encode = {target->
    target.replaceAll(" ", "_")
  }
  
  static decode = {target->
    target.replaceAll("_", " ")
  }
}

В состав Grails входят несколько встроенных кодеков: HtmlCodec, UrlCodec, Base64Codec и JavaScriptCodec (см. раздел Ресурсы). Класс HtmlCodec содержит методы encodeAsHtml() и decodeHtml(), которые вам уже встречались в автоматически генерируемых страницах GSP.

Существует возможность добавлять собственные кодеки. Grails может использовать любой класс, находящийся в каталоге grails-app/utils и имеющий окончание Codec, для добавления методов encodeAs() и decode() к классу Strings. In this case, all String. В нашем случае все строки в Blogito волшебным образом получат два новых метода: encodeAsUnderscore() и decodeUnderscore().

В этом можно убедиться, создав тестовый класс UnderscoreCodecTests.groovy в каталоге test/integration (листинг 11).

Листинг 11. Тестирование пользовательского кодека
class UnderscoreCodecTests extends GroovyTestCase {
  void testEncode() {
    String test = "this is a test"
    assertEquals "this_is_a_test", test.encodeAsUnderscore()
  }
  
  void testDecode() {
    String test = "this_is_a_test"
    assertEquals "this is a test", test.decodeUnderscore()
  }
}

Выполните команду grails test-app для запуска тестов. Вы должны увидеть результаты, аналогичные приведенным в листинге 12.

Листинг 12. Результат успешного выполнения теста
$ grails test-app
-------------------------------------------------------
Running 2 Integration Tests...
Running test UnderscoreCodecTests...
                    testEncode...SUCCESS
                    testDecode...SUCCESS
Integration Tests Completed in 157ms
-------------------------------------------------------

Кодеки в действии

Теперь, после добавления класса UnderscoreCodec, у нас есть все необходимое для того, чтобы включить поддержку URI, которые включают имя пользователя и заголовок записи, например http://localhost:9090/blogito/blog/jsmith/this_is_my_latest_entry.

Вначале необходимо немного изменить отображение /blog в UrlMappings.groovy для того, чтобы корректно обрабатывать необязательный параметр $title (листинг 13). Как вы помните, параметры, оканчивающиеся знаком вопроса, являются необязательными в Groovy.

Листинг 13. Необязательный компонент title в отображении URI
class UrlMappings {
    static mappings = {
      "/$controller/$action?/$id?"{
	      constraints {
			 // добавьте сюда необходимые ограничения
		  }
	  }
	  "/"(controller:"entry")
	  "/blog/$id/$title?"(controller:"entry", action="list")
	  "/entry/$action?/$id?/$title?"(controller:"entry")
	  "500"(view:'/error')
	}
}

Далее измените замыкание EntryController.list, приняв во внимание новый параметр params.title, как показано в листинге 14.

Листинг 14. Обработка значения params.title в контроллере
class EntryController {
  def scaffold = Entry
  
  def list = {
      if(!params.max) params.max = 10
      flash.id = params.id
      if(!params.id) params.id = "No User Supplied"
      flash.title = params.title
      if(!params.title) params.title = ""

      def author = User.findByLogin(params.id)
      def entryList
      def entryCount
      if(author){
        def query = { 
          and{
            eq('author', author) 
            like("title", params.title.decodeUnderscore() + '%')
          }
        }  
        entryList = Entry.createCriteria().list(params, query)        
        entryCount = Entry.createCriteria().count(query)
      }else{
        entryList = Entry.list( params )
        entryCount = Entry.count()
      }
      
      [ entryInstanceList:entryList, entryCount:entryCount  ]
  }  
}

В теле запроса в этом примере используется метод like, чтобы повысить гибкость URI. Например, пользователь может ввести /blog/jsmith/mastering_grails чтобы получить все записи, чьи заголовки начинаются со строки "mastering_grails". При желании вы можете сделать поиск более строгим, используя метод eq для показа только тех записей, чьи заголовки точно совпадают с введенным пользователем.

Откройте страницу http://localhost:9090/blogito/jsmith/Codecs_in_Grails в браузере, и вы увидите новый кодек в действии. Страница должна выглядеть как на рисунке 4.

Рисунок 4. Показ записи по заголовку и имени автора
Показ записи по заголовку и имени автора

Заключение

URI играют основную связующую роль в Web-приложении. Ссылки, генерируемые Grails по умолчанию, вполне подходят для начального этапа создания приложения, но у вас также есть возможность легко приспособить их под нужды вашего Web-сайта наилучшим образом. Благодаря нашим усилиям, Blogito теперь оперирует такими сущностями, как User и Entry. Но еще важнее то, что для их показа больше не требуется указывать первичные ключи в URI. В этой статье вы узнали о создании наглядных URI в коде контроллера, о добавлении нужных отображений в файл UrlMappings.groovy, а также об использовании собственных кодеков.

В следующем выпуске мы создадим форму для аутентификации пользователей Blogito. После входа в систему они смогут загружать файлы непосредственно в тело записей в блоге. Файлы могут представлять собой документы HTML, изображения и даже аудиозаписи в формате MP3. До тех пор – получайте удовольствие от изучения Grails.

Ресурсы

Научиться

Получить продукты и технологии

  • Grails: загрузите последнюю версию Grails. (EN)
  • Blogito: загрузите полную версию приложения Blogito. (EN)

Обсудить

Комментарии

developerWorks: Войти

Обязательные поля отмечены звездочкой (*).


Нужен IBM ID?
Забыли Ваш IBM ID?


Забыли Ваш пароль?
Изменить пароль

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


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

Вся введенная информация защищена.

Выберите имя, которое будет отображаться на экране



При первом входе в developerWorks для Вас будет создан профиль и Вам нужно будет выбрать Отображаемое имя. Оно будет выводиться рядом с контентом, опубликованным Вами в developerWorks.

Отображаемое имя должно иметь длину от 3 символов до 31 символа. Ваше Имя в системе должно быть уникальным. В качестве имени по соображениям приватности нельзя использовать контактный e-mail.

Обязательные поля отмечены звездочкой (*).

(Отображаемое имя должно иметь длину от 3 символов до 31 символа.)

Нажимая Отправить, Вы принимаете Условия использования developerWorks.

 


Вся введенная информация защищена.


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=40
Zone=Технология Java
ArticleID=556486
ArticleTitle=Изучение Grails: Использование собственных URI и кодеков в приложениях Grails
publish-date=10272010