Preface: Limitations of the SQL analogy
XQuery is widely known as a query language for XML. In the domain of web application development—where the classic technology stack is a relational database accessible via SQL paired with one of several popular programming languages (Java, .NET languages, Perl, PHP, Python, Ruby, etc.)—there is far less awareness that XQuery is actually a full-fledged, functional programming language. Even less recognized is the fact that XQuery, with a limited number of implementation-provided extensions, can serve in a web development context as both the database query language and the server-side programming language. When describing XQuery to web developers or project managers accustomed to the classic technology stack, it is tempting to define XQuery in a way that someone in that context can easily latch onto: XQuery is to XML databases as SQL is to relational databases. This commonplace analogy seems harmless enough initially, but it is actually misleading and counter-productive if it is not immediately qualified and expanded. Defining XQuery as a query language for XML is the truth but not the whole truth. When you have data in XML form that needs to be delivered in some way on the web, using XQuery as the server-side programming language has significant practical advantages. After briefly describing those advantages, this paper will lay out techniques for developing web applications in XQuery—techniques that can reduce complexity and help developers produce well-organized, testable, portable code that will be comparatively easy to build upon and maintain over time.
Advantages of XQuery for web delivery of XML content
XQuery is specifically designed to work hand-in-glove with XML data natively. There is no need to provide a mapping between the data and the programming language, such as the “object-relational mapping” that is inherently necessary when working in an object-oriented programming language in conjunction with a relational database. In fact, the need for such a mapping is not limited to relational data; an analogous kind of mapping—let’s call it an “object-document mapping”—is equally necessary when using an object-oriented language to work with data in XML form. Whether relational or XML-based, the data model still has to be translated or mapped to the facilities and affordances of the programming language. The one is not inherently designed to work with the other, and in fact the data and the language collide in a “clash of paradigms” (Seiferle, p. 20), resulting in the so-called “impedance mismatch” (Kaufmann, §1; Seiferle, p. 20-21). This gap between the data and the language must be bridged, adding a layer of complexity to the technology stack of the web application. With XQuery, there is no data-language gap and therefore no bridge.
If you don’t need a bridge, then you don’t need a web development framework (Ruby on Rails, CakePHP, Django, etc.) to provide it. Although such frameworks usually provide services in addition to object-relational mapping (scaffolding, templating, etc.; Seiferle, p. 3-4), all things considered, eschewing these frameworks actually provides real advantages by entirely sidestepping layers of complexity that frameworks can only hide, not eliminate (Seiferle, p. 22). Not only do such frameworks lock developers into the assumptions and requirements the framework espouses, they also invariably evolve over time, sometimes rapidly. Sooner or later it becomes necessary to upgrade the framework itself, either because there is a compelling reason (such as a new feature or a needed bug fix) or because there is an absolute necessity (such as a show-stopping incompatibility between the framework and a change to the environment, like a newer version of the database server, programming language or operating system). The inherent technological complexity and multiple moving parts of any given web development framework can end up putting developers into a hamster’s wheel of release notes, upgrades, deprecated features, and incompatibilities—all of which add up to time spent on mere maintenance rather than intentional refactoring or development of new or improved functionality. By contrast, the core XML-related specifications—including XQuery and its accompanying and supporting specs—have proven exceptionally stable. Their evolution has been characterized not only by infrequent updates but also by a high degree of backward compatibility.
The differences between these technology stacks have concrete, real-world consequences. Using a language designed for the data it is working with, thereby avoiding “the technology jungle of mixing different technologies and data models” (Kaufmann, §7), prevents a good deal of mundane work and frees developers to focus on the domain and goals of the application rather than an ever-changing web development environment.
Techniques for web application development with XQuery
Utilizing MVC
Despite the drawbacks of web development frameworks just described, there is one common feature that is just as relevant for XQuery web development as for any other language: the utilization of MVC (model, view, controller). There are countless readily available resources of varying depth, from books to blog posts, that describe MVC, including and especially in the domain of web application development, where the use of MVC has become wildly popular since the advent of Ruby on Rails. The problem is that as a concept MVC is general and flexible enough to allow many possible variations when the concept is put into practice. Various methodologies have proliferated, leaving the whole idea of MVC seeming rather arcane to the uninitiated—and to the initiated, for that matter. (As the adage goes, ask five web developers how they use MVC and you’ll get six different answers.) MVC is commonly called an “architecture,” but that term is already overburdened and therefore unilluminating. However, much of this confusion is unnecessary, because in its essence MVC is just a technique for organizing code, with the principal goal being the classic “separation of concerns” whereby presentation code has no knowledge of how data is stored and accessed, and database-related code has no knowledge of how data is presented to the user.
If for now we set aside the variations and elaborations of MVC, a few core principles remain. Models understand how data is represented (modeled) and handle database interaction. Views construct the presentation of data to the user. Controllers receive input and respond accordingly, utilizing models and views to do so.
Admittedly this (deliberately minimal) definition leaves out some important decisions. Any non-trivial web application will need multiple views, but some variations of MVC allow, or even require, multiple controllers or multiple models. Other methodologies differ as to which MVC component(s) can or cannot call upon the model for database interaction. Some web development frameworks, and indeed some web developers themselves, can be quite puristic about these differentiations, but stridency in this context is unhelpful. How exactly to organize code within an MVC approach should be based more on the nature and goals of a particular web application than on abstract principles. A more complex application may merit a more complex use of the MVC components. This is not to say that we should be lax or inconsistent in our implementation of MVC, only that we should select an MVC methodology based on practical considerations. To get started with MVC, there is nothing wrong with having one controller and one model, along with a view for each main kind of HTTP response the application provides.
The modular nature of XQuery lends itself easily and directly to the MVC concept. Each model, view, and controller is implemented as an XQuery library module.
Model
The model is responsible for interacting with the database; no other component should create, read, update or delete data (think “no CRUD outside the model”). In this sense it acts as a kind of API (or like an encapsulated object, in object-oriented terms), which other modules call upon without needing to know anything about the database or how to interact with it. Ideally this separation of concerns is enforced to the point that one could swap out the underlying database server for a different one without touching any code in the controller(s) or views. (Conversely, we should be able to overhaul the presentation or adjust business logic without touching any code in the model(s).)
However, in the context of this discussion, in which data is in XML form, there is an important additional consideration. We have to decide whether the model should not only handle all database interaction but also handle all navigation of the structure of the XML documents, which is to say, whether only the model should contain XPath expressions. This decision is essentially a question of how strictly we want to separate knowledge of the document structure from the knowledge of how the document content should be formatted and presented. For example, let’s say we have this kind of markup, containing information about a book:
<?xml version="1.0" encoding="UTF-8"?> <doc> <metadata> <id>ab1geschichtedes03gind</id> <name>Gindely, Anton, 1829-1892</name> <title>Geschichte des dreissigjährigen Krieges</title> <date>1869</date> <language code="de">German</language> <format>Book</format> <callNumber source="oclc">D258 .G49</callNumber> <class>History</class> <topics> <topic>Thirty Years' War, 1618-1648</topic> </topics> </metadata> </doc>Let’s also assume that since the model handles database interaction, we have set up a function named
get-xml-doc
in the model to retrieve from the database any
given XML document in its entirety, given its unique identifier. To get values such
as
book title and author name that we need to display, one option is to allow the
presentation code to have knowledge of the document structure and grab the data directly:
import module namespace m = "http://balisage.net/ns/Bal2016murr0319/model" at ... ; ... let $doc := m:get-xml-doc("ab1geschichtedes03gind") let $title := fn:string($doc/doc/metadata/title) let $name := fn:string($doc/doc/metadata/name)Alternatively we can require the model alone to have knowledge of document structure and provide functions for retrieving the data from the model. If the model has these functions:
module namespace m = "http://balisage.net/ns/Bal2016murr0319/model"; (:~ Returns the book title as a string. :) declare function m:get-title($doc as document-node()) as xs:string { fn:string($doc/doc/metadata/title) }; (:~ Returns the author name as a string. :) declare function m:get-name($doc as document-node()) as xs:string { fn:string($doc/doc/metadata/name) };then the presentation code doesn’t need to carry any knowledge of the document structure:
import module namespace m = "http://balisage.net/ns/Bal2016murr0319/model" at ... ; ... let $doc := m:get-xml-doc("ab1geschichtedes03gind") let $title := m:get-title($doc) let $name := m:get-name($doc)In an example this simple, the difference is trivial, but in a real-world web application the XPath expressions will likely be more elaborate as well as broadly distributed throughout a sizable body of presentation code spanning multiple XQuery modules. Moreover, the same XPath expressions will typically be needed more than once throughout the presentation code. Abstracting knowledge of the markup into the model enforces a stricter separation of concerns. With this approach, if the markup schema changes, only the model would need to be refactored, since knowledge of the markup is confined to the model.
As mentioned above, there is nothing inherently wrong with having a single model for a given web application, but there are some use cases for separating database-related code into multiple models. In situations where a web application needs to work with documents in multiple XML markup languages, it might make sense to have a different MVC model (that is, a discrete XQuery module) for each document type. Similarly, if an application includes full-text searching capability, it is logical to group search-related functions into a separate model. This is especially true if your chosen implementation provides its own functions for searching (as opposed to implementing the W3C XQuery Full-Text recommendation), so that in addition to improving code organization per se the search-specific model is isolating implementation-defined functions, to improve code portability and sharing (more on this below).
View
The role of the view is to provide the presentation, whether for human- or
machine-readability. For example, a very commonplace pattern (used by all manner of
web
applications from Amazon to Zappos) is to provide searching, leading to search results,
leading to an item-level page showing all relevant information for a particular item.
In
this approach, the home page containing the search form, the search results page,
and the
item-level page are separate views, each one implemented as its own XQuery library
module
(all of which could be grouped in a single views
directory). In a web
application designed for display in a browser, most views will return an HTML document,
whereas a web service might return data as JSON or XML, which is nonetheless a
view.
Because most web-based user interfaces are designed with shared components such as headers, sidebars and footers, it makes sense to have an XQuery module that serves as a “layout” (or to have multiple layout modules, if more than one page layout is needed within the same application). In this scenario the view constructs one or more HTML elements constituting the main content of a page, then passes the element(s) (such as a <div>) to a function in the layout module, which returns a complete HTML document with the main content inserted in the appropriate place within the encompassing page layout. The view then returns the complete HTML document.
Controller
The controller is responsible for receiving user input and responding accordingly. It “controls” what happens in response to input and how the model(s) and views are involved in that process. In a web application context, the controller accesses the input from the HTTP request (URL parameters, form data, request headers, cookies) and then calls the appropriate view, as indicated by those input values. Since the XQuery language doesn’t provide built-in functions for such HTTP-specific matters, the implementation must supply them, but multiple widely adopted implementations do so. (We will look at some practical examples in BaseX and MarkLogic below.)
Some variations on MVC assert that only the controller should call upon the model to retrieve data, after which the controller passes that data to a view, such that a view never interacts directly with the model. Other MVC variants allow the knowledge of what data is needed by a given view to reside in the view itself, giving it the authority to call the model to get the data it needs to construct a fully formed presentation of that data. Each approach has its justifications. Each adheres to the principle of the separation of concerns, but in the former method the controller carries the knowledge of what data is needed for each view, while in the latter method the view itself holds that knowledge. In other words, in the first approach the controller is more controlling, and in the second the view is more autonomous and self-aware. (My own preference is for the latter, because it seems reasonable for the view to know what data it needs to display, and because allowing only the controller to request data from the model seems arbitrary. However, I’m only speaking here of retrieving data; when creating/updating/deleting data in the database, the data can and should pass directly from the controller to the model for the insert/update/delete operation, after which the controller calls a “confirmation page” or other appropriate view.)
Ancillary modules
In addition to the core MVC components, additional specialized XQuery library modules are often helpful. A module that acts as a “config file” of global variable declarations allows easy management of settings that might change later or might be needed by multiple modules. Conversely, a “utility” module containing general-purpose, shared functions provides convenience and reduces code duplication. In more complex applications, shared functions that pertain to a particular problem domain or MVC component can be grouped and separated into discrete utility modules.
Keeping functions testable
As shown in Figures 2 and 3 above, when responding to
an HTTP request using XQuery and MVC, the chain of processing forms a loop in which
the
request is routed to the controller, which calls a view, which constructs a fully
formatted
representation and returns it to the controller, which returns the HTTP response.
Throughout
this loop, there will be decision points where a given function in a given module
must have
access to the input values from the HTTP request to make decisions and take actions
accordingly. For example, a “search results” view needs to know how the user wants
the
results sorted, how many results to display at a time, and so on, which can only be
known by
checking the input from the HTTP request. How best to make those values available
throughout
the codebase is not inherently obvious. One option is simply to call the
implementation-provided functions for accessing these input values wherever such a
value is
needed. For example, given a URL ending with ?id=abc123
we could do the
following:
(: MarkLogic example :) let $id := xdmp:get-request-field("id")
(: BaseX equivalent of preceding example :) import module namespace request = "http://exquery.org/ns/request"; ... let $id := request:parameter("id")This approach has a major disadvantage: it leaves implementation-specific function calls strewn throughout the codebase, inhibiting code portability and sharing (more on this below).
A less haphazard technique would be to assign the input values to variables using global (prolog-level) variable declarations in a library module, which could then be imported by any controller or view that needs to access those input values. This module would be similar to the “config file” module mentioned above, but instead of supplying predefined values it would provide values retrieved dynamically from the HTTP request. Let’s call it the “parameters” module.
(: MarkLogic example :) (: in the "parameters" module, we put the "id" value in a global variable :) module namespace params = "http://balisage.net/ns/Bal2016murr0319/parameters"; declare variable $id as xs:string? := xdmp:get-request-field("id");To access those values elsewhere in the codebase, we would simply import the parameters module and reference the relevant variable.
(: in the item-level view, we use the previously defined global variable :) import module namespace params = "http://balisage.net/ns/Bal2016murr0319/parameters" at "parameters.xqy"; ... let $id := $params:idThis technique seems reasonable enough and avoids repetitive, dispersed implementation-specific function calls. However, it doesn't solve a deeper problem: functions that access such global variables cannot be independently tested. Unit testing—whereby a single unit of code is tested in isolation from the rest of the program in a deliberately controlled environment, using sample data—is a common and highly advisable practice across many programming domains and languages. In a functional programming language like XQuery, the logical code unit for testing is the function. If a function relies on a global variable containing a value taken from the HTTP request, and if that function gets called (for testing purposes) outside the context of any HTTP request, the value of the global variable will always be empty. Therefore, we have no way to verify the real-world behavior of the function.
This problem can be avoided by providing a function in the controller (since the controller is responsible for receiving user input) that accesses URL parameters or other input values and adds them to an XQuery map (a set of key/value pairs, often called a hash or associative array in other programming languages). The controller can then pass that map to any given view, and each view in turn can pass the map to any functions that need user input values to make determinations about what data is needed, how to format it for display, or other such internal decisions.
(: BaseX example :) (: In the controller ... :) module namespace c = "http://balisage.net/ns/Bal2016murr0319/controller"; import module namespace request = "http://exquery.org/ns/request"; import module namespace item = "http://balisage.net/ns/Bal2016murr0319/views/item" at "views/item.xqm"; (:~ Returns a map containing the HTTP request parameters. :) declare function c:http-params() as map(*) { map:merge( for $name in request:parameter-names() let $value := request:parameter($name) return map:entry($name, $value) ) }; (:~ Calls the appropriate view, based on user input. :) declare function c:get-view() as element(html) { (: get HTTP request parameters :) let $params := c:http-params() return if (map:get($params, "id")) then (: the presence of "id" indicates that the user is requesting the item-level page for this unique identifier :) (: call the item-level view :) item:get-html($params) else if ... (: call some other view :) else if ... (: call some other view :) else (: call the view for the home page ... :) };
(: In the item-level view ... :) module namespace item = "http://balisage.net/ns/Bal2016murr0319/views/item"; (:~ Returns a complete HTML document for displaying a given item. :) declare function item:get-html($params as map(*)) as element(html) { let $id := map:get($params, "id") (: build an HTML document for displaying this item, passing $params to other functions as needed ... :) (: return HTML ... :) };To test any given function artificially, outside the context of an actual HTTP request, we can simply construct a map containing sample input and pass it to the function being tested.
(: In the testing module ... :) import module namespace item = "http://balisage.net/ns/Bal2016murr0319/views/item" at "../views/item.xqm"; let $params := map { "id": "abc123" } let $html := item:get-html($params) (: Now we can verify the HTML returned by item:get-html ... :)
Improving code portability and sharing
As mentioned above, running a web application written in XQuery requires implementation-specific extensions—not necessarily to the language itself, but to provide functions specific to the HTTP context, for such things as accessing URL parameters of GET requests and setting HTTP response headers. As a result, one’s codebase can quickly become peppered with implementation-specific function calls, resulting in code that not only requires extensive refactoring to port a web application from one XQuery implementation to another, but also inhibits sharing code with others, thereby promoting fragmentation within the XQuery community (Retter, §1.2). There are, however, some techniques that help alleviate this problem.
Using available standardizations
One such technique is to utilize what standardizations are available for XQuery web application development. The EXQuery organization (exquery.org) has proposed several specifications for standardizing functionality across XQuery implementations. Some of these proposals are specific to XQuery web application development, and of these the most widely implemented is RESTXQ. Proposed by Adam Retter in 2012, RESTXQ utilizes XQuery 3.0 annotations to associate URLs with XQuery functions (Retter). In other words, RESTXQ standardizes URL rewriting, the process whereby a clean, user-friendly URL accessed by the user (or by a program accessing a web service such as a REST API) is translated into a different URL that is actually used by the web application behind the scenes. More accurately, RESTXQ doesn't standardize URL rewriting so much as obviate the need for it altogether; instead of rewriting URLs from user-friendly to code-friendly, RESTXQ maps URLs to XQuery functions.
For example, returning to the commonplace behavior described above whereby a web application performs a search, leading to search results,
leading to an item-level page for each result, let’s say that any given item-level
page is
indicated by a URL in the format /id/abc123
where abc123
is a
unique identifier for a particular item. In an implementation that supports URL rewriting,
such as eXist or MarkLogic (Retter, §3.1.2 and §3.2.2), we could take
/id/abc123
and translate it to ?id=abc123
before handing it off
to the XQuery processor, so that id
is available as an HTTP request parameter
name.
(: MarkLogic example :) let $url:= xdmp:get-request-url() let $regex-id := "/id/(.+)/?" return if (fn:matches($url, $regex-id)) then (: extract identifier :) let $id := fn:replace($url, fn:concat("^.*", $regex-id, ".*$"), "$1") (: replace "/id/whatever" with "/" (implying "/default.xqy") and append "id=whatever" as a URL parameter :) let $separator := if (fn:contains($url, "?")) then "&" else "?" let $start := fn:replace($url, $regex-id, "/") return fn:concat($start, $separator, "id=", $id) (: else if ... :) (: else if ... :) else $urlSuch URL rewriting code tends to be idiosyncratic and cumbersome, “creating a spaghetti of if/else statements” (Retter, §3.2.2). With RESTXQ, by contrast,
/id/abc123
can be simply and directly associated with a function, like so:
(: BaseX example :) declare %rest:path("/id/{$id}") function c:get-item-view($id as xs:string) { (: the variable $id has the value "abc123" :) ... };
The RESTXQ approach is concise and straightforward, and it locates the URL mapping code at the function declaration itself (not in a separate “rewrite engine” module), leading to code that is easier to read and maintain. RESTXQ is supported in BaseX, eXist, and MarkLogic (Walmsley, p. 430), making it effectively a de facto standard. As a result, the code is portable across multiple implementations and shareable among developers and projects.
The use of RESTXQ can easily be incorporated into an MVC approach to code organization by adding RESTXQ annotations to functions in the controller (since the controller receives input from the HTTP request and takes action accordingly). Fleshing out the preceding example a little more:
(: BaseX example :) module namespace c = "http://balisage.net/ns/Bal2016murr0319/controller"; import module namespace item = "http://balisage.net/ns/Bal2016murr0319/views/item" at "views/item.xqm"; declare %rest:path("/id/{$id}") %output:method("html") %output:version("5.0") function c:get-item-view($id as xs:string) as element(html) { (: the variable $id has the value "abc123" :) (: call the item-level view :) item:get-html($id) };
Isolating implementation-specific functions
The main barrier to code portability across implementations and code sharing across the XQuery web development community is the necessity of implementation-provided functions. One technique to alleviate this problem is to abstract and isolate such functions. This goal can be achieved by creating one or more library modules containing generically named functions that internally utilize the corresponding implementation-specific functions. Then, throughout the codebase, whenever implementation-provided functionality is needed, we simply call the applicable generic function instead of the implementation-specific one. This approach creates a layer of abstraction, separating and hiding implementation-dependent code from the rest of the codebase. For example, we could create an “implementation” module and set up a generically named function for converting HTTP request parameters to an XQuery map, as shown above in a previous code example. Such a module written for BaseX would look like this:
xquery version "3.1"; (: BaseX example -- this module's filename is implementation-basex.xqm :) module namespace imp = "http://balisage.net/ns/Bal2016murr0319/implementation"; import module namespace request = "http://exquery.org/ns/request"; declare function imp:http-params() as map(*) { map:merge( for $name in request:parameter-names() let $value := request:parameter($name) return map:entry($name, $value) ) };The same functionality written for MarkLogic would look like this:
xquery version "1.0-ml"; (: MarkLogic example -- this module's filename is implementation-ml.xqy :) module namespace imp = "http://balisage.net/ns/Bal2016murr0319/implementation"; declare function imp:http-params() as map:map { let $map := map:map() let $empty := for $name in xdmp:get-request-field-names() let $value := xdmp:get-request-field($name) return map:put($map, $name, $value) return $map };Elsewhere in the codebase, whenever a map of HTTP parameters is needed, we can import the module appropriate to our current implementation and call the generically named function,
imp:http-params()
, rather than calling the implementation-specific function
directly:
(: BaseX example :) module namespace c = "http://balisage.net/ns/Bal2016murr0319/controller"; import module namespace imp = "http://balisage.net/ns/Bal2016murr0319/implementation" at "implementation-basex.xqm"; (: somewhere in some function ... :) let $params := imp:http-params()To target MarkLogic, the controller code would be identical except that the filename would be
implementation-ml.xqy
for the module import. Each “implementation” module
acts as a kind of API for the rest of the web application code, such that we can change
the underlying implementation without changing the outward-facing interface of the
API.
With this technique, sharing XQuery code from this web application with an individual
or
project using a different XQuery implementation, or even porting the entire application
to
a different implementation, could potentially only require swapping out one
“implementation” module for another. Such a module could, of course, be utilized across
multiple applications, rendering more and more of one’s code
implementation-independent.
Conclusion
When you have XML content that needs to be presented on the web, developing web applications with XQuery has notable advantages over the traditional technology stack and its accompanying web development frameworks. The techniques for web application development with XQuery described here are straightforward to apply, but they can have powerful and wide-ranging effects on the resulting code, leaving it more organized, readable, maintainable, testable, portable, and shareable.
References
[Kaufmann] Kaufmann, M., & Kossmann, D. (2009). Developing an enterprise web application in XQuery. Retrieved July 5, 2016, from https://www.semanticscholar.org/paper/Developing-an-Enterprise-Web-Application-in-XQuery-Kaufmann-Kossmann/6ddcffdaa35a836d21a73acf6b254e6ebc2b94e7. doi:https://doi.org/10.1007/978-3-642-02818-2_39
[Retter] Retter, A. (2012). RESTful XQuery: Standardised XQuery 3.0 annotations for REST. Retrieved July 5, 2016, from http://adamretter.org.uk/papers/restful-xquery_january-2012.pdf
[Seiferle] Seiferle, M. (2012). Implementing web applications using XQuery: XML from front to back. Retrieved July 5, 2016, from http://files.basex.org/publications/Seiferle%20[2012],%20Implementing%20Web%20Applications%20Using%20XQuery.pdf
[Walmsley] Walmsley, P. (2016). XQuery: Search across a variety of XML data (2nd ed.). Sebastopol: O’Reilly.