Skip to content

GettingStarted

Archie L. Cobbs edited this page Jan 11, 2024 · 24 revisions

The Big Picture

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!

Demonstration

To see a simple Permazen database in action, download the Permazen distribution ZIP file:

  1. Download and unzip the demo ZIP file
  2. Run java -jar permazen-gui.jar to start up the GUI on port 8080
  3. 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

Installation

See Downloads for how to include Permazen in your build.

Configuration

Creating and configuring a Permazen database requires these steps:

  1. Configure your underlying key/value store
  2. Gather your Java model classes
  3. 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();

Creating Transactions

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 ...
    }

Retrying Transactions

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 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.

Hello World

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);