Содержание


Изучение Grails

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

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

Comments

Серия контента:

Этот контент является частью # из серии # статей: Изучение Grails

Следите за выходом новых статей этой серии.

Этот контент является частью серии:Изучение Grails

Следите за выходом новых статей этой серии.

В предыдущей статье под названием Освежите ваше приложение 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.

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

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

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

Для начала выполните команду 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.


Ресурсы для скачивания


Похожие темы


Комментарии

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

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