Java persistence layer for NoSQL.
Java Makefile
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Failed to load latest commit information.
aws-core-util
cred-api
crypto-api
crypto-impl
jackson-transform
persistence-api
persistence-example
persistence-impl
.gitignore
LICENSE
Makefile
README.md
pom.xml
test-key.aes

README.md

Java Persistence API for NoSQL

The Java NoSQL Persistence for Java project enables Java developers to easily work with NoSQL databases using a single API.

Getting Started

Using from Maven (pom.xml)

The persistence-api module contains the API and models for interacting with your NoSQL persistence store. The persistence-impl contains the implementations for these APIs. We recommend using a JSR 330 dependency injector such as Guice for injecting the implementation in.

  <dependencies>
    <dependency>
      <groupId>com.distelli.persistence</groupId>
      <artifactId>persistence-api</artifactId>
      <version>1.1</version>
    </dependency>

    <dependency>
      <groupId>com.distelli.persistence</groupId>
      <artifactId>persistence-impl</artifactId>
      <version>1.1</version>
      <scope>test</scope>
    </dependency>
  <dependencies>

Create POJO Managers

Here is an example of a ToyShop that manages Toy object instances. For this example we are using Toy "structures", but these should probably be POJOs. This example also shows how you can use a "record version number" (rvn) to implement optimistic locking. The rvn is incremented on each update so we can abort an update if the record was changed before the last read.

import com.distelli.persistence.ConvertMarker;
import com.distelli.persistence.Index;
import com.distelli.persistence.PageIterator;
import com.fasterxml.jackson.databind.ObjectMapper;
import java.util.List;
import javax.inject.Inject;
import javax.persistence.EntityExistsException;
import javax.persistence.RollbackException;

public class ToyShop {
    /**
     * Declare a POJO to convert that will be persisted. Note that this can be a
     * complex object.
     */
    public static class Toy {
         public long id;
         public String name;
         public String description;
         public long priceInCents;
         public long weightInGrams;
         public String imageUrl;
         public Category category;
         public Dimensions dimensions;
         // Record Version Number (optimistic locking)
         public long rvn;
    }
    public static class Dimensions {
         public int heightInCm;
         public int widthInCm;
    }
    public static enum Category {
         PUZZLES, DOLLS, BUILDING, PARTY, EDUCATIONAL, SPORTS;
    }
    private Index<Toy> _toysById;
    private Index<Toy> _toysByCategory;

    @Inject
    protected ToyShop(Index.Factory indexFactory, ConvertMarker.Factory convertMarkerFactory) {
        ObjectMapper om = new ObjectMapper();
        _toysById = indexFactory.create(Toy.class)
            .withTableName("toys")
            .withNoEncrypt("id", "category", "name")
            .withHashKeyName("id")
            .withConvertValue(om::convertValue)
            .withConvertMarker(convertMarkerFactory.create("id"))
            .build();
        _toysByCategory = indexFactory.create(Toy.class)
            .withIndexName("toys", "category-name")
            .withNoEncrypt("id", "category", "name")
            .withHashKeyName("category")
            .withRangeKeyName("name")
            .withConvertValue(om::convertValue)
            .withConvertMarker(convertMarkerFactory.create("category", "name"))
            .build();
    }

    // Create
    public void addToy(Toy toy) throws EntityExistsException {
        toy.rvn = 0; // Start with 0 record version number.
        _toysById.putItemOrThrow(toy);
    }

    // Read
    public Toy getToyById(long id) {
        return _toysById.getItem(id);
    }

    // Query (alternative index)
    public List<Toy> getToysByCategory(Category category, PageIterator pageIterator) {
        return _toysByCategory.queryItems(category.toString(), pageIterator)
             .list();
    }

    // Scan everything
    public List<Toy> getAllToys(PageIterator pageIterator) {
        return _toysById.scanItems(pageIterator);
    }

    // Update
    public void updateDescription(long id, long rvn, String description) throws RollbackException {
        _toysById.updateItem(id, null)
             .set("description", description)
             .set("rvn", rvn+1)
             .when((cond) -> cond.eq("rvn", rvn));
    }

    // Delete
    public void removeToy(long id) {
        _toysById.deleteItem(id, null);
    }
}

Wiring with Guice

Here is an example of wiring up the implementations with Guice to drive your program.

import com.distelli.cred.CredPair;
import com.distelli.persistence.Index;
import com.distelli.persistence.PageIterator;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Module;
import java.io.BufferedReader;
import java.io.InputStreamReader;
import java.net.URI;
import java.util.List;
import javax.inject.Inject;
import javax.inject.Named;
import javax.inject.Provider;
import static java.nio.charset.StandardCharsets.UTF_8;

public class Main {
    /**
     * The "BASE" Index.Factory should be decorated with default
     * endpoints and credentials to avoid code duplication.
     */
    public static class IndexFactoryProvider implements Provider<Index.Factory> {
        @Inject @Named("BASE")
        private Index.Factory _baseIndexFactory;
        private URI _endpoint;
        private CredPair _creds;
        public IndexFactoryProvider(URI defaultEndpoint, CredPair defaultCreds) {
            _endpoint = defaultEndpoint;
            _creds = defaultCreds;
        }
        @Override
        public Index.Factory get() {
            return new Index.Factory() {
                @Override
                public <T> Index.Builder<T> create(Class<T> type) {
                    return _baseIndexFactory.create(type)
                        .withTableNameFormat("prefix-%s")
                        .withEndpoint(_endpoint)
                        .withCredProvider(() -> _creds);
                }
            };
        }
   }

    public static void main(String[] args) throws Exception {
        URI endpoint = URI.create(System.getenv("ENDPOINT"));
        CredPair creds = new CredPair()
            .withKeyId(System.getenv("KEY_ID"))
            .withSecret(System.getenv("SECRET"));
        Guice.createInjector(
            (Module)Class.forName("com.distelli.persistence.impl.PersistenceModule")
            .newInstance(),
            new AbstractModule() {
                @Override
                protected void configure() {
                    bind(Index.Factory.class)
                        .toProvider(new IndexFactoryProvider(endpoint, creds));
                }
            })
            .getInstance(Main.class)
            .run(args);
    }

    @Inject
    private ToyShop _toyShop;

    public void run(String[] args) throws Exception {
        ObjectMapper om = new ObjectMapper();
        om.disable(JsonGenerator.Feature.AUTO_CLOSE_TARGET);
        if ( 1 == args.length && "--list".equals(args[0]) ) {
            for ( PageIterator it : new PageIterator() ) {
                for ( ToyShop.Toy toy : _toyShop.getAllToys(it) ) {
                    om.writeValue(System.out, toy);
                    System.out.println();
                }
            }
        } else if ( 1 == args.length && "--put".equals(args[0]) ) {
            BufferedReader in =
                new BufferedReader(new InputStreamReader(System.in, UTF_8));
            String line;
            while ( null != (line = in.readLine()) ) {
                _toyShop.addToy(om.readValue(line, ToyShop.Toy.class));
            }
        } else if ( 2 == args.length && "--category".equals(args[0]) ) {
            ToyShop.Category category = ToyShop.Category.valueOf(args[1]);
            for ( PageIterator it : new PageIterator() ) {
                for ( ToyShop.Toy toy : _toyShop.getToysByCategory(category, it) ) {
                    om.writeValue(System.out, toy);
                    System.out.println();
                }
            }
        } else {
            System.err.println("Usage: java Main [--list] [--put] [--category <CATEGORY>]");
            System.exit(-1);
        }
    }
}

Using DynamoDB Implementation

After configuring the AWS CLI, run this command to create the "prefix-toys" table:

aws dynamodb create-table \
    --attribute-definitions 'AttributeName=id,AttributeType=N' \
         'AttributeName=category,AttributeType=S' \
         'AttributeName=name,AttributeType=S' \
    --table-name 'prefix-toys' \
    --key-schema 'AttributeName=id,KeyType=HASH' \
    --global-secondary-indexes 'IndexName=category-name,KeySchema=[{AttributeName=category,KeyType=HASH},{AttributeName=name,KeyType=RANGE}],Projection={ProjectionType=ALL},ProvisionedThroughput={ReadCapacityUnits=1,WriteCapacityUnits=1}' \
    --provisioned-throughput 'ReadCapacityUnits=1,WriteCapacityUnits=1'

You can then run Main to put some items:

cat <<EOF | ENDPOINT=ddb://us-east-1 KEY_ID=... SECRET=... java -cp ... Main --put
{"id":1,"name":"Ocean scape floor puzzle","category":"PUZZLES","description":"crazy"}
{"id":2,"name":"Magnetic fishing puzzle","category":"PUZZLES","description":"learn to fish"}
{"id":3,"name":"G.I.Joe action figure #3","category":"DOLLS","description":"great for little boys"}
{"id":4,"name":"Miss Pretty Dress Up Doll","category":"DOLLS","description":"great for little girls"}
EOF

...then list items by category or list all items:

ENDPOINT=ddb://us-east-1 KEY_ID=... SECRET=... java -cp ... Main --list
ENDPOINT=ddb://us-east-1 KEY_ID=... SECRET=... java -cp ... Main --category DOLLS

To delete the DynamoDB table, simply run:

aws --profile beta --region us-east-1 dynamodb delete-table --table-name prefix-toys

Using Mysql Implementation

The mysql implementation requires at least version 5.7.11 and makes use of the newer JSON primitives. First we need to create the table in mysql:

cat <<'EOF' | mysql ...
CREATE TABLE `prefix-toys` (
    `#` json,
    id DOUBLE,
    category TEXT AS (JSON_UNQUOTE(`#`->'$.category')),
    name TEXT AS (JSON_UNQUOTE(`#`->'$.name')),
    PRIMARY KEY (id) USING BTREE,
    INDEX `category-name` (category(2048), name(1024)) USING BTREE
)
EOF

Then you can run Main to put some items:

cat <<EOF | ENDPOINT='mysql://<host>:<port>/<database>?useSSL=true&serverSslCert=...' KEY_ID='<username>' SECRET='<password>' java -cp ... Main --put
{"id":1,"name":"Ocean scape floor puzzle","category":"PUZZLES","description":"crazy"}
{"id":2,"name":"Magnetic fishing puzzle","category":"PUZZLES","description":"learn to fish"}
{"id":3,"name":"G.I.Joe action figure #3","category":"DOLLS","description":"great for little boys"}
{"id":4,"name":"Miss Pretty Dress Up Doll","category":"DOLLS","description":"great for little girls"}
EOF

...then list items by category or list all items:

ENDPOINT='mysql://<host>:<port>/<database>?useSSL=true&serverSslCert=...' KEY_ID='<username>' SECRET='<password>' java -cp ... Main --list
ENDPOINT='mysql://<host>:<port>/<database>?useSSL=true&serverSslCert=...' KEY_ID='<username>' SECRET='<password>' java -cp ... Main --category DOLLS

To delete the MySQL table, simply run:

cat <<'EOF' | mysql ...
DROP TABLE `prefix-toys`
EOF