GettingStarted
First, here's a high level view of what's required to get Permazen up and running.
Define your model classes
Model classes are normal Java interfaces, abstract classes, or POJO's with the @PermazenType
annotation.
Database fields are automatically created for any abstract getter methods. To include a non-abstract method, or to override the default configuration for a field (e.g., to index it), add the @PermazenField
annotation.
Choose a key/value Store
The key/value layer is completely independent of the higher layers. Instances must be start()
ed before use and should be stop()
ed when you're done.
Construct a Permazen
instance
See example below for how to use PermazenFactory
to specify your key/value store, model classes, and other options.
Open some transactions and get to work!
To see a simple Permazen database in action, download the Permazen distribution ZIP file:
- Download and unzip the demo ZIP file
- Run
java -jar permazen-gui.jar
to start up the GUI on port 8080 - Run
java -jar permazen-cli.jar
to start up the command line interface
The distribution ZIP file includes a simple example database containing objects in the solar system. The Permazen command line interface (CLI) and automatic graphical user interface (GUI) tools will auto-detect the presence of the demonstration database in the current directory on startup when no other configuration is specified.
The demo-classes/
subdirectory contains the Java model classes used to create the example database. The database and GUI are entirely configured from the handful of annotations on these classes. Note that you are free to put these annotations anywhere in the type hierarchy (see for example Body.java
and AbstractBody.java
).
The underlying key/value store for the demo database is an XMLKVDatabase
instance. XMLKVDatabase
is a "toy" key/value store implementation that uses a simple flat XML file format for persisting the key/value pairs. That file is demo-database.xml
and is included in the distribution.
The database was originally loaded using the load
command in the CLI, which reads files in "object XML" format (in this case, the file was demo-objs.xml
, also included).
To view the demo database in the auto-generated Vaadin GUI:
java -jar permazen-gui.jar
Then point a web browser to port 8080. If you already have something running on port 8080, add the --port
flag. Use the --help
flag to see other command line options.
To view the demo database in the CLI:
java -jar permazen-cli.jar
Use the help
command to see all commands and functions. In particular, the eval
command evaluates any Java expression and is used for database queries. For example, to query for all Moon objects, enter eval all(Moon)
.
The eval
command supports several extensions to Java syntax, including:
- Function invocations (the
all()
function used above is an example) - Objects may be referred to by object ID literal with a leading at-sign, eg.,
@fc02ac0000000001
- Session variables have the form
$foo
; you can set them and use them later in other expressions
See Downloads for how to include Permazen in your build.
Creating and configuring a Permazen database requires these steps:
- Configure your underlying key/value store
- Gather your Java model classes
- Create a
Permazen
instance
If you are using Spring you can do this all in XML using Permazen's <permazen:permazen>
custom XML tag. Inside the <permazen:permazen>
is a <permazen:scan-classes>
tag, which works just like Spring's <context:component-scan>
, except instead of scanning for classes annotated with @Component
, etc., it scans for classes annotated with @PermazenType
.
Permazen also provides a Spring TransactionManager
so you can do all the normal Spring things like @Transactional
, transaction synchronizations, etc.
Here's an example of a setup that uses an XMLKVDatabase
:
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:permazen="http://permazen.io/schema/spring/permazen"
xmlns:tx="http://www.springframework.org/schema/tx"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:c="http://www.springframework.org/schema/c"
xsi:schemaLocation="
http://permazen.io/schema/spring/permazen
http://permazen.github.io/permazen/permazen-spring/src/main/resources/io/permazen/spring/permazen-1.0.xsd
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd">
<!-- Define the underlying key/value store -->
<bean id="kvstore" class="io.permazen.kv.simple.XMLKVDatabase" c:file="/tmp/permazen-data.xml"/>
<!-- Define the Permazen database on top of that -->
<permazen:permazen id="permazen" kvstore="kvstore" schema-version="-1">
<permazen:scan-classes base-package="com.example.myapp.model"/>
</permazen:permazen>
<!-- Create a Spring transaction manager -->
<bean id="transactionManager" class="io.permazen.spring.PermazenTransactionManager"
p:permazen-ref="permazen" p:allowNewSchema="true"/>
<!-- Enable @Transactional annotations -->
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
The PermazenFactory class provides a way to configure Permazen in regular Java:
final Permazen jdb = new PermazenFactory()
.setDatabase(new Database(new XMLKVDatabase(new File("/tmp/permazen-data.xml"))))
.setSchemaVersion(-1)
.setModelClasses(MyClass1.class, MyClass2.class, ... )
.newPermazen();
Once you have a Permazen
instance, you can create and commit transactions:
final PermazenTransaction jtx = jdb.createTransaction(true, ValidationMode.AUTOMATIC);
PermazenTransaction.setCurrent(jtx);
try {
// Do work here ...
tx.commit();
} finally {
tx.rollback(); // this is always safe to invoke, even if commit() succeeded
PermazenTransaction.setCurrent(null);
}
Transactions should always be either commit()
ed or rollback()
ed, otherwise you create a resource leak (exactly what "resource" is leaked is a function of the underlying key/value database). The simplest way to ensure this is by using try { ... } finally { ... }
as shown above.
When using the Spring transaction manager, this can be simplified:
@Transactional
public void doSomething() {
// Do work here ...
}
Most databases will throw an exception if they detect a deadlock or unresolvable lock conflict. For example, MySQL users may be familiar with this error message: "Deadlock found when trying to get lock; try restarting transaction".
In these cases, the user is expected to retry the transaction. This has a few of important implications:
- For some databases (e.g., distributed ones) a retry exception thrown from
commit()
may occur even though the transaction was successfully committed- Therefore, transaction operations should be idempotent with respect to the data in the database
- In particular, any transaction that adds new data should be able to detect if the data has already been added
- If a transaction has any side-effects, such as firing off an email or modifying in-memory object state, they should be performed in a transaction callback so they aren't accidentally duplicated
- In Permazen, register a
Transaction.Callback
on the underlying core API transaction available viaPermazenTransaction.getCurrent().getTransaction()
- If you're using Spring, you can use the
TransactionSynchronizationManager
- In Permazen, register a
In the case of Permazen
, retryable exceptions from the underlying database are wrapped in RetryTransactionException
; the Permazen Spring transaction manager converts this into a PessimisticLockingFailureException
.
For Spring users, the DellRoad Stuff library provides the @RetryTransaction annotation that automatically retries transactions:
@RetryTransaction
@Transactional
public void doSomething() {
// Do work here ...
}
Also see Spring Retry which does the same thing.
Here's a complete example using normal Java code that uses a flat file permazen-data.xml
for persistence:
import java.io.File;
import java.util.Arrays;
import java.util.Collections;
import java.util.NavigableSet;
import java.util.NavigableMap;
import java.util.Set;
import java.util.stream.Stream;
import io.permazen.PermazenObject;
import io.permazen.Permazen;
import io.permazen.PermazenFactory;
import io.permazen.PermazenTransaction;
import io.permazen.ValidationMode;
import io.permazen.annotation.PermazenType;
import io.permazen.annotation.PermazenField;
import io.permazen.core.Database;
import io.permazen.kv.RetryTransactionException;
import io.permazen.kv.simple.XMLKVDatabase;
public class PermazenExample {
public static void main(String[] args) throws Exception {
// Create and start underlying database
final XMLKVDatabase kvdb = new XMLKVDatabase(new File("permazen-data.xml"));
kvdb.start();
// Create core API and Permazen databases on top of it
final Permazen jdb = new PermazenFactory()
.setDatabase(new Database(kvdb))
.setSchemaVersion(-1)
.setModelClasses(Person.class)
.newPermazen();
// Write to database
doInTransaction(jdb, () -> {
System.out.println("\n*** Writing database...");
Person fred = Person.create();
Person joe = Person.create();
Person sally = Person.create();
fred.setName("Fred");
fred.setAge(25);
joe.setName("Joe");
joe.setAge(35);
sally.setName("Sally");
sally.setAge(45);
fred.getFriends().add(joe);
fred.getFriends().add(sally);
joe.getFriends().add(sally);
sally.getFriends().add(fred);
});
// Read from database
doInTransaction(jdb, () -> {
System.out.println("\n*** Showing all people...");
Person.getAll().forEach(System.out::println);
System.out.println("\n*** Showing people older than Joe...");
Person joe = Person.getAll().stream()
.filter(p -> p.getName().equals("Joe"))
.findAny()
.get();
joe.getPeopleOlderThan().forEach(System.out::println);
});
// Stop underlying database
kvdb.stop();
}
public static void doInTransaction(Permazen jdb, Runnable action) {
int numTries = 0;
while (true) {
final PermazenTransaction jtx = jdb.createTransaction(true, ValidationMode.AUTOMATIC);
PermazenTransaction.setCurrent(jtx);
try {
action.run();
jtx.commit();
return;
} catch (RetryTransactionException e) {
if (numTries++ == 3)
throw e;
continue;
} finally {
jtx.rollback();
PermazenTransaction.setCurrent(null);
}
}
}
// Model Classes
@PermazenType
public abstract static class Person implements PermazenObject {
// My age
@PermazenField(indexed = true)
public abstract int getAge();
public abstract void setAge(int age);
// My name
public abstract String getName();
public abstract void setName(String name);
// My friends
public abstract NavigableSet<Person> getFriends();
// Inverse of friends
public NavigableSet<Person> getFriendsToMe() {
return this.findReferring(Person.class, "friends.element");
}
// Query index on "age" field
public NavigableMap<Integer, NavigableSet<Person>> queryByAge() {
return this.getTransaction().queryIndex(Person.class, "age", Integer.class).asMap();
}
// Get everyone older than me
public Stream<Person> getPeopleOlderThan() {
return this.queryByAge()
.tailMap(this.getAge(), false)
.values()
.stream()
.flatMap(NavigableSet::stream);
}
@Override
public String toString() {
final StringBuilder buf = new StringBuilder();
buf.append("Person (id " + this.getObjId() + ")\n");
buf.append(" Name: " + this.getName() + "\n");
buf.append(" Age: " + this.getAge() + "\n");
buf.append(" ->Friends of " + this.getName() + ":");
for (Person friend : this.getFriends())
buf.append(" " + friend.getName());
buf.append("\n");
buf.append(" <-Friends with " + this.getName() + ":");
for (Person reverseFriend : this.getFriendsToMe())
buf.append(" " + reverseFriend.getName());
buf.append("\n");
return buf.toString().trim();
}
public static Person create() {
return PermazenTransaction.getCurrent().create(Person.class);
}
public static NavigableSet<Person> getAll() {
return PermazenTransaction.getCurrent().getAll(Person.class);
}
}
}
and sample output:
INFO: file `permazen-data.xml' not found and no initial content is configured; creating new, empty database
*** Writing database...
*** Showing all people...
Person (id fcc57255a32df6b7)
Name: Fred
Age: 25
->Friends of Fred: Sally Joe
<-Friends with Fred: Sally
Person (id fcc57255e47ea083)
Name: Sally
Age: 45
->Friends of Sally: Fred
<-Friends with Sally: Fred Joe
Person (id fcc572ee14715ff0)
Name: Joe
Age: 35
->Friends of Joe: Sally
<-Friends with Joe: Fred
*** Showing people older than Joe...
Person (id fcc57255e47ea083)
Name: Sally
Age: 45
->Friends of Sally: Fred
<-Friends with Sally: Fred Joe
To use RocksDB as the key/value store, replace the first line of the main()
method above with:
final RocksDBAtomicKVStore kvstore = new RocksDBAtomicKVStore();
kvstore.setDirectory("/tmp/test");
final RocksDBKVDatabase rocksdb = new RocksDBKVDatabase();
rocksdb.setKVStore(kvstore);
rocksdb.start();
final Database coreDb = new Database(rocksdb);