I finally got around to doing a quick test of calling Berkeley DB Java Edition (JE) from JRuby (JRuby is a 100% pure-Java implementation of Ruby).
Before we get to JE and JRuby you probably want to know the answer to this question: "Why you would want to run Ruby on a JVM?" The answer is threefold:
- Ruby Performance. A large amount of effort has
been put into tuning contemporary JVMs (e.g. Hotspot, Java 6, etc.) and Ruby programmers (through JRuby) can benefit from these
tuning efforts. The JRuby guys have set a goal to make JRuby the fastest Ruby implementation available and Sun is certainly
throwing their weight behind that effort. - Portability. JRuby is a Ruby interpreter that runs anywhere a Java 5 JVM runs. You download it as a single tar.gz and it will run pretty much anywhere.
- Legacy Code. JRuby makes legacy Java apps and libraries available to Ruby programmers (did you ever think you'd see the word "legacy" next to the word "Java"?).
programmers now have a simple, embeddable, ACID storage engine
(JE) available to them.
To test this interoperability, I cobbled together a simple Ruby test program which does the following:
- Opens an Environment, Database, and Transaction
- Creates 10 records with keys 1..10 and marshaled Ruby Time instances as the corresponding data. This uses the Ruby Marshal package for the data binding and the JE Integer binding on the key side. There's no reason why you couldn't use different marshaling packages or methods for keys and data.
- Commits the transaction,
- Performs a Cursor scan to read those 10 records and prints out the Time instances, and
- Searches for and reads the record with key 5 (an arbitrary key) and prints out the Time
instance that is the corresponding data
This was all done at the "base API" level of JE and no modifications to JE were required. I used Transactions in my code, but there's no reason that you need to. Mark and I have been talking about how to integrate JE's Direct Persistence Layer (DPL) with JRuby and we think it can be done with some remodularization of some of the DPL code. This is exciting because it would provide POJO ACID persistence to Ruby programmers. [Update 1/24/07: the next post in this blog discusses a compromise solution.]
Linda and I have been talking about whether it makes sense to possibly use Ruby as a scripting platform for JE in the future. Given how easy it was to bring up JE and JRuby, this certainly warrants some further thought.
The Ruby code and corresponding output is shown below. By the way, if you see something that I didn't do "The Ruby Way", feel free to let me know.
I'd love to hear about your experiences with JE and JRuby. Feel free to email me a charles.lamb at <theobviousdomain dot com>.
---------------------------------
require 'java'
module JESimple
require 'date'
# Include all the Java and JE classes that we need.
include_class 'java.io.File'
include_class 'com.sleepycat.je.Cursor'
include_class 'com.sleepycat.je.Database'
include_class 'com.sleepycat.je.DatabaseConfig'
include_class 'com.sleepycat.je.DatabaseEntry'
include_class 'com.sleepycat.je.Environment'
include_class 'com.sleepycat.je.EnvironmentConfig'
include_class 'com.sleepycat.je.OperationStatus'
include_class 'com.sleepycat.je.Transaction'
include_class 'com.sleepycat.bind.tuple.IntegerBinding'
include_class 'com.sleepycat.bind.tuple.StringBinding'
# Create a JE Environment and Database. Make them transactional.
envConf = EnvironmentConfig.new()
envConf.setAllowCreate(true)
envConf.setTransactional(true)
f = File.new('/export/home/cwl/work-jruby/JE')
env = Environment.new(f, envConf);
dbConf = DatabaseConfig.new()
dbConf.setAllowCreate(true)
dbConf.setSortedDuplicates(true)
dbConf.setTransactional(true)
db = env.openDatabase(nil, "fooDB", dbConf)
# Create JE DatabaseEntry's for the key and data.
key = DatabaseEntry.new()
data = DatabaseEntry.new()
# Begin a transaction
txn = env.beginTransaction(nil, nil)
# Write some simple marshaled strings to the database. Use Ruby
# Time just to demonstrate marshaling a random instance into JE.
for i in (1..10)
# For demonstration purposes, use JE's Binding for the key and
# Ruby's Marshal package for the data. There's no reason you
# couldn't use JE's bindings for key and data or visa versa or
# some other completely different binding.
IntegerBinding.intToEntry(i, key)
StringBinding.stringToEntry(Marshal.dump(Time.at(i * 3600 * 24)),
data)
status = db.put(txn, key, data)
if (status != OperationStatus::SUCCESS)
puts "Funky status on put #{status}"
end
end
txn.commit()
# Read back all of the records with a cursor scan.
puts "Cursor Scan"
c = db.openCursor(nil, nil)
while (true) do
status = c.getNext(key, data, nil)
if (status != OperationStatus::SUCCESS)
break
end
retKey = IntegerBinding.entryToInt(key)
retData = Marshal.load(StringBinding.entryToString(data))
dow =
puts "#{retKey} => #{retData.strftime('%a %b %d')}"
end
c.close()
# Read back the record with key 5.
puts "\nSingle Record Retrieval"
IntegerBinding.intToEntry(5, key)
status = db.get(nil, key, data, nil)
if (status != OperationStatus::SUCCESS)
puts "Funky status on get #{status}"
end
retData = Marshal.load(StringBinding.entryToString(data))
puts "5 => #{retData.strftime('%a %b %d')}"
db.close
env.close
end
--------------------------------
Cursor Scan
1 => Fri Jan 02
2 => Sat Jan 03
3 => Sun Jan 04
4 => Mon Jan 05
5 => Tue Jan 06
6 => Wed Jan 07
7 => Thu Jan 08
8 => Fri Jan 09
9 => Sat Jan 10
10 => Sun Jan 11
Single Record Retrieval
5 => Tue Jan 06