Skip to content

Latest commit

 

History

History
280 lines (216 loc) · 13.5 KB

README.md

File metadata and controls

280 lines (216 loc) · 13.5 KB

Improved Transaction Event API

This (Maven) module is part of the GraphAware Neo4j Framework.

Introduction

This module provides a decorator of the Neo4j Transaction Event API (called TransactionData). Before a transaction commits, the improved API allows users to traverse the new version of the graph (as it will be after the transaction commits), as well as a "snapshot" of the old graph (as it was before the transaction started). It provides a clean API to access information about changes performed by the transaction as well as the option to perform additional changes or prevent the transaction from committing.

The least you can gain from using this functionality is avoiding java.lang.IllegalStateException: Node/Relationship has been deleted in this tx when trying to access properties of nodes/relationships deleted in a transaction. You can also easily access relationships/nodes that were changed and/or deleted in a transaction, again completely exception-free.

The target audience of this module are advanced Neo4j users, mostly Java developers developing Neo4j TransactionEventHandlers. The module is also one of the key components of GraphAware Runtime.

Getting the Module

Add the following snippet to your pom.xml:

<dependency>
    <groupId>com.graphaware.neo4j</groupId>
    <artifactId>tx-api</artifactId>
    <version>3.2.1.50</version>
</dependency>

Usage

Example: An example is provided in examples/friendship-strength-counter.

To use the API, simply instantiate one of the ImprovedTransactionData implementations. LazyTransactionData is recommended as it is the easiest one to use.

 GraphDatabaseService database = new TestGraphDatabaseFactory().newImpermanentDatabase();

 database.registerTransactionEventHandler(new TransactionEventHandler<Object>() {
     @Override
     public Object beforeCommit(TransactionData data) throws Exception {
         ImprovedTransactionData improvedTransactionData = new LazyTransactionData(data);

         //have fun here with improvedTransactionData!

         return null;
     }

     @Override
     public void afterCommit(TransactionData data, Object state) {

     }

     @Override
     public void afterRollback(TransactionData data, Object state) {

     }
});

FilteredTransactionData can be used instead. They effectively hide portions of the graph, including any changes performed on nodes and relationships that are not interesting. InclusionPolicies are used to convey the information about what is interesting and what is not. For example, of only nodes with name equal to "Two" and no relationships at all are of interest, the example above could be modified as follows:

GraphDatabaseService database = new TestGraphDatabaseFactory().newImpermanentDatabase();
database.registerTransactionEventHandler(new TransactionEventHandler.Adapter<Object>() {
    @Override
    public Object beforeCommit(TransactionData data) throws Exception {
        InclusionPolicies inclusionPolicies = InclusionPolicies.all()
                .with(new NodeInclusionPolicy() {
                    @Override
                    public boolean include(Node node) {
                        return node.getProperty("name", "default").equals("Two");
                    }
                })
                .with(IncludeNoRelationships.getInstance());

        ImprovedTransactionData improvedTransactionData
                = new FilteredTransactionData(new LazyTransactionData(data), inclusionPolicies);

        //have fun here with improvedTransactionData!

        return null;
    }
});

Example Scenario

Example: The following example is provided in examples/friendship-strength-counter.

Let's illustrate why this might be useful on a very simple example. Let's say we have a FRIEND_OF relationship in the system and it has a strength property indicating the strength of the friendship from 1 to 3. Let's further assume that we are interested in the total strength of all FRIEND_OF relationships in the entire system.

We'll achieve this by creating a custom transaction event handler that keeps track of the total strength. While not an ideal choice from a system throughput perspective, let's say for the sake of simplicity that we are going to store the total strength on a special node (with label FriendshipCounter) as a totalFriendshipStrength property.

/**
 * Example of a Neo4j {@link org.neo4j.graphdb.event.TransactionEventHandler} that uses GraphAware {@link ImprovedTransactionData}
 * to do its job, which is counting the total strength of all friendships in the database and writing that to a special
 * node created for that purpose.
 */
public class FriendshipStrengthCounter extends TransactionEventHandler.Adapter<Void> {

    public static final RelationshipType FRIEND_OF = RelationshipType.withName("FRIEND_OF");
    public static final String STRENGTH = "strength";
    public static final String TOTAL_FRIENDSHIP_STRENGTH = "totalFriendshipStrength";
    public static final Label COUNTER_NODE_LABEL = Label.label("FriendshipCounter");

    private final GraphDatabaseService database;

    public FriendshipStrengthCounter(GraphDatabaseService database) {
        this.database = database;
        try (Transaction tx = database.beginTx()) {
            getCounterNode(database); //do this in constructor to prevent multiple threads creating multiple nodes
            tx.success();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    public Void beforeCommit(TransactionData data) throws Exception {
        ImprovedTransactionData improvedTransactionData = new LazyTransactionData(data);

        long delta = 0;

        //handle new friendships
        for (Relationship newFriendship : improvedTransactionData.getAllCreatedRelationships()) {
            if (newFriendship.isType(FRIEND_OF)) {
                delta += (long) newFriendship.getProperty(STRENGTH, 0L);
            }
        }

        //handle changed friendships
        for (Change<Relationship> changedFriendship : improvedTransactionData.getAllChangedRelationships()) {
            if (changedFriendship.getPrevious().isType(FRIEND_OF)) {
                delta -= (long) changedFriendship.getPrevious().getProperty(STRENGTH, 0L);
                delta += (long) changedFriendship.getCurrent().getProperty(STRENGTH, 0L);
            }
        }

        //handle deleted friendships
        for (Relationship deletedFriendship : improvedTransactionData.getAllDeletedRelationships()) {
            if (deletedFriendship.isType(FRIEND_OF)) {
                delta -= (long) deletedFriendship.getProperty(STRENGTH, 0L);
            }
        }

        if (delta != 0) {
            Node counter = getCounterNode(database);

            try (Transaction tx = database.beginTx()) {
                tx.acquireWriteLock(counter);
                counter.setProperty(TOTAL_FRIENDSHIP_STRENGTH, (long) counter.getProperty(TOTAL_FRIENDSHIP_STRENGTH, 0L) + delta);
                tx.success();
            }
        }

        return null;
    }

    /**
     * Get the counter node, where the friendship strength is stored. Create it if it does not exist.
     *
     * @param database to find the node in.
     * @return counter node.
     */
    private static Node getCounterNode(GraphDatabaseService database) {
        Node result = IterableUtils.getSingleOrNull(database.findNodes(COUNTER_NODE_LABEL));

        if (result != null) {
            return result;
        }

        return database.createNode(COUNTER_NODE_LABEL);
    }

    /**
     * Get the counter value of the total friendship strength counter.
     *
     * @param database to find the counter in.
     * @return total friendship strength.
     */
    public static long getTotalFriendshipStrength(GraphDatabaseService database) {
        long result = 0;

        try (Transaction tx = database.beginTx()) {
            result = (long) getCounterNode(database).getProperty(TOTAL_FRIENDSHIP_STRENGTH, 0L);
            tx.success();
        }

        return result;
    }
}

All that remains is registering this event handler on the database:

GraphDatabaseService database = new TestGraphDatabaseFactory().newImpermanentDatabase(); //this will in reality be a real database (i.e. EmbeddedGraphDatabase)
database.registerTransactionEventHandler(new FriendshipStrengthCounter(database));

Usage in Detail

Note: have a look at ImprovedTransactionData Javadoc.

The API categorizes PropertyContainers, i.e. Nodes and Relationships into:

  • created in this transaction
  • deleted in this transaction
  • changed in this transaction, i.e those with at least one property created, deleted, or changed. Nodes with assigned and removed labels also fall into this category.
  • untouched by this transaction

Users can find out, whether a PropertyContainer has been created, deleted, or changed in this transaction and obtain all the created, deleted, and changed PropertyContainers.

Properties that have been created, deleted, and changed in the transaction are categorized by the changed PropertyContainer they belong to. Users can find out, which properties have been created, deleted, and changed for a given changed PropertyContainer and check, whether a given property for a given changed PropertyContainer has been created, deleted, or changed.

Properties of created PropertyContainers are available through the actual created PropertyContainer. Properties of deleted PropertyContainers (as they were before the transaction started) are available through the snapshot of the deleted PropertyContainer, obtained by calling getDeleted(org.neo4j.graphdb.Node) or getDeleted(org.neo4j.graphdb.Relationship). Properties of created and deleted containers will not be returned by changedProperties(org.neo4j.graphdb.Node) and changedProperties(org.neo4j.graphdb.Relationship) as these only return changed properties of changed PropertyContainers.

Changed PropertyContainers and properties are wrapped in a Change object which holds the previous state of the object before the transaction started, and the current state of the object (when the transaction commits).

All created PropertyContainers and properties and current versions of changed PropertyContainers and properties can be accessed by native Neo4j API and the traversal API as one would expect. For example, one can traverse the graph starting from a newly created node, using a mixture of newly created and already existing relationships. In other words, one can traverse the graph as if the transaction has already been committed. This is similar to using TransactionData.

A major difference between this API and TransactionData, however, is what one can do with the returned information about deleted PropertyContainers and properties and the previous versions thereof. With this API, one can traverse a snapshot of the graph as it was before the transaction started. As opposed to the TransactionData API, this will not result in exceptions being thrown.

For example, one can start traversing the graph from a deleted Node, or the previous version of a changed Node. Such traversal will only traverse Relationships that existed before the transaction started and will return properties/labels and their values as they were before the transaction started. This is achieved using NodeSnapshot and RelationshipSnapshot decorators.

One can even perform additional mutating operations on the previous version (snapshot) of the graph, provided that the mutated objects have been changed in the transaction (as opposed to deleted). Mutating deleted PropertyContainers and properties does not make any sense and will cause exceptions.

To summarize, this API gives access to two versions of the same graph. Through created PropertyContainers and/or their current versions, one can traverse the current version of the graph as it will be after the transaction commits. Through deleted and/or previous versions of PropertyContainers, one can traverse the previous snapshot of the graph, as it was before the transaction started.

License

Copyright (c) 2013-2017 GraphAware

GraphAware is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see http://www.gnu.org/licenses/.