Deploying Triggers in MarkLogic Roxy
Let me save you three hours of annoyance.
Over the last two weeks, I've been building a RESTful backend in MarkLogic (ML) using the Roxy framework. If you're unfamiliar with MarkLogic, it's a pretty excellent XML database, search engine, and application host (where the app is written in xQuery). That's an extraordinary complement because I generally prefer JSON or YAML.
Motivation.
If you use ML, you will eventually encounter a situation like this (let's pretend I'm modeling a file system):
<root>
<folder id="123" file-count="1">
<file id="abc" />
</folder>
</root>
And I want to insert a new folder and have the file-count
updated for all folders in the filesystem XML document:
xquery version "1.0-ml";
module namespace m = "http://rclayton.silvrback.com/xml/blah";
declare function m:update-file-count($folder){
let $count = fn:count($folder/file)
return xdmp:node-replace(
$folder/@file-count,
attribute file-count { $count })
};
let $fs := doc("filesystem.xml")
let $_ := xdmp:node-insert-child($fs/root, <folder id="234" />)
return
for $folder in $fs//folder
return m:update-file-count($folder)
Ok, this won't work. The result will be:
<root>
<folder id="123" file-count="1">
<file id="abc" />
</folder>
<folder id="234" />
</root>
When we should have had the new folder element look like this: <folder id="234" file-count="0" />
.
The new folder we inserted will not be visible because it's being inserted in a transaction, but the read for $folder in $fs//folder
will only see the current version of the document (outside of the transaction).
For these cases, you need to either control transactions more granularly, or use ML triggers. In this case, I want to use a trigger to update file counts every time the document is modified. Writing a trigger is outside the scope of this post, instead, I'll show you how to automate the installation of triggers.
Installing Triggers in ML using Roxy.
The Roxy framework does not have any mechanism for deploying triggers. A quick search will get you to this Github issue and a wiki page suggesting you define the trigger in a Ruby string. GTFO! I like my syntax highlighting and I'm not maintaining xQuery code within a custom build step.
With a little rework of the wiki example, you will find a much cleaner solution:
# This is: "deploy/app_specific.rb"
# Custom configuration for the server.
class ServerConfig
alias_method :default_deploy_modules, :deploy_modules
# Location of the trigger installation scripts.
TRIGGER_INSTALL_PATH = "src/app/triggers/install/"
# Overrides the default deploy_modules function
# (called by 'ml <env> deploy <thing>'),
# adding our own custom installation steps.
def deploy_modules()
# Call the original method.
default_deploy_modules
# Install triggers for the workspace model.
install_trigger "filesystem-triggers.xqy"
end
# Install a trigger in MarkLogic.
# Assumes files are in the "triggers installation path",
# defined in the TRIGGER_INSTALL_PATH above.
# @param trigger_registration_file File that contains
# the installation instructions.
# @return Nothing
def install_trigger(trigger_registration_file)
filepath = TRIGGER_INSTALL_PATH + trigger_registration_file
file_as_string = get_file_as_string(filepath)
result = execute_query(
file_as_string, :app_name => @properties["ml.app-name"])
end
# Read the specified file and return a string with its contents.
# @param filename File to Read
# @return string contents of the file.
def get_file_as_string(filename)
data = ''
File.open(filename, "r").each_line { |line| data += line }
return data
end
end
So now, you can write your triggers in src/app/triggers/
and installation code in src/app/triggers/install/
.
Since this hooks into the default deploy function of the ml
command, registering your trigger is as simple as calling ml <environment> deploy modules
.
Stumbling my way through the great wastelands of enterprise software development.