Making OOP Work in XQuery

By Kurt Cagle
September 2, 2008 | Comments: 3

Traditionally, there has been a significant dividing line between object-oriented programming and database development. The concept of object-oriented programming (OOP) was at a fever pitch during the 1980s when SQL was under development, but perhaps because of the influence of languages like COBOL, perhaps because SQL was considered a set language that didn't lend itself well to OOP, or because OOP was still controversial without a sufficient track record, the developers of SQL's core functional language, stored procedures, chose to keep those functions very procedural rather than attempting to put an OOP wrapper around them, in essence making all stored procedures a single library.

One of the central problems that this approach has had, however, is that such libraries tend to become quite bulky and disorganized over time, as people add stored procedures for handling just their test case, even when such procedures may already exist. You end up with procedure calls with names like GET_BUSINESS_OBJECTS_WEEKLY_GEORGE(), and if you need to be able to create alternate sets of parameters, then you have to create a different procedure (with a different name) for each signature.

Suppose, however, that you could use an object oriented approach in building such stored procedures. For instance, take the example of a role playing game that included a section for generating characters. In this case, a Characters class could encompass a number of methods that would be exclusive to just the domain of characters, something like

let $characters := new Characters()
return $characters.renderByInterval('weekly',now())

While SQL doesn't support such a transaction, XQuery, the new XML Query language standard, is actually quite capable of doing just that. It does so by establishing a namespace for the functions (in essence turning them into methods):

declare namespace characters = 'http://www.metaphoricalweb.org/xmlns/characters';

declare function characters:render($characters as node()*) as node()*{
	(: Code goes here :)
	};

It's extraordinarily unlikely that your database contains the particular "character:" namespace - you are in fact defining the functionality for it yourself in the following section. Instead, the namespace is declared using the declare namespace keywords, where you assign to this namespace a prefix.

book coverbook coverbook cover
For a complete list of XML books, visit the
XML topic page in the O'Reilly Store.

There is a tendency when working with XML namespaces to make this prefix as short as possible, often a single letter. However, with XQuery, there are significant advantages to making a namespace name that is more descriptive, especially if the term describes those particular entities that the functions in the namespace operate on, such as characters: rather than c:, as such terms much more clearly articulate what object set the particular method is working on. For instance,

let $collection-path := "/db/characters"
let $filter := "$character/Gender = 'Female'"
let $order := "$character/Name ascending"
let $page := 1
let $page-size := 20
let $view := "table"
let $character-set := characters:bind($collect-path)
let $filtered-set := characters:filter($character-set,$filter)
let $sorted-set := characters:sort($filtered-set,$order)
let $paged-set := characters:page($sorted-set,$page,$page-size)
return characters:render($paged-set,$view)

This code fragment illustrates one of the characteristics that you see a great deal with many XQuery scripts - a pipeline process whereby the results of one process gets fed into the next. This particular pattern is in fact very common - grab a collection, apply a filter to it to reduce the set, sort the results, get a page from the sorted results, then render the output. This pattern is typically used to create a list or table of entries that satisfy a given query in a given order in such a way as to not overwhelm the browser.

Note, however, that there's a fair amount of state that is kept here. In addition to the collection itself, every step in the pipeline also needs to keep track of its own particular piece of state - the collection-path, the filter, an order descriptor, which page to serve and which view to present for that page. It's certainly possible (with REST-based interfaces that can read URL query strings) that this can be passed either via a URL or via a POST command, but there may be some advantage to maintaining some kind of formal state mechanism - which is, to a great extent, one of the principle reasons for enabling object oriented programming techniques in the first place.

In a traditional OOP system, there is a very tight coupling between the internal state of an object and the methods of that object - in essence, the object acts as a container for the class's state. XQuery OOP differs somewhat from that approach. In essence, to make OOP-like behavior in XQuery, what you need to do is to create an XML object that can hold the state for a given instance, then pass this instance in as a parameter to a function in a given namespace.

The following code makes use of the extension functions available to the eXist XML database (http://exist.sourceforge.net), though similar methods either exist or can be developed for other XML databases.

For instance, consider the pipeline above as a single method called process-data. With that, an object-oriented approach, to create the same output, the function defined above might be written as:

let $collection-path := "/db/characters"
let $filter := "$character/Gender = 'Female'"
let $order := "$character/Name ascending"
let $page := 1
let $page-size := 20
let $view := "table"

let $characters := characters:new($filter,$order,$page,$page-size,$view)
let $process := characters:process-data($characters)
let $output := characters:render($characters)
return $output



XQuery does not in fact have a native "new" command, unlike a number of other languages. This means that you can build your own constructors without having to worry about namespace collisions. The good side is that this gives you the flexibility to create fairly sophisticated constructors.

The down side is that you have to actually build these constructors. Given the nature of the state data, creating an object "container" should be pretty straightforward. For instance, to define the new() constructor as given above, you'd do something like the following:

declare function characters:new($filter as xs:string,$order as xs:string,$page as xs:integer,$page-size as xs:integer,$view as xs:string) as node(){
	let $state-object := 
<class>
	<name>characters</name>
	<class-id>uri:uuid:548cbc92-62dd-4401-afc6-158ef4807acf</class-id>
	<description>A class for manipulating character lists.</description>
	<class-path>/db/characters</class-path>
	<viewmodes>
		<view name="xml" type="xquery" path="xmldb:exist:///db/characters/to-xml.xq"/>
		<view name="table" type="xslt" path="xmldb:exist:///db/characters/to-table.xsl"/>
		<view name="list" type="xslt" path="xmldb:exist:///db/characters/to-list.xsl"/>
		<view name="page" type="xslt" path="xmldb:exist:///db/characters/to-page.xsl"/>
		<view name="record" type="xslt" path="xmldb:exist:///db/characters/to-record.xsl"/>
		<view name="atom" type="xslt" path="xmldb:exist:///db/characters/to-atom.xsl"/>
	</viewmodes>
	<instance>
		<instance-id>uri:uuid:{util:uuid()}</object-id>
		<date-created>{current-dateTime()}</date-created>
		<date-updated>{current-dateTime()}</date-updated>
		<state>
      		<collection-path>/db/characters</collection-path>
			<filter>{$filter}</filter>
			<order>{$order}</order>
			<page xs:type="xs:integer">{$page}</page>
			<page-size xs:type="xs:integer">{$page-size}</page-size>
			<total-records>0</total-records>
			<view>{$view}</view>
			<cache/>
		</state>
	</instance>
</class>
	return $state-object
	};



When the arguments are passed to this new() "method":

let $characters := characters:new($filter,$order,$page,$page-size,$view)

what results is an XML structure holding the transient state of the object:

<class xmlns:xs="http://www.w3.org/2001/XML-Schema">
	<name>characters</name>
	<class-id>uri:uuid:548cbc92-62dd-4401-afc6-158ef4807acf</class-id>
	<description>A class for manipulating character lists.</description>
	<class-path>/db/characters</class-path>
	<viewmodes>
		<viewmode name="xml" type="xquery" path="xmldb:exist:///db/characters/to-xml.xq"/>
		<viewmode name="table" type="xslt" path="xmldb:exist:///db/characters/to-table.xsl"/>
		<viewmode name="list" type="xslt" path="xmldb:exist:///db/characters/to-list.xsl"/>
		<viewmode name="page" type="xslt" path="xmldb:exist:///db/characters/to-page.xsl"/>
		<viewmode name="record" type="xslt" path="xmldb:exist:///db/characters/to-record.xsl"/>
		<viewmode name="atom" type="xslt" path="xmldb:exist:///db/characters/to-atom.xsl"/>
	</viewmodes>
	<instance>
		<instance-id>uri:uuid:492bd536-514b-4332-b5e5-220cf8895f4f</instance-id>
		<date-created>2008-08-31T18:17:58.891-07:00</date-created>
		<date-updated>2008-08-31T18:17:58.891-07:00</date-updated>
		<state>
       		<collection-path>/db/characters</collection-path>
			<filter>$character/Gender = 'Female'</filter>
			<order>$character/Name ascending</order>
			<page xs:type="xs:integer">1</page>
			<page-size xs:type="xs:integer">20</page-size>
			<total-records>0</total-records>
			<view>table</view>
			<cache/>
		</state>
	</instance>
</class>

Before discussing how to use this, it's worth spending a few moments analyzing the function itself. The long ids, such as the class-id (uri:uuid:548cbc92-62dd-4401-afc6-158ef4807acf) are intended to uniquely identify the class in a way that would avoid potential collisions. Each class defined should use a separate GUID or similar unique identifier (the util:uuid() method is unique to the eXist XML database, but similar functions usually exist for other XML implementations).

The new() constructor here establishes a static class-id, which means that with the proper set of tools (a class: namespace, which will be discussed in a subsequent article) you can compare two object references to determine if they are from the same class by comparing their class-id, among other things. It also establishes a specific instance-id that is is unique to this particular instance, for a given class.

Similarly, the date-created and date-updated fields are set up at creation time to provide a base level auditing capability, using the native XQuery current-dateTime() method. The viewmodes are used to provide presentation information, and will be covered later in this article.

The use of the XML Schema namespace should likewise be noted. One of the goals with such a state function is to insure that state data is as accessible as possible for development. You can assign atomic type values to specific properties within the state section of the instance definition that can then be used to insure that the right data-types are used, such as making sure that the page-size is an integer:

<page-size xs:type="xs:integer">20</page-size>

Now, getting to this data can take a bit of work. For instance, if the variable $characters held the above object declaration, then in order to get to a particular state value as the right type, you'd need to use the following XPath:

let $page-size := data($characters/instance/state/page-size)

While you could use some XPath shortcuts to reduce the size of that, it is at best awkward, especially if you want the possibility that a given state "variable" can hold more than just an atomic-value. Of course, this is one of those places where you can take advantage of a known structure in order to build tools that will let you access information in yet another namespace.

Creating Helper Classes

The class: namespace ("http://www.metaphoricalweb.org/xmlns/class") can be defined here to provide utility functions for actually working with class-like structures.

declare namespace class="http://www.metaphoricalweb.org/xmlns/class";

declare function class:get-prop($object-decl as node(),$prop as xs:string){
     let $instance := $object-decl/instance
     let $property := $instance/state/*[name(.)=$prop]
     return if ($property/*) then $property else data($property)
     };

This can reduce the above to a static call on a class method:

let $page-size := class:get-prop($characters,"page-size")

The class:get() function takes an object XML structure and the property name and returns the associated value, cast to the appropriate type if it is an atomic value, or returns an XML structure if it isn't. If the associated state is atomic but has no type, then the assumption is that it is a string.

Things get considerably more complicated if you want to change a property's value. First, your XQuery engine needs to support a mechanism to update inline XML content, such as either the XQuery Update Facility or something like eXists's similar update mechanism. You also need to recognize that you'll actually need two functions - one for setting properties for atomic types and the other for XML content.

declare function class:set-prop($object-decl as node(),$prop as xs:string,$value as xs:anyAtomicType) as xs:anyAtomicType {
     let $state := $object-decl/instance/state
     let $property := $state/*[name(.)=$prop]
     let $old-value := class:get-prop($object-decl,$prop)
     let $assignment := (update value $property with $value)
     return $old-value
     };

declare function class:set-prop-node($object-decl as node(),$prop as xs:string,$value as node()) as node() {
     let $state := $object-decl/instance/state
     let $property := $state/*[name(.)=$prop]
     let $old-value := util:deep-copy(class:get=prop($object-decl,$prop))
     let $assignment := (update replace $property with $value)
     return $old-value
     };

The class:set-prop() static method is used to change the value of a given property in the state block if that property is atomic (i.e., is a number, string or date). The set-prop-node() method is used to change the value of a given property if that property is itself an XML element with child elements. In both cases the function returns the previous value held by the property.

This concept illustrates that you can use such namespaced functions effectly as both methods working on objects and as static functions within a library.

With these three functions, it becomes much easier to implement the process-data() method:

declare function characters:process-data($object as node()) as node()*{
(: get state variables :)
	let $collection-path := class:get-prop($object,"collection-path")
	let $filter := class:get-prop($object,"filter")
	let $order := class:get-prop($object,"order")
	let $page := class:get-prop($object,"page")
	let $page-size := class:get-prop($object,"page-size")
(: retrieve the collection :)
	let $characters := collection($collection-path)/character
(: filter the collection to return only those items that satisfy the filter :)
	let $filter-query :=  concat("for $character in $characters where ",$filter," return $character")
	let $filter-results := util:eval($filter-query)
(: order those items in the desired sequence :)
	let $sort-query := concat("for $character in $filter-results order by ",$order," return $character")
	let $sort-results := util:eval($sort-query)
(: determine number of total filtered records :)
	let $total-records := count($sort-results)
	let $old-record-count := class:set-prop($object,"total-records",$total-records)
(: extract from that the current page of content :)
	let $page-results := subsequence($sort-results,($page - 1)* $page-size + 1,$page-size)
(: output the results :)
	let $cached := class:set-prop-node($object,"cache",<cache>{$page-results}</cache>)
	return $page-results
	};

The only "non-standard" part of this approach is the use of the util:eval() statement, which is not an XQuery command but an eXist one (again, most sophisticated XQuery engines support something like this). In essence, util:eval() serves as a mechanism for evaluating a string containing an XQuery expression in the current context and generating output for that content. The $characters sequence returns all <character> elements from the characters collection and makes them available for iterations via the $character variable - note that each of the query strings used the latter $character variable to set the context for filtering or sorting, i.e., for a filter like

<filter>$character/Gender = 'Female'</filter>

the value of $filter-query becomes:

for $character in $characters where $character/Gender = 'Female' return $character

This trick unfortunately is much harder to pull off without eval capability.

One aspect of all of this is to understand that at no point is asset construction going on - the for loop always returns just a bare $character element, which will be optimized by the XQuery engine to just pass the reference to the XML element itself back rather than constructed content. This effectively means that this three pass filter is very fast, since you don't have to worry about node parsing and serialization at each stage.

An additional piece of information that can be useful for building interfaces is the total number of filtered records. This is, unfortunately, information that can only be determined after the selection has been filtered, so it is necessary to update the object to reflect this new count. This accomplished with the statements:

(: determine number of total filtered records :)
	let $total-records := count($sort-results)
	let $old-record-count := class:set-prop($object,"total-records",$total-records)

Once the final sequence is "paged", the last step in the process is to "cache" the results in the object itself, done in the statement:

let $cached := class:set-prop-node($object,"cache",<cache>{$page-results}</cache>)

The content can be (and in fact is) passed as the result of this function, but by caching the content, it becomes possible to store this intermediate result and so reduce the need of passing external data. It also means that (with some additional coding not covered in this article) you could store the object referenced by a cookie, and the next time you make the query, this object will be reloaded with the cached results used instead of the generated ones if the session hasn't expired yet. This can make a major difference in performance when you're serving up data as part of a website.

Rendering Output

As I was working on this article, I debated whether rendering should in fact be part of the process-data() pipeline, but came to the conclusion that it probably shouldn't be, if only because the organizational structure of rendering is somewhat different than what it is for the rest of the pipeline.

There are a number of different ways that content can in fact be rendered - you can use XQuery routines, XSLT routines, make web services calls that render in a synchronous process or use an external functional call, or you can perform such actions inline. While inline coding is usually fastest to write, it has a number of disadvantages, the biggest of which being that inline code can be very difficult to maintain and doesn't provide a lot of flexibility. This suggested in this particular project that the most effective approach would be to make the rendering facility (the view mechanism) a part of the description for the class itself. This is handled with the <viewmodes> category:

<viewmodes>
   <view name="xml" type="xquery" path="xmldb:exist:///db/characters/to-xml.xq"/>
   <view name="table" type="xslt" path="xmldb:exist:///db/characters/to-table.xsl"/>
   <view name="list" type="xslt" path="xmldb:exist:///db/characters/to-list.xsl"/>		
   <view name="page" type="xslt" path="xmldb:exist:///db/characters/to-page.xsl"/>
   <view name="record" type="xslt" path="xmldb:exist:///db/characters/to-record.xsl"/>
   <view name="atom" type="xslt" path="xmldb:exist:///db/characters/to-atom.xsl"/>
</viewmodes>

The advantage to putting it here is that you can establish default behaviors for the characters class, but can also modify it on a per instance basis as necessary.

Each view element consists of a name that identifies the particular view, the type of view to be applied, and the path to the code for handling the view. Thus, if the view property is set as "table", then this indicates that the renderer should run an XSLT transformation located at /db/characters/to-table.xsl (the xmldb:exist:/// protocol indicates that this source comes from the database, and may differ for different implementations).

The characters:render() method can use this information to determine what method to use to actually generate the output:

declare function characters:render($object as node()) as node()*{
	let $view := class:get-prop($object,"view")
	let $view-node := $object/viewmodes/view[@name = @view]
	let $result := (
		if ($view-node/@type = "xquery") then
			util:eval(as:anyURI($view-node/@path)
		else <error/>,
		if ($view-node/@type = "xslt") then
			let $xslt := doc($view-node/@path)
			let $data := transform:transform($object,$xslt,<parameters/>)
			return $data
		else <error/>)
	return if ($result != <error/> then $result else <div>Render cannot be completed.</div>
	};

When util:eval() receives a single argument of type xs:anyURI, it will load that particular XQuery script then attempt to evaluate it in the current context. This means that the $object declaration will be available to this script. The following script in to-xml.xq illustrates how this context may be used:

<characters>
   {for $character in $object/cache/character return $character}
</characters>

The XSLT is a little more complex (such as for a table, in to-table.xsl), but not necessarily dramatically so:

<xsl:stylesheet xmlns:xsl="http://www.w3.org/1999/XSL/Transform" xmlns:h="http://www.w3.org/1990/xhtml" version="1.0">
	<xsl:template match="/">
		<xsl:apply-templates select="object"/>
	</xsl:template>
	<xsl:template match="object">
		<h:div id="table-display">
			<h:div id="caption"><xsl:value-of select="concat('Page ',$page,' of ',1 + ($total-records div $page-size))"/><h:div>
		<xsl:apply-templates select="cache"/>
		</h:div>
	</xsl:template>
	<xsl:template match="cache">
			<h:table>
				<h:tr>
					<h:th>Name</h:th>
					<h:th>Gender</h:th>
					<h:th>Vocation</h:th>
					<h:th>People</h:th>
					<h:th>Alignment</h:th>
					<h:th>Level</h:th>
					<h:th>STR</h:th>
					<h:th>INT</h:th>
					<h:th>WIS</h:th>
					<h:th>DEX</h:th>
					<h:th>CON</h:th>
					<h:th>CHA</h:th>
				</h:tr>
				<xsl:apply-templates select="$character"/>
			</h:table>
			</div>
		</h:div>
	</xsl:template>
	<xsl:template match="character">
		<h:tr>
			<h:td><xsl:value-of select="name"/></h:td>
			<h:td><xsl:value-of select="gender"/></h:td>
			<h:td><xsl:value-of select="vocation"/></h:td>
			<h:td><xsl:value-of select="people"/></h:td>
			<h:td><xsl:for-each select="alignment"><xsl:value-of select="concat(lnc-align,' ',gne-align)"/></h:td>
			<h:td><xsl:value-of select="level"/></h:td>
			<xsl:for-each select="attributes/*"><h:td><xsl:value-of select="."/></h:td>
		</h:tr>
	<xsl:template>
</xsl:stylesheet>

Either XQuery or XSLT can be used to handle the generation of the output, depending upon which is available and which you feel more comfortable using (this isn't the place for an XQuery vs. XSLT argument, as both have their places).

There is one fairly instructive design pattern here that may not be that obvious - note that until the render stage, there's no point in this particular class where the specific structure of the XML of a character is a factor. In this particular case, that structure is fairly simple:

<character>
<name>Aleria</name>
<gender>Female</gender>
<vocation>Mage</vocation>
<people>Human</people>
<alignment>
<lnc>Lawful</lnc>
<gne>Chaotic</gne>
</alignment>
<level>5</level>
<hit-points>24</hit-points>
<attributes>
<str>12</str>
<int>18</int>
<wis>16</wis>
<dex>14</dex>
<con>9</cont>
<cha>17</cha>
</attributes> </character>

The significance, however, is that by effective design, you can push a significant amount of the specifics for a given class out to the margins and create a more generalized working class that then uses the parameters inherent in the use of XQuery OOP to handle the final processing. (I'll leave this as grist for an exercise, though it's something you should think about as you are designing your own applications).

Organization

You may be forgiven for looking at this article and thinking that there is a vast mish-mash of scripts intertwined here. One of the other advantages that object-oriented programming offers is the ability to better organize your content. Realistically, there are two classes at work here - the Character class, and the ... um, well ... Class class, although the latter exists only to host "static" methods (ones that don't need some kind of formal <class> proxy in order to function). While both can be declared in the same document, it is in general preferable to separate "classes" by their respective namespaces, and then use XQuery's import facility in order to load one into the other. Thus, the Class namespace would actually have a structure that looks like this (class.xq):

module namespace class="http://www.metaphoricalweb.org/xmlns/class";

declare function class:get-prop($object-decl as node(),$prop as xs:string){
     let $instance := $object-decl/instance
     let $property := $instance/state/*[name(.)=$prop]
     return if ($property/*) then $property else data($property)
     };

declare function class:set-prop($object-decl as node(),$prop as xs:string,$value as xs:anyAtomicType) as xs:anyAtomicType {
     let $state := $object-decl/instance/state
     let $property := $state/*[name(.)=$prop]
     let $old-value := class:get-prop($object-decl,$prop)
     let $assignment := (update value $property with $value)
     return $old-value
     };

declare function class:set-prop-node($object-decl as node(),$prop as xs:string,$value as node()) as node() {
     let $state := $object-decl/instance/state
     let $property := $state/*[name(.)=$prop]
     let $old-value := util:deep-copy(class:get=prop($object-decl,$prop))
     let $assignment := (update replace $property with $value)
     return $old-value
     };

The primary difference here from the previous statements is the use of the module declaration at the top:

module namespace class="http://www.metaphoricalweb.org/xmlns/class";

which identifies this script as a library module rather than a running script.

The Characters class in turn can also be represented as a module (characters.xq), but one that imports the Class class:

module namespace characters = 'http://www.metaphoricalweb.org/xmlns/characters';
import namespace class = "http://www.metaphoricalweb.org/xmlns/class" at "class.xq";
declare namespace util = "http://exist-db.org/xquery/util";

declare function characters:new($filter as xs:string,$order as xs:string,$page as xs:integer,$page-size as xs:integer,$view as xs:string) as node(){
	let $state-object := 
<class>
	<name>characters</name>
	<class-id>uri:uuid:548cbc92-62dd-4401-afc6-158ef4807acf</class-id>
	<description>A class for manipulating character lists.</description>
	<class-path>/db/characters</class-path>
	<viewmodes>
		<view name="xml" type="xquery" path="xmldb:exist:///db/characters/to-xml.xq"/>
		<view name="table" type="xslt" path="xmldb:exist:///db/characters/to-table.xsl"/>
		<view name="list" type="xslt" path="xmldb:exist:///db/characters/to-list.xsl"/>
		<view name="page" type="xslt" path="xmldb:exist:///db/characters/to-page.xsl"/>
		<view name="record" type="xslt" path="xmldb:exist:///db/characters/to-record.xsl"/>
		<view name="atom" type="xslt" path="xmldb:exist:///db/characters/to-atom.xsl"/>
	</viewmodes>
	<instance>
		<instance-id>uri:uuid:{util:uuid()}</object-id>
		<date-created>{current-dateTime()}</date-created>
		<date-updated>{current-dateTime()}</date-updated>
		<state>
      		<collection-path>/db/characters</collection-path>
			<filter>{$filter}</filter>
			<order>{$order}</order>
			<page xs:type="xs:integer">{$page}</page>
			<page-size xs:type="xs:integer">{$page-size}</page-size>
			<total-records>0</total-records>
			<view>{$view}</view>
			<cache/>
		</state>
	</instance>
</class>
	return $state-object
	};

declare function characters:process-data($object as node()) as node()*{
(: get state variables :)
	let $collection-path := class:get-prop($object,"collection-path")
	let $filter := class:get-prop($object,"filter")
	let $order := class:get-prop($object,"order")
	let $page := class:get-prop($object,"page")
	let $page-size := class:get-prop($object,"page-size")
(: retrieve the collection :)
	let $characters := collection($collection-path)/character
(: filter the collection to return only those items that satisfy the filter :)
	let $filter-query :=  concat("for $character in $characters where ",$filter," return $character")
	let $filter-results := util:eval($filter-query)
(: order those items in the desired sequence :)
	let $sort-query := concat("for $character in $filter-results order by ",$order," return $character")
	let $sort-results := util:eval($sort-query)
(: determine number of total filtered records :)
	let $total-records := count($sort-results)
	let $old-record-count := class:set-prop($object,"total-records",$total-records)
(: extract from that the current page of content :)
	let $page-results := subsequence($sort-results,($page - 1)* $page-size + 1,$page-size)
(: output the results :)
	let $cached := class:set-prop-node($object,"cache",<cache>{$page-results}</cache>)
	return $page-results
	};

declare function characters:render($object as node()) as node()*{
	let $view := class:get-prop($object,"view")
	let $view-node := $object/viewmodes/view[@name = @view]
	let $result := (
		if ($view-node/@type = "xquery") then
			util:eval(as:anyURI($view-node/@path)
		else <error/>,
		if ($view-node/@type = "xslt") then
			let $xslt := doc($view-node/@path)
			let $data := transform:transform($object,$xslt,<parameters/>)
			return $data
		else <error/>)
	return if ($result != <error/> then $result else <div>Render cannot be completed.</div>
	};

In addition to this, most XQuery engines also require that local libraries still be declared where they're used - in this case, a number of util: functions are used within the characters.xq module, and so should at a minimum should be identified as belonging to known namespaces (XQuery generally does not internally recognized namespace prefixes, only namespaces themselves, as the key to adding functions, which is why you only have to identify (that is, map) the namespace to it's associated prefix rather than importing an existing library. In the case of our custom Class library, however, since this doesn't yet exist in the system, you have to import it.

The final step in this process should be to import the Characters library and actually handle the formal processing. The following shows how you could pass parametric values from either form posts or query string GET statements, in main.xq:

import namespace characters = 'http://www.metaphoricalweb.org/xmlns/characters' at "characters.xq";
declare namespace request = "http://exist-db.org/xquery/request";

let $characters := characters:new(
	request:get-parameter("collection-path","/db/characters"),
	request:get-parameter("filter",""),
	request:get-parameter("order","$character/Name ascending"),
	request:get-parameter("page",1),
	request:get-parameter("page-size",20),
	request:get-parameter("view","table")
	)
let $data := characters:process-date($characters)
let $output := characters:render($characters)
return $output

In this case, you do not need to import the class: namespace - it's not invoked directly in any method in this script, though you do need to import the characters: namespace because it is. Similarly, because you are using the request:get-parameter() method() (see below), you also need to declare the request: namespace (an eXist namespace) though you don't have to import it since it's already a global system namespace.

The request:get-parameter() function takes two arguments - the first being the name of the parameter in question, the second the value that this parameter should take if it is not otherwise supplied, either as a query string parameter or a form submission parameter. While this is part of eXist, similar capabilities exist in many other XML database and XQuery engines.

Wrapping Up with XRX

The concept of using XQuery as a mechanism for generating web pages is a comparatively new one in the XML Database and XQuery engine world, but the benefits to do so should be fairly obvious. Indeed, there's been a new meme that's begun appearing under the heading XRX, which stands for XQuery, REST, and XForms, though that last particular X could also stand, just as effectively, for XMLHttpRequestObject, the central component in the AJAX world.

XRX design is obviously quite potent - in essence you are replacing complex database calls using SQL or LDAP with XQuery calls talking to XML collections (either "real" or virtualized from relational databases) then using the filter/sort/page/render pipeline to generate pages and portions of pages.

Object Oriented XQuery, as this section points out, does not necessarily provide a way or writing that much code in the short term, but it does significantly open up ways of reducing the amount of code written for larger projects by more effectively modularizing the code. Moreover, it provides a very useful "integration point" between people working with SQL, with XML and with OOP languages by providing at least some of the strengths of OOP without necessarily tying it into some of the major weaknesses.

This particular article grew out of a late night "Intro to XQuery" session at the 2008 Balisage conference, and will be one of a series of articles on XRX related development moving forward. I wanted to thank Debbie Lapeyre, B. Tommie Usdin, Steven Newcomb and C.M. Sperberg-McQueen in particular for inviting me there and letting me give the first formal presentation of the conference, where I covered at least some of these ideas.

Kurt Cagle is Online Editor for O'Reilly Media, specializing in XML, Publishing and Web Development. He lives in Victoria, British Columbia, and is tickled pink that his kids are going off to school after a long summer at home.






        


    

You might also be interested in:


3 Comments

Object Oriented XQuery, as this section points out, does not necessarily provide a way or writing that much code in the short term, but it does significantly open up ways of reducing the amount of code written for larger projects by more effectively modularizing the code. Moreover, it provides a very useful "integration point" between people working with SQL, with XML and with OOP languages by providing at least some of the strengths of OOP without necessarily tying it into some of the major weaknesses.

Can you recommend a good book for this topic?

useful advice, very clearly explained - thank you.

Popular Topics

Archives

Or, visit our complete archives.

Recommended for You

Got a Question?