This post was orginally published at Ngoc's blog in Vietnamese
Using ActiveRecord is convenient due to its ease of use and richness of plugins. However, instead of a relational DB it can also be used with JCR. Let's try that for a simple blog application based on Rails.
Calling JCR API everywhere is a bit painful. Let's extend ActiveRecord::Base to write a simple object-content mapper. If you already have a Rails application and you want to replace the relational DB with JCR and you have been following the "fat model, thin controller, stupid view" best practice, doing this minimizes code changes to only some parts of the model. The only disavantage is that you can no longer use any ActiveRecord plugin which talks to relational DB.
Structure
Jackrabbit is used as the transient JCR server (actually any JCR server will do), and Rails connects to it through RMI. Generally there are two ways to combine Java and Ruby: bring Ruby to Java or bring Java to Ruby. There's already an article about using "JRuby". Let's use "Rjb" for a change.
We do not need a relational DB. To prevent Rails from connecting to a DB, edit config/environment.rb to remove ActiveRecord from config.frameworks (we still require ActiveRecord in the object-node mapper).
Our models will extend class JCR defined in jcr.rb, instead of ActiveRecord::Base.
/blog | +- app | | | +- controllers | | | | | +- articles_controller.rb | | | +- models | | | +- article.rb [<- extends JCR] | +- lib | | | +- java | | | | | +- [Jackrabbit JAR files] | | | +- jcr.rb [<- class JCR, the object-content mapper] | +- script | | | +- jcr [<- starts Jackrabbit] | +- [other things]
script/jcr
This script starts the Jackrabbit server on port 1100.
require 'rubygems'
require 'rjb'
LIB_DIR = File.dirname(__FILE__) + '/../lib'
CLASS_PATH = Dir.glob("#{LIB_DIR}/*.jar").join(File::PATH_SEPARATOR)
Rjb::load(CLASS_PATH, [])
JString = Rjb::import('java.lang.String')
LocateRegistry = Rjb::import('java.rmi.registry.LocateRegistry')
Repository = Rjb::import('javax.jcr.Repository')
SimpleCredentials = Rjb::import('javax.jcr.SimpleCredentials')
TransientRepository = Rjb::import('org.apache.jackrabbit.core.TransientRepository')
ServerAdapterFactory = Rjb::import('org.apache.jackrabbit.rmi.server.ServerAdapterFactory')
repository = TransientRepository.new
factory = ServerAdapterFactory.new
remote = factory.getRemoteRepository(repository)
reg = LocateRegistry.createRegistry(1100)
reg.rebind('jackrabbit', remote)
puts 'Jackrabbit started at rmi://localhost:1100/jackrabbit'
trap('INT') do
puts 'Shutting down...'
repository.shutdown
end
sleep
jcr.rb
class ActiveRecord::BaseWithoutTable < ActiveRecord::Base
...
end
class JCR < ActiveRecord::BaseWithoutTable
JCR_WORKSPACE = 'default'
JString = Rjb::import('java.lang.String')
JRepository = Rjb::import('javax.jcr.Repository')
JSimpleCredentials = Rjb::import('javax.jcr.SimpleCredentials')
JClientRepositoryFactory = Rjb::import('org.apache.jackrabbit.rmi.client.ClientRepositoryFactory')
factory = JClientRepositoryFactory.new
repository = factory.getRepository('rmi://localhost:1100/jackrabbit')
begin
@@session = repository.login(JSimpleCredentials.new('admin', JString.new('admin').toCharArray), JCR_WORKSPACE)
rescue
puts 'Could not connect to JCR server'
exit(-1)
end
def self.session
@@session
end
def session
@@session
end
#-----------------------------------------------------------------------------
# Avoid error ActiveRecord::ConnectionNotEstablished.
def self.table_exists?
false
end
def self.all
node = session.getRootNode.getNode(table_name)
it = node.getNodes
ret = []
while it.hasNext do ret << node_to_object(it.next) end
ret
end
def self.find(attributes_values)
work_space = session.getWorkspace
query_manager = work_space.getQueryManager
query_str = "//#{table_name}/node["
attributes_values.each do |key, value|
query_str << "@#{key.to_s} = '#{value}'"
end
query_str << ']'
query = query_manager.createQuery(query_str, 'xpath')
result = query.execute
it = result.getNodes
ret = []
while it.hasNext do ret << node_to_object(it.next) end
ret
end
def self.first(attributes_values)
find(attributes_values).first
end
#-----------------------------------------------------------------------------
def self.node_to_object(node)
ret = new
columns.each do |c|
property = node.getProperty(c.name)
value = property.send("get#{c.sql_type.to_s.capitalize}")
ret.send("#{c.name}=", value)
end
ret
end
def save
if valid?
node = session.getRootNode.getNode(self.class.table_name)
node = node.addNode('node')
self.class.columns.each do |c|
value = send(c.name)
node.setProperty(c.name, value)
end
session.save
true
else
false
end
end
def update_attributes(attributes_values)
attributes_values.each { |key, value| send("#{key.to_s}=", value) }
save
end
end
This file defines class Jcr which extends "BaseWithoutTable". If you want to get the session to the Jackrabbit server, call Jcr.session.
article.rb
class Article < Jcr
column :user_name, :string
column :title, :string
column :body, :string
validates_presence_of :user_name, :title, :body
def to_param
title
end
end
With the above simple definition, we can have functionalities like finding, saving. For more complex functionality, the mapper must be improved, or we can use the JCR session directly.
Hope you have an idea how to use JCR in your Rails applications.
Source code

COMMENTS