MENU +44 (0) 333 44 GRAPH

GraphAware Blog

Neo4j Improved Transaction Event API

11 Jul 2014 by Michal Bachman

One of the main goals of the GraphAware Framework is to simplify and speed up development with Neo4j. Although it is called a “framework” for reasons explained elsewhere, today we will simply treat it as a library of useful, tested, and documented Java code. The feature we will introduce is called Improved Transaction Event API, which is exactly what it says on the tin.

Motivation

Neo4j requires every mutating operation on the graph to be run in a transaction, which is great, because it keeps your data safe. Every operation is atomic, consistent, isolated, and durable. As a bonus, Neo4j gives you the opportunity to react to these mutations right before they are committed, or right after they have been committed. This might be useful, for instance, when you want prevent certain mutations from happening, log all changes in another system, or even perform additional mutations to the graph.

The mechanism is quite simple. You write a custom TransactionEventHandler and register it with the database. It is then notified of changes before and after a transaction commits (and, in fact, when it is rolled back). Let’s have a look at an example straight away.

@Test
public void showSimpleEventHandling() {
    GraphDatabaseService database
        = new TestGraphDatabaseFactory().newImpermanentDatabase();

    database.registerTransactionEventHandler(
        new TransactionEventHandler<Void>() {
            @Override
            public Void beforeCommit(TransactionData data) throws Exception {
                System.out.println("Committing transaction");
                return null;
            }

            @Override
            public void afterCommit(TransactionData data, Void state) {
                System.out.println("Committed transaction");
            }

            @Override
            public void afterRollback(TransactionData data, Void state) {
                System.out.println("Transaction rolled back");
            }
        });

    try (Transaction tx = database.beginTx()) {
        database.createNode();
        tx.success();
    }

    /**
     prints:
     > Committing transaction
     > Committed transaction
     **/
}

The code doesn’t do much but hopefully gets the point across. Now let’s do something a little bit more fun. Say you want to log every node that’s about to be deleted, including its properties. A first attempt could look something like this:

@Test(expected = TransactionFailureException.class)
public void attemptLoggingDeletedNodes() {
    GraphDatabaseService database = new TestGraphDatabaseFactory().newImpermanentDatabase();

    database.registerTransactionEventHandler(new TransactionEventHandler.Adapter<Void>() {
        @Override
        public Void beforeCommit(TransactionData data) throws Exception {
            for (Node deletedNode : data.deletedNodes()) {
                StringBuilder message = new StringBuilder("About to delete node ID ")
                        .append(deletedNode.getId())
                        .append(" ");

                for (String key : deletedNode.getPropertyKeys()) {
                    message.append("key=").append(key);
                    message.append("value=").append(deletedNode.getProperty(key));
                }

                System.out.println(message.toString());
            }

            return null;
        }
    });

    try (Transaction tx = database.beginTx()) {
        database.getNodeById(0).setProperty("test key", "test value");
        tx.success();
    }

    try (Transaction tx = database.beginTx()) {
        database.getNodeById(0).delete();
        tx.success();
    }
}

As the expected exception in the @Test annotation suggests, however, this approach fails in Neo4j. Properties of deleted nodes and relationships cannot be accessed using the standard APIs. Moreover, it isn’t trivial to find out, which properties have been added, deleted, and changed (and how they have been changed) for a given property container (i.e., node or relationship).

Improved API

GraphAware introduces an ImprovedTransactionData API, a sister of Neo4j’s TransactionData, which provides a convenient way to find out what has happened (or is about to happen) during the transaction that is about to be committed.

For instance, you can find out whether a node has been deleted in this transaction by calling boolean hasBeenCreated(Node node). Once you’ve established this is the case, calling Node getDeleted(Node node) will give you access to the node as it was before the transaction started. You can interrogate its properties and labels without getting exceptions.

What is perhaps even more interesting, you can start traversing the graph starting with a deleted node. What you will see is a snapshot of the graph as it was before the transaction started. On the other hand, starting to traverse the graph from a newly created node, obtained by calling _Collection getAllCreatedNodes()_, for instance, will give you access to the graph in the same state if will be after the transaction successfully commits.

Finally, changed nodes and relationships can be obtained wrapped in a Change object. By calling its getPrevious() method, you get the node/relationship as it was before the transaction started and again, can freely interrogate its properties and traverse the old snapshot of the graph. Calling getCurrent() gives you access to the future state of the node/relationship, provided that the commit succeeds.

We shall now refer the reader to the Javadoc for more details and illustrate the usage of ImprovedTransactionData by an example.

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 = DynamicRelationshipType.withName("FRIEND_OF");
    public static final String STRENGTH = "strength";
    public static final String TOTAL_FRIENDSHIP_STRENGTH = "totalFriendshipStrength";
    public static final Label COUNTER_NODE_LABEL = DynamicLabel.label("FriendshipCounter");

    private final GraphDatabaseService database;

    public FriendshipStrengthCounter(GraphDatabaseService database) {
        this.database = database;
        try (Transaction tx = database.beginTx()) {
            getCounterNode(database); //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(
            GlobalGraphOperations.at(database).getAllNodesWithLabel(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:

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

database.registerTransactionEventHandler(new FriendshipStrengthCounter(database));

We can now call the getTotalFriendshipStrength method on the FriendshipStrengthCounter and obtain an up-to-date total strength of all the friendships in the system.

Conclusion

GraphAware Framework provides a convenient wrapper of Neo4j’s TransactionData called ImprovedTransactionData. It is useful for all kinds of scenarios where mutations performed on the graph have to be inspected, acted upon, and perhaps even prevented. Head to the GitHub repository to read a detailed documentation and instructions on how to get started.

Share this blog post:

+1 LinkedIn
comments powered by Disqus

Popular

Recent

Posts by tag

Neo4j Conference NoSQL Czech Beginner Analytics Advanced Modelling Meetup GraphAware Intermediate GraphUnit Testing Transactions Cypher Events Spring SDN OGM Recommendations Search Elasticsearch Security Enterprise NLP HCM PeopleAnalytics HR HRTech Framework Internationalization Localization

Search this blog