VMS Tech Demo 4 - BDB JE

Content:

  1. Introduction
  2. Hypothetical context
  3. BLOB in BLOB out
  4. Working with defined records
  5. Multi key
  6. Big records
  7. Transactions
  8. Conclusion

Introduction:

This article will show how to use BDB JE (BerkeleyDB Java Edition) on VMS.

BDB JE is an excellent NoSQL Key Value Store database for JVM languages.

BDB JE 7.5 should run on VMS Itanium with Java 8. BDB JE 5.0 runs on VMS Alpha with Java 5.

There is not actually any VMS specific Java code in this article. The VMS aspect is that it tries to relate BDB JE to VMS index-sequential files. And there are VMS scripts to build and run the code.

Many of the examples will be borrowed from the BDB JE section in my NoSQL KVS article.

Besides Java examples there will also be a few Jython (JVM Python) examples to show that BDB JE works fine for other JVM languages as well.

Hypothetical context:

Let us assume that we have an older VMS application written in native language (Pascal/Cobol/Basic) using index-sequential files and it has been decided to migrate to a JVM language (Java/Kotlin/Groovy/Scala) to get to a newer language but wanting to stay on VMS.

For persistence there are a few options:

Relational database is the obvious choice. I have already written about that choice numerous times:

But maybe there are reasons for not to go that route and sticking to the Key Value Store / ISAM model.

Index-sequential files can be used from JVM languages. I have also written about that:

But there are some drawbacks from that approach:

So if one decide to go for another NoSQL Key Value Store database for Java, then BDB JE is a pretty obvious choice.

Be sure to check whether the license for the BDB JE version you use is compatible with your usage. Old versions are under AGPL. Newer versions are under Apache license. AGPL may be a problem for you.

BLOB in BLOB out:

BDB JE expose a low level API where both keys and values are just byte arrays. One can store a chunk of bytes (a BLOB) and later retrieve the same chunk of bytes (BLOB).

This is somewhat equivalent of the RMS API where one just pass address of data and length of data.

For an example see the C code here.

Note that this approach is easier in a native language than in Java as you in a native language can just take the address of the original data structure and pass while you in Java have to serialize an object and pass the result of that.

Example:

import java.io.Serializable;

public class Data implements Serializable {
    private static final long serialVersionUID = 1L;
    private int iv;
    private double xv;
    private String sv;
    public Data() {
        this(0, 0.0, "");
    }
    public Data(int iv, double xv, String sv) {
        this.iv = iv;
        this.xv = xv;
        this.sv = sv;
    }
    public int getIv() {
        return iv;
    }
    public void setIv(int iv) {
        this.iv = iv;
    }
    public double getXv() {
        return xv;
    }
    public void setXv(double xv) {
        this.xv = xv;
    }
    public String getSv() {
        return sv;
    }
    public void setSv(String sv) {
        this.sv = sv;
    }
    @Override
    public String toString() {
        return String.format("{iv: %d, xv: %f, sv: %s}", iv, xv, sv);
    }
}
import java.io.File;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.nio.ByteBuffer;
import java.nio.ByteOrder;

import com.sleepycat.je.Cursor;
import com.sleepycat.je.CursorConfig;
import com.sleepycat.je.Database;
import com.sleepycat.je.DatabaseConfig;
import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;
import com.sleepycat.je.OperationStatus;

public class Test {
    private static DatabaseEntry prep(String key) throws UnsupportedEncodingException {
        return new DatabaseEntry(key.getBytes("UTF-8"));
    }
    private static DatabaseEntry serialize(Data o) throws IOException {
        ByteBuffer bb = ByteBuffer.allocate(1000);
        bb.order(ByteOrder.LITTLE_ENDIAN);
        bb.putInt(o.getIv());
        bb.putDouble(o.getXv());
        byte[] b = o.getSv().getBytes("UTF-8");
        bb.putShort((short)b.length);
        bb.put(b);
        int n = bb.position();
        byte[] res = new byte[n];
        bb.rewind();
        bb.get(res);
        return new DatabaseEntry(res);
    }
    private static Data deserialize(DatabaseEntry b) throws IOException, ClassNotFoundException {
        ByteBuffer bb = ByteBuffer.wrap(b.getData());
        bb.order(ByteOrder.LITTLE_ENDIAN);
        int iv = bb.getInt();
        double xv = bb.getDouble();
        int len = bb.getShort();
        byte[] temp = new byte[len];
        bb.get(temp);
        String sv = new String(temp, "UTF-8");
        Data res = new Data(iv, xv, sv);
        return res;
    }
    private static void dump(Database db, String key) throws ClassNotFoundException, IOException {
        System.out.printf("Key=%s\n", key);
        DatabaseEntry rawvalue = new DatabaseEntry();
        if(db.get(null, prep(key), rawvalue, null) == OperationStatus.SUCCESS) {
            Data value = deserialize(rawvalue);
            System.out.println(value);
        } else {
            System.out.println("Not found");
        }
    }
    private static final int NREC = 1000;
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // open database
        EnvironmentConfig envcfg = new EnvironmentConfig();
        envcfg.setAllowCreateVoid(true);
        Environment env = new Environment(new File("data/"), envcfg);
        DatabaseConfig dbcfg = new DatabaseConfig();
        dbcfg.setAllowCreate(true);
        Database db = env.openDatabase(null, "testdb", dbcfg);
        // put data
        for(int i = 0; i < NREC; i++) {
            String key = "Key#" + (1000 + i + 1);
            Data value = new Data();
            value.setIv(i + 1);
            value.setXv(i + 1.0);
            value.setSv(String.format("This is value %d", i + 1));
            db.put(null, prep(key), serialize(value));
        }
        //
        String key;
        key = "Key#" + 1077;
        // get
        dump(db, key);
        // delete
        db.delete(null, prep(key));
        // get non existing
        dump(db, key);
        //
        key = "Key#" + 1088;
        // update and get
        dump(db, key);
        DatabaseEntry rawvalue = new DatabaseEntry();
        db.get(null, prep(key), rawvalue, null);
        Data value = (Data)deserialize(rawvalue);
        value.setIv(value.getIv() + 1);
        value.setXv(value.getXv() + 0.1);
        value.setSv(value.getSv() + " updated");
        db.put(null, prep(key), serialize(value));
        dump(db, key);
        // list all
        Cursor it = db.openCursor(null, new CursorConfig());
        DatabaseEntry itrawkey = new DatabaseEntry();
        DatabaseEntry itrawvalue = new DatabaseEntry();
        OperationStatus itstat = it.getFirst(itrawkey, itrawvalue, null);
        int n = 0;
        while(itstat == OperationStatus.SUCCESS) {
            String itkey = new String(itrawkey.getData(), "UTF-8");
            if(!itkey.startsWith("Key#")) {
                System.out.println("Unexpected key: " + itkey);
            }
            Data itvalue = (Data)deserialize(itrawvalue);
            if(itvalue.getIv() < 1 || NREC < itvalue.getIv()) {
                System.out.println("Unexpected value :" + itvalue);
            }
            n++;
            itstat = it.getNext(itrawkey, itrawvalue, null);
        }
        it.close();
        System.out.println(n);
        // list keys where "Key#1075" <= key < "Key#1085"
        Cursor it2 = db.openCursor(null, new CursorConfig());
        DatabaseEntry it2rawkey = prep("Key#" + 1075);
        DatabaseEntry it2rawvalue = new DatabaseEntry();
        OperationStatus it2stat = it2.getSearchKey(it2rawkey, it2rawvalue, null);
        int n2 = 0;
        while(it2stat == OperationStatus.SUCCESS) {
            String it2key = new String(it2rawkey.getData(), "UTF-8");
            if(it2key.compareTo("Key#" + 1085) >= 0) break;
            n2++;
            it2stat = it2.getNext(it2rawkey, it2rawvalue, null);
        }
        it2.close();
        System.out.println(n2);
        // close database
        db.close();
        env.close();
    }
}

Build and run:

$ javac -classpath /javalib/je-5_0_73.jar Test.java Data.java
$ define/nolog java$filename_controls 8
$ define/nolog decc$efs_charset true
$ define/nolog java$filename_match_list "*.jdb=shr=get,put"
$ java -classpath .:/javalib/je-5_0_73.jar "Test"

Working with defined records:

BDB JE also expose a high level API where one operates on classes/objects.

For the Java/C# people then that is a bit like ORM for RDBMS.

For the VMS people that is a bit like how some VMS languages has support for mapping records to VMS index-sequential files.

For an example of the latter see the Pascal code here.

Example:

import com.sleepycat.persist.model.Entity;
import com.sleepycat.persist.model.PrimaryKey;

@Entity
public class DataO {
    @PrimaryKey
    private String key;
    private int iv;
    private double xv;
    private String sv;
    public DataO() {
        this("", 0, 0.0, "");
    }
    public DataO(String key, int iv, double xv, String sv) {
        this.key = key;
        this.iv = iv;
        this.xv = xv;
        this.sv = sv;
    }
    public String getKey() {
        return key;
    }
    public void setKey(String key) {
        this.key = key;
    }
    public int getIv() {
        return iv;
    }
    public void setIv(int iv) {
        this.iv = iv;
    }
    public double getXv() {
        return xv;
    }
    public void setXv(double xv) {
        this.xv = xv;
    }
    public String getSv() {
        return sv;
    }
    public void setSv(String sv) {
        this.sv = sv;
    }
    @Override
    public String toString() {
        return String.format("{iv: %d, xv: %f, sv: %s}", iv, xv, sv);
    }
}
import java.io.File;
import java.io.IOException;
import java.util.Iterator;

import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;
import com.sleepycat.persist.EntityCursor;
import com.sleepycat.persist.EntityStore;
import com.sleepycat.persist.PrimaryIndex;
import com.sleepycat.persist.StoreConfig;

public class TestO {
    private static void dump(PrimaryIndex<String, DataO> pk, String key) throws ClassNotFoundException, IOException {
        System.out.printf("Key=%s\n", key);
        if(pk.contains(key)) {
            DataO o = pk.get(key);
            System.out.println(o);
        } else {
            System.out.println("Not found");
        }
    }
    private static final int NREC = 1000;
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // open database
        EnvironmentConfig envcfg = new EnvironmentConfig();
        envcfg.setAllowCreateVoid(true);
        Environment env = new Environment(new File("data/"), envcfg);
        StoreConfig stcfg = new StoreConfig();
        stcfg.setAllowCreate(true);
        EntityStore est = new EntityStore(env, "testdb", stcfg);
        PrimaryIndex<String, DataO> pk = est.getPrimaryIndex(String.class, DataO.class);
        // put data
        for(int i = 0; i < NREC; i++) {
            String key = "Key#" + (1000 + i + 1);
            DataO o = new DataO();
            o.setKey(key);
            o.setIv(i + 1);
            o.setXv(i + 1.0);
            o.setSv(String.format("This is value %d", i + 1));
            pk.put(o);
        }
        //
        String key;
        key = "Key#" + 1077;
        // get
        dump(pk, key);
        // delete
        pk.delete(key);
        // get non existing
        dump(pk, key);
        //
        key = "Key#" + 1088;
        // update and get
        dump(pk, key);
        DataO o = pk.get(key);
        o.setIv(o.getIv() + 1);
        o.setXv(o.getXv() + 0.1);
        o.setSv(o.getSv() + " updated");
        pk.put(o);
        dump(pk, key);
        // list all
        EntityCursor<DataO> c = pk.entities();
        Iterator<DataO> it = c.iterator();
        int n = 0;
        while(it.hasNext()) {
            DataO ito = it.next();
            if(!ito.getKey().startsWith("Key#")) {
                System.out.println("Unexpected key: " + ito.getKey());
            }
            if(ito.getIv() < 1 || NREC < ito.getIv()) {
                System.out.println("Unexpected value :" + ito);
            }
            n++;
        }
        c.close();
        System.out.println(n);
        // list keys where "Key#1075" <= key < "Key#1085"
        EntityCursor<DataO> c2 = pk.entities("Key#1075", true, "Key#1085", false);
        Iterator<DataO> it2 = c2.iterator();
        int n2 = 0;
        while(it2.hasNext()) {
            @SuppressWarnings("unused")
            DataO ito2 = it2.next();
            n2++;
        }
        c2.close();
        System.out.println(n2);
        // close database
        est.close();
        env.close();
    }
}

Build and run:

$ javac -classpath /javalib/je-5_0_73.jar TestO.java DataO.java
$ define/nolog java$filename_controls 8
$ define/nolog decc$efs_charset true
$ define/nolog java$filename_match_list "*.jdb=shr=get,put"
$ java -classpath .:/javalib/je-5_0_73.jar "TestO"
import com.sleepycat.persist.model.Entity;
import com.sleepycat.persist.model.PrimaryKey;

@Entity
public class DataO {
    @PrimaryKey
    private String key;
    private int iv;
    private double xv;
    private String sv;
    public DataO() {
        this("", 0, 0.0, "");
    }
    public DataO(String key, int iv, double xv, String sv) {
        this.key = key;
        this.iv = iv;
        this.xv = xv;
        this.sv = sv;
    }
    public String getKey() {
        return key;
    }
    public void setKey(String key) {
        this.key = key;
    }
    public int getIv() {
        return iv;
    }
    public void setIv(int iv) {
        this.iv = iv;
    }
    public double getXv() {
        return xv;
    }
    public void setXv(double xv) {
        this.xv = xv;
    }
    public String getSv() {
        return sv;
    }
    public void setSv(String sv) {
        this.sv = sv;
    }
    @Override
    public String toString() {
        return String.format("{iv: %d, xv: %f, sv: %s}", iv, xv, sv);
    }
}
from java.lang import String
from java.io import File

from com.sleepycat.je import Environment
from com.sleepycat.je import EnvironmentConfig
from com.sleepycat.persist import EntityStore
from com.sleepycat.persist import PrimaryIndex
from com.sleepycat.persist import StoreConfig

import DataO

def dump(pk, key):
    print('Key=%s' % (key))
    if pk.contains(key):
        o = pk.get(key)
        print(o)
    else:
        print('Not found')

NREC = 1000

# open database
envcfg = EnvironmentConfig()
envcfg.setAllowCreateVoid(True)
env = Environment(File("data/"), envcfg)
stcfg = StoreConfig()
stcfg.setAllowCreate(True)
est = EntityStore(env, "testdb", stcfg)
pk = est.getPrimaryIndex(String, DataO);
# put data
for i in range(NREC):
    o = DataO('Key#' + str(1000 + i + 1), i + 1, i + 1.0, 'This is value %d' % (i + 1))
    pk.put(o)
#
key = "Key#" + str(1077)
# get
dump(pk, key)
# delete
pk.delete(key)
# get non existing
dump(pk, key)
#
key = "Key#" + str(1088)
# update and get
dump(pk, key)
o = pk.get(key)
o.iv = o.iv + 1
o.xv = o.xv + 0.1
o.sv = o.sv + ' updated'
pk.put(o)
dump(pk, key)
# list all
c = pk.entities()
n = 0
for o in c.iterator():
    if not o.key.startswith('Key#'):
        print('Unexpected key: ' + o.id)
    if o.iv < 1 or NREC < o.iv:
        print('Unexpected value: ' + d)
    n = n + 1
c.close()
print(n)
# list keys where "Key#1075" <= key < "Key#1085"
c2 = pk.entities('Key#1075', True, 'Key#1085', False)
n2 = 0
for o in c2.iterator():
    n2 = n2 + 1
c2.close()
print(n2)
# close database
est.close()
env.close()

Run:

$ javac -classpath /javalib/je-5_0_73.jar DataO.java
$ define/nolog java$filename_controls 8
$ define/nolog decc$efs_charset true
$ define/nolog java$filename_match_list "*.jdb=shr=get,put"
$ define/nolog jython_libs "/javalib/je-5_0_73.jar"
$ jython testo.py

Multi key:

An important capability of VMS index-sequential files is the support for multiple keys.

This is easy to do on BDB JE with the high level record API.

Example:

import com.sleepycat.persist.model.Entity;
import com.sleepycat.persist.model.PrimaryKey;
import com.sleepycat.persist.model.Relationship;
import com.sleepycat.persist.model.SecondaryKey;

@Entity
public class BaseData {
    @PrimaryKey
    private int id;
    @SecondaryKey(relate=Relationship.MANY_TO_ONE)
    private String name;
    @SecondaryKey(relate=Relationship.MANY_TO_ONE)
    private String address;
    private int value;
    public BaseData() {
        this(0, "", "", 0);
    }
    public BaseData(int id, String name, String address, int value) {
        super();
        this.id = id;
        this.name = name;
        this.address = address;
        this.value = value;
    }
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getAddress() {
        return address;
    }
    public void setAddress(String address) {
        this.address = address;
    }
    public int getValue() {
        return value;
    }
    public void setValue(int value) {
        this.value = value;
    }
}

import java.io.File;

import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;
import com.sleepycat.persist.EntityStore;
import com.sleepycat.persist.PrimaryIndex;
import com.sleepycat.persist.SecondaryIndex;
import com.sleepycat.persist.StoreConfig;

public class MultiKey {
    public static void main(String[] args) {
        // open database
        EnvironmentConfig envcfg = new EnvironmentConfig();
        envcfg.setAllowCreateVoid(true);
        envcfg.setTransactional(true);
        Environment env = new Environment(new File("data/"), envcfg);
        StoreConfig stcfg = new StoreConfig();
        stcfg.setAllowCreate(true);
        stcfg.setTransactional(true);
        EntityStore est = new EntityStore(env, "mulkeydb", stcfg);
        PrimaryIndex<Integer, BaseData> idix = est.getPrimaryIndex(Integer.class, BaseData.class);
        SecondaryIndex<String, Integer, BaseData> namix = est.getSecondaryIndex(idix, String.class, "name");
        SecondaryIndex<String, Integer, BaseData> addrix = est.getSecondaryIndex(idix, String.class, "address");
        // put 3 records
        idix.put(new BaseData(1, "A", "A A A", 123));
        idix.put(new BaseData(2, "B", "B B B", 456));
        idix.put(new BaseData(3, "C", "C C C", 789));
        // lookup by id
        System.out.println(idix.get(2).getValue());
        System.out.println(idix.get(3).getValue());
        // lookup by name
        System.out.println(namix.get("B").getValue());
        System.out.println(namix.get("C").getValue());
        // lookup by address
        System.out.println(addrix.get("B B B").getValue());
        System.out.println(addrix.get("C C C").getValue());
        // close database
        est.close();
        env.close();
    }
}

Build and run:

$ javac -classpath /javalib/je-5_0_73.jar MultiKey.java BaseData.java
$ define/nolog java$filename_controls 8
$ define/nolog decc$efs_charset true
$ define/nolog java$filename_match_list "*.jdb=shr=get,put"
$ java -classpath .:/javalib/je-5_0_73.jar "MultiKey"
import com.sleepycat.persist.model.Entity;
import com.sleepycat.persist.model.PrimaryKey;
import com.sleepycat.persist.model.Relationship;
import com.sleepycat.persist.model.SecondaryKey;

@Entity
public class BaseData {
    @PrimaryKey
    private int id;
    @SecondaryKey(relate=Relationship.MANY_TO_ONE)
    private String name;
    @SecondaryKey(relate=Relationship.MANY_TO_ONE)
    private String address;
    private int value;
    public BaseData() {
        this(0, "", "", 0);
    }
    public BaseData(int id, String name, String address, int value) {
        super();
        this.id = id;
        this.name = name;
        this.address = address;
        this.value = value;
    }
    public int getId() {
        return id;
    }
    public void setId(int id) {
        this.id = id;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getAddress() {
        return address;
    }
    public void setAddress(String address) {
        this.address = address;
    }
    public int getValue() {
        return value;
    }
    public void setValue(int value) {
        this.value = value;
    }
}

from java.lang import Integer
from java.lang import String
from java.io import File

from com.sleepycat.je import Environment
from com.sleepycat.je import EnvironmentConfig
from com.sleepycat.persist import EntityStore
from com.sleepycat.persist import PrimaryIndex
from com.sleepycat.persist import SecondaryIndex
from com.sleepycat.persist import StoreConfig

import BaseData

# open database
envcfg = EnvironmentConfig()
envcfg.setAllowCreateVoid(True)
env = Environment(File("data/"), envcfg)
stcfg = StoreConfig()
stcfg.setAllowCreate(True)
est = EntityStore(env, "mulkeydb", stcfg)
idix = est.getPrimaryIndex(Integer, BaseData)
namix = est.getSecondaryIndex(idix, String, "name")
addrix = est.getSecondaryIndex(idix, String, "address")
# put 3 records
idix.put(BaseData(1, "A", "A A A", 123))
idix.put(BaseData(2, "B", "B B B", 456))
idix.put(BaseData(3, "C", "C C C", 789))
# lookup by id
print(idix.get(2).value)
print(idix.get(3).value)
# lookup by name
print(namix.get("B").value)
print(namix.get("C").value)
# lookup by address
print(addrix.get("B B B").value)
print(addrix.get("C C C").value)
# close database
est.close()
env.close()

Run:

$ javac -classpath /javalib/je-5_0_73.jar BaseData.java
$ define/nolog java$filename_controls 8
$ define/nolog decc$efs_charset true
$ define/nolog java$filename_match_list "*.jdb=shr=get,put"
$ define/nolog jython_libs "/javalib/je-5_0_73.jar"
$ jython multikey.py

relate=Relationship.MANY_TO_ONE means that duplicates are allowed for the secondary key.

relate=Relationship.ONE_TO_ONE would enforce unique values for the secondary key.

Big records:

BDB JE does not have a 32 KB limit on records like RMS index-sequential files.

No problems with big records.

Here is an example working with 1.5 MB texts:

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;

import com.sleepycat.je.Database;
import com.sleepycat.je.DatabaseConfig;
import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;

public class Big {
    private static DatabaseEntry toEntry(String s) {
        return new DatabaseEntry(s.getBytes());
    }
    private static String fromEntry(DatabaseEntry dbe) {
        return new String(dbe.getData());
    }
    private static String load(String fnm) throws IOException {
        StringBuilder sb = new StringBuilder();
        BufferedReader br = new BufferedReader(new FileReader(fnm));
        String line;
        while((line = br.readLine()) != null) {
            sb.append(line + "\n");
        }
        br.close();
        return sb.toString();
    }
    private static void store(String txt, String fnm) throws IOException {
        PrintWriter pw = new PrintWriter(new FileWriter(fnm));
        pw.print(txt);
        pw.close();
    }
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        // open database
        EnvironmentConfig envcfg = new EnvironmentConfig();
        envcfg.setAllowCreateVoid(true);
        Environment env = new Environment(new File("data/"), envcfg);
        DatabaseConfig dbcfg = new DatabaseConfig();
        dbcfg.setAllowCreate(true);
        Database db = env.openDatabase(null, "bigdb", dbcfg);
        db.put(null, toEntry("copy1"), toEntry(load("4300-0.txt")));
        db.put(null, toEntry("copy2"), toEntry(load("4300-0.txt")));
        DatabaseEntry copy1 = new DatabaseEntry();
        db.get(null, toEntry("copy1"), copy1, null);
        store(fromEntry(copy1), "copy1.txt");
        DatabaseEntry copy2 = new DatabaseEntry();
        db.get(null, toEntry("copy2"), copy2, null);
        store(fromEntry(copy2), "copy2.txt");
        db.close();
        env.close();
    }
}

Build and run:

$ javac -classpath /javalib/je-5_0_73.jar Big.java
$ define/nolog java$filename_controls 8
$ define/nolog decc$efs_charset true
$ define/nolog java$filename_match_list "*.jdb=shr=get,put"
$ java -classpath .:/javalib/je-5_0_73.jar "Big"

Transactions:

BDB JE supports traditional transactions:

BEGIN
operation 1
operation 2
...
operation n
COMMIT or ROLLBACK

Two benefits:

The first is handled in VMS index-sequential files using explicit locking.

For more info about traditional transactions and transaction isolation level see here.

This can be very convenient.

Here is a classic example.

BLOB style:

import java.io.File;
import java.math.BigDecimal;

import com.sleepycat.je.Database;
import com.sleepycat.je.DatabaseConfig;
import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;
import com.sleepycat.je.Transaction;
import com.sleepycat.je.TransactionConfig;

public class TestTx {
    public static class Account {
        private BigDecimal amount;
        public Account() {
            this.amount = new BigDecimal("0.00");
        }
        public Account(BigDecimal amount) {
            this.amount = amount;
        }
        public BigDecimal getAmount() {
            return amount;
        }
        public void transfer(BigDecimal delta) {
            amount = amount.add(delta);
        }
        // used for deserialization and serialization
        public Account(byte[] b) {
            this.amount = new BigDecimal(new String(b));
        }
        public byte[] toBytes() {
            return amount.toPlainString().getBytes();
        }
    }
    public static void saveAccount(Database db, String id, Account acct) {
        TransactionConfig txcfg = new TransactionConfig();
        txcfg.setReadCommitted(true);
        Transaction tx = db.getEnvironment().beginTransaction(null, txcfg);
        db.put(null,  new DatabaseEntry(id.getBytes()), new DatabaseEntry(acct.toBytes()));
        tx.commitSync();
    }
    public static Account getAccount(Database db, String id) {
        DatabaseEntry acctdata = new DatabaseEntry();
        db.get(null, new DatabaseEntry(id.getBytes()), acctdata, null);
        return new Account(acctdata.getData());
    }
    public static void transfer(Database db, String fromid, String toid, BigDecimal amount) {
        TransactionConfig txcfg = new TransactionConfig();
        txcfg.setReadCommitted(true);
        Transaction tx = db.getEnvironment().beginTransaction(null, txcfg);
        DatabaseEntry acctdata = new DatabaseEntry();
        db.get(tx, new DatabaseEntry(fromid.getBytes()), acctdata, null);
        Account fromacct = new Account(acctdata.getData());
        db.get(tx, new DatabaseEntry(toid.getBytes()), acctdata, null);
        Account toacct = new Account(acctdata.getData());
        fromacct.transfer(BigDecimal.ZERO.subtract(amount));
        toacct.transfer(amount);
        db.put(tx,  new DatabaseEntry(fromid.getBytes()), new DatabaseEntry(fromacct.toBytes()));
        db.put(tx,  new DatabaseEntry(toid.getBytes()), new DatabaseEntry(toacct.toBytes()));
        tx.commitSync();
        //tx.abort();
    }
    public static void main(String[] args) {
        // open database
        EnvironmentConfig envcfg = new EnvironmentConfig();
        envcfg.setAllowCreateVoid(true);
        envcfg.setTransactional(true);
        Environment env = new Environment(new File("data/"), envcfg);
        DatabaseConfig dbcfg = new DatabaseConfig();
        dbcfg.setAllowCreate(true);
        dbcfg.setTransactional(true);
        // setup accounts
        Database db = env.openDatabase(null, "acctdb", dbcfg);
        saveAccount(db, "A", new Account(new BigDecimal("100.00")));
        saveAccount(db, "B", new Account(new BigDecimal("0.00")));
        // display accounts
        Account acct_a = getAccount(db, "A");
        System.out.printf("%s : %s\n", "A", acct_a.getAmount());
        Account acct_b = getAccount(db, "B");
        System.out.printf("%s : %s\n", "B", acct_b.getAmount());
        // transfer money
        transfer(db, "A", "B", new BigDecimal("20.00"));
        // display accounts
        acct_a = getAccount(db, "A");
        System.out.printf("%s : %s\n", "A", acct_a.getAmount());
        acct_b = getAccount(db, "B");
        System.out.printf("%s : %s\n", "B", acct_b.getAmount());
        // close database
        db.close();
        env.close();
    }
}
$ javac -classpath /javalib/je-5_0_73.jar TestTx.java
$ define/nolog java$filename_controls 8
$ define/nolog decc$efs_charset true
$ define/nolog java$filename_match_list "*.jdb=shr=get,put"
$ java -classpath .:/javalib/je-5_0_73.jar "TestTx"

Record style:

import java.io.File;
import java.math.BigDecimal;

import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;
import com.sleepycat.je.Transaction;
import com.sleepycat.je.TransactionConfig;
import com.sleepycat.persist.EntityStore;
import com.sleepycat.persist.PrimaryIndex;
import com.sleepycat.persist.StoreConfig;
import com.sleepycat.persist.model.Entity;
import com.sleepycat.persist.model.PrimaryKey;

public class TestTxO {
    @Entity
    public static class Account {
        @PrimaryKey
        private String name;
        private BigDecimal amount;
        public Account() {
            this("", new BigDecimal("0.00"));
        }
        public Account(String name, BigDecimal amount) {
            this.name = name;
            this.amount = amount;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public BigDecimal getAmount() {
            return amount;
        }
        public void transfer(BigDecimal delta) {
            amount = amount.add(delta);
        }
    }
    public static void saveAccount(PrimaryIndex<String, Account> pk, Account acct) {
        TransactionConfig txcfg = new TransactionConfig();
        txcfg.setReadCommitted(true);
        Transaction tx = pk.getDatabase().getEnvironment().beginTransaction(null, txcfg);
        pk.put(acct);
        tx.commitSync();
    }
    public static void transfer(PrimaryIndex<String, Account> pk, String fromid, String toid, BigDecimal amount) {
        TransactionConfig txcfg = new TransactionConfig();
        txcfg.setReadCommitted(true);
        Transaction tx = pk.getDatabase().getEnvironment().beginTransaction(null, txcfg);
        Account fromacct = pk.get(fromid);
        Account toacct = pk.get(toid);
        fromacct.transfer(BigDecimal.ZERO.subtract(amount));
        toacct.transfer(amount);
        toacct.transfer(amount);
        pk.put(fromacct);
        pk.put(toacct);
        tx.commitSync();
        //tx.abort();
    }
    public static void main(String[] args) {
        // open database
        EnvironmentConfig envcfg = new EnvironmentConfig();
        envcfg.setAllowCreateVoid(true);
        envcfg.setTransactional(true);
        Environment env = new Environment(new File("data/"), envcfg);
        StoreConfig stcfg = new StoreConfig();
        stcfg.setAllowCreate(true);
        stcfg.setTransactional(true);
        EntityStore est = new EntityStore(env, "acctdb", stcfg);
        PrimaryIndex<String, Account> pk = est.getPrimaryIndex(String.class, Account.class);
        // setup accounts
        saveAccount(pk, new Account("A", new BigDecimal("100.00")));
        saveAccount(pk, new Account("B", new BigDecimal("0.00")));
        // display accounts
        Account acct_a = pk.get("A");
        System.out.printf("%s : %s\n", acct_a.getName(), acct_a.getAmount());
        Account acct_b = pk.get("B");
        System.out.printf("%s : %s\n", acct_b.getName(), acct_b.getAmount());
        // transfer money
        transfer(pk, "A", "B", new BigDecimal("20.00"));
        // display accounts
        acct_a = pk.get("A");
        System.out.printf("%s : %s\n", acct_a.getName(), acct_a.getAmount());
        acct_b = pk.get("B");
        System.out.printf("%s : %s\n", acct_b.getName(), acct_b.getAmount());
        // close database
        est.close();
        env.close();
    }
}
$ javac -classpath /javalib/je-5_0_73.jar TestTxO.java
$ define/nolog java$filename_controls 8
$ define/nolog decc$efs_charset true
$ define/nolog java$filename_match_list "*.jdb=shr=get,put"
$ java -classpath .:/javalib/je-5_0_73.jar "TestTxO"

But to really illustrate the power we need to see the example with some failures between the two updates to show that we end up in a consistent state.

BLOB style:

import java.io.File;
import java.math.BigDecimal;

import com.sleepycat.je.Database;
import com.sleepycat.je.DatabaseConfig;
import com.sleepycat.je.DatabaseEntry;
import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;
import com.sleepycat.je.Transaction;
import com.sleepycat.je.TransactionConfig;

public class TestTx2 {
    public static class Account {
        private BigDecimal amount;
        public Account() {
            this.amount = new BigDecimal("0.00");
        }
        public Account(BigDecimal amount) {
            this.amount = amount;
        }
        public BigDecimal getAmount() {
            return amount;
        }
        public void transfer(BigDecimal delta) {
            amount = amount.add(delta);
        }
        // used for deserialization and serialization
        public Account(byte[] b) {
            this.amount = new BigDecimal(new String(b));
        }
        public byte[] toBytes() {
            return amount.toPlainString().getBytes();
        }
    }
    public static void saveAccount(Database db, String id, Account acct) {
        TransactionConfig txcfg = new TransactionConfig();
        txcfg.setReadCommitted(true);
        Transaction tx = db.getEnvironment().beginTransaction(null, txcfg);
        db.put(null,  new DatabaseEntry(id.getBytes()), new DatabaseEntry(acct.toBytes()));
        tx.commitSync();
    }
    public static Account getAccount(Database db, String id) {
        DatabaseEntry acctdata = new DatabaseEntry();
        db.get(null, new DatabaseEntry(id.getBytes()), acctdata, null);
        return new Account(acctdata.getData());
    }
    public static void transfer(Database db, String fromid, String toid, BigDecimal amount, boolean failflag) {
        TransactionConfig txcfg = new TransactionConfig();
        txcfg.setReadCommitted(true);
        Transaction tx = db.getEnvironment().beginTransaction(null, txcfg);
        try {
            DatabaseEntry acctdata = new DatabaseEntry();
            db.get(tx, new DatabaseEntry(fromid.getBytes()), acctdata, null);
            Account fromacct = new Account(acctdata.getData());
            db.get(tx, new DatabaseEntry(toid.getBytes()), acctdata, null);
            Account toacct = new Account(acctdata.getData());
            fromacct.transfer(BigDecimal.ZERO.subtract(amount));
            toacct.transfer(amount);
            db.put(tx, new DatabaseEntry(fromid.getBytes()), new DatabaseEntry(fromacct.toBytes()));
            if(failflag) throw new RuntimeException("Something happended");
            db.put(tx, new DatabaseEntry(toid.getBytes()), new DatabaseEntry(toacct.toBytes()));
            tx.commitSync();
        } catch(Exception ex) {
            tx.abort();
        }
    }
    public static void main(String[] args) {
        // open database
        EnvironmentConfig envcfg = new EnvironmentConfig();
        envcfg.setAllowCreateVoid(true);
        envcfg.setTransactional(true);
        Environment env = new Environment(new File("data/"), envcfg);
        DatabaseConfig dbcfg = new DatabaseConfig();
        dbcfg.setAllowCreate(true);
        dbcfg.setTransactional(true);
        // setup accounts
        Database db = env.openDatabase(null, "acctdb", dbcfg);
        saveAccount(db, "A", new Account(new BigDecimal("100.00")));
        saveAccount(db, "B", new Account(new BigDecimal("0.00")));
        // display accounts
        Account acct_a = getAccount(db, "A");
        System.out.printf("%s : %s\n", "A", acct_a.getAmount());
        Account acct_b = getAccount(db, "B");
        System.out.printf("%s : %s\n", "B", acct_b.getAmount());
        // transfer money
        transfer(db, "A", "B", new BigDecimal("20.00"), false);
        transfer(db, "A", "B", new BigDecimal("20.00"), true);
        transfer(db, "A", "B", new BigDecimal("20.00"), false);
        transfer(db, "A", "B", new BigDecimal("20.00"), true);
        transfer(db, "A", "B", new BigDecimal("20.00"), false);
        // display accounts
        acct_a = getAccount(db, "A");
        System.out.printf("%s : %s\n", "A", acct_a.getAmount());
        acct_b = getAccount(db, "B");
        System.out.printf("%s : %s\n", "B", acct_b.getAmount());
        // close database
        db.close();
        env.close();
    }
}
$ javac -classpath /javalib/je-5_0_73.jar TestTx2.java
$ define/nolog java$filename_controls 8
$ define/nolog decc$efs_charset true
$ define/nolog java$filename_match_list "*.jdb=shr=get,put"
$ java -classpath .:/javalib/je-5_0_73.jar "TestTx2"

Record style:

import java.io.File;
import java.math.BigDecimal;

import com.sleepycat.je.Environment;
import com.sleepycat.je.EnvironmentConfig;
import com.sleepycat.je.Transaction;
import com.sleepycat.je.TransactionConfig;
import com.sleepycat.persist.EntityStore;
import com.sleepycat.persist.PrimaryIndex;
import com.sleepycat.persist.StoreConfig;
import com.sleepycat.persist.model.Entity;
import com.sleepycat.persist.model.PrimaryKey;

public class TestTxO2 {
    @Entity
    public static class Account {
        @PrimaryKey
        private String name;
        private BigDecimal amount;
        public Account() {
            this("", new BigDecimal("0.00"));
        }
        public Account(String name, BigDecimal amount) {
            this.name = name;
            this.amount = amount;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public BigDecimal getAmount() {
            return amount;
        }
        public void transfer(BigDecimal delta) {
            amount = amount.add(delta);
        }
    }
    public static void saveAccount(PrimaryIndex<String, Account> pk, Account acct) {
        TransactionConfig txcfg = new TransactionConfig();
        txcfg.setReadCommitted(true);
        Transaction tx = pk.getDatabase().getEnvironment().beginTransaction(null, txcfg);
        pk.put(acct);
        tx.commitSync();
    }
    public static void transfer(PrimaryIndex<String, Account> pk, String fromid, String toid, BigDecimal amount, boolean failflag) {
        TransactionConfig txcfg = new TransactionConfig();
        txcfg.setReadCommitted(true);
        Transaction tx = pk.getDatabase().getEnvironment().beginTransaction(null, txcfg);
        try {
            Account fromacct = pk.get(fromid);
            Account toacct = pk.get(toid);
            fromacct.transfer(BigDecimal.ZERO.subtract(amount));
            toacct.transfer(amount);
            toacct.transfer(amount);
            pk.put(fromacct);
            if(failflag) throw new RuntimeException("Something happended");
            pk.put(toacct);
            tx.commitSync();
        } catch(Exception ex) {
            tx.abort();
        }
    }
    public static void main(String[] args) {
        // open database
        EnvironmentConfig envcfg = new EnvironmentConfig();
        envcfg.setAllowCreateVoid(true);
        envcfg.setTransactional(true);
        Environment env = new Environment(new File("data/"), envcfg);
        StoreConfig stcfg = new StoreConfig();
        stcfg.setAllowCreate(true);
        stcfg.setTransactional(true);
        EntityStore est = new EntityStore(env, "acctdb", stcfg);
        PrimaryIndex<String, Account> pk = est.getPrimaryIndex(String.class, Account.class);
        // setup accounts
        saveAccount(pk, new Account("A", new BigDecimal("100.00")));
        saveAccount(pk, new Account("B", new BigDecimal("0.00")));
        // display accounts
        Account acct_a = pk.get("A");
        System.out.printf("%s : %s\n", acct_a.getName(), acct_a.getAmount());
        Account acct_b = pk.get("B");
        System.out.printf("%s : %s\n", acct_b.getName(), acct_b.getAmount());
        // transfer money
        // transfer money
        transfer(pk, "A", "B", new BigDecimal("20.00"), false);
        transfer(pk, "A", "B", new BigDecimal("20.00"), true);
        transfer(pk, "A", "B", new BigDecimal("20.00"), false);
        transfer(pk, "A", "B", new BigDecimal("20.00"), true);
        transfer(pk, "A", "B", new BigDecimal("20.00"), false);
        // display accounts
        acct_a = pk.get("A");
        System.out.printf("%s : %s\n", acct_a.getName(), acct_a.getAmount());
        acct_b = pk.get("B");
        System.out.printf("%s : %s\n", acct_b.getName(), acct_b.getAmount());
        // close database
        est.close();
        env.close();
    }
}
$ javac -classpath /javalib/je-5_0_73.jar TestTxO2.java
$ define/nolog java$filename_controls 8
$ define/nolog decc$efs_charset true
$ define/nolog java$filename_match_list "*.jdb=shr=get,put"
$ java -classpath .:/javalib/je-5_0_73.jar "TestTxO2"

Conclusion:

BDB JE is a very capable NOSQL Key Value Store database. And it works fine on VMS.

But I doubt that it will be used much on VMS. JVM languages has never been widely used on VMS.

Article history:

Version Date Description
1.0 January 2nd 2023 Initial version

Other articles:

See list of all articles here

Comments:

Please send comments to Arne Vajhøj